@saptools/service-flow 0.1.9 → 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) {
@@ -1177,7 +1219,7 @@ function linkWorkspace(db, workspaceId, vars = {}) {
1177
1219
  const callSummary = linkCalls(db, workspaceId, vars, generation);
1178
1220
  const impl = linkImplementations(db, workspaceId, generation);
1179
1221
  db.prepare("UPDATE repositories SET graph_generation=?, graph_stale_reason=NULL, graph_stale_at=NULL WHERE workspace_id=?").run(generation, workspaceId);
1180
- return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount };
1222
+ return { ...callSummary, edgeCount: deps.edgeCount + callSummary.edgeCount + impl.edgeCount, dependencyResolvedCount: deps.resolvedCount, dependencyAmbiguousCount: deps.ambiguousCount, implementationResolvedCount: impl.resolvedCount, implementationAmbiguousCount: impl.ambiguousCount, implementationUnresolvedCount: impl.unresolvedCount };
1181
1223
  });
1182
1224
  }
1183
1225
  function nextGraphGeneration(db, workspaceId) {
@@ -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,17 +1265,18 @@ 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
- 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 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);
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);
1229
1272
  let edgeCount = 0;
1230
1273
  let resolvedCount = 0;
1231
1274
  let ambiguousCount = 0;
1275
+ let unresolvedCount = 0;
1232
1276
  for (const operation of operations) {
1233
1277
  const candidates = rankedImplementationCandidates(db, workspaceId, operation);
1278
+ if (candidates.length === 0) continue;
1234
1279
  const accepted = candidates.filter((candidate) => candidate.accepted);
1235
- if (accepted.length === 0) continue;
1236
1280
  const topScore = accepted[0]?.score ?? 0;
1237
1281
  const winners = accepted.filter((candidate) => candidate.score === topScore);
1238
1282
  const unique = winners.length === 1 ? winners[0] : void 0;
@@ -1243,16 +1287,49 @@ function linkImplementations(db, workspaceId, generation) {
1243
1287
  modelPackage: { id: operation.modelRepoId, name: operation.modelRepo, packageName: operation.modelPackage },
1244
1288
  candidates: candidates.map((candidate, index) => candidateEvidence(candidate, index + 1))
1245
1289
  };
1290
+ if (accepted.length === 0) {
1291
+ db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", "unresolved", "operation", graphId(operation.operationId), "handler_method_candidates", candidates.map((row) => graphId(row.methodId)).join(","), 0, JSON.stringify(evidence), 0, "No implementation candidate passed policy", generation);
1292
+ edgeCount += 1;
1293
+ unresolvedCount += 1;
1294
+ continue;
1295
+ }
1246
1296
  db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "OPERATION_IMPLEMENTED_BY_HANDLER", unique ? "resolved" : "ambiguous", "operation", graphId(operation.operationId), unique ? "handler_method" : "handler_method_candidates", unique ? graphId(unique.methodId) : winners.map((row) => graphId(row.methodId)).join(","), unique ? 0.95 : 0.5, JSON.stringify(evidence), 0, unique ? null : "Ambiguous registered handler implementation candidates", generation);
1247
1297
  edgeCount += 1;
1248
1298
  if (unique) resolvedCount += 1;
1249
1299
  else ambiguousCount += 1;
1250
1300
  }
1251
- return { edgeCount, resolvedCount, ambiguousCount };
1301
+ return { edgeCount, resolvedCount, ambiguousCount, unresolvedCount };
1252
1302
  }
1253
1303
  function rankedImplementationCandidates(db, workspaceId, operation) {
1254
1304
  const rows2 = implementationCandidates(db, workspaceId, operation);
1255
- 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
+ });
1256
1333
  }
