@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/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  parsePackageJson,
11
11
  parseServiceBindings,
12
12
  trace
13
- } from "./chunk-UPXFMLUY.js";
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.14",
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 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
- 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, 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);
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 addSymbol = (kind, localName, node, parentName, exportedName) => {
705
- const declaredExportName = exportedName ?? exportNames.get(parentName ? parentName.split(".")[0] ?? localName : localName);
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(parentName.split(".")[0] ?? "") : false;
708
- 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 });
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
- if (exported(node) || exportNames.has(localName)) objectExports.add(localName);
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 importSource = (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
769
- const targetName = callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
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: "strict_symbol_call_quality", message: "Symbol-call quality aggregate", total: symbolTotal, resolved: Number(symbol.resolved ?? 0), unresolved: symbolUnresolved, unresolvedRatio: symbolTotal === 0 ? 0 : Number((symbolUnresolved / symbolTotal).toFixed(4)), topUnresolvedCallees: top },
1059
- { severity: "info", code: "strict_db_query_quality", message: "Local DB query quality aggregate", total: queryTotal, known: Number(dbq.known ?? 0), unknown: queryUnknown, unknownRatio: queryTotal === 0 ? 0 : Number((queryUnknown / queryTotal).toFixed(4)) }
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() {