@saptools/service-flow 0.1.9 → 0.1.11

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,12 +10,12 @@ import {
10
10
  parsePackageJson,
11
11
  parseServiceBindings,
12
12
  trace
13
- } from "./chunk-HQMAFSRR.js";
13
+ } from "./chunk-GG4XJGES.js";
14
14
 
15
15
  // src/cli.ts
16
16
  import { Command } from "commander";
17
- import path5 from "path";
18
- import fs5 from "fs/promises";
17
+ import path6 from "path";
18
+ import fs6 from "fs/promises";
19
19
  import pc from "picocolors";
20
20
 
21
21
  // src/config/defaults.ts
@@ -95,12 +95,13 @@ CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NU
95
95
  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);
96
96
  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);
97
97
  CREATE TABLE IF NOT EXISTS cds_operations (id INTEGER PRIMARY KEY, service_id INTEGER NOT NULL, operation_type TEXT NOT NULL, operation_name TEXT NOT NULL, operation_path TEXT NOT NULL, params_json TEXT NOT NULL, return_type TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(service_id) REFERENCES cds_services(id) ON DELETE CASCADE);
98
- CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
98
+ CREATE TABLE IF NOT EXISTS symbols (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, file_id INTEGER, kind TEXT NOT NULL, name TEXT NOT NULL, qualified_name TEXT NOT NULL, exported INTEGER NOT NULL, start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, start_offset INTEGER, end_offset INTEGER, source_file TEXT, exported_name TEXT, evidence_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE);
99
99
  CREATE TABLE IF NOT EXISTS handler_classes (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, class_name TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
100
100
  CREATE TABLE IF NOT EXISTS handler_methods (id INTEGER PRIMARY KEY, handler_class_id INTEGER NOT NULL, method_name TEXT NOT NULL, decorator_kind TEXT NOT NULL, decorator_value TEXT, decorator_raw_expression TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE CASCADE);
101
101
  CREATE TABLE IF NOT EXISTS handler_registrations (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, handler_class_id INTEGER, class_name TEXT, import_source TEXT, registration_file TEXT NOT NULL, registration_line INTEGER NOT NULL, registration_kind TEXT NOT NULL, confidence REAL NOT NULL, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(handler_class_id) REFERENCES handler_classes(id) ON DELETE SET NULL);
102
102
  CREATE TABLE IF NOT EXISTS service_bindings (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, symbol_id INTEGER, variable_name TEXT NOT NULL, alias TEXT, alias_expr TEXT, destination_expr TEXT, service_path_expr TEXT, is_dynamic INTEGER NOT NULL, placeholders_json TEXT NOT NULL, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, helper_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
103
- CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
103
+ CREATE TABLE IF NOT EXISTS outbound_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, source_symbol_id INTEGER, call_type TEXT NOT NULL, service_binding_id INTEGER, method TEXT, operation_path_expr TEXT, query_entity TEXT, event_name_expr TEXT, payload_summary TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, confidence REAL NOT NULL, unresolved_reason TEXT, local_service_name TEXT, local_service_lookup TEXT, alias_chain_json TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(source_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL, FOREIGN KEY(service_binding_id) REFERENCES service_bindings(id) ON DELETE SET NULL);
104
+ CREATE TABLE IF NOT EXISTS symbol_calls (id INTEGER PRIMARY KEY, repo_id INTEGER NOT NULL, caller_symbol_id INTEGER NOT NULL, callee_symbol_id INTEGER, callee_expression TEXT NOT NULL, import_source TEXT, source_file TEXT NOT NULL, source_line INTEGER NOT NULL, status TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, unresolved_reason TEXT, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(caller_symbol_id) REFERENCES symbols(id) ON DELETE CASCADE, FOREIGN KEY(callee_symbol_id) REFERENCES symbols(id) ON DELETE SET NULL);
104
105
  CREATE TABLE IF NOT EXISTS graph_edges (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, edge_type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'unresolved', from_kind TEXT NOT NULL, from_id TEXT NOT NULL, to_kind TEXT NOT NULL, to_id TEXT NOT NULL, confidence REAL NOT NULL, evidence_json TEXT NOT NULL, is_dynamic INTEGER NOT NULL, unresolved_reason TEXT, generation INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
105
106
  CREATE TABLE IF NOT EXISTS index_runs (id INTEGER PRIMARY KEY, workspace_id INTEGER NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, status TEXT NOT NULL, repo_count INTEGER NOT NULL, file_count INTEGER NOT NULL, diagnostic_count INTEGER NOT NULL, error_message TEXT, FOREIGN KEY(workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE);
106
107
  CREATE TABLE IF NOT EXISTS diagnostics (id INTEGER PRIMARY KEY, repo_id INTEGER, file_id INTEGER, severity TEXT NOT NULL, code TEXT NOT NULL, message TEXT NOT NULL, source_file TEXT, source_line INTEGER, FOREIGN KEY(repo_id) REFERENCES repositories(id) ON DELETE CASCADE, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE SET NULL);
@@ -108,11 +109,12 @@ CREATE INDEX IF NOT EXISTS idx_repo_name ON repositories(name);
108
109
  CREATE INDEX IF NOT EXISTS idx_service_path ON cds_services(service_path);
109
110
  CREATE INDEX IF NOT EXISTS idx_operation_name ON cds_operations(operation_name, operation_path);
110
111
  CREATE INDEX IF NOT EXISTS idx_calls_repo ON outbound_calls(repo_id, call_type);
112
+ CREATE INDEX IF NOT EXISTS idx_symbol_calls_caller ON symbol_calls(repo_id, caller_symbol_id);
111
113
  CREATE VIRTUAL TABLE IF NOT EXISTS search_index USING fts5(kind, name, path, repo);
112
114
  `;
113
115
 
114
116
  // src/db/migrations.ts
115
- var CURRENT_SCHEMA_VERSION = 3;
117
+ var CURRENT_SCHEMA_VERSION = 4;
116
118
  var columns = {
117
119
  service_bindings: [
118
120
  { name: "helper_chain_json", ddl: "ALTER TABLE service_bindings ADD COLUMN helper_chain_json TEXT" },
@@ -133,6 +135,18 @@ var columns = {
133
135
  { name: "class_name", ddl: "ALTER TABLE handler_registrations ADD COLUMN class_name TEXT" },
134
136
  { name: "import_source", ddl: "ALTER TABLE handler_registrations ADD COLUMN import_source TEXT" }
135
137
  ],
138
+ symbols: [
139
+ { name: "start_offset", ddl: "ALTER TABLE symbols ADD COLUMN start_offset INTEGER" },
140
+ { name: "end_offset", ddl: "ALTER TABLE symbols ADD COLUMN end_offset INTEGER" },
141
+ { name: "source_file", ddl: "ALTER TABLE symbols ADD COLUMN source_file TEXT" },
142
+ { name: "exported_name", ddl: "ALTER TABLE symbols ADD COLUMN exported_name TEXT" },
143
+ { name: "evidence_json", ddl: "ALTER TABLE symbols ADD COLUMN evidence_json TEXT" }
144
+ ],
145
+ outbound_calls: [
146
+ { name: "local_service_name", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_name TEXT" },
147
+ { name: "local_service_lookup", ddl: "ALTER TABLE outbound_calls ADD COLUMN local_service_lookup TEXT" },
148
+ { name: "alias_chain_json", ddl: "ALTER TABLE outbound_calls ADD COLUMN alias_chain_json TEXT" }
149
+ ],
136
150
  index_runs: [
137
151
  { name: "error_message", ddl: "ALTER TABLE index_runs ADD COLUMN error_message TEXT" }
138
152
  ]
@@ -168,6 +182,84 @@ function migrate(db) {
168
182
  });
169
183
  }
170
184
 
185
+ // package.json
186
+ var package_default = {
187
+ name: "@saptools/service-flow",
188
+ version: "0.1.11",
189
+ description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
190
+ type: "module",
191
+ publishConfig: {
192
+ access: "public",
193
+ registry: "https://registry.npmjs.org/"
194
+ },
195
+ bin: {
196
+ "service-flow": "dist/cli.js"
197
+ },
198
+ main: "./dist/index.js",
199
+ types: "./dist/index.d.ts",
200
+ exports: {
201
+ ".": {
202
+ types: "./dist/index.d.ts",
203
+ import: "./dist/index.js"
204
+ }
205
+ },
206
+ files: [
207
+ "CHANGELOG.md",
208
+ "README.md",
209
+ "TECHNICAL-NOTE.md",
210
+ "dist"
211
+ ],
212
+ engines: {
213
+ node: ">=24.0.0"
214
+ },
215
+ scripts: {
216
+ build: "tsup",
217
+ typecheck: "tsc --noEmit",
218
+ lint: "eslint src tests",
219
+ test: "vitest run tests/unit",
220
+ "test:unit": "vitest run tests/unit",
221
+ "test:e2e": "vitest run tests/e2e",
222
+ "test:e2e:fake": "vitest run tests/e2e"
223
+ },
224
+ keywords: [
225
+ "sap",
226
+ "cap",
227
+ "cds",
228
+ "btp",
229
+ "cli",
230
+ "service-graph",
231
+ "call-graph",
232
+ "sqlite",
233
+ "saptools"
234
+ ],
235
+ author: "Dong Tran",
236
+ license: "MIT",
237
+ repository: {
238
+ type: "git",
239
+ url: "git+https://github.com/dongitran/saptools.git",
240
+ directory: "packages/service-flow"
241
+ },
242
+ homepage: "https://github.com/dongitran/saptools/tree/main/packages/service-flow#readme",
243
+ bugs: {
244
+ url: "https://github.com/dongitran/saptools/issues"
245
+ },
246
+ dependencies: {
247
+ commander: "13.1.0",
248
+ picocolors: "1.1.1",
249
+ typescript: "5.9.3",
250
+ zod: "4.4.3"
251
+ },
252
+ devDependencies: {
253
+ "@vitest/coverage-v8": "3.2.4",
254
+ tsup: "8.5.1",
255
+ vitest: "3.2.4"
256
+ }
257
+ };
258
+
259
+ // src/version.ts
260
+ var VERSION = package_default.version;
261
+ var ANALYZER_VERSION = package_default.version;
262
+
171
263
  // src/db/connection.ts
172
264
  var sqliteWarningFilterInstalled = false;
173
265
  function installSqliteWarningFilter() {
@@ -192,7 +284,7 @@ function loadSqlite() {
192
284
  return sqlite;
193
285
  } catch (error) {
194
286
  throw new Error(
195
- "service-flow 0.1.8 requires Node.js >=24 with node:sqlite DatabaseSync support. Upgrade Node.js or install a service-flow build with a compatible SQLite driver.",
287
+ `service-flow ${VERSION} requires Node.js >=24 with node:sqlite DatabaseSync support. Upgrade Node.js or install a service-flow build with a compatible SQLite driver.`,
196
288
  { cause: error }
197
289
  );
198
290
  }
@@ -306,11 +398,12 @@ function clearRepoFacts(db, repoId) {
306
398
  for (const t of [
307
399
  "cds_requires",
308
400
  "cds_services",
309
- "symbols",
310
401
  "handler_classes",
402
+ "outbound_calls",
403
+ "symbol_calls",
311
404
  "handler_registrations",
312
405
  "service_bindings",
313
- "outbound_calls",
406
+ "symbols",
314
407
  "diagnostics",
315
408
  "files"
316
409
  ])
@@ -445,13 +538,26 @@ function insertBindings(db, repoId, rows) {
445
538
  r.helperChain ? JSON.stringify(r.helperChain) : null
446
539
  );
447
540
  }
541
+ function insertExecutableSymbols(db, repoId, rows) {
542
+ const stmt = db.prepare("INSERT INTO symbols(repo_id,file_id,kind,name,qualified_name,exported,start_line,end_line,start_offset,end_offset,source_file,exported_name,evidence_json) VALUES(?,(SELECT id FROM files WHERE repo_id=? AND relative_path=?),?,?,?,?,?,?,?,?,?,?,?)");
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
+ }
545
+ function insertSymbolCalls(db, repoId, rows) {
546
+ 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),(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),?,?,?,?,?,0.8,?,CASE WHEN (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) IS NULL THEN 'No local symbol target matched exactly' ELSE NULL END)`);
547
+ for (const r of rows) stmt.run(repoId, repoId, r.sourceFile, r.callerQualifiedName, repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeExpression, r.importSource, r.sourceFile, r.sourceLine, JSON.stringify(r.evidence), repoId, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.sourceFile, r.calleeLocalName, r.calleeLocalName, r.calleeLocalName, r.sourceFile);
548
+ db.prepare("UPDATE symbol_calls SET status=CASE WHEN callee_symbol_id IS NULL THEN 'unresolved' ELSE 'resolved' END WHERE repo_id=?").run(repoId);
549
+ }
448
550
  function insertCalls(db, repoId, rows) {
449
551
  const stmt = db.prepare(
450
- "INSERT INTO outbound_calls(repo_id,call_type,method,operation_path_expr,query_entity,event_name_expr,payload_summary,source_file,source_line,confidence,unresolved_reason,service_binding_id) VALUES(?,?,?,?,?,?,?,?,?,?,?,(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))"
552
+ "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))"
451
553
  );
452
554
  for (const r of rows)
453
555
  stmt.run(
454
556
  repoId,
557
+ repoId,
558
+ r.sourceFile,
559
+ r.sourceLine,
560
+ r.sourceLine,
455
561
  r.callType,
456
562
  r.method,
457
563
  r.operationPathExpr,
@@ -462,6 +568,9 @@ function insertCalls(db, repoId, rows) {
462
568
  r.sourceLine,
463
569
  r.confidence,
464
570
  r.unresolvedReason,
571
+ r.localServiceName,
572
+ r.localServiceLookup,
573
+ r.aliasChain ? JSON.stringify(r.aliasChain) : null,
465
574
  repoId,
466
575
  r.serviceVariableName,
467
576
  r.sourceFile,
@@ -513,8 +622,91 @@ function errorMessage(error) {
513
622
  }
514
623
 
515
624
  // src/indexer/repository-indexer.ts
625
+ import fs5 from "fs/promises";
626
+ import path5 from "path";
627
+
628
+ // src/parsers/symbol-parser.ts
516
629
  import fs4 from "fs/promises";
517
630
  import path4 from "path";
631
+ import ts from "typescript";
632
+ function lineOf(source, pos) {
633
+ return source.getLineAndCharacterOfPosition(pos).line + 1;
634
+ }
635
+ function nameOf(node) {
636
+ if (!node) return void 0;
637
+ if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) return node.text;
638
+ return void 0;
639
+ }
640
+ function isFunctionLike(node) {
641
+ return ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node);
642
+ }
643
+ function exported(node) {
644
+ return Boolean(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
645
+ }
646
+ function callName(expr) {
647
+ if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
648
+ if (ts.isPropertyAccessExpression(expr)) {
649
+ const left = expr.expression.getText();
650
+ return { expression: expr.getText(), local: left === "this" ? void 0 : left, member: expr.name.text };
651
+ }
652
+ return { expression: expr.getText() };
653
+ }
654
+ function nearest(symbols, line) {
655
+ return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine))[0];
656
+ }
657
+ async function parseExecutableSymbols(repoPath, filePath) {
658
+ const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
659
+ const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
660
+ const sourceFile = normalizePath(filePath);
661
+ const symbols = [];
662
+ const calls = [];
663
+ const imports = /* @__PURE__ */ new Map();
664
+ const addSymbol = (kind, localName, node, parentName, exportedName) => {
665
+ symbols.push({ kind, localName, exportedName, qualifiedName: parentName ? `${parentName}.${localName}` : localName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(exportedName), importExportEvidence: exportedName ? { exportedName } : void 0 });
666
+ };
667
+ const visitImports = (node) => {
668
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
669
+ const sourceText = node.moduleSpecifier.text;
670
+ const clause = node.importClause;
671
+ if (clause?.name) imports.set(clause.name.text, sourceText);
672
+ const named = clause?.namedBindings;
673
+ if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
674
+ if (named && ts.isNamespaceImport(named)) imports.set(named.name.text, sourceText);
675
+ }
676
+ ts.forEachChild(node, visitImports);
677
+ };
678
+ visitImports(source);
679
+ const visitSymbols = (node, parentClass) => {
680
+ if (ts.isClassDeclaration(node) && node.name) {
681
+ for (const member of node.members) visitSymbols(member, node.name.text);
682
+ return;
683
+ }
684
+ if (ts.isMethodDeclaration(node)) {
685
+ const localName = nameOf(node.name);
686
+ if (localName) addSymbol("method", localName, node, parentClass);
687
+ } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
688
+ else if (ts.isVariableStatement(node)) {
689
+ for (const d of node.declarationList.declarations) if (d.initializer && isFunctionLike(d.initializer)) {
690
+ const localName = nameOf(d.name);
691
+ if (localName) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : void 0);
692
+ }
693
+ } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
694
+ };
695
+ visitSymbols(source);
696
+ const visitCalls = (node) => {
697
+ if (ts.isCallExpression(node)) {
698
+ const line = lineOf(source, node.getStart(source));
699
+ const caller = nearest(symbols, line);
700
+ if (caller) {
701
+ const callee = callName(node.expression);
702
+ calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: callee.member ?? callee.local, receiverLocalName: callee.member ? callee.local : void 0, importSource: (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0), sourceFile, sourceLine: line, evidence: { relation: callee.importSource ? "imported" : callee.expression.startsWith("this.") ? "this_method" : "local" } });
703
+ }
704
+ }
705
+ ts.forEachChild(node, visitCalls);
706
+ };
707
+ visitCalls(source);
708
+ return { symbols, calls };
709
+ }
518
710
 
