@saptools/service-flow 0.1.24 → 0.1.25

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-RXJNPONU.js";
15
+ } from "./chunk-HS746OHV.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.24",
191
+ version: "0.1.25",
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: {
@@ -704,6 +704,40 @@ function isObjectFunction(node) {
704
704
  var commonTerminalMembers = /* @__PURE__ */ new Set(["push", "includes", "find", "findIndex", "map", "filter", "reduce", "forEach", "some", "every", "toUpperCase", "toLowerCase", "trim", "split", "join", "get", "set", "has"]);
705
705
  var loggerMembers = /* @__PURE__ */ new Set(["trace", "debug", "info", "warn", "error", "fatal", "log"]);
706
706
  var globalObjects = /* @__PURE__ */ new Set(["JSON", "Object", "Array", "String", "Number", "Boolean", "Math", "Date", "Promise", "Reflect"]);
707
+ var builtInConstructors = /* @__PURE__ */ new Set([
708
+ "Set",
709
+ "Map",
710
+ "WeakSet",
711
+ "WeakMap",
712
+ "Date",
713
+ "RegExp",
714
+ "URL",
715
+ "URLSearchParams",
716
+ "Error",
717
+ "EvalError",
718
+ "RangeError",
719
+ "ReferenceError",
720
+ "SyntaxError",
721
+ "TypeError",
722
+ "URIError",
723
+ "AggregateError",
724
+ "ArrayBuffer",
725
+ "SharedArrayBuffer",
726
+ "DataView",
727
+ "Int8Array",
728
+ "Uint8Array",
729
+ "Uint8ClampedArray",
730
+ "Int16Array",
731
+ "Uint16Array",
732
+ "Int32Array",
733
+ "Uint32Array",
734
+ "Float32Array",
735
+ "Float64Array",
736
+ "BigInt64Array",
737
+ "BigUint64Array",
738
+ "Promise",
739
+ "AbortController"
740
+ ]);
707
741
  var capDslRoots = /* @__PURE__ */ new Set(["SELECT", "INSERT", "UPSERT", "UPDATE", "DELETE"]);
708
742
  var requestHelpers = /* @__PURE__ */ new Set(["reject", "error", "info", "warn", "notify"]);
709
743
  var transportMembers = /* @__PURE__ */ new Set(["emit", "publish", "send", "on"]);
@@ -750,6 +784,25 @@ function argumentEvidence(args, source) {
750
784
  return { kind: "unsupported", expression: arg.getText(source) };
751
785
  });
752
786
  }
787
+ function bindingLocalName(name, initializer) {
788
+ if (ts.isIdentifier(name)) return name.text;
789
+ if (initializer && ts.isIdentifier(initializer)) return initializer.text;
790
+ return void 0;
791
+ }
792
+ function parameterBindings(params) {
793
+ return params.flatMap((param, index) => {
794
+ if (ts.isIdentifier(param.name)) return [{ index, kind: "identifier", name: param.name.text }];
795
+ if (!ts.isObjectBindingPattern(param.name)) return [];
796
+ const properties = param.name.elements.flatMap((element) => {
797
+ if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
798
+ const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
799
+ if (!property) return [];
800
+ const local = bindingLocalName(element.name, element.initializer);
801
+ return local ? [{ property, local }] : [];
802
+ });
803
+ return properties.length > 0 ? [{ index, kind: "object_pattern", properties }] : [];
804
+ });
805
+ }
753
806
  async function parseExecutableSymbols(repoPath, filePath) {
754
807
  const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
755
808
  const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
@@ -760,6 +813,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
760
813
  const exportNames = exportDeclarations(source);
761
814
  const objectExports = /* @__PURE__ */ new Set();
762
815
  const exportedClasses = /* @__PURE__ */ new Set();
816
+ const declaredClasses = /* @__PURE__ */ new Set();
763
817
  const proxyVariables = /* @__PURE__ */ new Map();
764
818
  const classInstances = /* @__PURE__ */ new Map();
765
819
  const addSymbol = (kind, localName, node, parentName, exportedName, evidence) => {
@@ -769,9 +823,11 @@ async function parseExecutableSymbols(repoPath, filePath) {
769
823
  const objectExported = parentName ? objectExports.has(parentRoot) : false;
770
824
  const classMemberExported = kind === "method" && parentName ? exportedClasses.has(parentRoot) && ts.isMethodDeclaration(node) && (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Static) !== 0 && isPublicClassMethod(node) : false;
771
825
  const effectiveExportedName = classMemberExported || objectExported ? qualifiedName : declaredExportName;
772
- const params = isFunctionLike(node) ? node.parameters.map((param) => nameOf(param.name)).filter((name) => Boolean(name)) : void 0;
826
+ const bindings = isFunctionLike(node) ? parameterBindings(node.parameters) : void 0;
827
+ const params = bindings?.flatMap((binding) => binding.kind === "identifier" ? [binding.name] : []);
773
828
  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 });
829
+ const parameterEvidence = bindings && bindings.length > 0 ? { parameters: params, parameterBindings: bindings } : {};
830
+ 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 ? { ...sourceEvidence, ...parameterEvidence } : bindings && bindings.length > 0 ? parameterEvidence : void 0 });
775
831
  };
