@saptools/service-flow 0.1.14 → 0.1.15
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 +8 -0
- package/README.md +4 -0
- package/TECHNICAL-NOTE.md +6 -0
- package/dist/{chunk-UPXFMLUY.js → chunk-ZSUL2X77.js} +6 -2
- package/dist/chunk-ZSUL2X77.js.map +1 -0
- package/dist/cli.js +69 -15
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-UPXFMLUY.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
parsePackageJson,
|
|
11
11
|
parseServiceBindings,
|
|
12
12
|
trace
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-ZSUL2X77.js";
|
|
14
14
|
|
|
15
15
|
// src/cli.ts
|
|
16
16
|
import { Command } from "commander";
|
|
@@ -185,7 +185,7 @@ function migrate(db) {
|
|
|
185
185
|
// package.json
|
|
186
186
|
var package_default = {
|
|
187
187
|
name: "@saptools/service-flow",
|
|
188
|
-
version: "0.1.
|
|
188
|
+
version: "0.1.15",
|
|
189
189
|
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
190
190
|
type: "module",
|
|
191
191
|
publishConfig: {
|
|
@@ -543,9 +543,25 @@ function insertExecutableSymbols(db, repoId, rows) {
|
|
|
543
543
|
for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.kind, r.localName, r.qualifiedName, r.exported ? 1 : 0, r.startLine, r.endLine, r.startOffset, r.endOffset, r.sourceFile, r.exportedName, r.importExportEvidence ? JSON.stringify(r.importExportEvidence) : null);
|
|
544
544
|
}
|
|
545
545
|
function insertSymbolCalls(db, repoId, rows) {
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
546
|
+
const findTarget = `(SELECT id FROM symbols WHERE repo_id=? AND ((source_file=? AND (name=? OR qualified_name=?)) OR (source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?))) ORDER BY CASE WHEN source_file=? THEN 0 ELSE 1 END,id LIMIT 1)`;
|
|
547
|
+
const stmt = db.prepare(`INSERT INTO symbol_calls(repo_id,caller_symbol_id,callee_symbol_id,callee_expression,import_source,source_file,source_line,status,confidence,evidence_json,unresolved_reason) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),${findTarget},?,?,?,?,CASE WHEN ${findTarget} IS NULL THEN 'unresolved' ELSE 'resolved' END,0.8,?,CASE WHEN ${findTarget} IS NULL THEN 'No local symbol target matched exactly' ELSE NULL END)`);
|
|
548
|
+
for (const r of rows) {
|
|
549
|
+
const targetArgs = [repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile];
|
|
550
|
+
stmt.run(
|
|
551
|
+
repoId,
|
|
552
|
+
repoId,
|
|
553
|
+
r.sourceFile,
|
|
554
|
+
r.callerQualifiedName,
|
|
555
|
+
...targetArgs,
|
|
556
|
+
r.calleeExpression,
|
|
557
|
+
r.importSource,
|
|
558
|
+
r.sourceFile,
|
|
559
|
+
r.sourceLine,
|
|
560
|
+
...targetArgs,
|
|
561
|
+
JSON.stringify(r.evidence),
|
|
562
|
+
...targetArgs
|
|
563
|
+
);
|
|
564
|
+
}
|
|
549
565
|
}
|
|
550
566
|
function insertCalls(db, repoId, rows) {
|
|
551
567
|
const stmt = db.prepare(
|
|
@@ -643,6 +659,10 @@ function isFunctionLike(node) {
|
|
|
643
659
|
function exported(node) {
|
|
644
660
|
return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
|
|
645
661
|
}
|
|
662
|
+
function isPublicStaticMethod(node) {
|
|
663
|
+
const flags = ts.getCombinedModifierFlags(node);
|
|
664
|
+
return (flags & ts.ModifierFlags.Static) !== 0 && (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
|
|
665
|
+
}
|
|
646
666
|
function exportDeclarations(source) {
|
|
647
667
|
const exports = /* @__PURE__ */ new Map();
|
|
648
668
|
const visit = (node) => {
|
|
@@ -701,11 +721,20 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
701
721
|
const imports = /* @__PURE__ */ new Map();
|
|
702
722
|
const exportNames = exportDeclarations(source);
|
|
703
723
|
const objectExports = /* @__PURE__ */ new Set();
|
|
704
|
-
const
|
|
705
|
-
|
|
724
|
+
const exportedClasses = /* @__PURE__ */ new Set();
|
|
725
|
+
const proxyVariables = /* @__PURE__ */ new Map();
|
|
726
|
+
const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
|
|
727
|
+
const parentRoot = parentName?.split(".")[0] ?? "";
|
|
728
|
+
const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
|
|
706
729
|
const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
|
|
707
|
-
const objectExported = parentName ? objectExports.has(
|
|
708
|
-
|
|
730
|
+
const objectExported = parentName ? objectExports.has(parentRoot) : false;
|
|
731
|
+
const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && isPublicStaticMethod(node) : false;
|
|
732
|
+
const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
|
|
733
|
+
const sourceEvidence = evidence ?? (classMemberExported ? { source: "exported_class_member", exportedClass: parentRoot, memberKind: "static_method" } : declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0);
|
|
734
|
+
symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: effectiveExportedName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(effectiveExportedName), importExportEvidence: sourceEvidence });
|
|
735
|
+
};
|
|
736
|
+
const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
|
|
737
|
+
symbols.push({ kind: "object_alias", localName: propertyName, exportedName: propertyName, qualifiedName: `${objectName}.${propertyName}`, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: true, importExportEvidence: { source: "exported_object_shorthand", objectName, propertyName, targetImportSource } });
|
|
709
738
|
};
|
|
710
739
|
const visitImports = (node) => {
|
|
711
740
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
@@ -730,6 +759,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
730
759
|
visitImports(source);
|
|
731
760
|
const visitSymbols = (node, parentClass) => {
|
|
732
761
|
if (ts.isClassDeclaration(node) && node.name) {
|
|
762
|
+
if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
|
|
733
763
|
for (const member of node.members) visitSymbols(member, node.name.text);
|
|
734
764
|
return;
|
|
735
765
|
}
|
|
@@ -743,8 +773,10 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
743
773
|
if (!localName || !d.initializer) continue;
|
|
744
774
|
if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
|
|
745
775
|
if (ts.isObjectLiteralExpression(d.initializer)) {
|
|
746
|
-
|
|
776
|
+
const objectIsExported = exported(node) || exportNames.has(localName);
|
|
777
|
+
if (objectIsExported) objectExports.add(localName);
|
|
747
778
|
for (const prop of d.initializer.properties) {
|
|
779
|
+
if (objectIsExported && ts.isShorthandPropertyAssignment(prop)) addAliasSymbol(localName, prop.name.text, prop.name, imports.get(prop.name.text));
|
|
748
780
|
if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
|
|
749
781
|
const propName = nameOf(prop.name);
|
|
750
782
|
if (propName) addSymbol("object_method", propName, prop.initializer, localName);
|
|
@@ -758,6 +790,15 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
758
790
|
} else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
|
|
759
791
|
};
|
|
760
792
|
visitSymbols(source);
|
|
793
|
+
const visitProxyVariables = (node) => {
|
|
794
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
|
|
795
|
+
const callee = callName(node.initializer.expression);
|
|
796
|
+
const importSource = callee.local ? imports.get(callee.local) : void 0;
|
|
797
|
+
if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression });
|
|
798
|
+
}
|
|
799
|
+
ts.forEachChild(node, visitProxyVariables);
|
|
800
|
+
};
|
|
801
|
+
visitProxyVariables(source);
|
|
761
802
|
const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
|
|
762
803
|
const visitCalls = (node) => {
|
|
763
804
|
if (ts.isCallExpression(node)) {
|
|
@@ -765,8 +806,9 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
765
806
|
const caller = nearest(symbols, line);
|
|
766
807
|
if (caller) {
|
|
767
808
|
const callee = callName(node.expression);
|
|
768
|
-
const
|
|
769
|
-
const
|
|
809
|
+
const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
|
|
810
|
+
const importSource = proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
|
|
811
|
+
const targetName = proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
|
|
770
812
|
const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
|
|
771
813
|
const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
|
|
772
814
|
const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
|
|
@@ -778,7 +820,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
778
820
|
const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
|
|
779
821
|
const resolvedTarget = provenThisMethod ? thisTarget : targetName;
|
|
780
822
|
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport);
|
|
781
|
-
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget } });
|
|
823
|
+
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: resolvedTarget, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, factory: proxy?.factory } });
|
|
782
824
|
}
|
|
783
825
|
}
|
|
784
826
|
ts.forEachChild(node, visitCalls);
|
|
@@ -1047,16 +1089,28 @@ function localServiceDiagnostics(db, strict) {
|
|
|
1047
1089
|
}
|
|
1048
1090
|
function parserQualityDiagnostics(db, strict) {
|
|
1049
1091
|
if (!strict) return [];
|
|
1092
|
+
const symbolUnresolvedThreshold = 0.05;
|
|
1093
|
+
const dbUnknownThreshold = 0.25;
|
|
1094
|
+
const outboundUnownedThreshold = 0.01;
|
|
1050
1095
|
const symbol = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) resolved, SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved FROM symbol_calls").get();
|
|
1051
1096
|
const top = db.prepare("SELECT callee_expression calleeExpression,COUNT(*) count FROM symbol_calls WHERE status='unresolved' GROUP BY callee_expression ORDER BY count DESC,callee_expression LIMIT 5").all();
|
|
1097
|
+
const evidence = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN json_valid(evidence_json)=0 OR json_type(evidence_json) <> 'object' THEN 1 ELSE 0 END) nonObject FROM symbol_calls").get();
|
|
1052
1098
|
const dbq = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN query_entity IS NOT NULL THEN 1 ELSE 0 END) known, SUM(CASE WHEN query_entity IS NULL THEN 1 ELSE 0 END) unknown FROM outbound_calls WHERE call_type='local_db_query'").get();
|
|
1099
|
+
const outbound = db.prepare("SELECT COUNT(*) total, SUM(CASE WHEN source_symbol_id IS NULL THEN 1 ELSE 0 END) withoutOwnership FROM outbound_calls").get();
|
|
1053
1100
|
const symbolTotal = Number(symbol.total ?? 0);
|
|
1054
1101
|
const symbolUnresolved = Number(symbol.unresolved ?? 0);
|
|
1102
|
+
const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
|
|
1055
1103
|
const queryTotal = Number(dbq.total ?? 0);
|
|
1056
1104
|
const queryUnknown = Number(dbq.unknown ?? 0);
|
|
1105
|
+
const queryUnknownRatio = queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4));
|
|
1106
|
+
const outboundTotal = Number(outbound.total ?? 0);
|
|
1107
|
+
const outboundWithoutOwnership = Number(outbound.withoutOwnership ?? 0);
|
|
1108
|
+
const outboundWithoutOwnershipRatio = outboundTotal === 0 ? 0 : Number((outboundWithoutOwnership / outboundTotal).toFixed(4));
|
|
1057
1109
|
return [
|
|
1058
|
-
{ severity: "info", code: "
|
|
1059
|
-
{ severity: "info", code: "
|
|
1110
|
+
{ severity: Number(evidence.nonObject ?? 0) > 0 ? "warning" : "info", code: "strict_symbol_call_evidence_quality", message: "Symbol-call evidence JSON object aggregate", total: Number(evidence.total ?? 0), nonObject: Number(evidence.nonObject ?? 0) },
|
|
1111
|
+
{ severity: symbolUnresolvedRatio > symbolUnresolvedThreshold ? "warning" : "info", code: "strict_symbol_call_quality", message: "Symbol-call quality aggregate", total: symbolTotal, resolved: Number(symbol.resolved ?? 0), unresolved: symbolUnresolved, unresolvedRatio: symbolUnresolvedRatio, unresolvedRatioThreshold: symbolUnresolvedThreshold, topUnresolvedCallees: top },
|
|
1112
|
+
{ severity: queryUnknownRatio > dbUnknownThreshold ? "warning" : "info", code: "strict_db_query_quality", message: "Local DB query quality aggregate", total: queryTotal, known: Number(dbq.known ?? 0), unknown: queryUnknown, unknownRatio: queryUnknownRatio, unknownRatioThreshold: dbUnknownThreshold },
|
|
1113
|
+
{ severity: outboundWithoutOwnershipRatio > outboundUnownedThreshold ? "warning" : "info", code: "strict_outbound_source_ownership_quality", message: "Outbound call source-symbol ownership aggregate", total: outboundTotal, withoutOwnership: outboundWithoutOwnership, withoutOwnershipRatio: outboundWithoutOwnershipRatio, withoutOwnershipRatioThreshold: outboundUnownedThreshold }
|
|
1060
1114
|
];
|
|
1061
1115
|
}
|
|
1062
1116
|
function createProgram() {
|