519
711
  // src/utils/hashing.ts
520
712
  import { createHash } from "crypto";
@@ -526,84 +718,6 @@ function sha256Text(text) {
526
718
  return createHash("sha256").update(text).digest("hex");
527
719
  }
528
720
 
529
- // package.json
530
- var package_default = {
531
- name: "@saptools/service-flow",
532
- version: "0.1.9",
533
- description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
534
- type: "module",
535
- publishConfig: {
536
- access: "public",
537
- registry: "https://registry.npmjs.org/"
538
- },
539
- bin: {
540
- "service-flow": "dist/cli.js"
541
- },
542
- main: "./dist/index.js",
543
- types: "./dist/index.d.ts",
544
- exports: {
545
- ".": {
546
- types: "./dist/index.d.ts",
547
- import: "./dist/index.js"
548
- }
549
- },
550
- files: [
551
- "CHANGELOG.md",
552
- "README.md",
553
- "TECHNICAL-NOTE.md",
554
- "dist"
555
- ],
556
- engines: {
557
- node: ">=24.0.0"
558
- },
559
- scripts: {
560
- build: "tsup",
561
- typecheck: "tsc --noEmit",
562
- lint: "eslint src tests",
563
- test: "vitest run tests/unit",
564
- "test:unit": "vitest run tests/unit",
565
- "test:e2e": "vitest run tests/e2e",
566
- "test:e2e:fake": "vitest run tests/e2e"
567
- },
568
- keywords: [
569
- "sap",
570
- "cap",
571
- "cds",
572
- "btp",
573
- "cli",
574
- "service-graph",
575
- "call-graph",
576
- "sqlite",
577
- "saptools"
578
- ],
579
- author: "Dong Tran",
580
- license: "MIT",
581
- repository: {
582
- type: "git",
583
- url: "git+https://github.com/dongitran/saptools.git",
584
- directory: "packages/service-flow"
585
- },
586
- homepage: "https://github.com/dongitran/saptools/tree/main/packages/service-flow#readme",
587
- bugs: {
588
- url: "https://github.com/dongitran/saptools/issues"
589
- },
590
- dependencies: {
591
- commander: "13.1.0",
592
- picocolors: "1.1.1",
593
- typescript: "5.9.3",
594
- zod: "4.4.3"
595
- },
596
- devDependencies: {
597
- "@vitest/coverage-v8": "3.2.4",
598
- tsup: "8.5.1",
599
- vitest: "3.2.4"
600
- }
601
- };
602
-
603
- // src/version.ts
604
- var VERSION = package_default.version;
605
- var ANALYZER_VERSION = package_default.version;
606
-
607
721
  // src/indexer/repository-indexer.ts