1257
1334
  function implementationCandidates(db, workspaceId, operation) {
1258
1335
  const modelRepoGraphId = graphId(operation.modelRepoId);
@@ -1275,6 +1352,7 @@ function implementationCandidates(db, workspaceId, operation) {
1275
1352
  ? modelRepoId,
1276
1353
  ? modelRepo,
1277
1354
  ? modelPackage,
1355
+ ? modelKind,
1278
1356
  ? servicePath,
1279
1357
  ? operationPath,
1280
1358
  ? operationName,
@@ -1283,6 +1361,7 @@ function implementationCandidates(db, workspaceId, operation) {
1283
1361
  CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
1284
1362
  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,
1285
1363
  CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
1364
+ 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,
1286
1365
  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,
1287
1366
  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,
1288
1367
  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
@@ -1296,12 +1375,16 @@ function implementationCandidates(db, workspaceId, operation) {
1296
1375
  operation.modelRepoId,
1297
1376
  operation.modelRepo,
1298
1377
  operation.modelPackage,
1378
+ operation.modelKind,
1299
1379
  operation.servicePath,
1300
1380
  operation.operationPath,
1301
1381
  operation.operationName,
1302
1382
  operation.modelRepoId,
1303
1383
  operation.modelRepoId,
1304
1384
  operation.servicePath,
1385
+ normalizedOperation(String(operation.operationPath ?? "")),
1386
+ operation.operationName,
1387
+ operation.operationName,
1305
1388
  modelRepoGraphId,
1306
1389
  modelRepoGraphId,
1307
1390
  workspaceId,
@@ -1319,9 +1402,15 @@ function scoreImplementationCandidate(row, operation) {
1319
1402
  const localServicePathMatch = flag(row.localServicePathMatch);
1320
1403
  const applicationHasLocalServices = flag(row.applicationHasLocalServices);
1321
1404
  const appDependsOnModel = flag(row.appDependsOnModel);
1405
+ const applicationHasLocalRegistrationForOperation = flag(row.applicationHasLocalRegistrationForOperation);
1322
1406
  const appDependsOnHandler = flag(row.appDependsOnHandler);
1323
1407
  const handlerDependsOnModel = flag(row.handlerDependsOnModel);
1324
1408
  const importSource = typeof row.importSource === "string" && row.importSource.length > 0;
1409
+ const sameRepoRegistration = flag(row.sameRepoRegistration);
1410
+ const modelOriented = row.modelKind === "cap-db-model" || !applicationHasLocalRegistrationForOperation;
1411
+ const methodMatches = true;
1412
+ const registeredAndLinked = sameRepoRegistration && importSource;
1413
+ const helperOwned = modelOriented && methodMatches && registeredAndLinked && sameRepoRegistration && !applicationHasLocalServices && !modelIsApplicationRepo && !modelIsHandlerRepo && !localServicePathMatch && !appDependsOnModel && !appDependsOnHandler && !handlerDependsOnModel;
1325
1414
  if (modelIsApplicationRepo) {
1326
1415
  score += 100;
1327
1416
  acceptedReasons.push("model package equals registration package");
@@ -1348,6 +1437,10 @@ function scoreImplementationCandidate(row, operation) {
1348
1437
  score += 20;
1349
1438
  acceptedReasons.push("handler package depends on model package");
1350
1439
  }
1440
+ if (helperOwned) {
1441
+ score += 60;
1442
+ acceptedReasons.push("unique registered helper implementation for model-only operation");
1443
+ }
1351
1444
  if (importSource) {
1352
1445
  score += 10;
1353
1446
  acceptedReasons.push("registration imports handler class");
@@ -1355,8 +1448,8 @@ function scoreImplementationCandidate(row, operation) {
1355
1448
  const hasOwnership = modelIsApplicationRepo || modelIsHandlerRepo;
1356
1449
  const hasCrossPackage = appDependsOnModel && (modelIsHandlerRepo || appDependsOnHandler || !importSource);
1357
1450
  const contradicted = applicationHasLocalServices && !localServicePathMatch && !appDependsOnModel && !hasOwnership;
1358
- if (!hasOwnership && !localServicePathMatch && !hasCrossPackage) rejectedReasons.push("missing direct ownership, exact local service path, or validated cross-package dependency evidence");
1359
- const accepted = !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel);
1451
+ if (!hasOwnership && !localServicePathMatch && !hasCrossPackage && !helperOwned) rejectedReasons.push("missing direct ownership, exact local service path, or validated cross-package dependency evidence");
1452
+ const accepted = !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel || helperOwned);
1360
1453
  if (!accepted && rejectedReasons.length === 0) rejectedReasons.push("candidate did not meet implementation ownership policy");
1361
1454
  return { ...row, methodId: Number(row.methodId), score, accepted, acceptedReasons, rejectedReasons };
1362
1455
  }
@@ -1373,6 +1466,7 @@ function candidateEvidence(candidate, rank) {
1373
1466
  sourceFile: candidate.sourceFile,
1374
1467
  sourceLine: candidate.sourceLine,
1375
1468
  registration: { file: candidate.registrationFile, line: candidate.registrationLine, kind: candidate.registrationKind, importSource: candidate.importSource },
1469
+ registrations: candidate.registrations ?? [],
1376
1470
  applicationPackage: { id: candidate.applicationRepoId, name: candidate.applicationRepo, packageName: candidate.applicationPackage },
1377
1471
  handlerPackage: { id: candidate.handlerRepoId, name: candidate.handlerRepo, packageName: candidate.handlerPackage },
1378
1472
  modelPackage: { id: candidate.modelRepoId, name: candidate.modelRepo, packageName: candidate.modelPackage },
@@ -1383,6 +1477,7 @@ function candidateEvidence(candidate, rank) {
1383
1477
  directOwnership: { modelIsApplicationRepo: flag(candidate.modelIsApplicationRepo), modelIsHandlerRepo: flag(candidate.modelIsHandlerRepo) },
1384
1478
  localServicePathMatch: flag(candidate.localServicePathMatch),
1385
1479
  applicationHasLocalServices: flag(candidate.applicationHasLocalServices),
1480
+ applicationHasLocalRegistrationForOperation: flag(candidate.applicationHasLocalRegistrationForOperation),
1386
1481
  appDependsOnModel: flag(candidate.appDependsOnModel),
1387
1482
  appDependsOnHandler: flag(candidate.appDependsOnHandler),
1388
1483
  handlerDependsOnModel: flag(candidate.handlerDependsOnModel),
@@ -1413,8 +1508,8 @@ function sourceFilesForStart(db, repoId, start) {
1413
1508
  const operation = normalizeOperation(start.operation ?? start.operationPath);
1414
1509
  if (!handler && !operation) return void 0;
1415
1510
  const rows2 = db.prepare(
1416
- `SELECT DISTINCT hc.source_file sourceFile
1417
- 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
1418
1513
  WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
1419
1514
  AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
1420
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=?)))`
@@ -1435,16 +1530,26 @@ function sourceFilesForStart(db, repoId, start) {
1435
1530
  operation,
1436
1531
  operation
1437
1532
  );
1438
- if (start.servicePath && rows2.length === 0) return void 0;
1439
- if (rows2.length === 0) return void 0;
1440
- 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)) };
1534
+ if (start.servicePath && operation) {
1535
+ const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
1536
+ FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
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)
1538
+ JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
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
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);
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)) };
1543
+ }
1544
+ return void 0;
1441
1545
  }
