@saptools/service-flow 0.1.3 → 0.1.5
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 -3
- package/TECHNICAL-NOTE.md +16 -2
- package/dist/{chunk-SIKIGT42.js → chunk-I33ASGX3.js} +322 -122
- package/dist/chunk-I33ASGX3.js.map +1 -0
- package/dist/cli.js +148 -95
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-SIKIGT42.js.map +0 -1
|
@@ -24,6 +24,27 @@ async function discoverRepositories(rootPath, ignore) {
|
|
|
24
24
|
const root = path2.resolve(rootPath);
|
|
25
25
|
const ignored = new Set(ignore);
|
|
26
26
|
const found = [];
|
|
27
|
+
async function isRealGitMarker(dir) {
|
|
28
|
+
const gitPath = path2.join(dir, ".git");
|
|
29
|
+
try {
|
|
30
|
+
const st = await fs.stat(gitPath);
|
|
31
|
+
if (st.isDirectory()) {
|
|
32
|
+
const children = await fs.readdir(gitPath);
|
|
33
|
+
return children.includes("HEAD") || children.includes("config");
|
|
34
|
+
}
|
|
35
|
+
if (st.isFile()) {
|
|
36
|
+
const text = await fs.readFile(gitPath, "utf8");
|
|
37
|
+
return text.trimStart().startsWith("gitdir:");
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const fixture = await fs.stat(path2.join(dir, ".git-fixture"));
|
|
43
|
+
return fixture.isFile() || fixture.isDirectory();
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
27
48
|
async function walk(dir) {
|
|
28
49
|
const rel = relativePath(root, dir);
|
|
29
50
|
if (rel !== "." && rel.split("/").some((part) => ignored.has(part))) return;
|
|
@@ -33,14 +54,14 @@ async function discoverRepositories(rootPath, ignore) {
|
|
|
33
54
|
} catch {
|
|
34
55
|
return;
|
|
35
56
|
}
|
|
36
|
-
|
|
57
|
+
const hasMarker = entries.some((e) => e.name === ".git" || e.name === ".git-fixture");
|
|
58
|
+
if (hasMarker && await isRealGitMarker(dir)) {
|
|
37
59
|
found.push({
|
|
38
60
|
name: path2.basename(dir),
|
|
39
61
|
absolutePath: dir,
|
|
40
62
|
relativePath: relativePath(root, dir),
|
|
41
63
|
isGitRepo: true
|
|
42
64
|
});
|
|
43
|
-
return;
|
|
44
65
|
}
|
|
45
66
|
for (const entry of entries)
|
|
46
67
|
if (entry.isDirectory() && !ignore.includes(entry.name))
|
|
@@ -507,13 +528,21 @@ function connectFactFromCall(call) {
|
|
|
507
528
|
return void 0;
|
|
508
529
|
const first = call.arguments[0];
|
|
509
530
|
if (!first) return void 0;
|
|
531
|
+
const second = call.arguments[1];
|
|
532
|
+
const objectArg = ts3.isObjectLiteralExpression(first) ? first : second && ts3.isObjectLiteralExpression(second) ? second : void 0;
|
|
533
|
+
let alias;
|
|
534
|
+
let aliasExpr;
|
|
510
535
|
if (ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first))
|
|
536
|
+
alias = first.text;
|
|
537
|
+
else if (!ts3.isObjectLiteralExpression(first))
|
|
538
|
+
aliasExpr = stringValue(first);
|
|
539
|
+
if ((ts3.isStringLiteralLike(first) || ts3.isNoSubstitutionTemplateLiteral(first)) && !objectArg)
|
|
511
540
|
return { alias: first.text, isDynamic: false, placeholders: [] };
|
|
512
|
-
if (!
|
|
541
|
+
if (!objectArg && aliasExpr)
|
|
513
542
|
return {
|
|
514
|
-
|
|
543
|
+
aliasExpr,
|
|
515
544
|
isDynamic: true,
|
|
516
|
-
placeholders:
|
|
545
|
+
placeholders: placeholders(aliasExpr)
|
|
517
546
|
};
|
|
518
547
|
let destinationExpr;
|
|
519
548
|
let servicePathExpr;
|
|
@@ -529,12 +558,15 @@ function connectFactFromCall(call) {
|
|
|
529
558
|
visitObject(prop.initializer);
|
|
530
559
|
}
|
|
531
560
|
}
|
|
532
|
-
visitObject(
|
|
561
|
+
if (objectArg) visitObject(objectArg);
|
|
533
562
|
const ph = [
|
|
563
|
+
...placeholders(aliasExpr ?? alias),
|
|
534
564
|
...placeholders(destinationExpr),
|
|
535
565
|
...placeholders(servicePathExpr)
|
|
536
566
|
];
|
|
537
567
|
return {
|
|
568
|
+
alias,
|
|
569
|
+
aliasExpr,
|
|
538
570
|
destinationExpr,
|
|
539
571
|
servicePathExpr,
|
|
540
572
|
isDynamic: ph.length > 0 || !destinationExpr && !servicePathExpr,
|
|
@@ -577,7 +609,9 @@ async function readSource(abs) {
|
|
|
577
609
|
}
|
|
578
610
|
async function resolveImport(repoPath, fromFile, spec) {
|
|
579
611
|
if (!spec.startsWith(".")) return void 0;
|
|
580
|
-
const
|
|
612
|
+
const rawBase = path8.resolve(repoPath, path8.dirname(fromFile), spec);
|
|
613
|
+
const parsed = path8.parse(rawBase);
|
|
614
|
+
const base = [".js", ".mjs", ".cjs", ".ts", ".mts", ".cts"].includes(parsed.ext) ? path8.join(parsed.dir, parsed.name) : rawBase;
|
|
581
615
|
for (const candidate of [
|
|
582
616
|
base,
|
|
583
617
|
`${base}.ts`,
|
|
@@ -622,15 +656,33 @@ async function importsFor(repoPath, filePath, sf) {
|
|
|
622
656
|
}
|
|
623
657
|
return imports;
|
|
624
658
|
}
|
|
659
|
+
function exportedLocalNames(sf) {
|
|
660
|
+
const exports = /* @__PURE__ */ new Map();
|
|
661
|
+
for (const stmt of sf.statements) {
|
|
662
|
+
const direct = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
|
|
663
|
+
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
|
|
664
|
+
) ?? false : false;
|
|
665
|
+
if (direct && ts3.isFunctionDeclaration(stmt) && stmt.name)
|
|
666
|
+
exports.set(stmt.name.text, stmt.name.text);
|
|
667
|
+
if (direct && ts3.isVariableStatement(stmt)) {
|
|
668
|
+
for (const decl of stmt.declarationList.declarations)
|
|
669
|
+
if (ts3.isIdentifier(decl.name)) exports.set(decl.name.text, decl.name.text);
|
|
670
|
+
}
|
|
671
|
+
if (!ts3.isExportDeclaration(stmt) || !stmt.exportClause) continue;
|
|
672
|
+
if (!ts3.isNamedExports(stmt.exportClause)) continue;
|
|
673
|
+
for (const el of stmt.exportClause.elements)
|
|
674
|
+
exports.set(el.name.text, el.propertyName?.text ?? el.name.text);
|
|
675
|
+
}
|
|
676
|
+
return exports;
|
|
677
|
+
}
|
|
625
678
|
async function helperBindings(repoPath, filePath) {
|
|
626
679
|
const sf = await readSource(path8.join(repoPath, filePath));
|
|
627
680
|
if (!sf) return [];
|
|
628
681
|
const sourceFileAst = sf;
|
|
629
682
|
const out = [];
|
|
683
|
+
const exportedLocals = exportedLocalNames(sf);
|
|
684
|
+
const factsByLocal = /* @__PURE__ */ new Map();
|
|
630
685
|
for (const stmt of sf.statements) {
|
|
631
|
-
const exported = ts3.canHaveModifiers(stmt) ? ts3.getModifiers(stmt)?.some(
|
|
632
|
-
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
|
|
633
|
-
) ?? false : false;
|
|
634
686
|
if (ts3.isFunctionDeclaration(stmt) && stmt.name) {
|
|
635
687
|
let fact;
|
|
636
688
|
stmt.forEachChild(function visit(node) {
|
|
@@ -638,27 +690,29 @@ async function helperBindings(repoPath, filePath) {
|
|
|
638
690
|
fact = findConnectInExpression(node.expression);
|
|
639
691
|
if (!fact) ts3.forEachChild(node, visit);
|
|
640
692
|
});
|
|
641
|
-
if (fact
|
|
642
|
-
out.push({
|
|
643
|
-
...fact,
|
|
644
|
-
exportedName: stmt.name.text,
|
|
645
|
-
sourceFile: normalizePath(filePath),
|
|
646
|
-
sourceLine: lineOf4(sf, stmt)
|
|
647
|
-
});
|
|
693
|
+
if (fact) factsByLocal.set(stmt.name.text, { ...fact, sourceLine: lineOf4(sf, stmt) });
|
|
648
694
|
}
|
|
649
695
|
if (ts3.isVariableStatement(stmt))
|
|
650
696
|
for (const decl of stmt.declarationList.declarations) {
|
|
651
697
|
if (!ts3.isIdentifier(decl.name) || !decl.initializer) continue;
|
|
652
698
|
const fact = findConnectInExpression(decl.initializer);
|
|
653
|
-
if (fact
|
|
654
|
-
|
|
699
|
+
if (fact)
|
|
700
|
+
factsByLocal.set(decl.name.text, {
|
|
655
701
|
...fact,
|
|
656
|
-
exportedName: decl.name.text,
|
|
657
|
-
sourceFile: normalizePath(filePath),
|
|
658
702
|
sourceLine: lineOf4(sourceFileAst, decl)
|
|
659
703
|
});
|
|
660
704
|
}
|
|
661
705
|
}
|
|
706
|
+
for (const [exportedName, localName] of exportedLocals) {
|
|
707
|
+
const fact = factsByLocal.get(localName);
|
|
708
|
+
if (fact)
|
|
709
|
+
out.push({
|
|
710
|
+
...fact,
|
|
711
|
+
exportedName,
|
|
712
|
+
sourceFile: normalizePath(filePath),
|
|
713
|
+
sourceLine: fact.sourceLine
|
|
714
|
+
});
|
|
715
|
+
}
|
|
662
716
|
return out;
|
|
663
717
|
}
|
|
664
718
|
async function parseServiceBindings(repoPath, filePath) {
|
|
@@ -668,6 +722,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
668
722
|
const out = [];
|
|
669
723
|
const imports = await importsFor(repoPath, filePath, sf);
|
|
670
724
|
const helperCache = /* @__PURE__ */ new Map();
|
|
725
|
+
const classHelpers = collectClassHelpers(sourceFileAst);
|
|
671
726
|
async function importedHelper(localName) {
|
|
672
727
|
const imp = imports.find((i) => i.localName === localName && i.sourceFile);
|
|
673
728
|
if (!imp?.sourceFile) return void 0;
|
|
@@ -697,6 +752,7 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
697
752
|
out.push({
|
|
698
753
|
variableName: decl.name.text,
|
|
699
754
|
alias: resolved.helper.alias,
|
|
755
|
+
aliasExpr: resolved.helper.aliasExpr,
|
|
700
756
|
destinationExpr: resolved.helper.destinationExpr,
|
|
701
757
|
servicePathExpr: resolved.helper.servicePathExpr,
|
|
702
758
|
isDynamic: resolved.helper.isDynamic,
|
|
@@ -716,15 +772,105 @@ async function parseServiceBindings(repoPath, filePath) {
|
|
|
716
772
|
});
|
|
717
773
|
}
|
|
718
774
|
}
|
|
775
|
+
function recordDestructuredClassHelper(decl) {
|
|
776
|
+
if (!ts3.isObjectBindingPattern(decl.name) || !decl.initializer) return;
|
|
777
|
+
const call = unwrapCall(decl.initializer);
|
|
778
|
+
if (!call || !ts3.isPropertyAccessExpression(call.expression)) return;
|
|
779
|
+
const target = call.expression;
|
|
780
|
+
if (target.expression.kind !== ts3.SyntaxKind.ThisKeyword) return;
|
|
781
|
+
for (const el of decl.name.elements) {
|
|
782
|
+
if (!ts3.isIdentifier(el.name)) continue;
|
|
783
|
+
const propertyName = el.propertyName && ts3.isIdentifier(el.propertyName) ? el.propertyName.text : el.name.text;
|
|
784
|
+
const helper = classHelpers.find(
|
|
785
|
+
(h) => h.helperName === target.name.text && h.propertyName === propertyName
|
|
786
|
+
);
|
|
787
|
+
if (!helper) continue;
|
|
788
|
+
out.push({
|
|
789
|
+
variableName: el.name.text,
|
|
790
|
+
...helper.fact,
|
|
791
|
+
sourceFile: normalizePath(filePath),
|
|
792
|
+
sourceLine: lineOf4(sourceFileAst, decl),
|
|
793
|
+
helperChain: [
|
|
794
|
+
{
|
|
795
|
+
callerVariable: el.name.text,
|
|
796
|
+
className: helper.className,
|
|
797
|
+
classHelper: helper.helperName,
|
|
798
|
+
returnedProperty: helper.propertyName,
|
|
799
|
+
helperVariable: helper.variableName,
|
|
800
|
+
helperSourceFile: normalizePath(filePath),
|
|
801
|
+
helperSourceLine: helper.sourceLine
|
|
802
|
+
}
|
|
803
|
+
]
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
719
807
|
const declarations = [];
|
|
720
808
|
function collectDeclarations(node) {
|
|
721
809
|
if (ts3.isVariableDeclaration(node)) declarations.push(node);
|
|
722
810
|
ts3.forEachChild(node, collectDeclarations);
|
|
723
811
|
}
|
|
724
812
|
collectDeclarations(sourceFileAst);
|
|
725
|
-
for (const decl of declarations)
|
|
813
|
+
for (const decl of declarations) {
|
|
814
|
+
recordDestructuredClassHelper(decl);
|
|
815
|
+
await recordVariable(decl);
|
|
816
|
+
}
|
|
726
817
|
return out;
|
|
727
818
|
}
|
|
819
|
+
function collectClassHelpers(sf) {
|
|
820
|
+
const helpers = [];
|
|
821
|
+
for (const stmt of sf.statements) {
|
|
822
|
+
if (!ts3.isClassDeclaration(stmt) || !stmt.name) continue;
|
|
823
|
+
for (const member of stmt.members) {
|
|
824
|
+
let visit2 = function(node) {
|
|
825
|
+
if (ts3.isVariableDeclaration(node) && ts3.isIdentifier(node.name) && node.initializer) {
|
|
826
|
+
const fact = findConnectInExpression(node.initializer);
|
|
827
|
+
if (fact) bindings.set(node.name.text, fact);
|
|
828
|
+
}
|
|
829
|
+
if (ts3.isReturnStatement(node) && node.expression && ts3.isObjectLiteralExpression(node.expression)) {
|
|
830
|
+
for (const prop of node.expression.properties) {
|
|
831
|
+
if (ts3.isShorthandPropertyAssignment(prop)) {
|
|
832
|
+
const fact = bindings.get(prop.name.text);
|
|
833
|
+
if (fact)
|
|
834
|
+
helpers.push({
|
|
835
|
+
className,
|
|
836
|
+
helperName,
|
|
837
|
+
propertyName: prop.name.text,
|
|
838
|
+
variableName: prop.name.text,
|
|
839
|
+
fact,
|
|
840
|
+
sourceLine: lineOf4(sf, prop)
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
if (ts3.isPropertyAssignment(prop) && ts3.isIdentifier(prop.initializer)) {
|
|
844
|
+
const propertyName = ts3.isIdentifier(prop.name) || ts3.isStringLiteralLike(prop.name) ? prop.name.text : void 0;
|
|
845
|
+
const fact = propertyName ? bindings.get(prop.initializer.text) : void 0;
|
|
846
|
+
if (propertyName && fact)
|
|
847
|
+
helpers.push({
|
|
848
|
+
className,
|
|
849
|
+
helperName,
|
|
850
|
+
propertyName,
|
|
851
|
+
variableName: prop.initializer.text,
|
|
852
|
+
fact,
|
|
853
|
+
sourceLine: lineOf4(sf, prop)
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
ts3.forEachChild(node, visit2);
|
|
859
|
+
};
|
|
860
|
+
var visit = visit2;
|
|
861
|
+
if (!ts3.isPropertyDeclaration(member) || !member.initializer) continue;
|
|
862
|
+
if (!ts3.isIdentifier(member.name)) continue;
|
|
863
|
+
const className = stmt.name.text;
|
|
864
|
+
const helperName = member.name.text;
|
|
865
|
+
const initializer = member.initializer;
|
|
866
|
+
if (!ts3.isArrowFunction(initializer) && !ts3.isFunctionExpression(initializer))
|
|
867
|
+
continue;
|
|
868
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
869
|
+
visit2(initializer);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return helpers;
|
|
873
|
+
}
|
|
728
874
|
|
|
729
875
|
// src/linker/dynamic-edge-resolver.ts
|
|
730
876
|
function applyVariables(template, vars) {
|
|
@@ -749,6 +895,13 @@ function rows(db, operationPath, workspaceId) {
|
|
|
749
895
|
);
|
|
750
896
|
}
|
|
751
897
|
function resolveOperation(db, signals, workspaceId) {
|
|
898
|
+
const missing = [signals.servicePath, signals.alias, signals.destination, signals.operationPath].flatMap((value) => [...(value ?? "").matchAll(/\$\{\s*(\w+)\s*\}/g)].map((match) => match[1] ?? "")).filter(Boolean);
|
|
899
|
+
if (missing.length > 0)
|
|
900
|
+
return {
|
|
901
|
+
status: "dynamic",
|
|
902
|
+
candidates: signals.operationPath ? rows(db, signals.operationPath, workspaceId) : [],
|
|
903
|
+
reasons: [...new Set(missing)].map((name) => `missing_variable:${name}`)
|
|
904
|
+
};
|
|
752
905
|
if (!signals.operationPath)
|
|
753
906
|
return {
|
|
754
907
|
status: "unresolved",
|
|
@@ -800,7 +953,7 @@ function resolveOperation(db, signals, workspaceId) {
|
|
|
800
953
|
candidates,
|
|
801
954
|
reasons: ["operation_path_only_has_no_strong_target_signal"]
|
|
802
955
|
};
|
|
803
|
-
if (best && best.score >= 0.9 && (!second || best.score - second.score >= 0.25))
|
|
956
|
+
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))
|
|
804
957
|
return {
|
|
805
958
|
status: "resolved",
|
|
806
959
|
target: best,
|
|
@@ -847,90 +1000,105 @@ function linkHelperPackages(db, workspaceId) {
|
|
|
847
1000
|
|
|
848
1001
|
// src/linker/cross-repo-linker.ts
|
|
849
1002
|
function linkWorkspace(db, workspaceId, vars = {}) {
|
|
850
|
-
db.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
1003
|
+
return db.transaction(() => {
|
|
1004
|
+
db.prepare("DELETE FROM graph_edges WHERE workspace_id=?").run(workspaceId);
|
|
1005
|
+
let edges = linkHelperPackages(db, workspaceId);
|
|
1006
|
+
let unresolved = 0;
|
|
1007
|
+
let resolvedCount = 0;
|
|
1008
|
+
let ambiguousCount = 0;
|
|
1009
|
+
let dynamicCount = 0;
|
|
1010
|
+
let terminalCount = 0;
|
|
1011
|
+
const calls = db.prepare(
|
|
1012
|
+
`SELECT c.*,r.name repoName,b.alias,b.alias_expr aliasExpr,b.destination_expr destinationExpr,b.service_path_expr servicePathExpr,b.is_dynamic isDynamic,b.placeholders_json placeholdersJson,b.helper_chain_json helperChainJson,req.service_path requireServicePath,req.destination requireDestination FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id LEFT JOIN service_bindings b ON b.id=c.service_binding_id LEFT JOIN cds_requires req ON req.repo_id=c.repo_id AND req.alias=b.alias WHERE r.workspace_id=?`
|
|
1013
|
+
).all(workspaceId);
|
|
1014
|
+
for (const call of calls) {
|
|
1015
|
+
const callType = String(call.call_type);
|
|
1016
|
+
const op = applyVariables(String(call.operation_path_expr ?? ""), vars);
|
|
1017
|
+
const servicePath = applyVariables(
|
|
1018
|
+
call.servicePathExpr ?? call.requireServicePath,
|
|
1019
|
+
vars
|
|
1020
|
+
);
|
|
1021
|
+
const destination = call.destinationExpr ?? call.requireDestination;
|
|
1022
|
+
const isDynamic = Boolean(Number(call.isDynamic ?? 0));
|
|
1023
|
+
const resolution = callType.startsWith("remote") ? resolveOperation(
|
|
1024
|
+
db,
|
|
1025
|
+
{
|
|
1026
|
+
servicePath,
|
|
1027
|
+
operationPath: op,
|
|
1028
|
+
alias: applyVariables(call.aliasExpr ?? call.alias, vars),
|
|
1029
|
+
destination: applyVariables(destination, vars),
|
|
1030
|
+
isDynamic,
|
|
1031
|
+
hasExplicitOverride: Object.keys(vars).length > 0
|
|
1032
|
+
},
|
|
1033
|
+
workspaceId
|
|
1034
|
+
) : { status: "unresolved", candidates: [], reasons: [] };
|
|
1035
|
+
const target = resolution.target;
|
|
1036
|
+
const evidence = {
|
|
1037
|
+
sourceFile: call.source_file,
|
|
1038
|
+
sourceLine: call.source_line,
|
|
1039
|
+
file: call.source_file,
|
|
1040
|
+
line: call.source_line,
|
|
1041
|
+
repo: call.repoName,
|
|
1042
|
+
serviceAlias: call.alias,
|
|
1043
|
+
serviceAliasExpr: call.aliasExpr,
|
|
1044
|
+
destination: applyVariables(destination, vars),
|
|
868
1045
|
servicePath,
|
|
869
1046
|
operationPath: op,
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
"
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
"
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
Number(call.confidence ?? 0.2),
|
|
923
|
-
JSON.stringify(evidence),
|
|
924
|
-
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
925
|
-
String(
|
|
926
|
-
call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? "Dynamic target requires runtime variable overrides" : "No indexed target operation matched")
|
|
927
|
-
)
|
|
928
|
-
);
|
|
929
|
-
edges += 1;
|
|
930
|
-
unresolved += edgeType === "UNRESOLVED_EDGE" ? 1 : 0;
|
|
1047
|
+
targetRepo: target?.repoName,
|
|
1048
|
+
targetOperation: target?.operationName,
|
|
1049
|
+
helperChain: call.helperChainJson ? JSON.parse(String(call.helperChainJson)) : void 0,
|
|
1050
|
+
candidates: resolution.candidates,
|
|
1051
|
+
candidateCount: resolution.candidates.length,
|
|
1052
|
+
resolutionStatus: resolution.status,
|
|
1053
|
+
resolutionReasons: resolution.reasons
|
|
1054
|
+
};
|
|
1055
|
+
if (target) {
|
|
1056
|
+
db.prepare(
|
|
1057
|
+
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic) VALUES(?,?,?,?,?,?,?,?,?,?)"
|
|
1058
|
+
).run(
|
|
1059
|
+
workspaceId,
|
|
1060
|
+
"REMOTE_CALL_RESOLVES_TO_OPERATION",
|
|
1061
|
+
"resolved",
|
|
1062
|
+
"call",
|
|
1063
|
+
String(call.id),
|
|
1064
|
+
"operation",
|
|
1065
|
+
String(target.operationId),
|
|
1066
|
+
target.score,
|
|
1067
|
+
JSON.stringify(evidence),
|
|
1068
|
+
isDynamic ? 1 : 0
|
|
1069
|
+
);
|
|
1070
|
+
edges += 1;
|
|
1071
|
+
resolvedCount += 1;
|
|
1072
|
+
} else {
|
|
1073
|
+
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";
|
|
1074
|
+
const status = edgeType === "DYNAMIC_EDGE_CANDIDATE" ? "dynamic" : resolution.status === "ambiguous" ? "ambiguous" : edgeType === "UNRESOLVED_EDGE" ? "unresolved" : "terminal";
|
|
1075
|
+
const unresolvedReason = status === "terminal" ? null : String(
|
|
1076
|
+
call.unresolved_reason ?? (resolution.status === "ambiguous" ? "Ambiguous operation candidates require a strong service signal" : resolution.status === "dynamic" ? `Dynamic target requires runtime variable overrides: ${(resolution.reasons.length ? resolution.reasons : ["missing runtime variables"]).join(", ")}` : "No indexed target operation matched")
|
|
1077
|
+
);
|
|
1078
|
+
db.prepare(
|
|
1079
|
+
"INSERT INTO graph_edges(workspace_id,edge_type,status,from_kind,from_id,to_kind,to_id,confidence,evidence_json,is_dynamic,unresolved_reason) VALUES(?,?,?,?,?,?,?,?,?,?,?)"
|
|
1080
|
+
).run(
|
|
1081
|
+
workspaceId,
|
|
1082
|
+
edgeType,
|
|
1083
|
+
status,
|
|
1084
|
+
"call",
|
|
1085
|
+
String(call.id),
|
|
1086
|
+
callType.startsWith("async_") ? "event" : "external",
|
|
1087
|
+
String(call.event_name_expr ?? call.query_entity ?? op ?? call.id),
|
|
1088
|
+
Number(call.confidence ?? 0.2),
|
|
1089
|
+
JSON.stringify(evidence),
|
|
1090
|
+
isDynamic || resolution.status === "dynamic" ? 1 : 0,
|
|
1091
|
+
unresolvedReason
|
|
1092
|
+
);
|
|
1093
|
+
edges += 1;
|
|
1094
|
+
unresolved += status === "unresolved" ? 1 : 0;
|
|
1095
|
+
ambiguousCount += status === "ambiguous" ? 1 : 0;
|
|
1096
|
+
dynamicCount += status === "dynamic" ? 1 : 0;
|
|
1097
|
+
terminalCount += status === "terminal" ? 1 : 0;
|
|
1098
|
+
}
|
|
931
1099
|
}
|
|
932
|
-
|
|
933
|
-
|
|
1100
|
+
return { edgeCount: edges, unresolvedCount: unresolved, resolvedCount, ambiguousCount, dynamicCount, terminalCount };
|
|
1101
|
+
});
|
|
934
1102
|
}
|
|
935
1103
|
|
|
936
1104
|
// src/trace/trace-engine.ts
|
|
@@ -949,7 +1117,8 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
949
1117
|
`SELECT DISTINCT hc.source_file sourceFile
|
|
950
1118
|
FROM handler_classes hc LEFT JOIN handler_methods hm ON hm.handler_class_id=hc.id
|
|
951
1119
|
WHERE (? IS NULL OR hc.repo_id=?) AND (? IS NULL OR hc.class_name=? OR hm.method_name=?)
|
|
952
|
-
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
|
|
1120
|
+
AND (? IS NULL OR hm.decorator_value=? OR hm.method_name=?)
|
|
1121
|
+
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=?)))`
|
|
953
1122
|
).all(
|
|
954
1123
|
repoId,
|
|
955
1124
|
repoId,
|
|
@@ -958,8 +1127,16 @@ function sourceFilesForStart(db, repoId, start) {
|
|
|
958
1127
|
handler,
|
|
959
1128
|
operation,
|
|
960
1129
|
operation,
|
|
1130
|
+
operation,
|
|
1131
|
+
start.servicePath,
|
|
1132
|
+
start.servicePath,
|
|
1133
|
+
operation,
|
|
1134
|
+
operation,
|
|
1135
|
+
operation,
|
|
1136
|
+
operation,
|
|
961
1137
|
operation
|
|
962
1138
|
);
|
|
1139
|
+
if (start.servicePath && rows2.length === 0) return void 0;
|
|
963
1140
|
if (rows2.length === 0) return void 0;
|
|
964
1141
|
return new Set(rows2.map((row) => row.sourceFile).filter(Boolean));
|
|
965
1142
|
}
|
|
@@ -1003,7 +1180,7 @@ function graphForCalls(db, callIds) {
|
|
|
1003
1180
|
if (callIds.length === 0) return map;
|
|
1004
1181
|
const rows2 = db.prepare(
|
|
1005
1182
|
`SELECT * FROM graph_edges WHERE from_kind='call' AND from_id IN (${callIds.map(() => "?").join(",")}) ORDER BY id`
|
|
1006
|
-
).all(...callIds);
|
|
1183
|
+
).all(...callIds.map((id) => String(id)));
|
|
1007
1184
|
for (const row of rows2) {
|
|
1008
1185
|
const id = Number(row.from_id);
|
|
1009
1186
|
map.set(id, [...map.get(id) ?? [], row]);
|
|
@@ -1035,6 +1212,30 @@ function evidenceWithRuntimeVariables(evidence, vars) {
|
|
|
1035
1212
|
} : void 0
|
|
1036
1213
|
};
|
|
1037
1214
|
}
|
|
1215
|
+
function operationNode(db, operationId) {
|
|
1216
|
+
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);
|
|
1217
|
+
if (!row) return void 0;
|
|
1218
|
+
return { id: `operation:${operationId}`, kind: "operation", label: `${String(row.repoName)}:${String(row.servicePath)}${String(row.operationPath)}`, ...row };
|
|
1219
|
+
}
|
|
1220
|
+
function workspaceIdForCall(db, callId) {
|
|
1221
|
+
return db.prepare("SELECT r.workspace_id workspaceId FROM outbound_calls c JOIN repositories r ON r.id=c.repo_id WHERE c.id=?").get(callId)?.workspaceId;
|
|
1222
|
+
}
|
|
1223
|
+
function runtimeResolution(db, row, evidence, vars) {
|
|
1224
|
+
if (!vars || Object.keys(vars).length === 0) return { row, evidence, unresolvedReason: row.unresolved_reason };
|
|
1225
|
+
const nextEvidence = evidenceWithRuntimeVariables(evidence, vars);
|
|
1226
|
+
const servicePath = typeof nextEvidence.servicePath === "string" ? nextEvidence.servicePath : void 0;
|
|
1227
|
+
const operationPath = typeof nextEvidence.operationPath === "string" ? nextEvidence.operationPath : void 0;
|
|
1228
|
+
const alias = typeof nextEvidence.serviceAliasExpr === "string" ? applyVariables(nextEvidence.serviceAliasExpr, vars) : typeof nextEvidence.serviceAlias === "string" ? applyVariables(nextEvidence.serviceAlias, vars) : void 0;
|
|
1229
|
+
const destination = typeof nextEvidence.destination === "string" ? applyVariables(nextEvidence.destination, vars) : void 0;
|
|
1230
|
+
const resolution = resolveOperation(db, { servicePath, operationPath, alias, destination, hasExplicitOverride: true, isDynamic: Boolean(row.status === "dynamic" || nextEvidence.resolutionStatus === "dynamic") }, workspaceIdForCall(db, row.from_id));
|
|
1231
|
+
nextEvidence.runtimeResolutionStatus = resolution.status;
|
|
1232
|
+
nextEvidence.runtimeResolutionReasons = resolution.reasons;
|
|
1233
|
+
if (resolution.target) {
|
|
1234
|
+
nextEvidence.runtimeResolvedCandidate = resolution.target;
|
|
1235
|
+
return { row: { ...row, to_kind: "operation", to_id: String(resolution.target.operationId), unresolved_reason: void 0, confidence: resolution.target.score }, evidence: nextEvidence, target: resolution.target };
|
|
1236
|
+
}
|
|
1237
|
+
return { row, evidence: nextEvidence, unresolvedReason: resolution.status === "dynamic" ? `Dynamic target is missing runtime variables: ${resolution.reasons.join(", ")}` : resolution.status === "ambiguous" ? "Ambiguous runtime operation candidates" : "No runtime operation candidate matched substituted service and operation path" };
|
|
1238
|
+
}
|
|
1038
1239
|
function edgeTarget(row, evidence) {
|
|
1039
1240
|
const runtimeCandidate = evidence.runtimeResolvedCandidate;
|
|
1040
1241
|
if (runtimeCandidate?.servicePath && runtimeCandidate.operationPath)
|
|
@@ -1095,33 +1296,32 @@ function trace(db, start, options) {
|
|
|
1095
1296
|
const rawEvidence = JSON.parse(
|
|
1096
1297
|
String(row.evidence_json || "{}")
|
|
1097
1298
|
);
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const
|
|
1103
|
-
nodes.set(targetNode, {
|
|
1299
|
+
const effective = runtimeResolution(db, row, rawEvidence, options.vars);
|
|
1300
|
+
const evidence = effective.evidence;
|
|
1301
|
+
const effectiveRow = effective.row;
|
|
1302
|
+
const targetNode = `${effectiveRow.to_kind}:${effectiveRow.to_id}`;
|
|
1303
|
+
const opNode = effectiveRow.to_kind === "operation" ? operationNode(db, effectiveRow.to_id) : void 0;
|
|
1304
|
+
nodes.set(targetNode, opNode ?? {
|
|
1104
1305
|
id: targetNode,
|
|
1105
|
-
kind:
|
|
1106
|
-
label:
|
|
1107
|
-
...evidence
|
|
1306
|
+
kind: effectiveRow.to_kind,
|
|
1307
|
+
label: effectiveRow.to_id
|
|
1108
1308
|
});
|
|
1109
|
-
const to = edgeTarget(
|
|
1309
|
+
const to = edgeTarget(effectiveRow, evidence);
|
|
1110
1310
|
edges.push({
|
|
1111
1311
|
step: current.depth,
|
|
1112
1312
|
type: String(call.call_type),
|
|
1113
1313
|
from: `${call.repoName}:${call.source_file}`,
|
|
1114
1314
|
to,
|
|
1115
1315
|
evidence,
|
|
1116
|
-
confidence: Number(
|
|
1117
|
-
unresolvedReason:
|
|
1316
|
+
confidence: Number(effectiveRow.confidence ?? call.confidence),
|
|
1317
|
+
unresolvedReason: effective.unresolvedReason
|
|
1118
1318
|
});
|
|
1119
|
-
if (
|
|
1120
|
-
const files = handlerFilesForOperation(db,
|
|
1319
|
+
if (effectiveRow.to_kind === "operation" && current.depth < maxDepth) {
|
|
1320
|
+
const files = handlerFilesForOperation(db, effectiveRow.to_id);
|
|
1121
1321
|
if (files.size > 0) {
|
|
1122
1322
|
const targetRepoId = db.prepare(
|
|
1123
1323
|
"SELECT s.repo_id repoId FROM cds_operations o JOIN cds_services s ON s.id=o.service_id WHERE o.id=?"
|
|
1124
|
-
).get(
|
|
1324
|
+
).get(effectiveRow.to_id)?.repoId;
|
|
1125
1325
|
const nextKey = `${targetRepoId ?? "*"}:${[...files].sort().join(",")}`;
|
|
1126
1326
|
if (seenScopes.has(nextKey))
|
|
1127
1327
|
edges.push({
|
|
@@ -1163,4 +1363,4 @@ export {
|
|
|
1163
1363
|
linkWorkspace,
|
|
1164
1364
|
trace
|
|
1165
1365
|
};
|
|
1166
|
-
//# sourceMappingURL=chunk-
|
|
1366
|
+
//# sourceMappingURL=chunk-I33ASGX3.js.map
|