608
722
  async function indexRepository(db, repo, force) {
609
723
  try {
@@ -621,6 +735,8 @@ async function indexRepository(db, repo, force) {
621
735
  for (const file of parsed.fileRecords) fileStmt.run(repo.id, file.relativePath, file.extension, file.sha256, file.sizeBytes, (/* @__PURE__ */ new Date()).toISOString());
622
736
  for (const s of parsed.services) insertService(db, repo.id, s);
623
737
  for (const h of parsed.handlers) insertHandler(db, repo.id, h);
738
+ insertExecutableSymbols(db, repo.id, parsed.symbols);
739
+ insertSymbolCalls(db, repo.id, parsed.symbolCalls);
624
740
  insertRegistrations(db, repo.id, parsed.registrations);
625
741
  insertBindings(db, repo.id, parsed.bindings);
626
742
  insertCalls(db, repo.id, parsed.calls);
@@ -636,16 +752,19 @@ async function indexRepository(db, repo, force) {
636
752
  }
637
753
  }
638
754
  async function parseAllSourceFacts(root, files) {
639
- const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], fileRecords: [] };
755
+ const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
640
756
  for (const file of files) {
641
- const abs = path4.join(root, file);
642
- const stat = await fs4.stat(abs);
643
- facts.fileRecords.push({ relativePath: normalizePath(file), extension: path4.extname(file), sha256: await sha256File(abs), sizeBytes: stat.size });
757
+ const abs = path5.join(root, file);
758
+ const stat = await fs5.stat(abs);
759
+ facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: await sha256File(abs), sizeBytes: stat.size });
644
760
  if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file));
