@saptools/service-flow 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +11 -3
- package/dist/{chunk-LQT67AU5.js → chunk-MXYYARUP.js} +238 -95
- package/dist/chunk-MXYYARUP.js.map +1 -0
- package/dist/cli.js +220 -46
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LQT67AU5.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
685
|
+
if (ts5.isStringLiteralLike(node) || ts5.isNoSubstitutionTemplateLiteral(node))
|
|
654
686
|
return node.text;
|
|
655
|
-
if (
|
|
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 (!
|
|
696
|
+
if (!ts5.isPropertyAccessExpression(expr) || expr.name.text !== "to")
|
|
665
697
|
return void 0;
|
|
666
698
|
const inner = expr.expression;
|
|
667
|
-
if (!
|
|
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 =
|
|
704
|
+
const objectArg = ts5.isObjectLiteralExpression(first) ? first : second && ts5.isObjectLiteralExpression(second) ? second : void 0;
|
|
673
705
|
let alias;
|
|
674
706
|
let aliasExpr;
|
|
675
|
-
if (
|
|
707
|
+
if (ts5.isStringLiteralLike(first) || ts5.isNoSubstitutionTemplateLiteral(first))
|
|
676
708
|
alias = first.text;
|
|
677
|
-
else if (!
|
|
709
|
+
else if (!ts5.isObjectLiteralExpression(first))
|
|
678
710
|
aliasExpr = stringValue(first);
|
|
679
|
-
if ((
|
|
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 (!
|
|
692
|
-
const name =
|
|
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 (
|
|
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 (
|
|
718
|
-
if (
|
|
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 (
|
|
731
|
-
if (!found)
|
|
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
|
|
771
|
+
return ts5.createSourceFile(
|
|
740
772
|
abs,
|
|
741
773
|
text,
|
|
742
|
-
|
|
774
|
+
ts5.ScriptTarget.Latest,
|
|
743
775
|
true,
|
|
744
|
-
|
|
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 (!
|
|
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 &&
|
|
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 =
|
|
803
|
-
(m) => m.kind ===
|
|
834
|
+
const direct = ts5.canHaveModifiers(stmt) ? ts5.getModifiers(stmt)?.some(
|
|
835
|
+
(m) => m.kind === ts5.SyntaxKind.ExportKeyword
|
|
804
836
|
) ?? false : false;
|
|
805
|
-
if (direct &&
|
|
837
|
+
if (direct && ts5.isFunctionDeclaration(stmt) && stmt.name)
|
|
806
838
|
exports.set(stmt.name.text, stmt.name.text);
|
|
807
|
-
if (direct &&
|
|
839
|
+
if (direct && ts5.isVariableStatement(stmt)) {
|
|
808
840
|
for (const decl of stmt.declarationList.declarations)
|
|
809
|
-
if (
|
|
841
|
+
if (ts5.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
|
|
810
842
|
}
|
|
811
|
-
if (!
|
|
812
|
-
if (!
|
|
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 (
|
|
858
|
+
if (ts5.isFunctionDeclaration(stmt) && stmt.name) {
|
|
827
859
|
let fact;
|
|
828
860
|
stmt.forEachChild(function visit(node) {
|
|
829
|
-
if (!fact &&
|
|
861
|
+
if (!fact && ts5.isReturnStatement(node) && node.expression)
|
|
830
862
|
fact = findConnectInExpression(node.expression);
|
|
831
|
-
if (!fact)
|
|
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 (
|
|
867
|
+
if (ts5.isVariableStatement(stmt))
|
|
836
868
|
for (const decl of stmt.declarationList.declarations) {
|
|
837
|
-
if (!
|
|
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 (!
|
|
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 (
|
|
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 (!
|
|
948
|
+
if (!ts5.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
917
949
|
const call = unwrapCall(decl.initializer);
|
|
918
|
-
if (!call || !
|
|
950
|
+
if (!call || !ts5.isPropertyAccessExpression(call.expression)) return;
|
|
919
951
|
const target = call.expression;
|
|
920
|
-
if (target.expression.kind !==
|
|
952
|
+
if (target.expression.kind !== ts5.SyntaxKind.ThisKeyword) return;
|
|
921
953
|
for (const el of decl.name.elements) {
|
|
922
|
-
if (!
|
|
923
|
-
const propertyName = el.propertyName &&
|
|
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 (
|
|
950
|
-
|
|
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 (!
|
|
994
|
+
if (!ts5.isClassDeclaration(stmt) || !stmt.name) continue;
|
|
963
995
|
for (const member of stmt.members) {
|
|
964
996
|
let visit2 = function(node) {
|
|
965
|
-
if (
|
|
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 (
|
|
1001
|
+
if (ts5.isReturnStatement(node) && node.expression && ts5.isObjectLiteralExpression(node.expression)) {
|
|
970
1002
|
for (const prop of node.expression.properties) {
|
|
971
|
-
if (
|
|
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 (
|
|
984
|
-
const propertyName =
|
|
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
|
-
|
|
1030
|
+
ts5.forEachChild(node, visit2);
|
|
999
1031
|
};
|
|
1000
1032
|
var visit = visit2;
|
|
1001
|
-
if (!
|
|
1002
|
-
if (!
|
|
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 (!
|
|
1038
|
+
if (!ts5.isArrowFunction(initializer) && !ts5.isFunctionExpression(initializer))
|
|
1007
1039
|
continue;
|
|
1008
1040
|
const bindings = /* @__PURE__ */ new Map();
|
|
1009
1041
|
visit2(initializer);
|
|
@@ -1042,7 +1074,7 @@ function substituteVariables(template, vars) {
|
|
|
1042
1074
|
// src/linker/service-resolver.ts
|
|
1043
1075
|
function rows(db, operationPath, workspaceId) {
|
|
1044
1076
|
return db.prepare(
|
|
1045
|
-
`SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
|
|
1077
|
+
`SELECT o.id operationId,r.name repoName,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine,0 score,'' reasons
|
|
1046
1078
|
FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id
|
|
1047
1079
|
WHERE (? IS NULL OR r.workspace_id=?) AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,s.service_path,o.operation_name`
|
|
1048
1080
|
).all(
|
|
@@ -1066,7 +1098,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1066
1098
|
candidates: [],
|
|
1067
1099
|
reasons: ["missing_operation_path"]
|
|
1068
1100
|
};
|
|
1069
|
-
const candidates = rows(db, signals.operationPath, workspaceId).map((c) => ({
|
|
1101
|
+
const candidates = rows(db, signals.operationPath, workspaceId).filter((c) => matchesLocalRepo(db, c.operationId, signals.repoId)).map((c) => ({
|
|
1070
1102
|
...c,
|
|
1071
1103
|
score: 0.2,
|
|
1072
1104
|
reasons: ["operation_path_match"]
|
|
@@ -1078,7 +1110,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1078
1110
|
reasons: ["no_operation_candidates"]
|
|
1079
1111
|
};
|
|
1080
1112
|
const hasStrongSignal = Boolean(
|
|
1081
|
-
signals.servicePath || signals.alias || signals.destination || signals.hasExplicitOverride
|
|
1113
|
+
signals.servicePath || signals.serviceName || signals.alias || signals.destination || signals.hasExplicitOverride
|
|
1082
1114
|
);
|
|
1083
1115
|
for (const c of candidates) {
|
|
1084
1116
|
if (signals.servicePath && c.servicePath === signals.servicePath) {
|
|
@@ -1089,9 +1121,25 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1089
1121
|
c.score -= 0.1;
|
|
1090
1122
|
c.reasons.push("service_path_mismatch");
|
|
1091
1123
|
}
|
|
1124
|
+
if (signals.serviceName) {
|
|
1125
|
+
const simple = signals.serviceName.split(".").at(-1) ?? signals.serviceName;
|
|
1126
|
+
if (c.qualifiedName === signals.serviceName) {
|
|
1127
|
+
c.score += 0.8;
|
|
1128
|
+
c.reasons.push("exact_local_qualified_service_name");
|
|
1129
|
+
} else if (c.serviceName === signals.serviceName || c.serviceName === simple) {
|
|
1130
|
+
c.score += 0.75;
|
|
1131
|
+
c.reasons.push("exact_local_simple_service_name");
|
|
1132
|
+
} else if (c.servicePath === signals.serviceName || c.servicePath === `/${signals.serviceName}` || c.servicePath === `/${simple}`) {
|
|
1133
|
+
c.score += 0.7;
|
|
1134
|
+
c.reasons.push("exact_local_service_path");
|
|
1135
|
+
} else if (c.servicePath.endsWith(`/${simple}`)) {
|
|
1136
|
+
c.score += candidates.filter((candidate) => candidate.servicePath.endsWith(`/${simple}`)).length === 1 ? 0.65 : 0.2;
|
|
1137
|
+
c.reasons.push("suffix_local_service_path");
|
|
1138
|
+
} else c.reasons.push("local_service_name_mismatch");
|
|
1139
|
+
}
|
|
1092
1140
|
if (signals.hasExplicitOverride) {
|
|
1093
1141
|
c.score += 0.2;
|
|
1094
|
-
c.reasons.push("explicit_dynamic_override");
|
|
1142
|
+
c.reasons.push(signals.repoId !== void 0 ? "explicit_local_service_call" : "explicit_dynamic_override");
|
|
1095
1143
|
}
|
|
1096
1144
|
}
|
|
1097
1145
|
for (const c of candidates) c.score = Math.max(0, Math.min(1, c.score));
|
|
@@ -1112,7 +1160,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1112
1160
|
candidates,
|
|
1113
1161
|
reasons: ["operation_path_only_has_no_strong_target_signal"]
|
|
1114
1162
|
};
|
|
1115
|
-
if (best && best.score >= 0.9 && best.servicePath === signals.servicePath && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
|
|
1163
|
+
if (best && best.score >= 0.9 && (best.servicePath === signals.servicePath || Boolean(signals.serviceName && !best.reasons.includes("local_service_name_mismatch"))) && (best.operationPath === signals.operationPath || best.operationName === signals.operationPath.replace(/^\//, "")) && (!second || best.score - second.score >= 0.25))
|
|
1116
1164
|
return {
|
|
1117
1165
|
status: "resolved",
|
|
1118
1166
|
target: best,
|
|
@@ -1125,6 +1173,11 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
1125
1173
|
reasons: ["candidate_score_below_resolution_threshold"]
|
|
1126
1174
|
};
|
|
1127
1175
|
}
|
|
1176
|
+
function matchesLocalRepo(db, operationId, repoId) {
|
|
1177
|
+
if (repoId === void 0) return true;
|
|
1178
|
+
const row = db.prepare("SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?").get(operationId);
|
|
1179
|
+
return row?.repoId === repoId;
|
|
1180
|
+
}
|
|
1128
1181
|
|
|
1129
1182
|
// src/linker/helper-package-linker.ts
|
|
1130
1183
|
function normalizeName(value) {
|
|
@@ -1209,10 +1262,11 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1209
1262
|
const servicePath = applyVariables(call.servicePathExpr ?? call.requireServicePath, vars);
|
|
1210
1263
|
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1211
1264
|
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1212
|
-
const
|
|
1265
|
+
const isOperationCall = callType.startsWith("remote") || callType === "local_service_call";
|
|
1266
|
+
const resolution = isOperationCall ? resolveOperation(db, { servicePath, operationPath: op, serviceName: call.local_service_name, repoId: callType === "local_service_call" ? Number(call.repo_id) : void 0, alias: applyVariables(call.aliasExpr ?? call.alias, vars), destination: destination ? applyVariables(destination, vars) : void 0, isDynamic, hasExplicitOverride: Object.keys(vars).length > 0 || callType === "local_service_call" }, workspaceId) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1213
1267
|
const evidence = callEvidence(call, resolution, servicePath, op, destination ? applyVariables(destination, vars) : void 0);
|
|
1214
1268
|
if (resolution.target) {
|
|
1215
|
-
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
|
|
1269
|
+
db.prepare("INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,generation) VALUES(?,?,?,?,?,?,?,?,?,?,?)").run(workspaceId, callType === "local_service_call" ? "LOCAL_CALL_RESOLVES_TO_OPERATION" : "REMOTE_CALL_RESOLVES_TO_OPERATION", "resolved", "call", String(call.id), "operation", String(resolution.target.operationId), resolution.target.score, JSON.stringify(evidence), isDynamic ? 1 : 0, generation);
|
|
1216
1270
|
return { status: "resolved" };
|
|
1217
1271
|
}
|
|
1218
1272
|
const edgeType = callType === "local_db_query" ? "HANDLER_RUNS_DB_QUERY" : callType === "external_http" ? "HANDLER_CALLS_EXTERNAL_HTTP" : callType === "async_emit" ? "HANDLER_EMITS_EVENT" : callType === "async_subscribe" ? "EVENT_CONSUMED_BY_HANDLER" : resolution.status === "dynamic" ? "DYNAMIC_EDGE_CANDIDATE" : "UNRESOLVED_EDGE";
|
|
@@ -1222,7 +1276,7 @@ function insertCallEdge(db, workspaceId, call, vars, generation) {
|
|
|
1222
1276
|
return { status };
|
|
1223
1277
|
}
|
|
1224
1278
|
function callEvidence(call, resolution, servicePath, op, destination) {
|
|
1225
|
-
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1279
|
+
return { sourceFile: call.source_file, sourceLine: call.source_line, file: call.source_file, line: call.source_line, repo: call.repoName, serviceAlias: call.alias, serviceAliasExpr: call.aliasExpr, destination, servicePath, operationPath: op, localServiceName: call.local_service_name, localServiceLookup: call.local_service_lookup, aliasChain: call.alias_chain_json ? JSON.parse(String(call.alias_chain_json)) : void 0, transport: call.call_type === "local_service_call" ? "local" : void 0, targetRepo: resolution.target?.repoName, targetOperation: resolution.target?.operationName, helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0, candidates: resolution.candidates, candidateCount: resolution.candidates.length, resolutionStatus: resolution.status, resolutionReasons: resolution.reasons, analysisCompleteness: call.unresolved_reason ? "partial" : "complete", parserWarning: call.unresolved_reason ? { code: "parser_warning", message: call.unresolved_reason } : void 0 };
|
|
1226
1280
|
}
|
|
1227
1281
|
function linkImplementations(db, workspaceId, generation) {
|
|
1228
1282
|
const operations = db.prepare(`SELECT o.id operationId,o.operation_path operationPath,o.operation_name operationName,s.service_path servicePath,s.repo_id modelRepoId,r.name modelRepo,r.package_name modelPackage,r.kind modelKind FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE r.workspace_id=?`).all(workspaceId);
|
|
@@ -1259,12 +1313,42 @@ function linkImplementations(db, workspaceId, generation) {
|
|
|
1259
1313
|
}
|
|
1260
1314
|
function rankedImplementationCandidates(db, workspaceId, operation) {
|
|
1261
1315
|
const rows2 = implementationCandidates(db, workspaceId, operation);
|
|
1262
|
-
return rows2.map((row) => scoreImplementationCandidate(row, operation)).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
|
|
1316
|
+
return deduplicateCandidates(rows2.map((row) => scoreImplementationCandidate(row, operation))).sort((a, b) => b.score - a.score || String(a.className).localeCompare(String(b.className)) || a.methodId - b.methodId);
|
|
1317
|
+
}
|
|
1318
|
+
function deduplicateCandidates(rows2) {
|
|
1319
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1320
|
+
for (const row of rows2) {
|
|
1321
|
+
const key = [row.methodId, row.classId, row.handlerRepoId].join(":");
|
|
1322
|
+
const registration = { file: row.registrationFile, line: row.registrationLine, kind: row.registrationKind, importSource: row.importSource };
|
|
1323
|
+
const existing = merged.get(key);
|
|
1324
|
+
if (!existing) {
|
|
1325
|
+
merged.set(key, { ...row, registrations: [registration] });
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
existing.registrations = uniqueRegistrations([...existing.registrations ?? [], registration]);
|
|
1329
|
+
existing.score = Math.max(existing.score, row.score);
|
|
1330
|
+
existing.accepted = existing.accepted || row.accepted;
|
|
1331
|
+
existing.acceptedReasons = [.../* @__PURE__ */ new Set([...existing.acceptedReasons, ...row.acceptedReasons])];
|
|
1332
|
+
existing.rejectedReasons = [.../* @__PURE__ */ new Set([...existing.rejectedReasons, ...row.rejectedReasons])];
|
|
1333
|
+
}
|
|
1334
|
+
return [...merged.values()];
|
|
1335
|
+
}
|
|
1336
|
+
function uniqueRegistrations(rows2) {
|
|
1337
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1338
|
+
return rows2.filter((row) => {
|
|
1339
|
+
const key = JSON.stringify(row);
|
|
1340
|
+
if (seen.has(key)) return false;
|
|
1341
|
+
seen.add(key);
|
|
1342
|
+
return true;
|
|
1343
|
+
});
|
|
1263
1344
|
}
|
|
1264
1345
|
function implementationCandidates(db, workspaceId, operation) {
|
|
1265
1346
|
const modelRepoGraphId = graphId(operation.modelRepoId);
|
|
1266
1347
|
return db.prepare(`SELECT DISTINCT
|
|
1267
1348
|
hm.id methodId,
|
|
1349
|
+
hm.method_name methodName,
|
|
1350
|
+
hm.decorator_value decoratorValue,
|
|
1351
|
+
hm.decorator_raw_expression decoratorRawExpression,
|
|
1268
1352
|
hc.id classId,
|
|
1269
1353
|
hc.class_name className,
|
|
1270
1354
|
hc.source_file sourceFile,
|
|
@@ -1291,7 +1375,7 @@ function implementationCandidates(db, workspaceId, operation) {
|
|
|
1291
1375
|
CASE WHEN appRepo.id=handlerRepo.id THEN 1 ELSE 0 END sameRepoRegistration,
|
|
1292
1376
|
CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id AND localService.service_path=?) THEN 1 ELSE 0 END localServicePathMatch,
|
|
1293
1377
|
CASE WHEN EXISTS (SELECT 1 FROM cds_services localService WHERE localService.repo_id=appRepo.id) THEN 1 ELSE 0 END applicationHasLocalServices,
|
|
1294
|
-
CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
|
|
1378
|
+
CASE WHEN EXISTS (SELECT 1 FROM handler_registrations localReg JOIN handler_classes localClass ON (localClass.id=localReg.handler_class_id OR localClass.class_name=localReg.class_name) JOIN handler_methods localMethod ON localMethod.handler_class_id=localClass.id WHERE localReg.repo_id=appRepo.id AND (localMethod.decorator_value=? OR localMethod.decorator_value=? OR localMethod.method_name=? OR localMethod.decorator_raw_expression LIKE ?)) THEN 1 ELSE 0 END applicationHasLocalRegistrationForOperation,
|
|
1295
1379
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END appDependsOnModel,
|
|
1296
1380
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(appRepo.id AS TEXT) AND dep.to_id=CAST(handlerRepo.id AS TEXT)) THEN 1 ELSE 0 END appDependsOnHandler,
|
|
1297
1381
|
CASE WHEN EXISTS (SELECT 1 FROM graph_edges dep WHERE dep.edge_type='REPO_IMPORTS_HELPER_PACKAGE' AND dep.status='resolved' AND dep.from_kind='repo' AND dep.from_id=CAST(handlerRepo.id AS TEXT) AND dep.to_id=?) THEN 1 ELSE 0 END handlerDependsOnModel
|
|
@@ -1301,7 +1385,7 @@ function implementationCandidates(db, workspaceId, operation) {
|
|
|
1301
1385
|
JOIN handler_registrations hr ON (hr.handler_class_id=hc.id OR (hr.class_name=hc.class_name AND (hr.repo_id=hc.repo_id OR hr.import_source IS NOT NULL)))
|
|
1302
1386
|
JOIN repositories appRepo ON appRepo.id=hr.repo_id
|
|
1303
1387
|
WHERE appRepo.workspace_id=?
|
|
1304
|
-
AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=?)`).all(
|
|
1388
|
+
AND (hm.decorator_value=? OR hm.decorator_value=? OR hm.method_name=? OR hm.decorator_raw_expression LIKE ?)`).all(
|
|
1305
1389
|
operation.modelRepoId,
|
|
1306
1390
|
operation.modelRepo,
|
|
1307
1391
|
operation.modelPackage,
|
|
@@ -1315,12 +1399,14 @@ function implementationCandidates(db, workspaceId, operation) {
|
|
|
1315
1399
|
normalizedOperation(String(operation.operationPath ?? "")),
|
|
1316
1400
|
operation.operationName,
|
|
1317
1401
|
operation.operationName,
|
|
1402
|
+
`%${upperFirst(normalizedOperation(String(operation.operationPath ?? operation.operationName ?? "")))}%`,
|
|
1318
1403
|
modelRepoGraphId,
|
|
1319
1404
|
modelRepoGraphId,
|
|
1320
1405
|
workspaceId,
|
|
1321
1406
|
normalizedOperation(String(operation.operationPath ?? "")),
|
|
1322
1407
|
operation.operationName,
|
|
1323
|
-
operation.operationName
|
|
1408
|
+
operation.operationName,
|
|
1409
|
+
`%${upperFirst(normalizedOperation(String(operation.operationPath ?? operation.operationName ?? "")))}%`
|
|
1324
1410
|
);
|
|
1325
1411
|
}
|
|
1326
1412
|
function scoreImplementationCandidate(row, operation) {
|
|
@@ -1338,7 +1424,10 @@ function scoreImplementationCandidate(row, operation) {
|
|
|
1338
1424
|
const importSource = typeof row.importSource === "string" && row.importSource.length > 0;
|
|
1339
1425
|
const sameRepoRegistration = flag(row.sameRepoRegistration);
|
|
1340
1426
|
const modelOriented = row.modelKind === "cap-db-model" || !applicationHasLocalRegistrationForOperation;
|
|
1341
|
-
const
|
|
1427
|
+
const methodSignal = implementationMethodSignal(row, operation);
|
|
1428
|
+
const methodMatches = methodSignal.matches;
|
|
1429
|
+
acceptedReasons.push(...methodSignal.acceptedReasons);
|
|
1430
|
+
rejectedReasons.push(...methodSignal.rejectedReasons);
|
|
1342
1431
|
const registeredAndLinked = sameRepoRegistration && importSource;
|
|
1343
1432
|
const helperOwned = modelOriented && methodMatches && registeredAndLinked && sameRepoRegistration && !applicationHasLocalServices && !modelIsApplicationRepo && !modelIsHandlerRepo && !localServicePathMatch && !appDependsOnModel && !appDependsOnHandler && !handlerDependsOnModel;
|
|
1344
1433
|
if (modelIsApplicationRepo) {
|
|
@@ -1379,7 +1468,7 @@ function scoreImplementationCandidate(row, operation) {
|
|
|
1379
1468
|
const hasCrossPackage = appDependsOnModel && (modelIsHandlerRepo || appDependsOnHandler || !importSource);
|
|
1380
1469
|
const contradicted = applicationHasLocalServices && !localServicePathMatch && !appDependsOnModel && !hasOwnership;
|
|
1381
1470
|
if (!hasOwnership && !localServicePathMatch && !hasCrossPackage && !helperOwned) rejectedReasons.push("missing direct ownership, exact local service path, or validated cross-package dependency evidence");
|
|
1382
|
-
const accepted = !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel || helperOwned);
|
|
1471
|
+
const accepted = methodMatches && !methodSignal.contradicted && !contradicted && (hasOwnership || localServicePathMatch || hasCrossPackage || handlerDependsOnModel || helperOwned);
|
|
1383
1472
|
if (!accepted && rejectedReasons.length === 0) rejectedReasons.push("candidate did not meet implementation ownership policy");
|
|
1384
1473
|
return { ...row, methodId: Number(row.methodId), score, accepted, acceptedReasons, rejectedReasons };
|
|
1385
1474
|
}
|
|
@@ -1396,6 +1485,7 @@ function candidateEvidence(candidate, rank) {
|
|
|
1396
1485
|
sourceFile: candidate.sourceFile,
|
|
1397
1486
|
sourceLine: candidate.sourceLine,
|
|
1398
1487
|
registration: { file: candidate.registrationFile, line: candidate.registrationLine, kind: candidate.registrationKind, importSource: candidate.importSource },
|
|
1488
|
+
registrations: candidate.registrations ?? [],
|
|
1399
1489
|
applicationPackage: { id: candidate.applicationRepoId, name: candidate.applicationRepo, packageName: candidate.applicationPackage },
|
|
1400
1490
|
handlerPackage: { id: candidate.handlerRepoId, name: candidate.handlerRepo, packageName: candidate.handlerPackage },
|
|
1401
1491
|
modelPackage: { id: candidate.modelRepoId, name: candidate.modelRepo, packageName: candidate.modelPackage },
|
|
@@ -1414,6 +1504,29 @@ function candidateEvidence(candidate, rank) {
|
|
|
1414
1504
|
}
|
|
1415
1505
|
};
|
|
1416
1506
|
}
|
|
1507
|
+
function implementationMethodSignal(row, operation) {
|
|
1508
|
+
const op = normalizedOperation(String(operation.operationPath ?? operation.operationName ?? ""));
|
|
1509
|
+
const decorator = normalizeDecoratorOperation(typeof row.decoratorValue === "string" ? row.decoratorValue : void 0, typeof row.decoratorRawExpression === "string" ? row.decoratorRawExpression : void 0);
|
|
1510
|
+
if (decorator && decorator === op) return { matches: true, contradicted: false, acceptedReasons: ["decorator targets operation"], rejectedReasons: [] };
|
|
1511
|
+
if (decorator && decorator !== op) return { matches: false, contradicted: true, acceptedReasons: [], rejectedReasons: ["method_name_matches_but_decorator_targets_different_operation"] };
|
|
1512
|
+
if (String(row.methodName ?? "") === op) return { matches: true, contradicted: false, acceptedReasons: ["method name fallback matched operation"], rejectedReasons: [] };
|
|
1513
|
+
return { matches: false, contradicted: false, acceptedReasons: [], rejectedReasons: ["method name does not match operation"] };
|
|
1514
|
+
}
|
|
1515
|
+
function normalizeDecoratorOperation(value, raw) {
|
|
1516
|
+
const candidate = value ?? raw?.split(".").filter(Boolean).at(-2);
|
|
1517
|
+
if (!candidate) return void 0;
|
|
1518
|
+
const cleaned = candidate.replace(/^['"`]|['"`]$/g, "");
|
|
1519
|
+
for (const prefix of ["Func", "Action"]) {
|
|
1520
|
+
if (cleaned.startsWith(prefix) && cleaned.length > prefix.length) return lowerFirst(cleaned.slice(prefix.length));
|
|
1521
|
+
}
|
|
1522
|
+
return normalizedOperation(cleaned);
|
|
1523
|
+
}
|
|
1524
|
+
function upperFirst(value) {
|
|
1525
|
+
return value ? `${value[0]?.toUpperCase() ?? ""}${value.slice(1)}` : value;
|
|
1526
|
+
}
|
|
1527
|
+
function lowerFirst(value) {
|
|
1528
|
+
return value ? `${value[0]?.toLowerCase() ?? ""}${value.slice(1)}` : value;
|
|
1529
|
+
}
|
|
1417
1530
|
function flag(value) {
|
|
1418
1531
|
return Boolean(Number(value ?? 0));
|
|
1419
1532
|
}
|
|
@@ -1437,8 +1550,8 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
1437
1550
|
const operation = normalizeOperation(start.operation ?? start.operationPath);
|
|
1438
1551
|
if (!handler && !operation) return void 0;
|
|
1439
1552
|
const rows2 = db.prepare(
|
|
1440
|
-
`SELECT DISTINCT hc.source_file sourceFile
|
|
1441
|
-
FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
1553
|
+
`SELECT DISTINCT hc.source_file sourceFile,s.id symbolId
|
|
1554
|
+
FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name
|
|
1442
1555
|
WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
|
|
1443
1556
|
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
|
|
1444
1557
|
AND (? IS NULL OR EXISTS (SELECT 1 FROM cds_services s JOIN cds_operations o ON o.service_id=s.id WHERE s.repo_id=hc.repo_id AND s.service_path=? AND (? IS NULL OR o.operation_path=? OR o.operation_name=? OR hm.decorator_value=? OR hm.method_name=?)))`
|
|
@@ -1459,15 +1572,16 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
1459
1572
|
operation,
|
|
1460
1573
|
operation
|
|
1461
1574
|
);
|
|
1462
|
-
if (rows2.length > 0) return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
1575
|
+
if (rows2.length > 0) return { files: new Set(rows2.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(rows2.map((row) => Number(row.symbolId)).filter(Boolean)) };
|
|
1463
1576
|
if (start.servicePath && operation) {
|
|
1464
|
-
const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile
|
|
1577
|
+
const implRows = db.prepare(`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId
|
|
1465
1578
|
FROM cds_services s JOIN cds_operations o ON o.service_id=s.id
|
|
1466
1579
|
JOIN graph_edges e ON e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='resolved' AND e.from_kind='operation' AND e.from_id=CAST(o.id AS TEXT)
|
|
1467
1580
|
JOIN handler_methods hm ON hm.id=CAST(e.to_id AS INTEGER)
|
|
1468
1581
|
JOIN handler_classes hc ON hc.id=hm.handler_class_id
|
|
1582
|
+
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
|
|
1469
1583
|
WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?)`).all(repoId, repoId, start.servicePath, operation, operation);
|
|
1470
|
-
if (implRows.length > 0) return new Set(implRows.map((row) => row.sourceFile).filter(Boolean));
|
|
1584
|
+
if (implRows.length > 0) return { files: new Set(implRows.map((row) => row.sourceFile).filter(Boolean)), symbols: new Set(implRows.map((row) => Number(row.symbolId)).filter(Boolean)) };
|
|
1471
1585
|
}
|
|
1472
1586
|
return void 0;
|
|
1473
1587
|
}
|
|
@@ -1476,7 +1590,8 @@ function startScope(db, start) {
|
|
|
1476
1590
|
"SELECT id,name FROM repositories WHERE name=? OR package_name=?"
|
|
1477
1591
|
).get(start.repo, start.repo) : void 0;
|
|
1478
1592
|
if (start.repo && !repo) return { repo, selectorMatched: false };
|
|
1479
|
-
const
|
|
1593
|
+
const sourceScope = sourceFilesForStart(db, repo?.id, start);
|
|
1594
|
+
const sourceFiles = sourceScope?.files;
|
|
1480
1595
|
const hasSelector = Boolean(
|
|
1481
1596
|
start.handler ?? start.operation ?? start.operationPath ?? start.servicePath
|
|
1482
1597
|
);
|
|
@@ -1485,6 +1600,7 @@ function startScope(db, start) {
|
|
|
1485
1600
|
return {
|
|
1486
1601
|
repo,
|
|
1487
1602
|
sourceFiles,
|
|
1603
|
+
symbolIds: sourceScope?.symbols,
|
|
1488
1604
|
selectorMatched: !hasSelector || sourceFiles !== void 0
|
|
1489
1605
|
};
|
|
1490
1606
|
}
|
|
@@ -1496,8 +1612,9 @@ function handlerFilesForOperation(db, operationId) {
|
|
|
1496
1612
|
if (!op) return /* @__PURE__ */ new Set();
|
|
1497
1613
|
const operation = normalizeOperation(op.operationPath ?? op.operationName);
|
|
1498
1614
|
const rows2 = db.prepare(
|
|
1499
|
-
`SELECT DISTINCT hc.source_file sourceFile FROM handler_classes hc
|
|
1615
|
+
`SELECT DISTINCT hc.source_file sourceFile,sym.id symbolId FROM handler_classes hc
|
|
1500
1616
|
JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
1617
|
+
LEFT JOIN symbols sym ON sym.repo_id=hc.repo_id AND sym.source_file=hc.source_file AND sym.name=hm.method_name
|
|
1501
1618
|
WHERE hc.repo_id=? AND (hm.decorator_value=? OR hm.method_name=? OR hm.decorator_value=?)`
|
|
1502
1619
|
).all(op.repoId, operation, operation, op.operationName);
|
|
1503
1620
|
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
@@ -1513,8 +1630,8 @@ function handlerMethodNode(db, methodId) {
|
|
|
1513
1630
|
function implementationScope(db, operationId) {
|
|
1514
1631
|
const edge = implementationEdge(db, operationId);
|
|
1515
1632
|
if (!edge || edge.status !== "resolved") return { files: /* @__PURE__ */ new Set(), edge };
|
|
1516
|
-
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id WHERE hm.id=?").get(edge.to_id);
|
|
1517
|
-
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), edge };
|
|
1633
|
+
const row = db.prepare("SELECT hc.repo_id repoId,hc.source_file sourceFile,s.id symbolId FROM handler_methods hm JOIN handler_classes hc ON hc.id=hm.handler_class_id LEFT JOIN symbols s ON s.repo_id=hc.repo_id AND s.source_file=hc.source_file AND s.name=hm.method_name WHERE hm.id=?").get(edge.to_id);
|
|
1634
|
+
return { repoId: row?.repoId, files: new Set(row?.sourceFile ? [row.sourceFile] : []), symbolId: row?.symbolId, edge };
|
|
1518
1635
|
}
|
|
1519
1636
|
function includeCall(type, options) {
|
|
1520
1637
|
if (!options.includeDb && type === "local_db_query") return false;
|
|
@@ -1559,6 +1676,12 @@ function evidenceWithRuntimeVariables(evidence, vars) {
|
|
|
1559
1676
|
if (missing.length > 0) next.missingRuntimeVariables = [...new Set(missing)];
|
|
1560
1677
|
return next;
|
|
1561
1678
|
}
|
|
1679
|
+
function symbolNode(db, symbolId) {
|
|
1680
|
+
const row = db.prepare(`SELECT s.id symbolId,s.name symbolName,s.qualified_name qualifiedName,s.source_file sourceFile,s.start_line startLine,s.end_line endLine,r.name repoName,r.id repoId FROM symbols s JOIN repositories r ON r.id=s.repo_id WHERE s.id=?`).get(symbolId);
|
|
1681
|
+
if (!row) return void 0;
|
|
1682
|
+
const fileName = String(row.sourceFile ?? "").split("/").at(-1) ?? String(row.sourceFile ?? "");
|
|
1683
|
+
return { id: `symbol:${symbolId}`, kind: "symbol", label: `${fileName}:${String(row.qualifiedName ?? row.symbolName)}`, ...row };
|
|
1684
|
+
}
|
|
1562
1685
|
function operationNode(db, operationId) {
|
|
1563
1686
|
const row = db.prepare(`SELECT o.id operationId,o.operation_name operationName,o.operation_type operationType,o.operation_path operationPath,o.source_file sourceFile,o.source_line sourceLine,s.id serviceId,s.service_name serviceName,s.qualified_name qualifiedName,s.service_path servicePath,r.id repoId,r.name repoName FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE o.id=?`).get(operationId);
|
|
1564
1687
|
if (!row) return void 0;
|
|
@@ -1612,10 +1735,12 @@ function trace(db, start, options) {
|
|
|
1612
1735
|
const maxDepth = positiveDepth(options.depth);
|
|
1613
1736
|
const edges = [];
|
|
1614
1737
|
const nodes = /* @__PURE__ */ new Map();
|
|
1615
|
-
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, depth: 1 }] : [];
|
|
1738
|
+
const queue = scope.selectorMatched ? [{ repoId: scope.repo?.id, files: scope.sourceFiles, symbolIds: scope.symbolIds, depth: 1 }] : [];
|
|
1616
1739
|
if (start.servicePath && (start.operation ?? start.operationPath)) {
|
|
1617
1740
|
const startOperation = normalizeOperation(start.operation ?? start.operationPath);
|
|
1618
|
-
const
|
|
1741
|
+
const startRows = db.prepare(`SELECT o.id operationId,r.name repoName,s.service_path servicePath,o.operation_path operationPath,o.operation_name operationName,o.source_file sourceFile,o.source_line sourceLine FROM cds_operations o JOIN cds_services s ON s.id=o.service_id JOIN repositories r ON r.id=s.repo_id WHERE (? IS NULL OR s.repo_id=?) AND s.service_path=? AND (o.operation_path=? OR o.operation_name=?) ORDER BY r.name,o.id LIMIT 2`).all(scope.repo?.id, scope.repo?.id, start.servicePath, startOperation, startOperation);
|
|
1742
|
+
if (!scope.repo && startRows.length > 1) diagnostics.unshift({ severity: "warning", code: "trace_start_ambiguous", message: "Service/path trace start matched multiple repositories; add --repo to disambiguate", candidates: startRows });
|
|
1743
|
+
const row = startRows.length === 1 ? startRows[0] : void 0;
|
|
1619
1744
|
if (row?.operationId !== void 0) {
|
|
1620
1745
|
const opId = String(row.operationId);
|
|
1621
1746
|
const op = operationNode(db, opId);
|
|
@@ -1634,15 +1759,31 @@ function trace(db, start, options) {
|
|
|
1634
1759
|
while (queue.length > 0) {
|
|
1635
1760
|
const current = queue.shift();
|
|
1636
1761
|
if (!current || current.depth > maxDepth) continue;
|
|
1637
|
-
const key = `${current.repoId ?? "*"}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
|
|
1762
|
+
const key = `${current.repoId ?? "*"}:${[...current.symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...current.files ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}`;
|
|
1638
1763
|
if (seenScopes.has(key)) continue;
|
|
1639
1764
|
seenScopes.add(key);
|
|
1640
1765
|
const calls = db.prepare(
|
|
1641
1766
|
`SELECT c.*,r.name repoName FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE (? IS NULL OR c.repo_id=?) ORDER BY c.source_file,c.source_line`
|
|
1642
1767
|
).all(current.repoId, current.repoId);
|
|
1643
1768
|
const filtered = calls.filter(
|
|
1644
|
-
(c) => (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
|
|
1769
|
+
(c) => (!current.symbolIds || current.symbolIds.has(Number(c.source_symbol_id))) && (!current.files || current.files.has(String(c.source_file))) && includeCall(String(c.call_type), options)
|
|
1645
1770
|
);
|
|
1771
|
+
if (current.symbolIds && current.symbolIds.size > 0 && current.depth < maxDepth) {
|
|
1772
|
+
const symbolRows = db.prepare(`SELECT sc.*,s.repo_id calleeRepoId,s.source_file calleeFile FROM symbol_calls sc LEFT JOIN symbols s ON s.id=sc.callee_symbol_id WHERE sc.caller_symbol_id IN (${[...current.symbolIds].map(() => "?").join(",")}) ORDER BY sc.source_file,sc.source_line`).all(...current.symbolIds);
|
|
1773
|
+
for (const symbolCall of symbolRows) {
|
|
1774
|
+
if (!symbolCall.callee_symbol_id) continue;
|
|
1775
|
+
const nextSymbols = /* @__PURE__ */ new Set([Number(symbolCall.callee_symbol_id)]);
|
|
1776
|
+
const nextFiles = /* @__PURE__ */ new Set([String(symbolCall.calleeFile)]);
|
|
1777
|
+
const nextRepoId = Number(symbolCall.calleeRepoId);
|
|
1778
|
+
const nextKey = `${nextRepoId}:${[...nextSymbols].join(",")}:${[...nextFiles].join(",")}`;
|
|
1779
|
+
const calleeNode = symbolNode(db, Number(symbolCall.callee_symbol_id));
|
|
1780
|
+
if (calleeNode) nodes.set(String(calleeNode.id), calleeNode);
|
|
1781
|
+
const evidence = { ...JSON.parse(String(symbolCall.evidence_json || "{}")), sourceFile: symbolCall.source_file, sourceLine: symbolCall.source_line, calleeSymbolId: symbolCall.callee_symbol_id, calleeSymbolName: calleeNode?.symbolName, calleeSymbolFile: calleeNode?.sourceFile, resolutionStatus: symbolCall.status };
|
|
1782
|
+
edges.push({ step: current.depth, type: "local_symbol_call", from: String(symbolCall.callee_expression), to: calleeNode?.label ? String(calleeNode.label) : `symbol:${String(symbolCall.callee_symbol_id)}`, evidence, confidence: Number(symbolCall.confidence ?? 0.8), unresolvedReason: String(symbolCall.status) === "resolved" ? void 0 : symbolCall.unresolved_reason ? String(symbolCall.unresolved_reason) : void 0 });
|
|
1783
|
+
if (seenScopes.has(nextKey)) edges.push({ step: current.depth, type: "cycle", from: String(symbolCall.callee_expression), to: nextKey, evidence: { cycle: true, symbolCallId: symbolCall.id }, confidence: 1, unresolvedReason: "Cycle detected; downstream symbol already visited" });
|
|
1784
|
+
else queue.push({ repoId: nextRepoId, files: nextFiles, symbolIds: nextSymbols, depth: current.depth + 1 });
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1646
1787
|
const graph = graphForCalls(
|
|
1647
1788
|
db,
|
|
1648
1789
|
filtered.map((c) => Number(c.id))
|
|
@@ -1678,7 +1819,7 @@ function trace(db, start, options) {
|
|
|
1678
1819
|
edges.push({
|
|
1679
1820
|
step: current.depth,
|
|
1680
1821
|
type: String(call.call_type),
|
|
1681
|
-
from: `${call.repoName}:${call.source_file}`,
|
|
1822
|
+
from: `${call.repoName}:${call.source_file}:${call.source_line}`,
|
|
1682
1823
|
to,
|
|
1683
1824
|
evidence,
|
|
1684
1825
|
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
@@ -1692,7 +1833,7 @@ function trace(db, start, options) {
|
|
|
1692
1833
|
const implTo = handlerNode?.label ? String(handlerNode.label) : `${implementation.edge.to_kind}:${implementation.edge.to_id}`;
|
|
1693
1834
|
if (handlerNode) nodes.set(String(handlerNode.id), handlerNode);
|
|
1694
1835
|
edges.push({
|
|
1695
|
-
step: current.depth
|
|
1836
|
+
step: current.depth,
|
|
1696
1837
|
type: "operation_implemented_by_handler",
|
|
1697
1838
|
from: to,
|
|
1698
1839
|
to: implTo,
|
|
@@ -1703,14 +1844,15 @@ function trace(db, start, options) {
|
|
|
1703
1844
|
}
|
|
1704
1845
|
if (current.depth >= maxDepth) continue;
|
|
1705
1846
|
const files = implementation.files.size > 0 ? implementation.files : handlerFilesForOperation(db, effectiveRow.to_id);
|
|
1847
|
+
const symbolIds = implementation.symbolId ? /* @__PURE__ */ new Set([implementation.symbolId]) : void 0;
|
|
1706
1848
|
if (implementation.edge?.status === "resolved" && files.size > 0) {
|
|
1707
1849
|
const targetRepoId = implementation.repoId ?? db.prepare(
|
|
1708
1850
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1709
1851
|
).get(effectiveRow.to_id)?.repoId;
|
|
1710
|
-
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
1852
|
+
const nextKey = `${targetRepoId ?? "*"}:${[...symbolIds ?? /* @__PURE__ */ new Set(["*"])].sort().join(",")}:${[...files].sort().join(",")}`;
|
|
1711
1853
|
if (seenScopes.has(nextKey))
|
|
1712
1854
|
edges.push({
|
|
1713
|
-
step: current.depth
|
|
1855
|
+
step: current.depth,
|
|
1714
1856
|
type: "cycle",
|
|
1715
1857
|
from: to,
|
|
1716
1858
|
to: nextKey,
|
|
@@ -1722,6 +1864,7 @@ function trace(db, start, options) {
|
|
|
1722
1864
|
queue.push({
|
|
1723
1865
|
repoId: targetRepoId,
|
|
1724
1866
|
files,
|
|
1867
|
+
symbolIds,
|
|
1725
1868
|
depth: current.depth + 1
|
|
1726
1869
|
});
|
|
1727
1870
|
}
|
|
@@ -1750,4 +1893,4 @@ export {
|
|
|
1750
1893
|
linkWorkspace,
|
|
1751
1894
|
trace
|
|
1752
1895
|
};
|
|
1753
|
-
//# sourceMappingURL=chunk-
|
|
1896
|
+
//# sourceMappingURL=chunk-MXYYARUP.js.map
|