@saptools/service-flow 0.1.10 → 0.1.11

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);
@@ -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"]
@@ -1089,6 +1121,11 @@ 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 && (c.servicePath === `/${signals.serviceName}` || c.servicePath.endsWith(`/${signals.serviceName}`))) {
1125
+ c.score += 0.7;
1126
+ c.reasons.push("exact_local_service_name");
1127
+ }
1128
+ if (signals.serviceName && c.servicePath !== `/${signals.serviceName}` && !c.servicePath.endsWith(`/${signals.serviceName}`)) c.reasons.push("local_service_name_mismatch");
1092
1129
  if (signals.hasExplicitOverride) {
1093
1130
  c.score += 0.2;
1094
1131
  c.reasons.push("explicit_dynamic_override");
@@ -1112,7 +1149,7 @@ function resolveOperation(db, signals, workspaceId) {
1112
1149
  candidates,
1113
1150
  reasons: ["operation_path_only_has_no_strong_target_signal"]
1114
1151
  };
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))
1152
+ if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && (best.servicePath === `/${signals.serviceName}` || best.servicePath.endsWith(`/${signals.serviceName}`)))) && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
1116
1153
  return {
1117
1154
  status: "resolved",
1118
1155
  target: best,
@@ -1125,6 +1162,11 @@ function resolveOperation(db, signals, workspaceId) {
1125
1162
  reasons: ["candidate_score_below_resolution_threshold"]
1126
1163
  };
1127
1164
  }
1165
+ function matchesLocalRepo(db, operationId, repoId) {
1166
+ if (repoId === void 0) return true;
1167
+ 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);
1168
+ return row?.repoId === repoId;
1169
+ }
1128
1170
 
1129
1171
  // src/linker/helper-package-linker.ts
1130
1172
  function normalizeName(value) {
@@ -1209,10 +1251,11 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1209
1251
  const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
1210
1252
  const destination = call.destinationExpr ?? call.requireDestination;
1211
1253
  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: [] };
1254
+ const isOperationCall = callType.startsWith("remote") || callType === "local_service_call";
1255
+ 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
1256
  const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
1214
1257
  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);
1258
+ 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
1259
  return { status: "resolved" };
1217
1260
  }
1218
1261
  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 +1265,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
1222
1265
  return { status };
1223
1266
  }
1224
1267
  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 };
1268
+ 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
1269
  }
1227
1270
  function linkImplementations(db, workspaceId, generation) {
1228
1271
  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,7 +1302,34 @@ function linkImplementations(db, workspaceId, generation) {
1259
1302
  }
1260
1303
  function rankedImplementationCandidates(db, workspaceId, operation) {
1261
1304
  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);
1305
+ 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);
1306
+ }
1307
+ function deduplicateCandidates(rows2) {
1308
+ const merged = /* @__PURE__ */ new Map();
1309
+ for (const row of rows2) {
1310
+ const key = [row.methodId, row.classId, row.applicationRepoId, row.importSource ?? "", row.registrationKind ?? ""].join(":");
1311
+ const registration = { file: row.registrationFile, line: row.registrationLine, kind: row.registrationKind, importSource: row.importSource };
1312
+ const existing = merged.get(key);
1313
+ if (!existing) {
1314
+ merged.set(key, { ...row, registrations: [registration] });
1315
+ continue;
1316
+ }
1317
+ existing.registrations = uniqueRegistrations([...existing.registrations ?? [], registration]);
1318
+ existing.score = Math.max(existing.score, row.score);
1319
+ existing.accepted = existing.accepted || row.accepted;
1320
+ existing.acceptedReasons = [.../* @__PURE__ */ new Set([...existing.acceptedReasons, ...row.acceptedReasons])];
1321
+ existing.rejectedReasons = [.../* @__PURE__ */ new Set([...existing.rejectedReasons, ...row.rejectedReasons])];
1322
+ }
1323
+ return [...merged.values()];
1324
+ }
1325
+ function uniqueRegistrations(rows2) {
1326
+ const seen = /* @__PURE__ */ new Set();
1327
+ return rows2.filter((row) => {
1328
+ const key = JSON.stringify(row);
1329
+ if (seen.has(key)) return false;
1330
+ seen.add(key);
1331
+ return true;
1332
+ });
1263
1333
  }