645
761
  if (/\.[jt]s$/.test(file)) {
646
762
  facts.handlers.push(...await parseDecorators(root, file));
647
763
  facts.registrations.push(...await parseHandlerRegistrations(root, file));
648
764
  facts.bindings.push(...await parseServiceBindings(root, file));
765
+ const symbolFacts = await parseExecutableSymbols(root, file);
766
+ facts.symbols.push(...symbolFacts.symbols);
767
+ facts.symbolCalls.push(...symbolFacts.calls);
649
768
  facts.calls.push(...await parseOutboundCalls(root, file));
650
769
  }
651
770
  }
@@ -654,11 +773,11 @@ async function parseAllSourceFacts(root, files) {
654
773
  async function findSourceFiles(root) {
655
774
  const out = [];
656
775
  async function walk(dir, prefix = "") {
657
- const entries = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
776
+ const entries = await fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
658
777
  for (const e of entries) {
659
778
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
660
779
  if (e.isDirectory()) {
661
- if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(path4.join(dir, e.name), rel);
780
+ if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(path5.join(dir, e.name), rel);
662
781
  } else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel)) out.push(rel);
663
782
  }
664
783
  }
@@ -671,7 +790,7 @@ function isDefaultTestFile(relativeFile) {
671
790
  return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
672
791
  }
