@saptools/service-flow 0.1.44 → 0.1.46

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
@@ -3,17 +3,19 @@ import {
3
3
  classifyODataPathIntent,
4
4
  containsSupportedOutboundCall,
5
5
  discoverRepositories,
6
+ implementationHintSuggestions,
6
7
  linkWorkspace,
7
8
  normalizeODataOperationInvocationPath,
8
9
  normalizePath,
9
10
  parseCdsFile,
10
11
  parseDecorators,
11
12
  parseHandlerRegistrations,
13
+ parseImplementationHint,
12
14
  parseOutboundCalls,
13
15
  parsePackageJson,
14
16
  parseServiceBindings,
15
17
  trace
16
- } from "./chunk-UKNPHTUS.js";
18
+ } from "./chunk-EGY2A4AT.js";
17
19
 
18
20
  // src/cli.ts
19
21
  import { Command } from "commander";
@@ -209,7 +211,7 @@ function migrate(db) {
209
211
  // package.json
210
212
  var package_default = {
211
213
  name: "@saptools/service-flow",
212
- version: "0.1.44",
214
+ version: "0.1.46",
213
215
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
214
216
  type: "module",
215
217
  publishConfig: {
@@ -814,6 +816,10 @@ function argumentEvidence(args, source) {
814
816
  }
815
817
  return { kind: "object_literal", properties };
816
818
  }
819
+ if (ts.isArrayLiteralExpression(arg)) {
820
+ const elements = arg.elements.flatMap((element, index) => ts.isIdentifier(element) ? [{ index, kind: "identifier", name: element.text }] : []);
821
+ return { kind: "array_literal", elements };
822
+ }
817
823
  return { kind: "unsupported", expression: arg.getText(source) };
818
824
  });
819
825
  }
