@saptools/service-flow 0.1.23 → 0.1.24
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 +12 -0
- package/dist/{chunk-LCXDOSWL.js → chunk-RXJNPONU.js} +93 -23
- package/dist/chunk-RXJNPONU.js.map +1 -0
- package/dist/cli.js +69 -19
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-LCXDOSWL.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
parsePackageJson,
|
|
13
13
|
parseServiceBindings,
|
|
14
14
|
trace
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-RXJNPONU.js";
|
|
16
16
|
|
|
17
17
|
// src/cli.ts
|
|
18
18
|
import { Command } from "commander";
|
|
@@ -188,7 +188,7 @@ function migrate(db) {
|
|
|
188
188
|
// package.json
|
|
189
189
|
var package_default = {
|
|
190
190
|
name: "@saptools/service-flow",
|
|
191
|
-
version: "0.1.
|
|
191
|
+
version: "0.1.24",
|
|
192
192
|
description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
|
|
193
193
|
type: "module",
|
|
194
194
|
publishConfig: {
|
|
@@ -554,11 +554,19 @@ function insertSymbolCalls(db, repoId, rows) {
|
|
|
554
554
|
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);
|
|
555
555
|
}
|
|
556
556
|
}
|
|
557
|
+
function isRelativeImportedSymbolCall(r) {
|
|
558
|
+
return Boolean(r.importSource?.startsWith("."));
|
|
559
|
+
}
|
|
557
560
|
function resolveSymbolCallTarget(db, repoId, r) {
|
|
558
561
|
const evidence = r.evidence;
|
|
559
562
|
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);
|
|
560
563
|
if (localRows.length === 1) return { id: localRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "same_file_exact", candidateCount: 1 };
|
|
561
564
|
if (localRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple same-file symbol targets matched exactly", strategy: "same_file_exact", candidateCount: localRows.length };
|
|
565
|
+
if (evidence.relation === "class_instance_method" && isRelativeImportedSymbolCall(r)) {
|
|
566
|
+
const classRows = db.prepare("SELECT id FROM symbols WHERE repo_id=? AND source_file<>? AND qualified_name=? ORDER BY id").all(repoId, r.sourceFile, r.calleeLocalName);
|
|
567
|
+
if (classRows.length === 1) return { id: classRows[0]?.id ?? null, status: "resolved", reason: null, strategy: "relative_import_class_instance_method", candidateCount: 1 };
|
|
568
|
+
if (classRows.length > 1) return { id: null, status: "ambiguous", reason: "Multiple relative class instance method targets matched exactly", strategy: "relative_import_class_instance_method", candidateCount: classRows.length };
|
|
569
|
+
}
|
|
562
570
|
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);
|
|
563
571
|
if (evidence.relation === "relative_import_proxy_member" && rows.length > 1) {
|
|
564
572
|
const objectMapRows = rows.filter((row) => String(row.evidenceJson ?? "").includes("exported_object_shorthand") || String(row.evidenceJson ?? "").includes("exported_object_literal"));
|
|
@@ -672,9 +680,9 @@ function isFunctionLike(node) {
|
|
|
672
680
|
function exported(node) {
|
|
673
681
|
return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
|
|
674
682
|
}
|
|
675
|
-
function
|
|
683
|
+
function isPublicClassMethod(node) {
|
|
676
684
|
const flags = ts.getCombinedModifierFlags(node);
|
|
677
|
-
return (flags & ts.ModifierFlags.
|
|
685
|
+
return (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
|
|
678
686
|
}
|
|
679
687
|
function exportDeclarations(source) {
|
|
680
688
|
const exports = /* @__PURE__ */ new Map();
|
|
@@ -725,6 +733,23 @@ function ignoredFrameworkCall(callee) {
|
|
|
725
733
|
function nearest(symbols, line) {
|
|
726
734
|
return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine))[0];
|
|
727
735
|
}
|
|
736
|
+
function argumentEvidence(args, source) {
|
|
737
|
+
return args.map((arg) => {
|
|
738
|
+
if (ts.isIdentifier(arg)) return { kind: "identifier", name: arg.text };
|
|
739
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
740
|
+
const properties = [];
|
|
741
|
+
for (const prop of arg.properties) {
|
|
742
|
+
if (ts.isShorthandPropertyAssignment(prop)) properties.push({ kind: "shorthand", property: prop.name.text, argument: prop.name.text });
|
|
743
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
744
|
+
const propName = nameOf(prop.name);
|
|
745
|
+
if (propName && ts.isIdentifier(prop.initializer)) properties.push({ kind: "property_assignment", property: propName, argument: prop.initializer.text });
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return { kind: "object_literal", properties };
|
|
749
|
+
}
|
|
750
|
+
return { kind: "unsupported", expression: arg.getText(source) };
|
|
751
|
+
});
|
|
752
|
+
}
|
|
728
753
|
async function parseExecutableSymbols(repoPath, filePath) {
|
|
729
754
|
const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
|
|
730
755
|
const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
|
|
@@ -736,15 +761,17 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
736
761
|
const objectExports = /* @__PURE__ */ new Set();
|
|
737
762
|
const exportedClasses = /* @__PURE__ */ new Set();
|
|
738
763
|
const proxyVariables = /* @__PURE__ */ new Map();
|
|
764
|
+
const classInstances = /* @__PURE__ */ new Map();
|
|
739
765
|
const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
|
|
740
766
|
const parentRoot = parentName?.split(".")[0] ?? "";
|
|
741
767
|
const declaredExportName = exportedName ?? exportNames.get(parentName ? parentRoot : localName);
|
|
742
768
|
const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
|
|
743
769
|
const objectExported = parentName ? objectExports.has(parentRoot) : false;
|
|
744
|
-
const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) &&
|
|
770
|
+
const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 && isPublicClassMethod(node) : false;
|
|
745
771
|
const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
|
|
746
|
-
const
|
|
747
|
-
|
|
772
|
+
const params = isFunctionLike(node) ? node.parameters.map((param) => nameOf(param.name)).filter((name) => Boolean(name)) : void 0;
|
|
773
|
+
const sourceEvidence = evidence ?? (classMemberExported ? { source: "exported_class_member", exportedClass: parentRoot, memberKind: (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 ? "static_method" : "class_method", parameters: params } : declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0);
|
|
774
|
+
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: params && sourceEvidence ? { ...sourceEvidence, parameters: params } : sourceEvidence });
|
|
748
775
|
};
|
|
749
776
|
const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
|
|
750
777
|
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 } });
|
|
@@ -845,6 +872,15 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
845
872
|
ts.forEachChild(node, visitProxyVariables);
|
|
846
873
|
};
|
|
847
874
|
visitProxyVariables(source);
|
|
875
|
+
const visitClassInstances = (node) => {
|
|
876
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
|
|
877
|
+
const className = node.initializer.expression.text;
|
|
878
|
+
const importSource = imports.get(className);
|
|
879
|
+
if (!importSource || isRelativeImport(importSource)) classInstances.set(node.name.text, { className, importSource });
|
|
880
|
+
}
|
|
881
|
+
ts.forEachChild(node, visitClassInstances);
|
|
882
|
+
};
|
|
883
|
+
visitClassInstances(source);
|
|
848
884
|
const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
|
|
849
885
|
const visitCalls = (node) => {
|
|
850
886
|
if (ts.isCallExpression(node)) {
|
|
@@ -853,8 +889,9 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
853
889
|
if (caller) {
|
|
854
890
|
const callee = callName(node.expression);
|
|
855
891
|
const proxy = callee.local ? proxyVariables.get(callee.local) : void 0;
|
|
856
|
-
const
|
|
857
|
-
const
|
|
892
|
+
const instance = callee.local ? classInstances.get(callee.local) : void 0;
|
|
893
|
+
const importSource = instance?.importSource ?? proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
|
|
894
|
+
const targetName = instance && callee.member ? `${instance.className}.${callee.member}` : proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
|
|
858
895
|
const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
|
|
859
896
|
const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
|
|
860
897
|
const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
|
|
@@ -862,11 +899,12 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
862
899
|
const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
|
|
863
900
|
const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
|
|
864
901
|
const provenRelativeImport = Boolean(isRelativeImport(importSource) && targetName);
|
|
902
|
+
const provenClassInstance = Boolean(instance && callee.member && targetName);
|
|
865
903
|
const importedFromPackage = Boolean(importSource && !isRelativeImport(importSource));
|
|
866
904
|
const ignored = loggerLike || terminalMember || importedFromPackage || ignoredFrameworkCall(callee);
|
|
867
905
|
const resolvedTarget = provenThisMethod ? thisTarget : targetName;
|
|
868
|
-
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport);
|
|
869
|
-
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 } });
|
|
906
|
+
const keep = Boolean(resolvedTarget) && !ignored && (provenLocal || provenThisMethod || provenRelativeImport || provenClassInstance);
|
|
907
|
+
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: instance ? "class_instance_method" : proxy ? "relative_import_proxy_member" : importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : "indexed_local_symbol", caller: caller.qualifiedName, targetName: resolvedTarget, instanceVariable: instance ? callee.local : void 0, className: instance?.className, methodName: instance ? callee.member : void 0, classImportSource: instance?.importSource, callArguments: argumentEvidence(node.arguments, source), proxyVariableName: proxy?.variableName, factory: proxy?.factory, factoryExpression: proxy?.factory, factoryImportSource: proxy?.importSource, candidateStrategy: instance ? instance.importSource ? "relative_import_class_instance_method" : "same_file_class_instance_method" : proxy ? "proxy_member_exact_export_or_unique_member" : void 0 } });
|
|
870
908
|
}
|
|
871
909
|
}
|
|
872
910
|
ts.forEachChild(node, visitCalls);
|
|
@@ -1211,22 +1249,34 @@ function identityAliasBindingQuality(db) {
|
|
|
1211
1249
|
return { severity: examples.length > 0 ? "warning" : "info", code: "strict_identity_alias_binding_quality", message: "Remote sends that look like missed same-file identity aliases", missedAliasBindingCalls: examples.length, examples };
|
|
1212
1250
|
}
|
|
1213
1251
|
function remoteActionNoBindingQuality(db) {
|
|
1214
|
-
const
|
|
1252
|
+
const categoryCase = `CASE
|
|
1253
|
+
WHEN c.unresolved_reason='dynamic_operation_path_identifier' THEN 'dynamic_path_identifier'
|
|
1254
|
+
WHEN json_extract(c.evidence_json,'$.classifier')='higher_order_wrapper_literal_path' OR json_extract(c.evidence_json,'$.operationPathExpression') IS NOT NULL THEN 'likely_higher_order_wrapper_path_needed'
|
|
1255
|
+
WHEN json_extract(c.evidence_json,'$.receiver') LIKE '%.%' THEN 'likely_parameter_context_needed'
|
|
1256
|
+
WHEN EXISTS (SELECT 1 FROM symbol_calls sc WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.relation')='class_instance_method' AND sc.source_file=c.source_file) THEN 'likely_instance_method_context_needed'
|
|
1257
|
+
WHEN EXISTS (SELECT 1 FROM service_bindings b WHERE b.repo_id=c.repo_id AND b.source_file=c.source_file AND ABS(b.source_line-c.source_line) < 50) THEN 'likely_missing_assignment_binding'
|
|
1258
|
+
WHEN e.status='unresolved' AND COALESCE(e.unresolved_reason,'') LIKE '%No indexed target operation%' THEN 'no_indexed_target_operation'
|
|
1259
|
+
WHEN c.operation_path_expr IS NOT NULL AND (c.operation_path_expr LIKE '/%' OR c.operation_path_expr NOT LIKE '%/%') THEN 'operation_path_only_no_static_service_signal'
|
|
1260
|
+
ELSE 'external_or_entity_path_not_action' END`;
|
|
1261
|
+
const rows = db.prepare(`SELECT ${categoryCase} category,COALESCE(e.status,'missing_edge') status,COUNT(*) count
|
|
1215
1262
|
FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
|
|
1216
1263
|
WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL
|
|
1217
|
-
GROUP BY
|
|
1218
|
-
const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath,COALESCE(e.status,'missing_edge') status
|
|
1219
|
-
CASE WHEN EXISTS (SELECT 1 FROM service_bindings b WHERE b.repo_id=c.repo_id AND b.source_file=c.source_file AND COALESCE(b.helper_chain_json,'') LIKE '%' || '"aliasOf":"' || json_extract(c.evidence_json,'$.receiver') || '"' || '%') THEN 'likely_missed_alias_or_helper_propagation' ELSE 'operation_path_only_call' END category
|
|
1264
|
+
GROUP BY category,status ORDER BY count DESC,category,status`).all();
|
|
1265
|
+
const examples = db.prepare(`SELECT c.source_file sourceFile,c.source_line sourceLine,json_extract(c.evidence_json,'$.receiver') receiverName,c.operation_path_expr operationPath,COALESCE(e.status,'missing_edge') status,${categoryCase} category
|
|
1220
1266
|
FROM outbound_calls c LEFT JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
|
|
1221
|
-
WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL ORDER BY c.source_file,c.source_line LIMIT
|
|
1267
|
+
WHERE c.call_type='remote_action' AND c.operation_path_expr IS NOT NULL AND c.service_binding_id IS NULL ORDER BY c.source_file,c.source_line LIMIT 8`).all();
|
|
1222
1268
|
const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
|
|
1223
1269
|
return { severity: total > 0 ? "warning" : "info", code: "strict_remote_action_no_binding_quality", message: "Remote actions with operation paths but no service binding id", total, breakdown: rows, examples };
|
|
1224
1270
|
}
|
|
1225
1271
|
function contextualImplementationQuality(db) {
|
|
1226
|
-
const rows = db.prepare(`SELECT COALESCE(
|
|
1227
|
-
FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') GROUP BY status ORDER BY count DESC,
|
|
1272
|
+
const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count
|
|
1273
|
+
FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') GROUP BY status,reason ORDER BY status,count DESC,reason`).all();
|
|
1274
|
+
const examples = db.prepare(`SELECT json_extract(evidence_json,'$.servicePath') servicePath,json_extract(evidence_json,'$.operationPath') operationPath,status,unresolved_reason unresolvedReason,
|
|
1275
|
+
json_extract(evidence_json,'$.candidates[0].rejectedReasons[0]') topRejectedReason,
|
|
1276
|
+
json_extract(evidence_json,'$.candidates[0].acceptedReasons[0]') topAcceptedReason
|
|
1277
|
+
FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') ORDER BY status,id LIMIT 6`).all();
|
|
1228
1278
|
const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
|
|
1229
|
-
return { severity: total > 0 ? "warning" : "info", code: "strict_contextual_implementation_quality", message: "
|
|
1279
|
+
return { severity: total > 0 ? "warning" : "info", code: "strict_contextual_implementation_quality", message: "Implementation hops stopped by ambiguous or unresolved implementation edges", total, breakdown: rows, examples };
|
|
1230
1280
|
}
|
|
1231
1281
|
function wrapperPathPropagationQuality(db) {
|
|
1232
1282
|
const examples = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,json_extract(evidence_json,'$.receiver') receiverName,json_extract(evidence_json,'$.operationPathExpression') pathIdentifier,CASE WHEN json_extract(evidence_json,'$.literalCallerArgumentDetected') IS NOT NULL THEN 1 ELSE 0 END literalCallerArgumentDetected
|