@saptools/service-flow 0.1.11 → 0.1.13
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 +20 -0
- package/README.md +6 -4
- package/TECHNICAL-NOTE.md +10 -1
- package/dist/{chunk-GG4XJGES.js → chunk-Q7W7TE3W.js} +158 -37
- package/dist/chunk-Q7W7TE3W.js.map +1 -0
- package/dist/cli.js +89 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-GG4XJGES.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-Q7W7TE3W.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.13",
|
|
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: {
|
|
@@ -545,7 +545,7 @@ function insertExecutableSymbols(db, repoId, rows) {
|
|
|
545
545
|
function insertSymbolCalls(db, repoId, rows) {
|
|
546
546
|
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),(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),?,?,?,?,?,0.8,?,CASE WHEN (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) IS NULL THEN 'No local symbol target matched exactly' ELSE NULL END)`);
|
|
547
547
|
for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.callerQualifiedName, repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, JSON.stringify(r.evidence), repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile);
|
|
548
|
-
db.prepare("UPDATE symbol_calls SET status=CASE WHEN callee_symbol_id IS NULL THEN 'unresolved' ELSE 'resolved' END WHERE repo_id=?").run(repoId);
|
|
548
|
+
db.prepare("UPDATE symbol_calls SET status=CASE WHEN callee_symbol_id IS NULL THEN 'unresolved' ELSE 'resolved' END, unresolved_reason=CASE WHEN callee_symbol_id IS NULL THEN COALESCE(unresolved_reason,'No local symbol target matched exactly') ELSE NULL END WHERE repo_id=?").run(repoId);
|
|
549
549
|
}
|
|
550
550
|
function insertCalls(db, repoId, rows) {
|
|
551
551
|
const stmt = db.prepare(
|
|
@@ -643,11 +643,31 @@ function isFunctionLike(node) {
|
|
|
643
643
|
function exported(node) {
|
|
644
644
|
return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
|
|
645
645
|
}
|
|
646
|
+
function exportDeclarations(source) {
|
|
647
|
+
const exports = /* @__PURE__ */ new Map();
|
|
648
|
+
const visit = (node) => {
|
|
649
|
+
if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
650
|
+
for (const el of node.exportClause.elements) exports.set((el.propertyName ?? el.name).text, el.name.text);
|
|
651
|
+
}
|
|
652
|
+
ts.forEachChild(node, visit);
|
|
653
|
+
};
|
|
654
|
+
visit(source);
|
|
655
|
+
return exports;
|
|
656
|
+
}
|
|
657
|
+
function isRelativeImport(value) {
|
|
658
|
+
return Boolean(value?.startsWith("."));
|
|
659
|
+
}
|
|
660
|
+
function isObjectFunction(node) {
|
|
661
|
+
return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
|
|
662
|
+
}
|
|
663
|
+
var commonTerminalMembers = /* @__PURE__ */ new Set(["push", "includes", "find", "findIndex", "map", "filter", "reduce", "forEach", "some", "every", "toUpperCase", "toLowerCase", "trim", "split", "join", "get", "set", "has"]);
|
|
664
|
+
var loggerMembers = /* @__PURE__ */ new Set(["trace", "debug", "info", "warn", "error", "fatal", "log"]);
|
|
665
|
+
var globalObjects = /* @__PURE__ */ new Set(["JSON", "Object", "Array", "String", "Number", "Boolean", "Math", "Date", "Promise", "Reflect"]);
|
|
646
666
|
function callName(expr) {
|
|
647
667
|
if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
|
|
648
668
|
if (ts.isPropertyAccessExpression(expr)) {
|
|
649
669
|
const left = expr.expression.getText();
|
|
650
|
-
return { expression: expr.getText(), local: left === "this" ? void 0 : left, member: expr.name.text };
|
|
670
|
+
return { expression: expr.getText(), local: left === "this" ? void 0 : left, member: expr.name.text, receiver: left };
|
|
651
671
|
}
|
|
652
672
|
return { expression: expr.getText() };
|
|
653
673
|
}
|
|
@@ -661,8 +681,13 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
661
681
|
const symbols = [];
|
|
662
682
|
const calls = [];
|
|
663
683
|
const imports = /* @__PURE__ */ new Map();
|
|
684
|
+
const exportNames = exportDeclarations(source);
|
|
685
|
+
const objectExports = /* @__PURE__ */ new Set();
|
|
664
686
|
const addSymbol = (kind, localName, node, parentName, exportedName) => {
|
|
665
|
-
|
|
687
|
+
const declaredExportName = exportedName ?? exportNames.get(parentName ? parentName.split(".")[0] ?? localName : localName);
|
|
688
|
+
const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
|
|
689
|
+
const objectExported = parentName ? objectExports.has(parentName.split(".")[0] ?? "") : false;
|
|
690
|
+
symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: objectExported ? qualifiedName : declaredExportName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(declaredExportName) || objectExported, importExportEvidence: declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0 });
|
|
666
691
|
};
|
|
667
692
|
const visitImports = (node) => {
|
|
668
693
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
@@ -686,20 +711,46 @@ async function parseExecutableSymbols(repoPath, filePath) {
|
|
|
686
711
|
if (localName) addSymbol("method", localName, node, parentClass);
|
|
687
712
|
} else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
|
|
688
713
|
else if (ts.isVariableStatement(node)) {
|
|
689
|
-
for (const d of node.declarationList.declarations)
|
|
714
|
+
for (const d of node.declarationList.declarations) {
|
|
690
715
|
const localName = nameOf(d.name);
|
|
691
|
-
if (localName
|
|
716
|
+
if (!localName || !d.initializer) continue;
|
|
717
|
+
if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
|
|
718
|
+
if (ts.isObjectLiteralExpression(d.initializer)) {
|
|
719
|
+
if (exported(node) || exportNames.has(localName)) objectExports.add(localName);
|
|
720
|
+
for (const prop of d.initializer.properties) {
|
|
721
|
+
if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
|
|
722
|
+
const propName = nameOf(prop.name);
|
|
723
|
+
if (propName) addSymbol("object_method", propName, prop.initializer, localName);
|
|
724
|
+
} else if (ts.isMethodDeclaration(prop)) {
|
|
725
|
+
const propName = nameOf(prop.name);
|
|
726
|
+
if (propName) addSymbol("object_method", propName, prop, localName);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
692
730
|
}
|
|
693
731
|
} else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
|
|
694
732
|
};
|
|
695
733
|
visitSymbols(source);
|
|
734
|
+
const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
|
|
696
735
|
const visitCalls = (node) => {
|
|
697
736
|
if (ts.isCallExpression(node)) {
|
|
698
737
|
const line = lineOf(source, node.getStart(source));
|
|
699
738
|
const caller = nearest(symbols, line);
|
|
700
739
|
if (caller) {
|
|
701
740
|
const callee = callName(node.expression);
|
|
702
|
-
|
|
741
|
+
const importSource = (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
|
|
742
|
+
const targetName = callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
|
|
743
|
+
const className = caller.qualifiedName.includes(".") ? caller.qualifiedName.split(".")[0] : void 0;
|
|
744
|
+
const thisTarget = className && callee.member ? `${className}.${callee.member}` : void 0;
|
|
745
|
+
const loggerLike = callee.receiver?.endsWith(".logger") || callee.local === "logger" || (callee.expression.startsWith("this.logger.") && callee.member ? loggerMembers.has(callee.member) : false);
|
|
746
|
+
const terminalMember = callee.member ? commonTerminalMembers.has(callee.member) || loggerMembers.has(callee.member) : false;
|
|
747
|
+
const provenLocal = Boolean(targetName) && localCallables.has(String(targetName));
|
|
748
|
+
const provenThisMethod = Boolean(thisTarget && localCallables.has(thisTarget));
|
|
749
|
+
const provenRelativeImport = Boolean(isRelativeImport(importSource));
|
|
750
|
+
const globalLike = callee.local ? globalObjects.has(callee.local) : false;
|
|
751
|
+
const importedFromPackage = Boolean(importSource && !isRelativeImport(importSource));
|
|
752
|
+
const keep = Boolean(targetName) && !loggerLike && !globalLike && !importedFromPackage && (provenLocal || provenThisMethod || provenRelativeImport || !terminalMember && !callee.expression.startsWith("this."));
|
|
753
|
+
if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: provenThisMethod ? thisTarget : targetName, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: importSource ? "relative_import" : provenThisMethod ? "indexed_this_method" : provenLocal ? "indexed_local_symbol" : "unresolved_actionable_property_call", caller: caller.qualifiedName, targetName: provenThisMethod ? thisTarget : targetName } });
|
|
703
754
|
}
|
|
704
755
|
}
|
|
705
756
|
ts.forEachChild(node, visitCalls);
|
|
@@ -854,10 +905,14 @@ function location(evidence) {
|
|
|
854
905
|
}
|
|
855
906
|
return ":";
|
|
856
907
|
}
|
|
908
|
+
function displayTarget(value, evidence) {
|
|
909
|
+
if (/^\d+$/.test(value) && evidence.parserWarning) return "Entity: unknown";
|
|
910
|
+
return value;
|
|
911
|
+
}
|
|
857
912
|
function renderTraceTable(result) {
|
|
858
913
|
const lines = ["Step Type From To Evidence"];
|
|
859
914
|
for (const e of result.edges) {
|
|
860
|
-
lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
|
|
915
|
+
lines.push(`${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${displayTarget(e.to, e.evidence).slice(0, 35).padEnd(36)} ${location(e.evidence)}`);
|
|
861
916
|
}
|
|
862
917
|
if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.map((d) => `${String(d.severity ?? "info")} ${String(d.code ?? "diagnostic")} ${String(d.message ?? "")}`));
|
|
863
918
|
return `${lines.join("\n")}
|
|
@@ -877,11 +932,17 @@ function renderTraceJson(trace2) {
|
|
|
877
932
|
function safe(value) {
|
|
878
933
|
return value.replace(/[^\w-]/g, "_").slice(0, 60);
|
|
879
934
|
}
|
|
935
|
+
function label(trace2, idOrLabel) {
|
|
936
|
+
const edge = trace2.edges.find((item) => item.to === idOrLabel);
|
|
937
|
+
if (/^\d+$/.test(idOrLabel) && edge?.evidence.parserWarning) return "Entity: unknown";
|
|
938
|
+
const node = trace2.nodes.find((item) => item.id === idOrLabel || item.label === idOrLabel);
|
|
939
|
+
return String(node?.label ?? idOrLabel);
|
|
940
|
+
}
|
|
880
941
|
function renderMermaid(trace2) {
|
|
881
942
|
const lines = ["flowchart TD"];
|
|
882
943
|
for (const e of trace2.edges)
|
|
883
944
|
lines.push(
|
|
884
|
-
` ${safe(e.from)}["${e.from}"] -->|${e.type}| ${safe(e.to)}["${e.to}"]`
|
|
945
|
+
` ${safe(e.from)}["${label(trace2, e.from)}"] -->|${e.type}| ${safe(e.to)}["${label(trace2, e.to)}"]`
|
|
885
946
|
);
|
|
886
947
|
return `${lines.join("\n")}
|
|
887
948
|
`;
|
|
@@ -941,6 +1002,19 @@ async function withReadOnlyWorkspace(workspace, fn) {
|
|
|
941
1002
|
db.close();
|
|
942
1003
|
}
|
|
943
1004
|
}
|
|
1005
|
+
function localServiceDiagnostics(db, strict) {
|
|
1006
|
+
const rows = db.prepare(`SELECT e.status status,e.unresolved_reason reason,e.evidence_json evidenceJson FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call'`).all();
|
|
1007
|
+
const implementationContext = rows.filter((row) => row.status === "resolved" && String(row.evidenceJson ?? "").includes("implementation_context_caller_ownership")).length;
|
|
1008
|
+
const withoutOwnership = rows.filter((row) => row.reason === "local_service_candidate_without_caller_ownership" || String(row.evidenceJson ?? "").includes("local_service_candidate_without_caller_ownership")).length;
|
|
1009
|
+
const unresolved = rows.filter((row) => row.status === "unresolved").length;
|
|
1010
|
+
const outsideScope = rows.filter((row) => row.status === "unresolved" && String(row.evidenceJson ?? "").includes("candidateCount")).length;
|
|
1011
|
+
const out = [];
|
|
1012
|
+
if (withoutOwnership > 0) out.push({ severity: "warning", code: "local_service_candidate_without_caller_ownership", message: `Local service calls have operation candidates but no caller ownership evidence: ${withoutOwnership}` });
|
|
1013
|
+
if (outsideScope > 0) out.push({ severity: "warning", code: "local_service_candidates_outside_local_scope", message: `Local service calls found candidates outside same-repository scope: ${outsideScope}` });
|
|
1014
|
+
if (strict && unresolved > 0) out.push({ severity: "warning", code: "local_service_calls_unresolved", message: `Unresolved local service calls: ${unresolved}` });
|
|
1015
|
+
if (strict && implementationContext > 0) out.push({ severity: "info", code: "local_service_calls_resolved_by_implementation_context", message: `Local service calls resolved by implementation-context ownership: ${implementationContext}` });
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
944
1018
|
function createProgram() {
|
|
945
1019
|
const program = new Command();
|
|
946
1020
|
program.name("service-flow").description(
|
|
@@ -1138,8 +1212,8 @@ function createProgram() {
|
|
|
1138
1212
|
SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
|
|
1139
1213
|
FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
|
|
1140
1214
|
UNION ALL
|
|
1141
|
-
SELECT
|
|
1142
|
-
FROM outbound_calls WHERE source_symbol_id IS NULL HAVING COUNT(*) >=
|
|
1215
|
+
SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
|
|
1216
|
+
FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
|
|
1143
1217
|
UNION ALL
|
|
1144
1218
|
SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
|
|
1145
1219
|
WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
|
|
@@ -1149,8 +1223,9 @@ function createProgram() {
|
|
|
1149
1223
|
UNION ALL
|
|
1150
1224
|
SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
|
|
1151
1225
|
FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
|
|
1152
|
-
).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict)
|
|
1153
|
-
const
|
|
1226
|
+
).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
|
|
1227
|
+
const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
|
|
1228
|
+
const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth];
|
|
1154
1229
|
process.stdout.write(
|
|
1155
1230
|
allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
|
|
1156
1231
|
`
|