776
832
  const addAliasSymbol = (objectName, propertyName, node, targetImportSource) => {
777
833
  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 } });
@@ -799,6 +855,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
799
855
  visitImports(source);
800
856
  const visitSymbols = (node, parentClass) => {
801
857
  if (ts.isClassDeclaration(node) && node.name) {
858
+ declaredClasses.add(node.name.text);
802
859
  if (exported(node) || exportNames.has(node.name.text)) exportedClasses.add(node.name.text);
803
860
  for (const member of node.members) visitSymbols(member, node.name.text);
804
861
  return;
@@ -876,7 +933,7 @@ async function parseExecutableSymbols(repoPath, filePath) {
876
933
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isNewExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
877
934
  const className = node.initializer.expression.text;
878
935
  const importSource = imports.get(className);
879
- if (!importSource || isRelativeImport(importSource)) classInstances.set(node.name.text, { className, importSource });
936
+ if (!builtInConstructors.has(className) && (importSource && isRelativeImport(importSource) || declaredClasses.has(className))) classInstances.set(node.name.text, { className, importSource });
880
937
  }
881
938
  ts.forEachChild(node, visitClassInstances);
882
939
  };
@@ -1221,11 +1278,15 @@ function parserQualityDiagnostics(db, strict) {
1221
1278
  const aliasQuality = identityAliasBindingQuality(db);
1222
1279
  const noBindingQuality = remoteActionNoBindingQuality(db);
1223
1280
  const contextualQuality = contextualImplementationQuality(db);
1281
+ const classInstanceQuality = classInstanceNoiseQuality(db);
1282
+ const bindingPropagationQuality = contextualBindingPropagationQuality(db);
1224
1283
  const wrapperQuality = wrapperPathPropagationQuality(db);
1225
1284
  return [
1226
1285
  aliasQuality,
1227
1286
  noBindingQuality,
1228
1287
  contextualQuality,
1288
+ classInstanceQuality,
1289
+ bindingPropagationQuality,
1229
1290
  wrapperQuality,
1230
1291
  remoteQuery,
1231
1292
  invocation,
@@ -1253,7 +1314,16 @@ function remoteActionNoBindingQuality(db) {
1253
1314
  WHEN c.unresolved_reason='dynamic_operation_path_identifier' THEN 'dynamic_path_identifier'
1254
1315
  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
1316
  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'
1317
+ WHEN EXISTS (
1318
+ SELECT 1 FROM symbol_calls sc
1319
+ JOIN symbols caller ON caller.id=sc.caller_symbol_id
1320
+ JOIN symbols callee ON callee.id=sc.callee_symbol_id
1321
+ WHERE sc.status='resolved'
1322
+ AND sc.source_file=c.source_file
1323
+ AND caller.id=c.source_symbol_id
1324
+ AND json_extract(sc.evidence_json,'$.relation')='class_instance_method'
1325
+ AND (callee.evidence_json IS NULL OR json_extract(callee.evidence_json,'$.parameterBindings') IS NULL)
1326
+ ) THEN 'likely_instance_method_parameter_metadata_needed'
1257
1327
  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
1328
  WHEN e.status='unresolved' AND COALESCE(e.unresolved_reason,'') LIKE '%No indexed target operation%' THEN 'no_indexed_target_operation'
1259
1329
  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'
@@ -1268,6 +1338,31 @@ function remoteActionNoBindingQuality(db) {
1268
1338
  const total = rows.reduce((sum, row) => sum + Number(row.count ?? 0), 0);
1269
1339
  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 };
1270
1340
  }
1341
+ function classInstanceNoiseQuality(db) {
1342
+ const builtIns = ["Set", "Map", "WeakSet", "WeakMap", "Date", "RegExp", "URL", "URLSearchParams", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "AggregateError", "ArrayBuffer", "SharedArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", "Int32Array", "Uint32Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array", "Promise", "AbortController"];
1343
+ const placeholders = builtIns.map(() => "?").join(",");
1344
+ const aggregate = db.prepare(`SELECT COUNT(*) total,
1345
+ SUM(CASE WHEN status='unresolved' THEN 1 ELSE 0 END) unresolved,
1346
+ SUM(CASE WHEN status='unresolved' AND json_extract(evidence_json,'$.className') IN (${placeholders}) THEN 1 ELSE 0 END) unresolvedBuiltIn
1347
+ FROM symbol_calls WHERE json_extract(evidence_json,'$.relation')='class_instance_method'`).get(...builtIns);
1348
+ const byConstructor = db.prepare(`SELECT json_extract(evidence_json,'$.className') constructorName,COUNT(*) unresolvedCount
1349
+ FROM symbol_calls WHERE status='unresolved' AND json_extract(evidence_json,'$.relation')='class_instance_method'
1350
+ GROUP BY constructorName ORDER BY unresolvedCount DESC,constructorName LIMIT 10`).all();
1351
+ return { severity: Number(aggregate.unresolvedBuiltIn ?? 0) > 0 ? "warning" : "info", code: "strict_class_instance_noise_quality", message: "Class-instance symbol-call aggregate with built-in constructor guard", totalClassInstanceCalls: Number(aggregate.total ?? 0), unresolvedClassInstanceCalls: Number(aggregate.unresolved ?? 0), unresolvedBuiltInClassInstanceCalls: Number(aggregate.unresolvedBuiltIn ?? 0), unresolvedByConstructor: byConstructor };
1352
+ }
1353
+ function contextualBindingPropagationQuality(db) {
1354
+ const serviceClientCalls = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc
1355
+ WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')`).get();
1356
+ const missingMetadata = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1357
+ WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1358
+ AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get();
1359
+ const destructuredUnmapped = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1360
+ WHERE json_extract(sc.evidence_json,'$.callArguments[0].kind')='object_literal'
1361
+ AND json_extract(s.evidence_json,'$.parameterBindings[0].kind')='object_pattern'
1362
+ AND json_array_length(json_extract(sc.evidence_json,'$.callArguments[0].properties')) > json_array_length(json_extract(s.evidence_json,'$.parameterBindings[0].properties'))`).get();
1363
+ const contextualResolved = db.prepare(`SELECT COUNT(*) count FROM graph_edges WHERE json_extract(evidence_json,'$.contextualServiceBindingSelected')=1 AND status='resolved'`).get();
1364
+ return { severity: Number(missingMetadata.count ?? 0) + Number(destructuredUnmapped.count ?? 0) > 0 ? "warning" : "info", code: "strict_contextual_binding_propagation_quality", message: "Contextual service-client propagation aggregate", localSymbolCallsWithServiceClientArguments: Number(serviceClientCalls.count ?? 0), calleeSymbolsMissingParameterMetadata: Number(missingMetadata.count ?? 0), destructuredObjectParametersPossiblyUnmapped: Number(destructuredUnmapped.count ?? 0), contextualRemoteActionsResolved: Number(contextualResolved.count ?? 0) };
1365
+ }
1271
1366
  function contextualImplementationQuality(db) {
1272
1367
  const rows = db.prepare(`SELECT status,COALESCE(unresolved_reason,status) reason,COUNT(*) count
1273
1368
  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();