673
792
  async function repositoryFingerprint(root, files, facts) {
674
- const packageJson = await fs4.readFile(path4.join(root, "package.json"), "utf8").catch(() => "");
793
+ const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
675
794
  const normalizedFacts = {
676
795
  analyzerVersion: ANALYZER_VERSION,
677
796
  packageName: facts.packageName,
@@ -684,7 +803,7 @@ async function repositoryFingerprint(root, files, facts) {
684
803
  };
685
804
  const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
686
805
  for (const file of files) {
687
- const content = await fs4.readFile(path4.join(root, file), "utf8");
806
+ const content = await fs5.readFile(path5.join(root, file), "utf8");
688
807
  entries.push(`${file}:${sha256Text(content)}`);
689
808
  }
690
809
  return sha256Text(entries.join("\n"));
@@ -722,25 +841,25 @@ function parseVars(values) {
722
841
  }
723
842
  return out;
724
843
  }
725
- function startLabel(start) {
726
- return [
727
- start.repo,
728
- start.servicePath,
729
- start.operation ?? start.operationPath ?? start.handler
730
- ].filter(Boolean).join(" ");
731
- }
732
844
 
733
845
  // src/output/table-output.ts
734
- function renderTraceTable(trace2) {
735
- const lines = [
736
- `Start: ${startLabel(trace2.start)}`,
737
- "",
738
- "Step Type From To Evidence"
739
- ];
740
- for (const e of trace2.edges)
741
- lines.push(
742
- `${String(e.step).padEnd(5)} ${e.type.padEnd(20)} ${e.from.slice(0, 34).padEnd(35)} ${e.to.slice(0, 35).padEnd(36)} ${String(e.evidence.file ?? "")}:${String(e.evidence.line ?? "")}`
743
- );
846
+ function location(evidence) {
847
+ const file = evidence.file ?? evidence.sourceFile ?? evidence.handlerSourceFile ?? evidence.operationSourceFile ?? evidence.registrationSourceFile;
848
+ const line = evidence.line ?? evidence.sourceLine ?? evidence.handlerSourceLine ?? evidence.operationSourceLine ?? evidence.registrationSourceLine;
849
+ if (file || line) return `${String(file ?? "")}:${String(line ?? "")}`;
850
+ const candidates = evidence.candidates;
851
+ if (Array.isArray(candidates) && candidates.length > 0) {
852
+ const first = candidates[0];
853
+ return `${String(first.sourceFile ?? "")}:${String(first.sourceLine ?? "")}`;
854
+ }
855
+ return ":";
856
+ }
857
+ function renderTraceTable(result) {
858
+ const lines = ["Step Type From To Evidence"];
859
+ for (const e of result.edges) {
860
+ 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)}`);
861
+ }
862
+ if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.map((d) => `${String(d.severity ?? "info")} ${String(d.code ?? "diagnostic")} ${String(d.message ?? "")}`));
744
863
  return `${lines.join("\n")}
745
864
  `;
746
865
  }
@@ -846,7 +965,7 @@ function createProgram() {
846
965
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
847
966
  const r = linkWorkspace(db, workspaceId);
848
967
  process.stdout.write(
849
- `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
968
+ `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
850
969
  `
851
970
  );
852
971
  }).catch(fail)
