@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/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  parsePackageJson,
11
11
  parseServiceBindings,
12
12
  trace
13
- } from "./chunk-ZSUL2X77.js";
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.15",
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 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)`);
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 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
- );
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.resolvedCount} remote resolved, ${r.unresolvedCount} remote unresolved, ${r.ambiguousCount} remote ambiguous, ${r.dynamicCount} dynamic, ${r.terminalCount} terminal, ${r.dependencyResolvedCount} dependency resolved, ${r.dependencyAmbiguousCount} dependency ambiguous, ${r.implementationResolvedCount} implementation resolved, ${r.implementationAmbiguousCount} implementation ambiguous, ${r.implementationUnresolvedCount} implementation unresolved
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)