@@ -860,15 +866,21 @@ function parameterPropertyAliases(fn, source) {
860
866
  function parameterBindings(params) {
861
867
  return params.flatMap((param, index) => {
862
868
  if (ts.isIdentifier(param.name)) return [{ index, kind: "identifier", name: param.name.text }];
863
- if (!ts.isObjectBindingPattern(param.name)) return [];
864
- const properties = param.name.elements.flatMap((element) => {
865
- if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
866
- const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
867
- if (!property) return [];
868
- const local = bindingLocalName(element.name, element.initializer);
869
- return local ? [{ property, local }] : [];
870
- });
871
- return properties.length > 0 ? [{ index, kind: "object_pattern", properties }] : [];
869
+ if (ts.isObjectBindingPattern(param.name)) {
870
+ const properties = param.name.elements.flatMap((element) => {
871
+ if (element.dotDotDotToken || ts.isObjectBindingPattern(element.name) || ts.isArrayBindingPattern(element.name)) return [];
872
+ const property = element.propertyName ? nameOf(element.propertyName) : nameOf(element.name);
873
+ if (!property) return [];
874
+ const local = bindingLocalName(element.name, element.initializer);
875
+ return local ? [{ property, local }] : [];
876
+ });
877
+ return properties.length > 0 ? [{ index, kind: "object_pattern", properties }] : [];
878
+ }
879
+ if (ts.isArrayBindingPattern(param.name)) {
880
+ const elements = param.name.elements.flatMap((element, elementIndex) => ts.isBindingElement(element) && !element.dotDotDotToken && ts.isIdentifier(element.name) ? [{ index: elementIndex, local: element.name.text }] : []);
881
+ return elements.length > 0 ? [{ index, kind: "array_pattern", elements }] : [];
882
+ }
883
+ return [];
872
884
  });
873
885
  }
874
886
  async function parseExecutableSymbols(repoPath, filePath) {
@@ -1295,7 +1307,7 @@ async function indexWorkspace(db, workspaceId, options) {
1295
1307
  function linkUpgradeWarnings(db) {
1296
1308
  return [...schemaDriftDiagnostics(db, true), ...analyzerVersionDiagnostics(db, true)].filter((item) => ["schema_legacy_columns_present", "external_target_columns_missing_data", "reindex_required_after_upgrade", "reindex_required_after_analyzer_upgrade"].includes(String(item.code)));
1297
1309
  }
1298
- function doctorDiagnostics(db, strict) {
1310
+ function doctorDiagnostics(db, strict, options = {}) {
1299
1311
  const diagnostics = db.prepare("SELECT severity,code,message,source_file sourceFile,source_line sourceLine FROM diagnostics ORDER BY id").all();
1300
1312
  return [
1301
1313
  ...diagnostics,
@@ -1303,7 +1315,7 @@ function doctorDiagnostics(db, strict) {
1303
1315
  ...localServiceDiagnostics(db, strict),
1304
1316
  ...schemaDriftDiagnostics(db, strict),
1305
1317
  ...analyzerVersionDiagnostics(db, strict),
1306
- ...parserQualityDiagnostics(db, strict)
1318
+ ...parserQualityDiagnostics(db, strict, options)
1307
1319
  ];
1308
1320
  }
1309
1321
  function healthDiagnostics(db, strict) {
@@ -1380,7 +1392,7 @@ function localServiceDiagnostics(db, strict) {
1380
1392
  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}` });
1381
1393
  return out;
1382
1394
  }
1383
- function parserQualityDiagnostics(db, strict) {
1395
+ function parserQualityDiagnostics(db, strict, options) {
1384
1396
  if (!strict) return [];
1385
1397
  const symbol = symbolCallQuality(db);
1386
1398
  const dbq = dbQueryQuality(db);
@@ -1389,7 +1401,7 @@ function parserQualityDiagnostics(db, strict) {
1389
1401
  identityAliasBindingQuality(db),
1390
1402
  remoteActionNoBindingQuality(db),
1391
1403
  contextualImplementationQuality(db),
1392
- implementationCandidateQuality(db),
1404
+ implementationCandidateQuality(db, Boolean(options.detail)),
1393
1405
  classInstanceNoiseQuality(db),
1394
1406
  contextualBindingPropagationQuality(db),
1395
1407
  nestedThisReceiverQuality(db),
@@ -1452,19 +1464,26 @@ function graphDynamicFlagQuality(db) {
1452
1464
  const row = db.prepare("SELECT COUNT(*) count FROM graph_edges WHERE status='terminal' AND is_dynamic=1").get();
1453
1465
  return { severity: Number(row.count ?? 0) > 0 ? "warning" : "info", code: "strict_graph_dynamic_flag_consistency", message: "Graph dynamic flag consistency aggregate", dynamicTerminalEdges: Number(row.count ?? 0) };
1454
1466
  }
1455
- function implementationCandidateQuality(db) {
1456
- const categories = [...implementationEdgeCategories(db), missingParameterMetadataCategory(db)].filter((item) => item.count > 0);
1467
+ function implementationCandidateQuality(db, detail) {
1468
+ const categories = [...implementationEdgeCategories(db, detail), missingParameterMetadataCategory(db, detail), dynamicWrapperCategory(db, detail)].filter((item) => item.count > 0);
1457
1469
  const total = categories.reduce((sum, item) => sum + item.count, 0);
1458
- return { severity: total > 0 ? "warning" : "info", code: "strict_implementation_candidate_quality", message: "Implementation candidate ambiguity and rejection aggregate", total, categories };
1470
+ return { severity: total > 0 ? "warning" : "info", code: "strict_implementation_candidate_quality", message: "Implementation candidate ambiguity and rejection aggregate", total, summary: implementationSummary(categories), categories };
1459
1471
  }
1460
- function implementationEdgeCategories(db) {
1472
+ function implementationEdgeCategories(db, detail) {
1461
1473
  const rows = db.prepare(`SELECT e.status,e.unresolved_reason unresolvedReason,e.evidence_json evidenceJson,o.operation_name operationName,base.operation_name baseOperation,s.service_path servicePath
1462
1474
  FROM graph_edges e JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
1463
1475
  JOIN cds_services s ON s.id=o.service_id LEFT JOIN cds_operations base ON base.id=o.base_operation_id
1464
1476
  WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status IN ('ambiguous','unresolved') ORDER BY s.service_path,o.operation_name,e.id`).all();
1465
1477
  const grouped = /* @__PURE__ */ new Map();
1466
1478
  for (const row of rows) addImplementationCategory(grouped, row);
1467
- return [...grouped.values()].map(({ servicePaths, ...item }) => ({ ...item, servicePathPattern: pathPattern(servicePaths), examples: item.examples.slice(0, 3) }));
1479
+ return [...grouped.values()].map(({ servicePaths, ...item }) => ({
1480
+ ...item,
1481
+ servicePathPattern: pathPattern(servicePaths),
1482
+ suggestedAction: categoryAction(String(item.category)),
1483
+ suggestedHints: suggestedHints(item.examples),
1484
+ examples: item.examples.slice(0, 3),
1485
+ expandedExamples: detail ? item.examples : void 0
1486
+ }));
1468
1487
  }
1469
1488
  function addImplementationCategory(grouped, row) {
1470
1489
  const evidence = parseObject(row.evidenceJson);
@@ -1474,21 +1493,57 @@ function addImplementationCategory(grouped, row) {
1474
1493
  const baseOperation = String(row.baseOperation ?? row.operationName ?? evidence.operationName ?? "unknown");
1475
1494
  const key = [category, baseOperation, reason, family].join("\0");
1476
1495
  const current = grouped.get(key) ?? { category, baseOperation, reason, candidateFamily: family, count: 0, servicePaths: [], examples: [] };
1496
+ const hintSuggestions = implementationSuggestions(evidence);
1477
1497
  current.count += 1;
1478
1498
  current.servicePaths.push(String(row.servicePath ?? evidence.servicePath ?? ""));
1479
- current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason });
1499
+ current.examples.push({ servicePath: row.servicePath, operation: row.operationName, status: row.status, reason: row.unresolvedReason, implementationHintSuggestions: hintSuggestions });
1480
1500
  grouped.set(key, current);
1481
1501
  }
1482
- function missingParameterMetadataCategory(db) {
1502
+ function implementationSuggestions(evidence) {
1503
+ const persisted = asRecords(evidence.implementationHintSuggestions);
1504
+ const suggestions = persisted.length ? persisted : implementationHintSuggestions(evidence);
1505
+ return suggestions.length ? suggestions : void 0;
1506
+ }
1507
+ function suggestedHints(examples) {
1508
+ const hints = examples.flatMap((example) => asRecords(example.implementationHintSuggestions).flatMap((suggestion) => typeof suggestion.cli === "string" ? [String(suggestion.cli)] : []));
1509
+ const unique = [...new Set(hints)].slice(0, 3);
1510
+ return unique.length ? unique : void 0;
1511
+ }
1512
+ function missingParameterMetadataCategory(db, detail = false) {
1483
1513
  const examples = db.prepare(`SELECT sc.source_file sourceFile,sc.source_line sourceLine,sc.callee_expression calleeExpression
1484
1514
  FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1485
1515
  WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1486
1516
  AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)
1487
- ORDER BY sc.source_file,sc.source_line LIMIT 3`).all();
1517
+ ORDER BY sc.source_file,sc.source_line`).all();
1488
1518
  const row = db.prepare(`SELECT COUNT(*) count FROM symbol_calls sc JOIN symbols s ON s.id=sc.callee_symbol_id
1489
1519
  WHERE sc.status='resolved' AND json_extract(sc.evidence_json,'$.callArguments[0].kind') IN ('identifier','object_literal')
1490
1520
  AND (s.evidence_json IS NULL OR json_extract(s.evidence_json,'$.parameterBindings') IS NULL)`).get();
1491
- return { category: "missing_parameter_metadata", reason: "callee symbol is missing parameter binding metadata", candidateFamily: "symbol_parameter_metadata", count: Number(row.count ?? 0), examples };
1521
+ return { category: "missing_parameter_metadata", reason: "callee symbol is missing parameter binding metadata", candidateFamily: "symbol_parameter_metadata", count: Number(row.count ?? 0), suggestedAction: categoryAction("missing_parameter_metadata"), examples: examples.slice(0, 3), expandedExamples: detail ? examples : void 0 };
1522
+ }
1523
+ function dynamicWrapperCategory(db, detail) {
1524
+ const rows = db.prepare(`SELECT source_file sourceFile,source_line sourceLine,
1525
+ json_extract(evidence_json,'$.receiver') receiverName,
1526
+ COALESCE(json_extract(evidence_json,'$.missingPathIdentifier'),json_extract(evidence_json,'$.operationPathExpression')) pathIdentifier
1527
+ FROM outbound_calls WHERE call_type='remote_action' AND unresolved_reason='dynamic_operation_path_identifier'
1528
+ ORDER BY source_file,source_line`).all();
1529
+ return { category: "dynamic_wrapper_paths", reason: "wrapper path cannot be proven statically", candidateFamily: "wrapper_path", count: rows.length, suggestedAction: categoryAction("dynamic_wrapper_paths"), examples: rows.slice(0, 3), expandedExamples: detail ? rows : void 0 };
1530
+ }
1531
+ function implementationSummary(categories) {
1532
+ const grouped = /* @__PURE__ */ new Map();
1533
+ for (const category of categories) {
1534
+ const key = [category.category, category.candidateFamily, category.reason].join("\0");
1535
+ const current = grouped.get(key) ?? { category: category.category, candidateFamily: category.candidateFamily, reason: category.reason, severity: "warning", suggestedAction: category.suggestedAction, count: 0 };
1536
+ current.count += category.count;
1537
+ grouped.set(key, current);
1538
+ }
1539
+ return [...grouped.values()].sort((left, right) => String(left.category).localeCompare(String(right.category)) || String(left.candidateFamily).localeCompare(String(right.candidateFamily)));
1540
+ }
1541
+ function categoryAction(category) {
1542
+ if (category === "duplicate_package_name_candidates") return "Use scoped --implementation-hint fields to select one repository for each ambiguous hop.";
1543
+ if (category === "missing_strong_ownership_evidence") return "Add an explicit package dependency, local service-path ownership, or registration ownership evidence.";
1544
+ if (category === "missing_parameter_metadata") return "Export a statically analyzable helper with named or destructured parameters.";
1545
+ if (category === "dynamic_wrapper_paths") return "Pass a literal path or provide the reported runtime identifier with --var key=value.";
1546
+ return "Inspect the capped examples and add stronger implementation ownership evidence.";
1492
1547
  }
1493
1548
  function implementationCategory(row, evidence) {
1494
1549
  const reasons = JSON.stringify([evidence.ambiguityReasons, evidence.candidateFamilies, evidence.candidates, row.unresolvedReason]);
@@ -1670,11 +1725,24 @@ function renderTraceTable(result) {
1670
1725
  const lines = ["Step Type From To Evidence"];
1671
1726
  for (const e of result.edges) {
1672
1727
  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)}`);
1728
+ const hint = firstHint(e.evidence);
1729
+ if (e.unresolvedReason && hint) lines.push(` try ${hint}`);
1673
1730
  }
1674
- if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.map((d) => `${String(d.severity ?? "info")} ${String(d.code ?? "diagnostic")} ${String(d.message ?? "")}`));
1731
+ if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.flatMap(diagnosticLines));
1675
1732
  return `${lines.join("\n")}
1676
1733
  `;
1677
1734
  }
1735
+ function diagnosticLines(diagnostic) {
1736
+ const first = `${String(diagnostic.severity ?? "info")} ${String(diagnostic.code ?? "diagnostic")} ${String(diagnostic.message ?? "")}`;
1737
+ const hint = firstHint(diagnostic);
1738
+ return hint ? [first, ` try ${hint}`] : [first];
1739
+ }
1740
+ function firstHint(evidence) {
1741
+ const suggestions = evidence.implementationHintSuggestions;
1742
+ if (!Array.isArray(suggestions)) return void 0;
1743
+ const first = suggestions.find((item) => Boolean(item) && typeof item === "object");
1744
+ return typeof first?.cli === "string" ? first.cli : void 0;
1745
+ }
1678
1746
 
1679
1747
  // src/output/json-output.ts
1680
1748
  function renderJson(value) {
@@ -1788,7 +1856,7 @@ function createProgram() {
1788
1856
  );
1789
1857
  }).catch(fail)
1790
1858
  );
1791
- program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").option("--format <format>", "table|json|mermaid", "table").option("--include-external").option("--include-db").option("--include-async").option("--implementation-repo <name>").option("--var <key=value>", "dynamic variable", collect, []).action(
1859
+ program.command("trace").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--handler <name>").option("--depth <n>", "trace depth", "25").option("--format <format>", "table|json|mermaid", "table").option("--include-external").option("--include-db").option("--include-async").option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).action(
1792
1860
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1793
1861
  const result = trace(
1794
1862
  db,
@@ -1805,7 +1873,8 @@ function createProgram() {
1805
1873
  includeExternal: Boolean(opts.includeExternal),
1806
1874
  includeDb: Boolean(opts.includeDb),
1807
1875
  includeAsync: Boolean(opts.includeAsync),
1808
- implementationRepo: opts.implementationRepo
1876
+ implementationRepo: opts.implementationRepo,
1877
+ implementationHints: opts.implementationHint.map(parseImplementationHint)
1809
1878
  }
1810
1879
  );
1811
1880
  process.stdout.write(
@@ -1874,7 +1943,7 @@ function createProgram() {
1874
1943
  process.stdout.write(renderJson(rows));
1875
1944
  }).catch(fail)
1876
1945
  );
1877
- program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").option("--implementation-repo <name>").option("--var <key=value>", "dynamic variable", collect, []).action(
1946
+ program.command("graph").option("--workspace <path>").option("--repo <name>").option("--operation <name>").option("--service <path>").option("--path <operationPath>").option("--format <format>", "mermaid|json", "mermaid").option("--implementation-repo <name>").option("--implementation-hint <scope>", "scoped implementation hint", collect, []).option("--var <key=value>", "dynamic variable", collect, []).action(
1878
1947
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1879
1948
  const result = trace(
1880
1949
  db,
@@ -1890,7 +1959,8 @@ function createProgram() {
1890
1959
  includeDb: true,
1891
1960
  includeExternal: true,
1892
1961
  vars: parseVars(opts.var),
1893
- implementationRepo: opts.implementationRepo
1962
+ implementationRepo: opts.implementationRepo,
1963
+ implementationHints: opts.implementationHint.map(parseImplementationHint)
1894
1964
  }
1895
1965
  );
1896
1966
  process.stdout.write(
@@ -1915,9 +1985,9 @@ function createProgram() {
1915
1985
  process.stdout.write(renderJson(rows));
1916
1986
  }).catch(fail)
1917
1987
  );
1918
- program.command("doctor").option("--workspace <path>").option("--strict").action(
1988
+ program.command("doctor").option("--workspace <path>").option("--strict").option("--detail").action(
1919
1989
  (opts) => void withReadOnlyWorkspace(opts.workspace, (db) => {
1920
- const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict));
1990
+ const allDiagnostics = doctorDiagnostics(db, Boolean(opts.strict), { detail: Boolean(opts.detail) });
1921
1991
  process.stdout.write(
1922
1992
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
1923
1993
  `