@@ -1001,12 +1120,36 @@ function createProgram() {
1001
1120
  SELECT 'warning','legacy_schema_weaker_foreign_keys','Legacy table lacks fresh-schema foreign-key metadata; rebuild the database or re-run init/index in a new database',NULL,NULL
1002
1121
  WHERE (SELECT COUNT(*) FROM pragma_foreign_key_list('graph_edges'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('index_runs'))=0 OR (SELECT COUNT(*) FROM pragma_foreign_key_list('diagnostics'))=0
1003
1122
  UNION ALL
1123
+ SELECT 'warning','implementation_candidates_rejected','Implementation candidates were rejected for ' || s.service_path || o.operation_path,o.source_file,o.source_line
1124
+ FROM graph_edges e
1125
+ JOIN cds_operations o ON o.id=CAST(e.from_id AS INTEGER)
1126
+ JOIN cds_services s ON s.id=o.service_id
1127
+ WHERE e.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND e.status='unresolved' AND (? OR EXISTS (SELECT 1 FROM graph_edges remote WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' AND remote.to_id=e.from_id))
1128
+ UNION ALL
1129
+ SELECT 'warning','remote_target_without_implementation','Remote target operation has no implementation edge: ' || s.service_path || o.operation_path,o.source_file,o.source_line
1130
+ FROM graph_edges remote
1131
+ JOIN cds_operations o ON o.id=CAST(remote.to_id AS INTEGER)
1132
+ JOIN cds_services s ON s.id=o.service_id
1133
+ WHERE remote.edge_type='REMOTE_CALL_RESOLVES_TO_OPERATION' AND remote.to_kind='operation' AND NOT EXISTS (SELECT 1 FROM graph_edges impl WHERE impl.edge_type='OPERATION_IMPLEMENTED_BY_HANDLER' AND impl.from_kind='operation' AND impl.from_id=remote.to_id) AND ?
1134
+ UNION ALL
1135
+ SELECT CASE WHEN ? THEN 'warning' ELSE 'error' END,'local_service_calls_all_unresolved','All local service calls are unresolved; verify local service alias parsing and linking',NULL,NULL
1136
+ WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE call_type='local_service_call') AND NOT EXISTS (SELECT 1 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' AND e.status='resolved')
1137
+ UNION ALL
1138
+ SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
1139
+ FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
1140
+ UNION ALL
1141
+ SELECT CASE WHEN ? THEN 'warning' ELSE 'error' END,'outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
1142
+ FROM outbound_calls WHERE source_symbol_id IS NULL HAVING COUNT(*) >= CASE WHEN ? THEN 1 ELSE 10 END
1143
+ UNION ALL
1144
+ SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
1145
+ WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
1146
+ UNION ALL
1004
1147
  SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
1005
1148
  WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
1006
1149
  UNION ALL
1007
1150
  SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
1008
1151
  FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
1009
- ).all(Boolean(opts.strict), Boolean(opts.strict));
1152
+ ).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
1010
1153
  const allDiagnostics = [...diagnostics, ...health];
1011
1154
  process.stdout.write(
1012
1155
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
@@ -1017,20 +1160,20 @@ function createProgram() {
1017
1160
  program.command("clean").option("--workspace <path>").option("--db-only").action(
1018
1161
  (opts) => void (async () => {
1019
1162
  const config = await loadWorkspaceConfig(opts.workspace);
1020
- const dbDir = path5.resolve(path5.dirname(config.dbPath));
1021
- const workspaceRoot = path5.resolve(config.rootPath);
1022
- await fs5.rm(config.dbPath, { force: true });
1163
+ const dbDir = path6.resolve(path6.dirname(config.dbPath));
1164
+ const workspaceRoot = path6.resolve(config.rootPath);
1165
+ await fs6.rm(config.dbPath, { force: true });
1023
1166
  if (!opts.dbOnly) {
1024
- const marker = path5.join(dbDir, ".service-flow-state");
1167
+ const marker = path6.join(dbDir, ".service-flow-state");
1025
1168
  const dangerous = /* @__PURE__ */ new Set([
1026
- path5.parse(dbDir).root,
1169
+ path6.parse(dbDir).root,
1027
1170
  "/tmp",
1028
- process.env.HOME ? path5.resolve(process.env.HOME) : "",
1171
+ process.env.HOME ? path6.resolve(process.env.HOME) : "",
1029
1172
  workspaceRoot
1030
1173
  ]);
1031
1174
  let ownsState;
1032
1175
  try {
1033
- ownsState = (await fs5.stat(marker)).isFile();
1176
+ ownsState = (await fs6.stat(marker)).isFile();
1034
1177
  } catch {
1035
1178
  ownsState = false;
1036
1179
  }
@@ -1038,7 +1181,7 @@ function createProgram() {
1038
1181
  throw new Error(
1039
1182
  `Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
1040
1183
  );
1041
- await fs5.rm(dbDir, { recursive: true, force: true });
1184
+ await fs6.rm(dbDir, { recursive: true, force: true });
1042
1185
  }
1043
1186
  process.stdout.write("Cleaned service-flow state\n");
1044
1187
  })().catch(fail)