1264
1334
  function implementationCandidates(db, workspaceId, operation) {
1265
1335
  const modelRepoGraphId = graphId(operation.modelRepoId);
@@ -1396,6 +1466,7 @@ function candidateEvidence(candidate, rank) {
1396
1466
  sourceFile: candidate.sourceFile,
1397
1467
  sourceLine: candidate.sourceLine,
1398
1468
  registration: { file: candidate.registrationFile, line: candidate.registrationLine, kind: candidate.registrationKind, importSource: candidate.importSource },
1469
+ registrations: candidate.registrations ?? [],
1399
1470
  applicationPackage: { id: candidate.applicationRepoId, name: candidate.applicationRepo, packageName: candidate.applicationPackage },
1400
1471
  handlerPackage: { id: candidate.handlerRepoId, name: candidate.handlerRepo, packageName: candidate.handlerPackage },
1401
1472
  modelPackage: { id: candidate.modelRepoId, name: candidate.modelRepo, packageName: candidate.modelPackage },
@@ -1437,8 +1508,8 @@ function sourceFilesForStart(db, repoId, start) {
1437
1508
  const operation = normalizeOperation(start.operation ?? start.operationPath);
1438
1509
  if (!handler && !operation) return void 0;
1439
1510
  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
1511
+ `SELECT DISTINCT hc.source_file sourceFile,s.id symbolId
1512
+ 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
1513
  WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
1443
1514
  AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
1444
1515
  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 +1530,16 @@ function sourceFilesForStart(db, repoId, start) {
1459
1530
  operation,
1460
1531
  operation
1461
1532
  );
1462
- if (rows2.length > 0) return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1533
+ 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
1534
  if (start.servicePath && operation) {
1464
- const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile
1535
+ const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
1465
1536
  FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
1466
1537
  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
1538
  JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
1468
1539
  JOIN handler_classes hc ON hc.id=hm.handler_class_id
1540
+ 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
1541
  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));
1542
+ 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
1543
  }
1472
1544
  return void 0;
1473
1545
  }
@@ -1476,7 +1548,8 @@ function startScope(db, start) {
1476
1548
  "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
1477
1549
  ).get(start.repo, start.repo) : void 0;
1478
1550
  if (start.repo && !repo) return { repo, selectorMatched: false };
1479
- const sourceFiles = sourceFilesForStart(db, repo?.id, start);
1551
+ const sourceScope = sourceFilesForStart(db, repo?.id, start);
1552
+ const sourceFiles = sourceScope?.files;
1480
1553
  const hasSelector = Boolean(
1481
1554
  start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
1482
1555
  );
@@ -1485,6 +1558,7 @@ function startScope(db, start) {
1485
1558
  return {
1486
1559
  repo,
1487
1560
  sourceFiles,
1561
+ symbolIds: sourceScope?.symbols,
1488
1562
  selectorMatched: !hasSelector || sourceFiles !== void 0
1489
1563
  };
1490
1564
  }
@@ -1496,8 +1570,9 @@ function handlerFilesForOperation(db, operationId) {
1496
1570
  if (!op) return /* @__PURE__ */ new Set();
1497
1571
  const operation = normalizeOperation(op.operationPath ?? op.operationName);
1498
1572
  const rows2 = db.prepare(
1499
- `SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
1573
+ `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
1500
1574
  JOIN handler_methods hm ON hm.handler_class_id=hc.id
1575
+ 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
1576
  WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
1502
1577
  ).all(op.repoId, operation, operation, op.operationName);
1503
1578
  return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
@@ -1513,8 +1588,8 @@ function handlerMethodNode(db, methodId) {
1513
1588
  function implementationScope(db, operationId) {
1514
1589
  const edge = implementationEdge(db, operationId);
1515
1590
  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 };
1591
+ 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);
1592
+ return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
1518
1593
  }
1519
1594
  function includeCall(type, options) {
1520
1595
  if (!options.includeDb && type === "local_db_query") return false;
@@ -1612,10 +1687,12 @@ function trace(db, start, options) {
1612
1687
  const maxDepth = positiveDepth(options.depth);
1613
1688
  const edges = [];
1614
1689
  const nodes = /* @__PURE__ */ new Map();
1615
- const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
1690
+ const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1 }] : [];
1616
1691
  if (start.servicePath && (start.operation ?? start.operationPath)) {
1617
1692
  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);
1693
+ 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);
1694
+ 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 });
1695
+ const row = startRows.length === 1 ? startRows[0] : void 0;
1619
1696
  if (row?.operationId !== void 0) {
1620
1697
  const opId = String(row.operationId);
1621
1698
  const op = operationNode(db, opId);
@@ -1634,15 +1711,28 @@ function trace(db, start, options) {
1634
1711
  while (queue.length > 0) {
1635
1712
  const current = queue.shift();
1636
1713
  if (!current || current.depth > maxDepth) continue;
1637
- const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
1714
+ const key = `${current.repoId ?? "*"}:${[...current.symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
1638
1715
  if (seenScopes.has(key)) continue;
1639
1716
  seenScopes.add(key);
1640
1717
  const calls = db.prepare(
1641
1718
  `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
1719
  ).all(current.repoId, current.repoId);
1643
1720
  const filtered = calls.filter(
1644
- (c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
1721
+ (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
1722
  );
1723
+ if (current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
1724
+ 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);
1725
+ for (const symbolCall of symbolRows) {
1726
+ if (!symbolCall.callee_symbol_id) continue;
1727
+ const nextSymbols = /* @__PURE__ */ new Set([Number(symbolCall.callee_symbol_id)]);
1728
+ const nextFiles = /* @__PURE__ */ new Set([String(symbolCall.calleeFile)]);
1729
+ const nextRepoId = Number(symbolCall.calleeRepoId);
1730
+ const nextKey = `${nextRepoId}:${[...nextSymbols].join(",")}:${[...nextFiles].join(",")}`;
1731
+ edges.push({ step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: `symbol:${String(symbolCall.callee_symbol_id)}`, evidence: JSON.parse(String(symbolCall.evidence_json || "{}")), confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0 });
1732
+ 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" });
1733
+ else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1 });
1734
+ }
1735
+ }
1646
1736
  const graph = graphForCalls(
1647
1737
  db,
1648
1738
  filtered.map((c) => Number(c.id))
@@ -1678,7 +1768,7 @@ function trace(db, start, options) {
1678
1768
  edges.push({
1679
1769
  step: current.depth,
1680
1770
  type: String(call.call_type),
1681
- from: `${call.repoName}:${call.source_file}`,
1771
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
1682
1772
  to,
1683
1773
  evidence,
1684
1774
  confidence: Number(effectiveRow.confidence ?? call.confidence),
@@ -1692,7 +1782,7 @@ function trace(db, start, options) {
1692
1782
  const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
1693
1783
  if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1694
1784
  edges.push({
1695
- step: current.depth + 1,
1785
+ step: current.depth,
1696
1786
  type: "operation_implemented_by_handler",
1697
1787
  from: to,
1698
1788
  to: implTo,
@@ -1703,14 +1793,15 @@ function trace(db, start, options) {
1703
1793
  }
1704
1794
  if (current.depth >= maxDepth) continue;
1705
1795
  const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1796
+ const symbolIds = implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1706
1797
  if (implementation.edge?.status === "resolved" && files.size > 0) {
1707
1798
  const targetRepoId = implementation.repoId ?? db.prepare(
1708
1799
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1709
1800
  ).get(effectiveRow.to_id)?.repoId;
1710
- const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
1801
+ const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
1711
1802
  if (seenScopes.has(nextKey))
1712
1803
  edges.push({
1713
- step: current.depth + 1,
1804
+ step: current.depth,
1714
1805
  type: "cycle",
1715
1806
  from: to,
1716
1807
  to: nextKey,
@@ -1722,6 +1813,7 @@ function trace(db, start, options) {
1722
1813
  queue.push({
1723
1814
  repoId: targetRepoId,
1724
1815
  files,
1816
+ symbolIds,
1725
1817
  depth: current.depth + 1
1726
1818
  });
1727
1819
  }
@@ -1750,4 +1842,4 @@ export {
1750
1842
  linkWorkspace,
1751
1843
  trace
1752
1844
  };
1753
- //# sourceMappingURL=chunk-LQT67AU5.js.map
1845
+ //# sourceMappingURL=chunk-GG4XJGES.js.map