@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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  parsePackageJson,
13
13
  parseServiceBindings,
14
14
  trace
15
- } from "./chunk-LCXDOSWL.js";
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.23",
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 isPublicStaticMethod(node) {
683
+ function isPublicClassMethod(node) {
676
684
  const flags = ts.getCombinedModifierFlags(node);
677
- return (flags & ts.ModifierFlags.Static) !== 0 && (flags & ts.ModifierFlags.Private) === 0 && (flags & ts.ModifierFlags.Protected) === 0;
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) && isPublicStaticMethod(node) : false;
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 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);
747
- 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 });
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 importSource = proxy?.importSource ?? (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
857
- const targetName = proxy && callee.member ? callee.member : callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
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 rows = db.prepare(`SELECT COALESCE(json_extract(c.evidence_json,'$.receiver'),'unknown') receiverName,COALESCE(e.status,'missing_edge') status,CASE WHEN operation_path_expr LIKE '%(%' THEN 1 ELSE 0 END isODataInvocation,COUNT(*) count
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 receiverName,status,isODataInvocation ORDER BY count DESC,receiverName,status`).all();
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 5`).all();
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(json_extract(evidence_json,'$.contextualImplementation.status'),'not_applicable') status,COUNT(*) count
1227
- FROM graph_edges WHERE edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND status IN ('ambiguous','unresolved') GROUP BY status ORDER BY count DESC,status`).all();
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: "Runtime-resolved implementation hops stopped by ambiguous or unresolved implementation edges", total, breakdown: rows };
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