@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.
- package/CHANGELOG.md +18 -0
- package/README.md +11 -5
- package/dist/{chunk-HQMAFSRR.js → chunk-GG4XJGES.js} +259 -95
- package/dist/chunk-GG4XJGES.js.map +1 -0
- package/dist/cli.js +266 -123
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-HQMAFSRR.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);
|
|
@@ -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
|
|
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 (
|
|
1439
|
-
if (
|
|
1440
|
-
|
|
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
|
|
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
|
|
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"
|
|
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
|
-
|
|
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
|
|
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-
|
|
1845
|
+
//# sourceMappingURL=chunk-GG4XJGES.js.map
|