@saptools/service-flow 0.1.10 → 0.1.12

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.
@@ -524,6 +524,7 @@ function summarizeExpression(text) {
524
524
  // src/parsers/outbound-call-parser.ts
525
525
  import fs6 from "fs/promises";
526
526
  import path7 from "path";
527
+ import ts4 from "typescript";
527
528
  function lineOf3(text, idx) {
528
529
  return text.slice(0, idx).split("\n").length;
529
530
  }
@@ -627,32 +628,63 @@ async function parseOutboundCalls(repoPath, filePath) {
627
628
  confidence: 0.7,
628
629
  unresolvedReason: "External HTTP destination is outside indexed CAP services"
629
630
  });
630
- for (const m of text.matchAll(
631
- /cds\.services(?:\[['"]([^'"]+)['"]\]|\.(\w+))\.(\w+)\s*\(/g
632
- ))
633
- out.push({
634
- callType: "local_service_call",
635
- operationPathExpr: `/${m[3] ?? ""}`,
636
- payloadSummary: m[1] ?? m[2],
637
- sourceFile: normalizePath(filePath),
638
- sourceLine: lineOf3(text, m.index ?? 0),
639
- confidence: 0.75
640
- });
631
+ out.push(...parseLocalServiceCalls(text, filePath));
641
632
  return out;
642
633
  }
634
+ function parseLocalServiceCalls(text, filePath) {
635
+ const source = ts4.createSourceFile(filePath, text, ts4.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts4.ScriptKind.TS : ts4.ScriptKind.JS);
636
+ const aliases = /* @__PURE__ */ new Map();
637
+ const calls = [];
638
+ const visit = (node) => {
639
+ if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
640
+ const origin = serviceLookup(node.initializer, aliases);
641
+ if (origin) aliases.set(node.name.text, { ...origin, chain: [...origin.chain, node.name.text] });
642
+ }
643
+ if (ts4.isCallExpression(node)) {
644
+ const parsed = serviceOperationCall(node.expression, aliases);
645
+ if (parsed && parsed.operation !== "entities") calls.push({
646
+ callType: "local_service_call",
647
+ operationPathExpr: `/${parsed.operation}`,
648
+ payloadSummary: parsed.service,
649
+ localServiceName: parsed.service,
650
+ localServiceLookup: parsed.lookup,
651
+ aliasChain: parsed.chain,
652
+ sourceFile: normalizePath(filePath),
653
+ sourceLine: lineOf3(text, node.getStart(source)),
654
+ confidence: 0.9
655
+ });
656
+ }
657
+ ts4.forEachChild(node, visit);
658
+ };
659
+ visit(source);
660
+ return calls;
661
+ }
662
+ function serviceLookup(expr, aliases) {
663
+ if (ts4.isIdentifier(expr)) return aliases.get(expr.text);
664
+ if (ts4.isPropertyAccessExpression(expr) && expr.expression.getText() === "cds.services") return { service: expr.name.text, lookup: expr.getText(), chain: [expr.getText()] };
665
+ if (ts4.isElementAccessExpression(expr) && expr.expression.getText() === "cds.services" && ts4.isStringLiteral(expr.argumentExpression)) return { service: expr.argumentExpression.text, lookup: expr.getText(), chain: [expr.getText()] };
666
+ return void 0;
667
+ }
668
+ function serviceOperationCall(expr, aliases) {
669
+ if (!ts4.isPropertyAccessExpression(expr)) return void 0;
670
+ const operation = expr.name.text;
671
+ const origin = serviceLookup(expr.expression, aliases);
672
+ if (!origin) return void 0;
673
+ return { ...origin, operation };
674
+ }
643
675
 
644
676
  // src/parsers/service-binding-parser.ts
645
677
  import fs7 from "fs/promises";
646
678
  import path8 from "path";
647
- import ts4 from "typescript";
679
+ import ts5 from "typescript";
648
680
  function lineOf4(sf, node) {
649
681
  return sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
650
682
  }
651
683
  function stringValue(node) {
652
684
  if (!node) return void 0;
653
- if (ts4.isStringLiteralLike(node) || ts4.isNoSubstitutionTemplateLiteral(node))
685
+ if (ts5.isStringLiteralLike(node) || ts5.isNoSubstitutionTemplateLiteral(node))
654
686
  return node.text;
655
- if (ts4.isTemplateExpression(node))
687
+ if (ts5.isTemplateExpression(node))
656
688
  return node.getText().replace(/^`|`$/g, "");
657
689
  return node.getText();
658
690
  }
@@ -661,22 +693,22 @@ function placeholders(value) {
661
693
  }
662
694
  function connectFactFromCall(call) {
663
695
  const expr = call.expression;
664
- if (!ts4.isPropertyAccessExpression(expr) || expr.name.text !== "to")
696
+ if (!ts5.isPropertyAccessExpression(expr) || expr.name.text !== "to")
665
697
  return void 0;
666
698
  const inner = expr.expression;
667
- if (!ts4.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
699
+ if (!ts5.isPropertyAccessExpression(inner) || inner.name.text !== "connect" || inner.expression.getText() !== "cds")
668
700
  return void 0;
669
701
  const first = call.arguments[0];
670
702
  if (!first) return void 0;
671
703
  const second = call.arguments[1];
672
- const objectArg = ts4.isObjectLiteralExpression(first) ? first : second && ts4.isObjectLiteralExpression(second) ? second : void 0;
704
+ const objectArg = ts5.isObjectLiteralExpression(first) ? first : second && ts5.isObjectLiteralExpression(second) ? second : void 0;
673
705
  let alias;
674
706
  let aliasExpr;
675
- if (ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first))
707
+ if (ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first))
676
708
  alias = first.text;