1442
1546
  function startScope(db, start) {
1443
1547
  const repo = start.repo ? db.prepare(
1444
1548
  "SELECT id,name FROM repositories WHERE name=? OR package_name=?"
1445
1549
  ).get(start.repo, start.repo) : void 0;
1446
1550
  if (start.repo && !repo) return { repo, selectorMatched: false };
1447
- const sourceFiles = sourceFilesForStart(db, repo?.id, start);
1551
+ const sourceScope = sourceFilesForStart(db, repo?.id, start);
1552
+ const sourceFiles = sourceScope?.files;
1448
1553
  const hasSelector = Boolean(
1449
1554
  start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
1450
1555
  );
@@ -1453,6 +1558,7 @@ function startScope(db, start) {
1453
1558
  return {
1454
1559
  repo,
1455
1560
  sourceFiles,
1561
+ symbolIds: sourceScope?.symbols,
1456
1562
  selectorMatched: !hasSelector || sourceFiles !== void 0
1457
1563
  };
1458
1564
  }
@@ -1464,17 +1570,26 @@ function handlerFilesForOperation(db, operationId) {
1464
1570
  if (!op) return /* @__PURE__ */ new Set();
1465
1571
  const operation = normalizeOperation(op.operationPath ?? op.operationName);
1466
1572
  const rows2 = db.prepare(
1467
- `SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
1573
+ `SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
1468
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
1469
1576
  WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
1470
1577
  ).all(op.repoId, operation, operation, op.operationName);
1471
1578
  return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
1472
1579
  }
1580
+ function implementationEdge(db, operationId) {
1581
+ return db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND from_kind='operation' AND from_id=? ORDER BY CASE status WHEN 'resolved' THEN 0 WHEN 'ambiguous' THEN 1 ELSE 2 END,id LIMIT 1").get(operationId);
1582
+ }
1583
+ function handlerMethodNode(db, methodId) {
1584
+ const row = db.prepare(`SELECT hm.id methodId,hm.method_name methodName,hm.decorator_value decoratorValue,hm.source_line sourceLine,hc.class_name className,hc.source_file sourceFile,r.name repoName,r.id repoId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id JOIN repositories r ON r.id=hc.repo_id WHERE hm.id=?`).get(methodId);
1585
+ if (!row) return void 0;
1586
+ return { id: `handler_method:${methodId}`, kind: "handler_method", label: `${String(row.repoName)}:${String(row.className)}.${String(row.methodName)}`, ...row };
1587
+ }
1473
1588
  function implementationScope(db, operationId) {
1474
- const edge = db.prepare("SELECT * FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status='resolved' AND from_kind='operation' AND from_id=? ORDER BY id LIMIT 1").get(operationId);
1475
- if (!edge) return { files: /* @__PURE__ */ new Set() };
1476
- 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);
1477
- return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []) };
1589
+ const edge = implementationEdge(db, operationId);
1590
+ if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), 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 };
1478
1593
  }
