@saptools/service-flow 0.1.33 → 0.1.35

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
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ classifyODataPathIntent,
3
4
  containsSupportedOutboundCall,
4
5
  discoverRepositories,
5
6
  linkWorkspace,
@@ -12,7 +13,7 @@ import {
12
13
  parsePackageJson,
13
14
  parseServiceBindings,
14
15
  trace
15
- } from "./chunk-CWJYVIG2.js";
16
+ } from "./chunk-R47GA5VW.js";
16
17
 
17
18
  // src/cli.ts
18
19
  import { Command } from "commander";
@@ -92,7 +93,7 @@ import path2 from "path";
92
93
  // src/db/schema.ts
93
94
  var schemaSql = `
94
95
  CREATE TABLE IF NOT EXISTS workspaces (id INTEGER PRIMARY KEY, root_path TEXT UNIQUE NOT NULL, db_path TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
95
- CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
96
+ CREATE TABLE IF NOT EXISTS repositories (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, name TEXT NOT NULL, absolute_path TEXT NOT NULL, relative_path TEXT NOT NULL, package_name TEXT, package_version TEXT, dependencies_json TEXT DEFAULT '{}', kind TEXT NOT NULL, is_git_repo INTEGER NOT NULL, last_indexed_at TEXT, index_status TEXT DEFAULT 'pending', error_count INTEGER DEFAULT 0, fingerprint TEXT, fact_generation INTEGER NOT NULL DEFAULT 0, graph_generation INTEGER NOT NULL DEFAULT 0, graph_stale_reason TEXT, graph_stale_at TEXT, fact_analyzer_version TEXT, UNIQUE(workspace_id, absolute_path), FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
96
97
  CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, relative_path TEXT NOT NULL, extension TEXT NOT NULL, sha256 TEXT NOT NULL, size_bytes INTEGER NOT NULL, last_indexed_at TEXT NOT NULL, UNIQUE(repo_id, relative_path), FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
97
98
  CREATE TABLE IF NOT EXISTS cds_requires (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, alias TEXT NOT NULL, kind TEXT, model TEXT, destination TEXT, service_path TEXT, request_timeout INTEGER, raw_json TEXT NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
98
99
  CREATE TABLE IF NOT EXISTS cds_services (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, namespace TEXT, service_name TEXT NOT NULL, qualified_name TEXT NOT NULL, service_path TEXT NOT NULL, is_extend INTEGER NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE);
@@ -116,7 +117,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, rep
116
117
  `;
117
118
 
118
119
  // src/db/migrations.ts