677
- else if (!ts4.isObjectLiteralExpression(first))
709
+ else if (!ts5.isObjectLiteralExpression(first))
678
710
  aliasExpr = stringValue(first);
679
- if ((ts4.isStringLiteralLike(first) || ts4.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
711
+ if ((ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
680
712
  return { alias: first.text, isDynamic: false, placeholders: [] };
681
713
  if (!objectArg && aliasExpr)
682
714
  return {
@@ -688,13 +720,13 @@ function connectFactFromCall(call) {
688
720
  let servicePathExpr;
689
721
  function visitObject(obj) {
690
722
  for (const prop of obj.properties) {
691
- if (!ts4.isPropertyAssignment(prop)) continue;
692
- const name = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
723
+ if (!ts5.isPropertyAssignment(prop)) continue;
724
+ const name = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
693
725
  if (name === "destination")
694
726
  destinationExpr = stringValue(prop.initializer);
695
727
  if (name === "path" || name === "servicePath")
696
728
  servicePathExpr = stringValue(prop.initializer);
697
- if (ts4.isObjectLiteralExpression(prop.initializer))
729
+ if (ts5.isObjectLiteralExpression(prop.initializer))
698
730
  visitObject(prop.initializer);
699
731
  }
700
732
  }
@@ -714,8 +746,8 @@ function connectFactFromCall(call) {
714
746
  };
715
747
  }
716
748
  function unwrapCall(expr) {
717
- if (ts4.isAwaitExpression(expr)) return unwrapCall(expr.expression);
718
- if (ts4.isCallExpression(expr)) return expr;
749
+ if (ts5.isAwaitExpression(expr)) return unwrapCall(expr.expression);
750
+ if (ts5.isCallExpression(expr)) return expr;
719
751
  return void 0;
720
752
  }
721
753
  function findConnectInExpression(expr) {
@@ -727,8 +759,8 @@ function findConnectInExpression(expr) {
727
759
  let found;
728
760
  function visit(node) {
729
761
  if (found) return;
730
- if (ts4.isCallExpression(node)) found = connectFactFromCall(node);
731
- if (!found) ts4.forEachChild(node, visit);
762
+ if (ts5.isCallExpression(node)) found = connectFactFromCall(node);
763
+ if (!found) ts5.forEachChild(node, visit);
732
764
  }
733
765
  visit(expr);
734
766
  return found;
@@ -736,12 +768,12 @@ function findConnectInExpression(expr) {
736
768
  async function readSource(abs) {
737
769
  try {
738
770
  const text = await fs7.readFile(abs, "utf8");
739
- return ts4.createSourceFile(
771
+ return ts5.createSourceFile(
740
772
  abs,
741
773
  text,
742
- ts4.ScriptTarget.Latest,
774
+ ts5.ScriptTarget.Latest,
743
775
  true,
744
- ts4.ScriptKind.TS
776
+ ts5.ScriptKind.TS
745
777
  );
746
778
  } catch {
747
779
  return void 0;
@@ -770,7 +802,7 @@ async function resolveImport(repoPath, fromFile, spec) {
770
802
  async function importsFor(repoPath, filePath, sf) {
771
803
  const imports = [];
772
804
  for (const stmt of sf.statements) {
773
- if (!ts4.isImportDeclaration(stmt) || !ts4.isStringLiteralLike(stmt.moduleSpecifier))
805
+ if (!ts5.isImportDeclaration(stmt) || !ts5.isStringLiteralLike(stmt.moduleSpecifier))
774
806
  continue;
775
807
  const sourceFile = await resolveImport(
776
808
  repoPath,
@@ -786,7 +818,7 @@ async function importsFor(repoPath, filePath, sf) {
786
818
  sourceFile
787
819
  });
788
820
  const bindings = clause.namedBindings;
789
- if (bindings && ts4.isNamedImports(bindings))
821
+ if (bindings && ts5.isNamedImports(bindings))
790
822
  for (const el of bindings.elements)
791
823
  imports.push({
792
824
  localName: el.name.text,
@@ -799,17 +831,17 @@ async function importsFor(repoPath, filePath, sf) {
799
831
  function exportedLocalNames(sf) {
800
832
  const exports = /* @__PURE__ */ new Map();
801
833
  for (const stmt of sf.statements) {
802
- const direct = ts4.canHaveModifiers(stmt) ? ts4.getModifiers(stmt)?.some(
803
- (m) => m.kind === ts4.SyntaxKind.ExportKeyword
834
+ const direct = ts5.canHaveModifiers(stmt) ? ts5.getModifiers(stmt)?.some(
835
+ (m) => m.kind === ts5.SyntaxKind.ExportKeyword
804
836
  ) ?? false : false;
805
- if (direct && ts4.isFunctionDeclaration(stmt) && stmt.name)
837
+ if (direct && ts5.isFunctionDeclaration(stmt) && stmt.name)
806
838
  exports.set(stmt.name.text, stmt.name.text);
807
- if (direct && ts4.isVariableStatement(stmt)) {
839
+ if (direct && ts5.isVariableStatement(stmt)) {
808
840
  for (const decl of stmt.declarationList.declarations)
809
- if (ts4.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
841
+ if (ts5.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
810
842
  }
811
- if (!ts4.isExportDeclaration(stmt) || !stmt.exportClause) continue;
812
- if (!ts4.isNamedExports(stmt.exportClause)) continue;
843
+ if (!ts5.isExportDeclaration(stmt) || !stmt.exportClause) continue;
844
+ if (!ts5.isNamedExports(stmt.exportClause)) continue;
813
845
  for (const el of stmt.exportClause.elements)
814
846
  exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
815
847
  }
@@ -823,18 +855,18 @@ async function helperBindings(repoPath, filePath) {
823
855
  const exportedLocals = exportedLocalNames(sf);
824
856
  const factsByLocal = /* @__PURE__ */ new Map();
825
857
  for (const stmt of sf.statements) {
826
- if (ts4.isFunctionDeclaration(stmt) && stmt.name) {
858
+ if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
827
859
  let fact;
828
860
  stmt.forEachChild(function visit(node) {
829
- if (!fact && ts4.isReturnStatement(node) && node.expression)
861
+ if (!fact && ts5.isReturnStatement(node) && node.expression)
830
862
  fact = findConnectInExpression(node.expression);
831
- if (!fact) ts4.forEachChild(node, visit);
863
+ if (!fact) ts5.forEachChild(node, visit);
832
864
  });
833
865
  if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
834
866
  }
835
- if (ts4.isVariableStatement(stmt))
867
+ if (ts5.isVariableStatement(stmt))
836
868
  for (const decl of stmt.declarationList.declarations) {
837
- if (!ts4.isIdentifier(decl.name) || !decl.initializer) continue;
869
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) continue;
838
870
  const fact = findConnectInExpression(decl.initializer);
839
871
  if (fact)
840
872
  factsByLocal.set(decl.name.text, {
@@ -875,7 +907,7 @@ async function parseServiceBindings(repoPath, filePath) {
875
907
  return helper ? { imp, helper } : void 0;
876
908
  }
877
909
  async function recordVariable(decl) {
878
- if (!ts4.isIdentifier(decl.name) || !decl.initializer) return;
910
+ if (!ts5.isIdentifier(decl.name) || !decl.initializer) return;
879
911
  const call = unwrapCall(decl.initializer);
880
912
  if (!call) return;
881
913
  const direct = connectFactFromCall(call);
@@ -886,7 +918,7 @@ async function parseServiceBindings(repoPath, filePath) {
886
918
  sourceFile: normalizePath(filePath),
887
919
  sourceLine: lineOf4(sourceFileAst, decl)
888
920
  });
889
- else if (ts4.isIdentifier(call.expression)) {
921
+ else if (ts5.isIdentifier(call.expression)) {
890
922
  const resolved = await importedHelper(call.expression.text);
891
923
  if (resolved)
892
924
  out.push({
@@ -913,14 +945,14 @@ async function parseServiceBindings(repoPath, filePath) {
913
945
  }
914
946
  }
915
947
  function recordDestructuredClassHelper(decl) {
916
- if (!ts4.isObjectBindingPattern(decl.name) || !decl.initializer) return;
948
+ if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
917
949
  const call = unwrapCall(decl.initializer);
918
- if (!call || !ts4.isPropertyAccessExpression(call.expression)) return;
950
+ if (!call || !ts5.isPropertyAccessExpression(call.expression)) return;
919
951
  const target = call.expression;
920
- if (target.expression.kind !== ts4.SyntaxKind.ThisKeyword) return;
952
+ if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
921
953
  for (const el of decl.name.elements) {
922
- if (!ts4.isIdentifier(el.name)) continue;
923
- const propertyName = el.propertyName && ts4.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
954
+ if (!ts5.isIdentifier(el.name)) continue;
955
+ const propertyName = el.propertyName && ts5.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
924
956
  const helper = classHelpers.find(
925
957
  (h) => h.helperName === target.name.text && h.propertyName === propertyName
926
958
  );
@@ -946,8 +978,8 @@ async function parseServiceBindings(repoPath, filePath) {
946
978
  }
947
979
  const declarations = [];
948
980
  function collectDeclarations(node) {
949
- if (ts4.isVariableDeclaration(node)) declarations.push(node);
950
- ts4.forEachChild(node, collectDeclarations);
981
+ if (ts5.isVariableDeclaration(node)) declarations.push(node);
982
+ ts5.forEachChild(node, collectDeclarations);
951
983
  }
952
984
  collectDeclarations(sourceFileAst);
953
985
  for (const decl of declarations) {
@@ -959,16 +991,16 @@ async function parseServiceBindings(repoPath, filePath) {
959
991
  function collectClassHelpers(sf) {
960
992
  const helpers = [];
961
993
  for (const stmt of sf.statements) {
962
- if (!ts4.isClassDeclaration(stmt) || !stmt.name) continue;
994
+ if (!ts5.isClassDeclaration(stmt) || !stmt.name) continue;
963
995
  for (const member of stmt.members) {
964
996
  let visit2 = function(node) {
965
- if (ts4.isVariableDeclaration(node) && ts4.isIdentifier(node.name) && node.initializer) {
997
+ if (ts5.isVariableDeclaration(node) && ts5.isIdentifier(node.name) && node.initializer) {
966
998
  const fact = findConnectInExpression(node.initializer);
967
999
  if (fact) bindings.set(node.name.text, fact);
968
1000
  }
969
- if (ts4.isReturnStatement(node) && node.expression && ts4.isObjectLiteralExpression(node.expression)) {
1001
+ if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
970
1002
  for (const prop of node.expression.properties) {
971
- if (ts4.isShorthandPropertyAssignment(prop)) {
1003
+ if (ts5.isShorthandPropertyAssignment(prop)) {
972
1004
  const fact = bindings.get(prop.name.text);
973
1005
  if (fact)
974
1006
  helpers.push({
@@ -980,8 +1012,8 @@ function collectClassHelpers(sf) {
980
1012
  sourceLine: lineOf4(sf, prop)
981
1013
  });
982
1014
  }
983
- if (ts4.isPropertyAssignment(prop) && ts4.isIdentifier(prop.initializer)) {
984
- const propertyName = ts4.isIdentifier(prop.name) || ts4.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
1015
+ if (ts5.isPropertyAssignment(prop) && ts5.isIdentifier(prop.initializer)) {
1016
+ const propertyName = ts5.isIdentifier(prop.name) || ts5.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
985
1017
  const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
986
1018
  if (propertyName && fact)
987
1019
  helpers.push({
@@ -995,15 +1027,15 @@ function collectClassHelpers(sf) {
995
1027
  }
996
1028
  }
997
1029
  }
998
- ts4.forEachChild(node, visit2);
1030
+ ts5.forEachChild(node, visit2);
999
1031
  };
1000
1032
  var visit = visit2;
1001
- if (!ts4.isPropertyDeclaration(member) || !member.initializer) continue;
1002
- if (!ts4.isIdentifier(member.name)) continue;
1033
+ if (!ts5.isPropertyDeclaration(member) || !member.initializer) continue;
1034
+ if (!ts5.isIdentifier(member.name)) continue;
1003
1035
  const className = stmt.name.text;
1004
1036
  const helperName = member.name.text;
1005
1037
  const initializer = member.initializer;
1006
- if (!ts4.isArrowFunction(initializer) && !ts4.isFunctionExpression(initializer))
1038
+ if (!ts5.isArrowFunction(initializer) && !ts5.isFunctionExpression(initializer))
1007
1039
  continue;
1008
1040
  const bindings = /* @__PURE__ */ new Map();
1009
1041
  visit2(initializer);
@@ -1042,7 +1074,7 @@ function substituteVariables(template, vars) {
1042
1074
  // src/linker/service-resolver.ts
1043
1075
  function rows(db, operationPath, workspaceId) {
1044
1076
  return db.prepare(
1045
- `SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
1077
+ `SELECT o.id operationId,r.name repoName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
1046
1078
  FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
1047
1079
  WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
1048
1080
  ).all(
@@ -1066,7 +1098,7 @@ function resolveOperation(db, signals, workspaceId) {
1066
1098
  candidates: [],
1067
1099
  reasons: ["missing_operation_path"]
1068
1100
  };
1069
- const candidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
1101
+ const candidates = rows(db, signals.operationPath, workspaceId).filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId)).map((c) => ({
1070
1102
  ...c,
1071
1103
  score: 0.2,
1072
1104
  reasons: ["operation_path_match"]
@@ -1078,7 +1110,7 @@ function resolveOperation(db, signals, workspaceId) {
1078
1110
  reasons: ["no_operation_candidates"]
1079
1111
  };
1080
1112
  const hasStrongSignal = Boolean(
1081
- signals.servicePath || signals.alias || signals.destination || signals.hasExplicitOverride
1113
+ signals.servicePath || signals.serviceName || signals.alias || signals.destination || signals.hasExplicitOverride
1082
1114
  );
1083
1115
  for (const c of candidates) {
1084
1116
  if (signals.servicePath && c.servicePath === signals.servicePath) {
@@ -1089,9 +1121,25 @@ function resolveOperation(db, signals, workspaceId) {
1089
1121
  c.score -= 0.1;
1090
1122
  c.reasons.push("service_path_mismatch");
1091
1123
  }
1124
+ if (signals.serviceName) {
1125
+ const simple = signals.serviceName.split(".").at(-1) ?? signals.serviceName;
1126
+ if (c.qualifiedName === signals.serviceName) {
1127
+ c.score += 0.8;
1128
+ c.reasons.push("exact_local_qualified_service_name");
1129
+ } else if (c.serviceName === signals.serviceName || c.serviceName === simple) {
1130
+ c.score += 0.75;
1131
+ c.reasons.push("exact_local_simple_service_name");
1132
+ } else if (c.servicePath === signals.serviceName || c.servicePath === `/${signals.serviceName}` || c.servicePath === `/${simple}`) {
1133
+ c.score += 0.7;
1134
+ c.reasons.push("exact_local_service_path");
1135
+ } else if (c.servicePath.endsWith(`/${simple}`)) {
1136
+ c.score += candidates.filter((candidate) => candidate.servicePath.endsWith(`/${simple}`)).length === 1 ? 0.65 : 0.2;
1137
+ c.reasons.push("suffix_local_service_path");
1138
+ } else c.reasons.push("local_service_name_mismatch");
1139
+ }
1092
1140
  if (signals.hasExplicitOverride) {
1093
1141
  c.score += 0.2;
1094
- c.reasons.push("explicit_dynamic_override");
1142
+ c.reasons.push(signals.repoId !== void 0 ? "explicit_local_service_call" : "explicit_dynamic_override");
1095
1143
  }
1096
1144
  }
1097
1145
  for (const c of candidates) c.score = Math.max(0, Math.min(1, c.score));
@@ -1112,7 +1160,7 @@ function resolveOperation(db, signals, workspaceId) {
1112
1160
  candidates,
1113
1161
  reasons: ["operation_path_only_has_no_strong_target_signal"]
1114
1162
  };
1115
- if (best && best.score >= 0.9 && best.servicePath === signals.servicePath && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
1163
+ if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && !best.reasons.includes("local_service_name_mismatch"))) && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
1116
1164
  return {
1117
1165
  status: "resolved",
1118
1166
  target: best,
@@ -1125,6 +1173,11 @@ function resolveOperation(db, signals, workspaceId) {
1125
1173
  reasons: ["candidate_score_below_resolution_threshold"]
1126
1174
  };
1127
1175
  }
1176
+ function matchesLocalRepo(db, operationId, repoId) {
1177
+ if (repoId === void 0) return true;
1178
+ const row = db.prepare("SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?").get(operationId);
1179
+ return row?.repoId === repoId;
1180
+ }
1128
1181
 
1129
1182
  // src/linker/helper-package-linker.ts
1130
1183
  function normalizeName(value) {
@@ -1209,10 +1262,11 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1209
1262
  const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
1210
1263
  const destination = call.destinationExpr ?? call.requireDestination;
1211
1264
  const isDynamic = Boolean(Number(call.isDynamic ?? 0));
1212
- const resolution = callType.startsWith("remote") ? resolveOperation(db, { servicePath, operationPath: op, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
1265
+ const isOperationCall = callType.startsWith("remote") || callType === "local_service_call";
1266
+ const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
1213
1267
  const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
1214
1268
  if (resolution.target) {
1215
- db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
1269
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, callType === "local_service_call" ? "LOCAL_CALL_RESOLVES_TO_OPERATION" : "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
1216
1270
  return { status: "resolved" };
1217
1271
  }
1218
1272
  const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
@@ -1222,7 +1276,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1222
1276
  return { status };
1223
1277
  }
1224
1278
  function callEvidence(call, resolution, servicePath, op, destination) {
1225
- return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
1279
+ return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
1226
1280
  }
1227
1281
  function linkImplementations(db, workspaceId, generation) {
1228
1282
  const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
@@ -1259,12 +1313,42 @@ function linkImplementations(db, workspaceId, generation) {
1259
1313
  }
1260
1314
  function rankedImplementationCandidates(db, workspaceId, operation) {
1261
1315
  const rows2 = implementationCandidates(db, workspaceId, operation);
1262
- return rows2.map((row) => scoreImplementationCandidate(row, operation)).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
1316
+ return deduplicateCandidates(rows2.map((row) => scoreImplementationCandidate(row, operation))).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
1317
+ }
1318
+ function deduplicateCandidates(rows2) {
1319
+ const merged = /* @__PURE__ */ new Map();
1320
+ for (const row of rows2) {
1321
+ const key = [row.methodId, row.classId, row.handlerRepoId].join(":");
1322
+ const registration = { file: row.registrationFile, line: row.registrationLine, kind: row.registrationKind, importSource: row.importSource };
1323
+ const existing = merged.get(key);
1324
+ if (!existing) {
1325
+ merged.set(key, { ...row, registrations: [registration] });
1326
+ continue;
1327
+ }
1328
+ existing.registrations = uniqueRegistrations([...existing.registrations ?? [], registration]);
1329
+ existing.score = Math.max(existing.score, row.score);
1330
+ existing.accepted = existing.accepted || row.accepted;
1331
+ existing.acceptedReasons = [.../* @__PURE__ */ new Set([...existing.acceptedReasons, ...row.acceptedReasons])];
1332
+ existing.rejectedReasons = [.../* @__PURE__ */ new Set([...existing.rejectedReasons, ...row.rejectedReasons])];
1333
+ }
1334
+ return [...merged.values()];
1335
+ }
1336
+ function uniqueRegistrations(rows2) {
1337
+ const seen = /* @__PURE__ */ new Set();
1338
+ return rows2.filter((row) => {
1339
+ const key = JSON.stringify(row);
1340
+ if (seen.has(key)) return false;
1341
+ seen.add(key);
1342
+ return true;
1343
+ });
1263
1344
  }
1264
1345
  function implementationCandidates(db, workspaceId, operation) {
1265
1346
  const modelRepoGraphId = graphId(operation.modelRepoId);
1266
1347
  return db.prepare(`SELECT DISTINCT
1267
1348
  hm.id methodId,
1349
+ hm.method_name methodName,
1350
+ hm.decorator_value decoratorValue,
1351
+ hm.decorator_raw_expression decoratorRawExpression,
1268
1352
  hc.id classId,
1269
1353
  hc.class_name className,
1270
1354
  hc.source_file sourceFile,
@@ -1291,7 +1375,7 @@ function implementationCandidates(db, workspaceId, operation) {
1291
1375
  CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
1292
1376
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
1293
1377
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
1294
- CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
1378
+ CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
1295
1379
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
1296
1380
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
1297
1381
  CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
@@ -1301,7 +1385,7 @@ function implementationCandidates(db, workspaceId, operation) {
1301
1385
  JOIN handler_registrations hr ON (hr.handler_class_id=hc.id OR (hr.class_name=hc.class_name AND (hr.repo_id=hc.repo_id OR hr.import_source IS NOT NULL)))
1302
1386
  JOIN repositories appRepo ON appRepo.id=hr.repo_id
1303
1387
  WHERE appRepo.workspace_id=?
1304
- AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(
1388
+ AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=? OR hm.decorator_raw_expression LIKE ?)`).all(
1305
1389
  operation.modelRepoId,
1306
1390
  operation.modelRepo,
1307
1391
  operation.modelPackage,
@@ -1315,12 +1399,14 @@ function implementationCandidates(db, workspaceId, operation) {
1315
1399
  normalizedOperation(String(operation.operationPath ?? "")),
1316
1400
  operation.operationName,
1317
1401
  operation.operationName,
1402
+ `%${upperFirst(normalizedOperation(String(operation.operationPath ?? operation.operationName ?? "")))}%`,
1318
1403
  modelRepoGraphId,
1319
1404
  modelRepoGraphId,
1320
1405
  workspaceId,
1321
1406
  normalizedOperation(String(operation.operationPath ?? "")),
1322
1407
  operation.operationName,
1323
- operation.operationName
1408
+ operation.operationName,
1409
+ `%${upperFirst(normalizedOperation(String(operation.operationPath ?? operation.operationName ?? "")))}%`
1324
1410
  );
1325
1411
  }
1326
1412
  function scoreImplementationCandidate(row, operation) {
@@ -1338,7 +1424,10 @@ function scoreImplementationCandidate(row, operation) {
1338
1424
  const importSource = typeof row.importSource === "string" && row.importSource.length > 0;
1339
1425
  const sameRepoRegistration = flag(row.sameRepoRegistration);
1340
1426
  const modelOriented = row.modelKind === "cap-db-model" || !applicationHasLocalRegistrationForOperation;
1341
- const methodMatches = true;
1427
+ const methodSignal = implementationMethodSignal(row, operation);
1428
+ const methodMatches = methodSignal.matches;
1429
+ acceptedReasons.push(...methodSignal.acceptedReasons);
1430
+ rejectedReasons.push(...methodSignal.rejectedReasons);
1342
1431
  const registeredAndLinked = sameRepoRegistration && importSource;
1343
1432
  const helperOwned = modelOriented && methodMatches && registeredAndLinked && sameRepoRegistration && !applicationHasLocalServices && !modelIsApplicationRepo && !modelIsHandlerRepo && !localServicePathMatch && !appDependsOnModel && !appDependsOnHandler && !handlerDependsOnModel;
1344
1433
  if (modelIsApplicationRepo) {
@@ -1379,7 +1468,7 @@ function scoreImplementationCandidate(row, operation) {
1379
1468
  const hasCrossPackage = appDependsOnModel && (modelIsHandlerRepo || appDependsOnHandler || !importSource);
1380
1469
  const contradicted = applicationHasLocalServices && !localServicePathMatch && !appDependsOnModel && !hasOwnership;
1381
1470
  if (!hasOwnership && !localServicePathMatch && !hasCrossPackage && !helperOwned) rejectedReasons.push("missing direct ownership, exact local service path, or validated cross-package dependency evidence");
1382
- const accepted = !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel || helperOwned);
1471
+ const accepted = methodMatches && !methodSignal.contradicted && !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel || helperOwned);
1383
1472
  if (!accepted && rejectedReasons.length === 0) rejectedReasons.push("candidate did not meet implementation ownership policy");
1384
1473
  return { ...row, methodId: Number(row.methodId), score, accepted, acceptedReasons, rejectedReasons };
1385
1474
  }
@@ -1396,6 +1485,7 @@ function candidateEvidence(candidate, rank) {
1396
1485
  sourceFile: candidate.sourceFile,
1397
1486
  sourceLine: candidate.sourceLine,
1398
1487
  registration: { file: candidate.registrationFile, line: candidate.registrationLine, kind: candidate.registrationKind, importSource: candidate.importSource },
1488
+ registrations: candidate.registrations ?? [],
1399
1489
  applicationPackage: { id: candidate.applicationRepoId, name: candidate.applicationRepo, packageName: candidate.applicationPackage },
1400
1490
  handlerPackage: { id: candidate.handlerRepoId, name: candidate.handlerRepo, packageName: candidate.handlerPackage },
1401
1491
  modelPackage: { id: candidate.modelRepoId, name: candidate.modelRepo, packageName: candidate.modelPackage },
@@ -1414,6 +1504,29 @@ function candidateEvidence(candidate, rank) {
1414
1504
  }
1415
1505
  };
1416
1506
  }
1507
+ function implementationMethodSignal(row, operation) {
1508
+ const op = normalizedOperation(String(operation.operationPath ?? operation.operationName ?? ""));
1509
+ const decorator = normalizeDecoratorOperation(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0);
1510
+ if (decorator && decorator === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
1511
+ if (decorator && decorator !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
1512
+ if (String(row.methodName ?? "") === op) return { matches: true, contradicted: false, acceptedReasons: ["method name fallback matched operation"], rejectedReasons: [] };
1513
+ return { matches: false, contradicted: false, acceptedReasons: [], rejectedReasons: ["method name does not match operation"] };
1514
+ }
1515
+ function normalizeDecoratorOperation(value, raw) {
1516
+ const candidate = value ?? raw?.split(".").filter(Boolean).at(-2);
1517
+ if (!candidate) return void 0;
1518
+ const cleaned = candidate.replace(/^['"`]|['"`]$/g, "");
1519
+ for (const prefix of ["Func", "Action"]) {
1520
+ if (cleaned.startsWith(prefix) && cleaned.length > prefix.length) return lowerFirst(cleaned.slice(prefix.length));
1521
+ }
1522
+ return normalizedOperation(cleaned);
1523
+ }
1524
+ function upperFirst(value) {
1525
+ return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
1526
+ }
1527
+ function lowerFirst(value) {
1528
+ return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
1529
+ }
1417
1530
  function flag(value) {
1418
1531
  return Boolean(Number(value ?? 0));
1419
1532
  }
@@ -1437,8 +1550,8 @@ function sourceFilesForStart(db, repoId, start) {
1437
1550
  const operation = normalizeOperation(start.operation ?? start.operationPath);
1438
1551
  if (!handler && !operation) return void 0;
1439
1552
  const rows2 = db.prepare(
1440
- `SELECT DISTINCT hc.source_file sourceFile
1441
- FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
1553
+ `SELECT DISTINCT hc.source_file sourceFile,s.id symbolId
1554
+ FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.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
1442
1555
  WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
1443
1556
  AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
1444
1557
  AND (? IS NULL OR EXISTS (SELECT 1 FROM cds_services s JOIN cds_operations o ON o.service_id=s.id WHERE s.repo_id=hc.repo_id AND s.service_path=? AND (? IS NULL OR o.operation_path=? OR o.operation_name=? OR hm.decorator_value=? OR hm.method_name=?)))`
@@ -1459,15 +1572,16 @@ function sourceFilesForStart(db, repoId, start) {
1459
1572
  operation,
1460
1573
  operation
1461
1574
  );
1462
- if (rows2.length > 0) return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1575
+ if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)) };
1463
1576
  if (start.servicePath && operation) {
1464
- const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile
1577
+ const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
1465
1578
  FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
1466
1579
  JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved' AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
1467
1580
  JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
1468
1581
  JOIN handler_classes hc ON hc.id=hm.handler_class_id
1582
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
1469
1583
  WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation);
1470
- if (implRows.length > 0) return new Set(implRows.map((row) => row.sourceFile).filter(Boolean));
1584
+ if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)) };
1471
1585
  }
1472
1586
  return void 0;
1473
1587
  }
@@ -1476,7 +1590,8 @@ function startScope(db, start) {
1476
1590
  "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
1477
1591
  ).get(start.repo, start.repo) : void 0;
1478
1592
  if (start.repo && !repo) return { repo, selectorMatched: false };
1479
- const sourceFiles = sourceFilesForStart(db, repo?.id, start);
1593
+ const sourceScope = sourceFilesForStart(db, repo?.id, start);
1594
+ const sourceFiles = sourceScope?.files;
1480
1595
  const hasSelector = Boolean(
1481
1596
  start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
1482
1597
  );
@@ -1485,6 +1600,7 @@ function startScope(db, start) {
1485
1600
  return {
1486
1601
  repo,
1487
1602
  sourceFiles,
1603
+ symbolIds: sourceScope?.symbols,
1488
1604
  selectorMatched: !hasSelector || sourceFiles !== void 0
1489
1605
  };
1490
1606
  }
@@ -1496,8 +1612,9 @@ function handlerFilesForOperation(db, operationId) {
1496
1612
  if (!op) return /* @__PURE__ */ new Set();
1497
1613
  const operation = normalizeOperation(op.operationPath ?? op.operationName);
1498
1614
  const rows2 = db.prepare(
1499
- `SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
1615
+ `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
1500
1616
  JOIN handler_methods hm ON hm.handler_class_id=hc.id
1617
+ LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
1501
1618
  WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
1502
1619
  ).all(op.repoId, operation, operation, op.operationName);
1503
1620
  return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
@@ -1513,8 +1630,8 @@ function handlerMethodNode(db, methodId) {
1513
1630
  function implementationScope(db, operationId) {
1514
1631
  const edge = implementationEdge(db, operationId);
1515
1632
  if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), edge };
1516
- const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
1517
- return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), edge };
1633
+ 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);
1634
+ return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
1518
1635
  }
1519
1636
  function includeCall(type, options) {
1520
1637
  if (!options.includeDb && type === "local_db_query") return false;
@@ -1559,6 +1676,12 @@ function evidenceWithRuntimeVariables(evidence, vars) {
1559
1676
  if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)];
1560
1677
  return next;
1561
1678
  }
1679
+ function symbolNode(db, symbolId) {
1680
+ 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);
1681
+ if (!row) return void 0;
1682
+ const fileName = String(row.sourceFile ?? "").split("/").at(-1) ?? String(row.sourceFile ?? "");
1683
+ return { id: `symbol:${symbolId}`, kind: "symbol", label: `${fileName}:${String(row.qualifiedName ?? row.symbolName)}`, ...row };
1684
+ }
1562
1685
  function operationNode(db, operationId) {
1563
1686
  const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_type operationType,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,r.id repoId,r.name repoName FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
1564
1687
  if (!row) return void 0;
@@ -1612,10 +1735,12 @@ function trace(db, start, options) {
1612
1735
  const maxDepth = positiveDepth(options.depth);
1613
1736
  const edges = [];
1614
1737
  const nodes = /* @__PURE__ */ new Map();
1615
- const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
1738
+ const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1 }] : [];
1616
1739
  if (start.servicePath && (start.operation ?? start.operationPath)) {
1617
1740
  const startOperation = normalizeOperation(start.operation ?? start.operationPath);
1618
- const row = db.prepare(`SELECT o.id operationId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE s.service_path=? AND (o.operation_path=? OR o.operation_name=?) ORDER BY o.id LIMIT 1`).get(start.servicePath, startOperation, startOperation);
1741
+ const startRows = db.prepare(`SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,o.id LIMIT 2`).all(scope.repo?.id, scope.repo?.id, start.servicePath, startOperation, startOperation);
1742
+ if (!scope.repo && startRows.length > 1) diagnostics.unshift({ severity: "warning", code: "trace_start_ambiguous", message: "Service/path trace start matched multiple repositories; add --repo to disambiguate", candidates: startRows });
1743
+ const row = startRows.length === 1 ? startRows[0] : void 0;
1619
1744
  if (row?.operationId !== void 0) {
1620
1745
  const opId = String(row.operationId);
1621
1746
  const op = operationNode(db, opId);
@@ -1634,15 +1759,31 @@ function trace(db, start, options) {
1634
1759
  while (queue.length > 0) {
1635
1760
  const current = queue.shift();
1636
1761
  if (!current || current.depth > maxDepth) continue;
1637
- const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
1762
+ const key = `${current.repoId ?? "*"}:${[...current.symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
1638
1763
  if (seenScopes.has(key)) continue;
1639
1764
  seenScopes.add(key);
1640
1765
  const calls = db.prepare(
1641
1766
  `SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`
1642
1767
  ).all(current.repoId, current.repoId);
1643
1768
  const filtered = calls.filter(
1644
- (c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
1769
+ (c) => (!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
1645
1770
  );
1771
+ if (current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
1772
+ const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,s.source_file calleeFile FROM symbol_calls sc LEFT JOIN symbols s ON s.id=sc.callee_symbol_id WHERE sc.caller_symbol_id IN (${[...current.symbolIds].map(() => "?").join(",")}) ORDER BY sc.source_file,sc.source_line`).all(...current.symbolIds);
1773
+ for (const symbolCall of symbolRows) {
1774
+ if (!symbolCall.callee_symbol_id) continue;
1775
+ const nextSymbols = /* @__PURE__ */ new Set([Number(symbolCall.callee_symbol_id)]);
1776
+ const nextFiles = /* @__PURE__ */ new Set([String(symbolCall.calleeFile)]);
1777
+ const nextRepoId = Number(symbolCall.calleeRepoId);
1778
+ const nextKey = `${nextRepoId}:${[...nextSymbols].join(",")}:${[...nextFiles].join(",")}`;
1779
+ const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
1780
+ if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
1781
+ const evidence = { ...JSON.parse(String(symbolCall.evidence_json || "{}")), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
1782
+ edges.push({ step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === "resolved" ? void 0 : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0 });
1783
+ if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: "cycle", from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: "Cycle detected; downstream symbol already visited" });
1784
+ else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1 });
1785
+ }
1786
+ }
1646
1787
  const graph = graphForCalls(
1647
1788
  db,
1648
1789
  filtered.map((c) => Number(c.id))
@@ -1678,7 +1819,7 @@ function trace(db, start, options) {
1678
1819
  edges.push({
1679
1820
  step: current.depth,
1680
1821
  type: String(call.call_type),
1681
- from: `${call.repoName}:${call.source_file}`,
1822
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
1682
1823
  to,
1683
1824
  evidence,
1684
1825
  confidence: Number(effectiveRow.confidence ?? call.confidence),
@@ -1692,7 +1833,7 @@ function trace(db, start, options) {
1692
1833
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
1693
1834
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1694
1835
  edges.push({
1695
- step: current.depth + 1,
1836
+ step: current.depth,
1696
1837
  type: "operation_implemented_by_handler",
1697
1838
  from: to,
1698
1839
  to: implTo,
@@ -1703,14 +1844,15 @@ function trace(db, start, options) {
1703
1844
  }
1704
1845
  if (current.depth >= maxDepth) continue;
1705
1846
  const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1847
+ const symbolIds = implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1706
1848
  if (implementation.edge?.status === "resolved" && files.size > 0) {
1707
1849
  const targetRepoId = implementation.repoId ?? db.prepare(
1708
1850
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1709
1851
  ).get(effectiveRow.to_id)?.repoId;
1710
- const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
1852
+ const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
1711
1853
  if (seenScopes.has(nextKey))
1712
1854
  edges.push({
1713
- step: current.depth + 1,
1855
+ step: current.depth,
1714
1856
  type: "cycle",
1715
1857
  from: to,
1716
1858
  to: nextKey,
@@ -1722,6 +1864,7 @@ function trace(db, start, options) {
1722
1864
  queue.push({
1723
1865
  repoId: targetRepoId,
1724
1866
  files,
1867
+ symbolIds,
1725
1868
  depth: current.depth + 1
1726
1869
  });
1727
1870
  }
@@ -1750,4 +1893,4 @@ export {
1750
1893
  linkWorkspace,
1751
1894
  trace
1752
1895
  };
1753
- //# sourceMappingURL=chunk-LQT67AU5.js.map
1896
+ //# sourceMappingURL=chunk-MXYYARUP.js.map