1479
1594
  function includeCall(type, options) {
1480
1595
  if (!options.includeDb && type === "local_db_query") return false;
@@ -1572,21 +1687,52 @@ function trace(db, start, options) {
1572
1687
  const maxDepth = positiveDepth(options.depth);
1573
1688
  const edges = [];
1574
1689
  const nodes = /* @__PURE__ */ new Map();
1575
- 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 }] : [];
1691
+ if (start.servicePath && (start.operation ?? start.operationPath)) {
1692
+ const startOperation = normalizeOperation(start.operation ?? start.operationPath);
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;
1696
+ if (row?.operationId !== void 0) {
1697
+ const opId = String(row.operationId);
1698
+ const op = operationNode(db, opId);
1699
+ const impl = implementationScope(db, opId);
1700
+ if (op) nodes.set(String(op.id), op);
1701
+ if (impl.edge) {
1702
+ const implEvidence = JSON.parse(String(impl.edge.evidence_json || "{}"));
1703
+ const handlerNode = impl.edge.status === "resolved" ? handlerMethodNode(db, impl.edge.to_id) : void 0;
1704
+ if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1705
+ edges.push({ step: 1, type: "operation_implemented_by_handler", from: op?.label ? String(op.label) : `${start.servicePath}/${startOperation ?? ""}`, to: handlerNode?.label ? String(handlerNode.label) : `${impl.edge.to_kind}:${impl.edge.to_id}`, evidence: implEvidence, confidence: Number(impl.edge.confidence ?? 0), unresolvedReason: impl.edge.status === "resolved" ? void 0 : String(impl.edge.unresolved_reason ?? impl.edge.status) });
1706
+ }
1707
+ }
1708
+ }
1576
1709
  const seenScopes = /* @__PURE__ */ new Set();
