@saptools/service-flow 0.1.15 → 0.1.16
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 +9 -0
- package/README.md +4 -4
- package/TECHNICAL-NOTE.md +8 -0
- package/dist/{chunk-ZSUL2X77.js → chunk-SSSBQIMK.js} +9 -5
- package/dist/chunk-SSSBQIMK.js.map +1 -0
- package/dist/cli.js +68 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZSUL2X77.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-SSSBQIMK.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.16",
|
|
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,35 +543,44 @@ 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
|
-
const
|
|
546
|
+
const callerStmt = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1");
|
|
547
|
+
const insertStmt = 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(?,?,?,?,?,?,?,?,?,?,?)");
|
|
548
548
|
for (const r of rows) {
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
);
|
|
549
|
+
const caller = callerStmt.get(repoId, r.sourceFile, r.callerQualifiedName);
|
|
550
|
+
const target = resolveSymbolCallTarget(db, repoId, r);
|
|
551
|
+
insertStmt.run(repoId, caller?.id, target.id, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, target.status, 0.8, JSON.stringify({ ...r.evidence, candidateStrategy: target.strategy, candidateCount: target.candidateCount }), target.reason);
|
|
564
552
|
}
|
|
565
553
|
}
|
|
554
|
+
function resolveSymbolCallTarget(db, repoId, r) {
|
|
555
|
+
const evidence = r.evidence;
|
|
556
|
+
const localRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND (name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName);
|
|
557
|
+
if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "same_file_exact", candidateCount: 1 };
|
|
558
|
+
if (localRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: localRows.length };
|
|
559
|
+
const rows = db.prepare("SELECT id,kind,evidence_json evidenceJson FROM symbols WHERE repo_id=? AND source_file<>? AND exported=1 AND (exported_name=? OR name=? OR qualified_name=?) ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName);
|
|
560
|
+
if (evidence.relation === "relative_import_proxy_member" && rows.length > 1) {
|
|
561
|
+
const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
|
|
562
|
+
if (objectMapRows.length > 0) {
|
|
563
|
+
const concrete = rows.find((row) => row.kind !== "object_alias") ?? objectMapRows[0];
|
|
564
|
+
return { id: concrete?.id ?? null, status: "resolved", reason: null, strategy: "proxy_member_exported_object_map", candidateCount: rows.length };
|
|
565
|
+
}
|
|
566
|
+
return { id: null, status: "ambiguous", reason: "Proxy member target requires explicit factory/module/type evidence; global member name is ambiguous", strategy: "proxy_member_no_global_name_fallback", candidateCount: rows.length };
|
|
567
|
+
}
|
|
568
|
+
if (rows.length === 1) return { id: rows[0]?.id ?? null, status: "resolved", reason: null, strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_unique_exported_candidate" : "relative_import_exported_exact", candidateCount: 1 };
|
|
569
|
+
if (rows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple exported symbol targets matched exactly", strategy: "exported_exact", candidateCount: rows.length };
|
|
570
|
+
return { id: null, status: "unresolved", reason: "No local symbol target matched exactly", strategy: evidence.relation === "relative_import_proxy_member" ? "proxy_member_no_global_name_fallback" : "exact_symbol_match", candidateCount: 0 };
|
|
571
|
+
}
|
|
566
572
|
function insertCalls(db, repoId, rows) {
|
|
567
573
|
const stmt = db.prepare(
|
|
568
|
-
"INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,service_binding_id) VALUES(?,(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1),?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))"
|
|
574
|
+
"INSERT INTO outbound_calls(repo_id,source_symbol_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,local_service_name,local_service_lookup,alias_chain_json,service_binding_id) VALUES(?,COALESCE((SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND qualified_name=? ORDER BY id LIMIT 1),(SELECT id FROM symbols WHERE repo_id=? AND source_file=? AND start_line<=? AND end_line>=? ORDER BY (end_line-start_line),id LIMIT 1)),?,?,?,?,?,?,?,?,?,?,?,?,?,(SELECT id FROM service_bindings WHERE repo_id=? AND variable_name=? AND source_file=? ORDER BY CASE WHEN source_line<=? THEN 0 ELSE 1 END, ABS(source_line-?) ASC, id DESC LIMIT 1))"
|
|
569
575
|
);
|
|
570
576
|
for (const r of rows)
|
|
571
577
|
stmt.run(
|
|
572
578
|
repoId,
|
|
573
579
|
repoId,
|
|
574
580
|
r.sourceFile,
|
|
581
|
+
r.sourceSymbolQualifiedName,
|
|
582
|
+
repoId,
|
|
583
|
+
r.sourceFile,
|
|
575
584
|
r.sourceLine,
|
|
576
585
|
r.sourceLine,
|
|
577
586
|
r.callType,
|
|
@@ -766,6 +775,9 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
766
775
|
if (ts.isMethodDeclaration(node)) {
|
|
767
776
|
const localName = nameOf(node.name);
|
|
768
777
|
if (localName) addSymbol("method", localName, node, parentClass);
|
|
778
|
+
} else if (ts.isPropertyDeclaration(node) && parentClass && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
779
|
+
const localName = nameOf(node.name);
|
|
780
|
+
if (localName) addSymbol("method", localName, node.initializer, parentClass, void 0, { source: "class_property_function", memberKind: ts.isArrowFunction(node.initializer) ? "arrow_function_property" : "function_expression_property" });
|
|
769
781
|
} else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
|
|
770
782
|
else if (ts.isVariableStatement(node)) {
|
|
771
783
|
for (const d of node.declarationList.declarations) {
|
|
@@ -790,11 +802,41 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
790
802
|
} else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
|
|
791
803
|
};
|
|
792
804
|
visitSymbols(source);
|
|
805
|
+
const outboundCallNames = /* @__PURE__ */ new Set(["cds.run", "emit", "publish", "on", "send", "axios", "executeHttpRequest", "useOrFetchDestination"]);
|
|
806
|
+
const containsSupportedOutbound = (node) => {
|
|
807
|
+
let found = false;
|
|
808
|
+
const visit = (child) => {
|
|
809
|
+
if (found) return;
|
|
810
|
+
if (ts.isCallExpression(child)) {
|
|
811
|
+
const callee = callName(child.expression);
|
|
812
|
+
if (outboundCallNames.has(callee.expression) || (callee.member ? outboundCallNames.has(callee.member) : false)) found = true;
|
|
813
|
+
}
|
|
814
|
+
ts.forEachChild(child, visit);
|
|
815
|
+
};
|
|
816
|
+
visit(node);
|
|
817
|
+
return found;
|
|
818
|
+
};
|
|
819
|
+
const isTopLevelCallback = (node) => {
|
|
820
|
+
if (!ts.isArrowFunction(node) && !ts.isFunctionExpression(node)) return false;
|
|
821
|
+
if (!ts.isCallExpression(node.parent)) return false;
|
|
822
|
+
const callee = callName(node.parent.expression);
|
|
823
|
+
const member = callee.member ?? callee.local;
|
|
824
|
+
return Boolean(member && ["bootstrap", "served", "connect", "on", "once", "use", "get", "post", "put", "patch", "delete", "subscribe"].includes(member));
|
|
825
|
+
};
|
|
826
|
+
const visitCallbackSymbols = (node) => {
|
|
827
|
+
if (isTopLevelCallback(node) && containsSupportedOutbound(node)) {
|
|
828
|
+
const startLine = lineOf(source, node.getStart(source));
|
|
829
|
+
const name = `callback:${startLine}`;
|
|
830
|
+
symbols.push({ kind: "callback", localName: name, qualifiedName: `module:${sourceFile}#${name}`, sourceFile, startLine, endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: false, importExportEvidence: { source: "synthetic_outbound_callback", callbackLine: startLine } });
|
|
831
|
+
}
|
|
832
|
+
ts.forEachChild(node, visitCallbackSymbols);
|
|
833
|
+
};
|
|
834
|
+
visitCallbackSymbols(source);
|
|
793
835
|
const visitProxyVariables = (node) => {
|
|
794
836
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isPropertyAccessExpression(node.initializer.expression)) {
|
|
795
837
|
const callee = callName(node.initializer.expression);
|
|
796
838
|
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 });
|
|
839
|
+
if (callee.member && importSource && isRelativeImport(importSource)) proxyVariables.set(node.name.text, { importSource, factory: callee.expression, variableName: node.name.text });
|
|
798
840
|
}
|
|
799
841
|
ts.forEachChild(node, visitProxyVariables);
|
|
800
842
|
};
|
|
@@ -820,7 +862,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
820
862
|
const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
|
|
821
863
|
const resolvedTarget = provenThisMethod ? thisTarget : targetName;
|
|
822
864
|
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport);
|
|
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 } });
|
|
865
|
+
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, proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
|
|
824
866
|
}
|
|
825
867
|
}
|
|
826
868
|
ts.forEachChild(node, visitCalls);
|
|
@@ -1097,6 +1139,8 @@ function parserQualityDiagnostics(db, strict) {
|
|
|
1097
1139
|
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();
|
|
1098
1140
|
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
1141
|
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();
|
|
1142
|
+
const ownerlessByType = db.prepare("SELECT call_type callType, COUNT(*) count FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY call_type ORDER BY count DESC, call_type").all();
|
|
1143
|
+
const ownerlessByGap = db.prepare("SELECT CASE WHEN source_line <= 1 THEN 'module_preamble' ELSE 'no_indexed_enclosing_symbol' END gap, COUNT(*) count FROM outbound_calls WHERE source_symbol_id IS NULL GROUP BY gap ORDER BY count DESC, gap").all();
|
|
1100
1144
|
const symbolTotal = Number(symbol.total ?? 0);
|
|
1101
1145
|
const symbolUnresolved = Number(symbol.unresolved ?? 0);
|
|
1102
1146
|
const symbolUnresolvedRatio = symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4));
|
|
@@ -1110,7 +1154,7 @@ function parserQualityDiagnostics(db, strict) {
|
|
|
1110
1154
|
{ 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
1155
|
{ 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
1156
|
{ 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 }
|
|
1157
|
+
{ 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, ownerlessByType, ownerlessByGap }
|
|
1114
1158
|
];
|
|
1115
1159
|
}
|
|
1116
1160
|
function createProgram() {
|
|
@@ -1137,7 +1181,7 @@ function createProgram() {
|
|
|
1137
1181
|
(opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
|
|
1138
1182
|
const r = linkWorkspace(db, workspaceId);
|
|
1139
1183
|
process.stdout.write(
|
|
1140
|
-
`Linked ${r.edgeCount} edges: ${r.
|
|
1184
|
+
`Linked ${r.edgeCount} edges: ${r.remoteResolvedCount} remote operation calls resolved, ${r.localResolvedCount} local operation calls resolved, ${r.unresolvedCount} unresolved operation calls, ${r.ambiguousCount} ambiguous operation calls, ${r.dynamicCount} dynamic operation calls, ${r.terminalCount} terminal call edges, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved
|
|
1141
1185
|
`
|
|
1142
1186
|
);
|
|
1143
1187
|
}).catch(fail)
|