119
- var CURRENT_SCHEMA_VERSION = 7;
120
+ var CURRENT_SCHEMA_VERSION = 8;
120
121
  var columns = {
121
122
  service_bindings: [
122
123
  { name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
@@ -127,7 +128,8 @@ var columns = {
127
128
  { name: "fact_generation", ddl: "ALTER TABLE repositories ADD COLUMN fact_generation INTEGER NOT NULL DEFAULT 0" },
128
129
  { name: "graph_generation", ddl: "ALTER TABLE repositories ADD COLUMN graph_generation INTEGER NOT NULL DEFAULT 0" },
129
130
  { name: "graph_stale_reason", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_reason TEXT" },
130
- { name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" }
131
+ { name: "graph_stale_at", ddl: "ALTER TABLE repositories ADD COLUMN graph_stale_at TEXT" },
132
+ { name: "fact_analyzer_version", ddl: "ALTER TABLE repositories ADD COLUMN fact_analyzer_version TEXT DEFAULT 'legacy'" }
131
133
  ],
132
134
  graph_edges: [
133
135
  { name: "status", ddl: "ALTER TABLE graph_edges ADD COLUMN status TEXT NOT NULL DEFAULT 'unresolved'" },
@@ -192,7 +194,7 @@ function migrate(db) {
192
194
  // package.json
193
195
  var package_default = {
194
196
  name: "@saptools/service-flow",
195
- version: "0.1.33",
197
+ version: "0.1.35",
196
198
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
197
199
  type: "module",
198
200
  publishConfig: {
@@ -1053,7 +1055,7 @@ async function indexRepository(db, repo, force) {
1053
1055
  insertRegistrations(db, repo.id, parsed.registrations);
1054
1056
  insertBindings(db, repo.id, parsed.bindings);
1055
1057
  insertCalls(db, repo.id, parsed.calls);
1056
- db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fingerprint, (/* @__PURE__ */ new Date()).toISOString(), repo.id);
1058
+ db.prepare("UPDATE repositories SET last_indexed_at=?, index_status='indexed', error_count=0, fingerprint=?, fact_generation=COALESCE(fact_generation,0)+1, graph_stale_reason='facts_changed', graph_stale_at=?, fact_analyzer_version=? WHERE id=?").run((/* @__PURE__ */ new Date()).toISOString(), fingerprint, (/* @__PURE__ */ new Date()).toISOString(), ANALYZER_VERSION, repo.id);
1057
1059
  });
1058
1060
  return { fileCount: sourceFiles.length, diagnosticCount: 0, skipped: false };
1059
1061
  } catch (error) {
@@ -1270,7 +1272,66 @@ function schemaDriftDiagnostics(db, strict) {
1270
1272
  return diagnostics;
1271
1273
  }
1272
1274
  function linkUpgradeWarnings(db) {
1273
- return schemaDriftDiagnostics(db, true).filter((item) => ["schema_legacy_columns_present", "external_target_columns_missing_data", "reindex_required_after_upgrade"].includes(String(item.code)));
1275
+ 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)));
1276
+ }
1277
+ function analyzerVersionDiagnostics(db, strict) {
1278
+ if (!strict) return [];
1279
+ const rows = db.prepare("SELECT name,COALESCE(fact_analyzer_version,'legacy') factAnalyzerVersion FROM repositories WHERE index_status='indexed' AND COALESCE(fact_analyzer_version,'legacy')<>?").all(ANALYZER_VERSION);
1280
+ if (rows.length === 0) return [];
1281
+ return [{ severity: "warning", code: "reindex_required_after_analyzer_upgrade", message: "Repository facts were produced by an older or unknown analyzer; run service-flow index --force before relink to apply current parser semantics.", scope: "workspace", affectedRepositoryCount: rows.length, currentAnalyzerVersion: ANALYZER_VERSION, repositories: rows, remediation: "service-flow index --force && service-flow link" }];
1282
+ }
1283
+ function remoteEntityOperationCollisionQuality(db) {
1284
+ const rows = db.prepare(`SELECT c.id callId,c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,c.query_entity entitySegment,e.to_id selectedTerminalEntityTarget,e.evidence_json evidenceJson
1285
+ FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1286
+ WHERE c.call_type LIKE 'remote_entity_%' AND e.edge_type='HANDLER_ACCESSES_REMOTE_ENTITY' AND e.status='terminal'
1287
+ ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1288
+ const examples = [];
1289
+ for (const row of rows) {
1290
+ const normalized = normalizeODataOperationInvocationPath(String(row.rawPath ?? ""));
1291
+ const rawPath = String(row.rawPath ?? "");
1292
+ const candidatePath = normalized?.wasInvocation ? normalized.normalizedOperationPath : rawPath;
1293
+ const name = candidatePath.replace(/^\//, "");
1294
+ const simple = name.split(".").at(-1) ?? name;
1295
+ const candidates = db.prepare("SELECT COUNT(*) count FROM cds_operations WHERE operation_path IN (?,?) OR operation_name IN (?,?)").get(candidatePath, `/${simple}`, name, simple);
1296
+ const candidateCount = Number(candidates.count ?? 0);
1297
+ const operationLike = Boolean(normalized?.wasInvocation) || candidateCount > 0;
1298
+ if (!operationLike) continue;
1299
+ let classifierReason;
1300
+ try {
1301
+ const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1302
+ classifierReason = evidence.odataPathIntent?.reason;
1303
+ } catch {
1304
+ classifierReason = void 0;
1305
+ }
1306
+ examples.push({ callId: row.callId, sourceFile: row.sourceFile, sourceLine: row.sourceLine, method: row.method, rawPath, normalizedOperationPath: normalized?.wasInvocation ? normalized.normalizedOperationPath : candidatePath, entitySegment: row.entitySegment, operationCandidateCount: candidateCount, selectedTerminalEntityTarget: row.selectedTerminalEntityTarget, classifierReason });
1307
+ }
1308
+ return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_operation_collision_quality", message: "Terminal remote entity edges that look like indexed operation invocations", collisionCount: examples.length, examples: examples.slice(0, 10) };
1309
+ }
1310
+ function remoteEntityDynamicOperationFalsePositiveQuality(db) {
1311
+ const rows = db.prepare(`SELECT c.id callId,c.source_file sourceFile,c.source_line sourceLine,c.method method,c.operation_path_expr rawPath,e.id graphEdgeId,e.status status,e.to_kind targetKind,e.unresolved_reason unresolvedReason,e.evidence_json evidenceJson
1312
+ FROM outbound_calls c JOIN graph_edges e ON e.from_kind='call' AND e.from_id=CAST(c.id AS TEXT)
1313
+ WHERE c.call_type LIKE 'remote_entity_%' AND e.status IN ('dynamic','unresolved') AND e.to_kind='operation_candidate'
1314
+ ORDER BY c.source_file,c.source_line LIMIT 100`).all();
1315
+ const examples = [];
1316
+ for (const row of rows) {
1317
+ const rawPath = String(row.rawPath ?? "");
1318
+ const method = String(row.method ?? "GET");
1319
+ const intent = classifyODataPathIntent(rawPath, method);
1320
+ const entityIntent = ["entity_key_read", "entity_navigation_query", "entity_media"].includes(intent.kind) || intent.kind === "entity_mutation" && (intent.hasEntityKeyPredicate || intent.hasNavigationSuffix);
1321
+ if (!entityIntent) continue;
1322
+ let candidateCount;
1323
+ try {
1324
+ const evidence = JSON.parse(String(row.evidenceJson ?? "{}"));
1325
+ candidateCount = Number(evidence.indexedOperationCandidateCount ?? evidence.candidateCount ?? 0);
1326
+ } catch {
1327
+ candidateCount = 0;
1328
+ }
1329
+ const reason = String(row.unresolvedReason ?? "");
1330
+ const keyEvidence = intent.keyPredicatePlaceholderKeys.length > 0 || reason.includes("runtime variable") || reason.includes("placeholder");
1331
+ if (candidateCount > 0 || !keyEvidence) continue;
1332
+ examples.push({ sourceFile: row.sourceFile, sourceLine: row.sourceLine, rawPath, method, pathIntent: intent.kind, keyPlaceholderKeys: intent.keyPredicatePlaceholderKeys, navigationOrMediaSuffix: intent.navigationSuffix ?? intent.mediaOrPropertySuffix, operationCandidateCount: candidateCount, graphEdgeId: row.graphEdgeId, recommendedRemediation: "Reindex and relink with service-flow 0.1.35 or newer so entity key placeholders remain entity-addressing evidence." });
1333
+ }
1334
+ return { severity: examples.length > 0 ? "warning" : "info", code: "strict_remote_entity_dynamic_operation_false_positive_quality", message: "Parser-classified entity paths linked as dynamic operation candidates without indexed operation evidence", falsePositiveCount: examples.length, examples: examples.slice(0, 10) };
1274
1335
  }
1275
1336
  function localServiceDiagnostics(db, strict) {
1276
1337
  const rows = db.prepare(`SELECT e.status status,e.unresolved_reason reason,e.evidence_json evidenceJson FROM graph_edges e JOIN outbound_calls c ON e.from_kind='call' AND c.id=CAST(e.from_id AS INTEGER) WHERE c.call_type='local_service_call'`).all();
@@ -1340,6 +1401,8 @@ function parserQualityDiagnostics(db, strict) {
1340
1401
  const remoteQuery = remoteQueryTargetQuality(db);
1341
1402
  const invocation = odataInvocationResolutionQuality(db);
1342
1403
  const remoteAction = remoteActionTargetQuality(db);
1404
+ const entityOperationCollision = remoteEntityOperationCollisionQuality(db);
1405
+ const entityDynamicFalsePositive = remoteEntityDynamicOperationFalsePositiveQuality(db);
1343
1406
  const externalHttp = externalHttpTargetQuality(db);
1344
1407
  const aliasQuality = identityAliasBindingQuality(db);
1345
1408
  const noBindingQuality = remoteActionNoBindingQuality(db);
@@ -1357,6 +1420,8 @@ function parserQualityDiagnostics(db, strict) {
1357
1420
  wrapperQuality,
1358
1421
  nestedThisQuality,
1359
1422
  remoteQuery,
1423
+ entityOperationCollision,
1424
+ entityDynamicFalsePositive,
1360
1425
  invocation,
1361
1426
  remoteAction,
1362
1427
  externalHttp,
@@ -1777,7 +1842,8 @@ function createProgram() {
1777
1842
  const localServiceHealth = localServiceDiagnostics(db, Boolean(opts.strict));
1778
1843
  const parserQualityHealth = parserQualityDiagnostics(db, Boolean(opts.strict));
1779
1844
  const schemaDriftHealth = schemaDriftDiagnostics(db, Boolean(opts.strict));
1780
- const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...schemaDriftHealth, ...parserQualityHealth];
1845
+ const analyzerVersionHealth = analyzerVersionDiagnostics(db, Boolean(opts.strict));
1846
+ const allDiagnostics = [...diagnostics, ...health, ...localServiceHealth, ...schemaDriftHealth, ...analyzerVersionHealth, ...parserQualityHealth];
1781
1847
  process.stdout.write(
1782
1848
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
1783
1849
  `