1577
1710
  const seenEdges = /* @__PURE__ */ new Set();
1578
1711
  while (queue.length > 0) {
1579
1712
  const current = queue.shift();
1580
1713
  if (!current || current.depth > maxDepth) continue;
1581
- 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(",")}`;
1582
1715
  if (seenScopes.has(key)) continue;
1583
1716
  seenScopes.add(key);
1584
1717
  const calls = db.prepare(
1585
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`
1586
1719
  ).all(current.repoId, current.repoId);
1587
1720
  const filtered = calls.filter(
1588
- (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)
1589
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
+ }
1590
1736
  const graph = graphForCalls(
1591
1737
  db,
1592
1738
  filtered.map((c) => Number(c.id))
@@ -1622,23 +1768,40 @@ function trace(db, start, options) {
1622
1768
  edges.push({
1623
1769
  step: current.depth,
1624
1770
  type: String(call.call_type),
1625
- from: `${call.repoName}:${call.source_file}`,
1771
+ from: `${call.repoName}:${call.source_file}:${call.source_line}`,
1626
1772
  to,
1627
1773
  evidence,
1628
1774
  confidence: Number(effectiveRow.confidence ?? call.confidence),
1629
1775
  unresolvedReason: effective.unresolvedReason
1630
1776
  });
1631
- if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
1777
+ if (effectiveRow.to_kind === "operation") {
1632
1778
  const implementation = implementationScope(db, effectiveRow.to_id);
1779
+ if (implementation.edge) {
1780
+ const implEvidence = JSON.parse(String(implementation.edge.evidence_json || "{}"));
1781
+ const handlerNode = implementation.edge.status === "resolved" ? handlerMethodNode(db, implementation.edge.to_id) : void 0;
1782
+ const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
1783
+ if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
1784
+ edges.push({
1785
+ step: current.depth,
1786
+ type: "operation_implemented_by_handler",
1787
+ from: to,
1788
+ to: implTo,
1789
+ evidence: implEvidence,
1790
+ confidence: Number(implementation.edge.confidence ?? 0),
1791
+ unresolvedReason: implementation.edge.status === "resolved" ? void 0 : String(implementation.edge.unresolved_reason ?? implementation.edge.status)
1792
+ });
1793
+ }
1794
+ if (current.depth >= maxDepth) continue;
1633
1795
  const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
1634
- if (files.size > 0) {
1796
+ const symbolIds = implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
1797
+ if (implementation.edge?.status === "resolved" && files.size > 0) {
1635
1798
  const targetRepoId = implementation.repoId ?? db.prepare(
1636
1799
  "SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
1637
1800
  ).get(effectiveRow.to_id)?.repoId;
1638
- const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
1801
+ const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
1639
1802
  if (seenScopes.has(nextKey))
1640
1803
  edges.push({
1641
- step: current.depth + 1,
1804
+ step: current.depth,
1642
1805
  type: "cycle",
1643
1806
  from: to,
1644
1807
  to: nextKey,
@@ -1650,6 +1813,7 @@ function trace(db, start, options) {
1650
1813
  queue.push({
1651
1814
  repoId: targetRepoId,
1652
1815
  files,
1816
+ symbolIds,
1653
1817
  depth: current.depth + 1
1654
1818
  });
1655
1819
  }
@@ -1678,4 +1842,4 @@ export {
1678
1842
  linkWorkspace,
1679
1843
  trace
1680
1844
  };
1681
- //# sourceMappingURL=chunk-HQMAFSRR.js.map
1845
+ //# sourceMappingURL=chunk-GG4XJGES.js.map