@saptools/service-flow 0.1.10 → 0.1.12

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-LQT67AU5.js";
13
+ } from "./chunk-MXYYARUP.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
  ]
@@ -171,7 +185,7 @@ function migrate(db) {
171
185
  // package.json
172
186
  var package_default = {
173
187
  name: "@saptools/service-flow",
174
- version: "0.1.10",
188
+ version: "0.1.12",
175
189
  description: "Trace SAP CAP service-to-service flows across multi-repository workspaces with runtime-aware graph resolution",
176
190
  type: "module",
177
191
  publishConfig: {
@@ -384,11 +398,12 @@ function clearRepoFacts(db, repoId) {
384
398
  for (const t of [
385
399
  "cds_requires",
386
400
  "cds_services",
387
- "symbols",
388
401
  "handler_classes",
402
+ "outbound_calls",
403
+ "symbol_calls",
389
404
  "handler_registrations",
390
405
  "service_bindings",
391
- "outbound_calls",
406
+ "symbols",
392
407
  "diagnostics",
393
408
  "files"
394
409
  ])
@@ -523,13 +538,26 @@ function insertBindings(db, repoId, rows) {
523
538
  r.helperChain ? JSON.stringify(r.helperChain) : null
524
539
  );
525
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, unresolved_reason=CASE WHEN callee_symbol_id IS NULL THEN COALESCE(unresolved_reason,'No local symbol target matched exactly') ELSE NULL END WHERE repo_id=?").run(repoId);
549
+ }
526
550
  function insertCalls(db, repoId, rows) {
527
551
  const stmt = db.prepare(
528
- "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))"
529
553
  );
530
554
  for (const r of rows)
531
555
  stmt.run(
532
556
  repoId,
557
+ repoId,
558
+ r.sourceFile,
559
+ r.sourceLine,
560
+ r.sourceLine,
533
561
  r.callType,
534
562
  r.method,
535
563
  r.operationPathExpr,
@@ -540,6 +568,9 @@ function insertCalls(db, repoId, rows) {
540
568
  r.sourceLine,
541
569
  r.confidence,
542
570
  r.unresolvedReason,
571
+ r.localServiceName,
572
+ r.localServiceLookup,
573
+ r.aliasChain ? JSON.stringify(r.aliasChain) : null,
543
574
  repoId,
544
575
  r.serviceVariableName,
545
576
  r.sourceFile,
@@ -591,8 +622,130 @@ function errorMessage(error) {
591
622
  }
592
623
 
593
624
  // src/indexer/repository-indexer.ts
625
+ import fs5 from "fs/promises";
626
+ import path5 from "path";
627
+
628
+ // src/parsers/symbol-parser.ts
594
629
  import fs4 from "fs/promises";
595
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 exportDeclarations(source) {
647
+ const exports = /* @__PURE__ */ new Map();
648
+ const visit = (node) => {
649
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) {
650
+ for (const el of node.exportClause.elements) exports.set((el.propertyName ?? el.name).text, el.name.text);
651
+ }
652
+ ts.forEachChild(node, visit);
653
+ };
654
+ visit(source);
655
+ return exports;
656
+ }
657
+ function isRelativeImport(value) {
658
+ return Boolean(value?.startsWith("."));
659
+ }
660
+ function isObjectFunction(node) {
661
+ return ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
662
+ }
663
+ function callName(expr) {
664
+ if (ts.isIdentifier(expr)) return { expression: expr.text, local: expr.text };
665
+ if (ts.isPropertyAccessExpression(expr)) {
666
+ const left = expr.expression.getText();
667
+ return { expression: expr.getText(), local: left === "this" ? void 0 : left, member: expr.name.text };
668
+ }
669
+ return { expression: expr.getText() };
670
+ }
671
+ function nearest(symbols, line) {
672
+ return symbols.filter((s) => s.startLine <= line && s.endLine >= line).sort((a, b) => a.endLine - a.startLine - (b.endLine - b.startLine))[0];
673
+ }
674
+ async function parseExecutableSymbols(repoPath, filePath) {
675
+ const text = await fs4.readFile(path4.join(repoPath, filePath), "utf8");
676
+ const source = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true, filePath.endsWith(".ts") ? ts.ScriptKind.TS : ts.ScriptKind.JS);
677
+ const sourceFile = normalizePath(filePath);
678
+ const symbols = [];
679
+ const calls = [];
680
+ const imports = /* @__PURE__ */ new Map();
681
+ const exportNames = exportDeclarations(source);
682
+ const objectExports = /* @__PURE__ */ new Set();
683
+ const addSymbol = (kind, localName, node, parentName, exportedName) => {
684
+ const declaredExportName = exportedName ?? exportNames.get(parentName ? parentName.split(".")[0] ?? localName : localName);
685
+ const qualifiedName = parentName ? `${parentName}.${localName}` : localName;
686
+ const objectExported = parentName ? objectExports.has(parentName.split(".")[0] ?? "") : false;
687
+ symbols.push({ kind, localName: kind === "object_method" ? qualifiedName : localName, exportedName: objectExported ? qualifiedName : declaredExportName, qualifiedName, sourceFile, startLine: lineOf(source, node.getStart(source)), endLine: lineOf(source, node.getEnd()), startOffset: node.getStart(source), endOffset: node.getEnd(), exported: exported(node) || Boolean(declaredExportName) || objectExported, importExportEvidence: declaredExportName ? { exportedName: declaredExportName, source: "export_declaration" } : objectExported ? { exportedName: qualifiedName, source: "exported_object_literal" } : void 0 });
688
+ };
689
+ const visitImports = (node) => {
690
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
691
+ const sourceText = node.moduleSpecifier.text;
692
+ const clause = node.importClause;
693
+ if (clause?.name) imports.set(clause.name.text, sourceText);
694
+ const named = clause?.namedBindings;
695
+ if (named && ts.isNamedImports(named)) for (const el of named.elements) imports.set(el.name.text, sourceText);
696
+ if (named && ts.isNamespaceImport(named)) imports.set(named.name.text, sourceText);
697
+ }
698
+ ts.forEachChild(node, visitImports);
699
+ };
700
+ visitImports(source);
701
+ const visitSymbols = (node, parentClass) => {
702
+ if (ts.isClassDeclaration(node) && node.name) {
703
+ for (const member of node.members) visitSymbols(member, node.name.text);
704
+ return;
705
+ }
706
+ if (ts.isMethodDeclaration(node)) {
707
+ const localName = nameOf(node.name);
708
+ if (localName) addSymbol("method", localName, node, parentClass);
709
+ } else if (ts.isFunctionDeclaration(node) && node.name) addSymbol("function", node.name.text, node, void 0, exported(node) ? node.name.text : void 0);
710
+ else if (ts.isVariableStatement(node)) {
711
+ for (const d of node.declarationList.declarations) {
712
+ const localName = nameOf(d.name);
713
+ if (!localName || !d.initializer) continue;
714
+ if (isFunctionLike(d.initializer)) addSymbol("function", localName, d.initializer, void 0, exported(node) ? localName : exportNames.get(localName));
715
+ if (ts.isObjectLiteralExpression(d.initializer)) {
716
+ if (exported(node) || exportNames.has(localName)) objectExports.add(localName);
717
+ for (const prop of d.initializer.properties) {
718
+ if (ts.isPropertyAssignment(prop) && isObjectFunction(prop.initializer)) {
719
+ const propName = nameOf(prop.name);
720
+ if (propName) addSymbol("object_method", propName, prop.initializer, localName);
721
+ } else if (ts.isMethodDeclaration(prop)) {
722
+ const propName = nameOf(prop.name);
723
+ if (propName) addSymbol("object_method", propName, prop, localName);
724
+ }
725
+ }
726
+ }
727
+ }
728
+ } else ts.forEachChild(node, (child) => visitSymbols(child, parentClass));
729
+ };
730
+ visitSymbols(source);
731
+ const localCallables = new Set(symbols.flatMap((sym) => [sym.localName, sym.qualifiedName]));
732
+ const visitCalls = (node) => {
733
+ if (ts.isCallExpression(node)) {
734
+ const line = lineOf(source, node.getStart(source));
735
+ const caller = nearest(symbols, line);
736
+ if (caller) {
737
+ const callee = callName(node.expression);
738
+ const importSource = (callee.local ? imports.get(callee.local) : void 0) ?? (callee.member && callee.local ? imports.get(callee.local) : void 0);
739
+ const targetName = callee.expression.startsWith("this.") ? callee.member : callee.member && callee.local ? `${callee.local}.${callee.member}` : callee.local;
740
+ const keep = Boolean(targetName) && (localCallables.has(String(targetName)) || callee.expression.startsWith("this.") || isRelativeImport(importSource));
741
+ if (keep) calls.push({ callerQualifiedName: caller.qualifiedName, calleeExpression: callee.expression, calleeLocalName: targetName, receiverLocalName: callee.member ? callee.local : void 0, importSource, sourceFile, sourceLine: line, evidence: { relation: importSource ? "relative_import" : callee.expression.startsWith("this.") ? "this_method" : "local", caller: caller.qualifiedName, targetName } });
742
+ }
743
+ }
744
+ ts.forEachChild(node, visitCalls);
745
+ };
746
+ visitCalls(source);
747
+ return { symbols, calls };
748
+ }
596
749
 
597
750
  // src/utils/hashing.ts
598
751
  import { createHash } from "crypto";
@@ -621,6 +774,8 @@ async function indexRepository(db, repo, force) {
621
774
  for (const file of parsed.fileRecords) fileStmt.run(repo.id, file.relativePath, file.extension, file.sha256, file.sizeBytes, (/* @__PURE__ */ new Date()).toISOString());
622
775
  for (const s of parsed.services) insertService(db, repo.id, s);
623
776
  for (const h of parsed.handlers) insertHandler(db, repo.id, h);
777
+ insertExecutableSymbols(db, repo.id, parsed.symbols);
778
+ insertSymbolCalls(db, repo.id, parsed.symbolCalls);
624
779
  insertRegistrations(db, repo.id, parsed.registrations);
625
780
  insertBindings(db, repo.id, parsed.bindings);
626
781
  insertCalls(db, repo.id, parsed.calls);
@@ -636,16 +791,19 @@ async function indexRepository(db, repo, force) {
636
791
  }
637
792
  }
638
793
  async function parseAllSourceFacts(root, files) {
639
- const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], fileRecords: [] };
794
+ const facts = { services: [], handlers: [], registrations: [], bindings: [], calls: [], symbols: [], symbolCalls: [], fileRecords: [] };
640
795
  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 });
796
+ const abs = path5.join(root, file);
797
+ const stat = await fs5.stat(abs);
798
+ facts.fileRecords.push({ relativePath: normalizePath(file), extension: path5.extname(file), sha256: await sha256File(abs), sizeBytes: stat.size });
644
799
  if (file.endsWith(".cds")) facts.services.push(...await parseCdsFile(root, file));
645
800
  if (/\.[jt]s$/.test(file)) {
646
801
  facts.handlers.push(...await parseDecorators(root, file));
647
802
  facts.registrations.push(...await parseHandlerRegistrations(root, file));
648
803
  facts.bindings.push(...await parseServiceBindings(root, file));
804
+ const symbolFacts = await parseExecutableSymbols(root, file);
805
+ facts.symbols.push(...symbolFacts.symbols);
806
+ facts.symbolCalls.push(...symbolFacts.calls);
649
807
  facts.calls.push(...await parseOutboundCalls(root, file));
650
808
  }
651
809
  }
@@ -654,11 +812,11 @@ async function parseAllSourceFacts(root, files) {
654
812
  async function findSourceFiles(root) {
655
813
  const out = [];
656
814
  async function walk(dir, prefix = "") {
657
- const entries = await fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
815
+ const entries = await fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
658
816
  for (const e of entries) {
659
817
  const rel = prefix ? `${prefix}/${e.name}` : e.name;
660
818
  if (e.isDirectory()) {
661
- if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(path4.join(dir, e.name), rel);
819
+ if (!["node_modules", "dist", "gen", "coverage", ".git"].includes(e.name)) await walk(path5.join(dir, e.name), rel);
662
820
  } else if (/\.(cds|ts|js)$/.test(e.name) && !isDefaultTestFile(rel)) out.push(rel);
663
821
  }
664
822
  }
@@ -671,7 +829,7 @@ function isDefaultTestFile(relativeFile) {
671
829
  return /\.(test|spec)\.[jt]s$/.test(parts.at(-1) ?? "");
672
830
  }
673
831
  async function repositoryFingerprint(root, files, facts) {
674
- const packageJson = await fs4.readFile(path4.join(root, "package.json"), "utf8").catch(() => "");
832
+ const packageJson = await fs5.readFile(path5.join(root, "package.json"), "utf8").catch(() => "");
675
833
  const normalizedFacts = {
676
834
  analyzerVersion: ANALYZER_VERSION,
677
835
  packageName: facts.packageName,
@@ -684,7 +842,7 @@ async function repositoryFingerprint(root, files, facts) {
684
842
  };
685
843
  const entries = [`facts:${JSON.stringify(normalizedFacts)}`];
686
844
  for (const file of files) {
687
- const content = await fs4.readFile(path4.join(root, file), "utf8");
845
+ const content = await fs5.readFile(path5.join(root, file), "utf8");
688
846
  entries.push(`${file}:${sha256Text(content)}`);
689
847
  }
690
848
  return sha256Text(entries.join("\n"));
@@ -722,25 +880,25 @@ function parseVars(values) {
722
880
  }
723
881
  return out;
724
882
  }
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
883
 
733
884
  // 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
- );
885
+ function location(evidence) {
886
+ const file = evidence.file ?? evidence.sourceFile ?? evidence.handlerSourceFile ?? evidence.operationSourceFile ?? evidence.registrationSourceFile;
887
+ const line = evidence.line ?? evidence.sourceLine ?? evidence.handlerSourceLine ?? evidence.operationSourceLine ?? evidence.registrationSourceLine;
888
+ if (file || line) return `${String(file ?? "")}:${String(line ?? "")}`;
889
+ const candidates = evidence.candidates;
890
+ if (Array.isArray(candidates) && candidates.length > 0) {
891
+ const first = candidates[0];
892
+ return `${String(first.sourceFile ?? "")}:${String(first.sourceLine ?? "")}`;
893
+ }
894
+ return ":";
895
+ }
896
+ function renderTraceTable(result) {
897
+ const lines = ["Step Type From To Evidence"];
898
+ for (const e of result.edges) {
899
+ 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)}`);
900
+ }
901
+ if (result.diagnostics.length > 0) lines.push("", "Diagnostics:", ...result.diagnostics.map((d) => `${String(d.severity ?? "info")} ${String(d.code ?? "diagnostic")} ${String(d.message ?? "")}`));
744
902
  return `${lines.join("\n")}
745
903
  `;
746
904
  }
@@ -758,11 +916,15 @@ function renderTraceJson(trace2) {
758
916
  function safe(value) {
759
917
  return value.replace(/[^\w-]/g, "_").slice(0, 60);
760
918
  }
919
+ function label(trace2, idOrLabel) {
920
+ const node = trace2.nodes.find((item) => item.id === idOrLabel || item.label === idOrLabel);
921
+ return String(node?.label ?? idOrLabel);
922
+ }
761
923
  function renderMermaid(trace2) {
762
924
  const lines = ["flowchart TD"];
763
925
  for (const e of trace2.edges)
764
926
  lines.push(
765
- ` ${safe(e.from)}["${e.from}"] -->|${e.type}| ${safe(e.to)}["${e.to}"]`
927
+ ` ${safe(e.from)}["${label(trace2, e.from)}"] -->|${e.type}| ${safe(e.to)}["${label(trace2, e.to)}"]`
766
928
  );
767
929
  return `${lines.join("\n")}
768
930
  `;
@@ -846,7 +1008,7 @@ function createProgram() {
846
1008
  (opts) => void withWorkspace(opts.workspace, (db, workspaceId) => {
847
1009
  const r = linkWorkspace(db, workspaceId);
848
1010
  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
1011
+ `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
1012
  `
851
1013
  );
852
1014
  }).catch(fail)
@@ -1013,12 +1175,24 @@ function createProgram() {
1013
1175
  JOIN cds_services s ON s.id=o.service_id
1014
1176
  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 ?
1015
1177
  UNION ALL
1178
+ 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
1179
+ 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')
1180
+ UNION ALL
1181
+ SELECT 'error','local_service_accessor_misclassified','Entity accessor calls were indexed as /entities operations',source_file,source_line
1182
+ FROM outbound_calls WHERE call_type='local_service_call' AND operation_path_expr='/entities' AND (? OR 1)
1183
+ UNION ALL
1184
+ SELECT 'warning','outbound_calls_without_source_symbol','Outbound calls lack source symbol ownership: ' || COUNT(*),NULL,NULL
1185
+ FROM outbound_calls WHERE source_symbol_id IS NULL AND ? HAVING COUNT(*) >= 1
1186
+ UNION ALL
1187
+ SELECT 'warning','trace_scope_fell_back_to_file','Trace may fall back to source-file scope for calls without symbols',NULL,NULL
1188
+ WHERE EXISTS (SELECT 1 FROM outbound_calls WHERE source_symbol_id IS NULL) AND ?
1189
+ UNION ALL
1016
1190
  SELECT 'warning','graph_stale','Graph is stale after repository fact changes; run service-flow link',NULL,NULL
1017
1191
  WHERE EXISTS (SELECT 1 FROM repositories WHERE graph_stale_reason IS NOT NULL)
1018
1192
  UNION ALL
1019
1193
  SELECT 'warning','index_run_abandoned','Index run ' || id || ' started at ' || started_at || ' is still running after the 60 minute abandonment threshold',NULL,NULL
1020
1194
  FROM index_runs WHERE status='running' AND datetime(started_at) < datetime('now','-60 minutes')`
1021
- ).all(Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict), Boolean(opts.strict));
1195
+ ).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));
1022
1196
  const allDiagnostics = [...diagnostics, ...health];
1023
1197
  process.stdout.write(
1024
1198
  allDiagnostics.length ? renderJson(allDiagnostics) : `${pc.green("No diagnostics recorded")}
@@ -1029,20 +1203,20 @@ function createProgram() {
1029
1203
  program.command("clean").option("--workspace <path>").option("--db-only").action(
1030
1204
  (opts) => void (async () => {
1031
1205
  const config = await loadWorkspaceConfig(opts.workspace);
1032
- const dbDir = path5.resolve(path5.dirname(config.dbPath));
1033
- const workspaceRoot = path5.resolve(config.rootPath);
1034
- await fs5.rm(config.dbPath, { force: true });
1206
+ const dbDir = path6.resolve(path6.dirname(config.dbPath));
1207
+ const workspaceRoot = path6.resolve(config.rootPath);
1208
+ await fs6.rm(config.dbPath, { force: true });
1035
1209
  if (!opts.dbOnly) {
1036
- const marker = path5.join(dbDir, ".service-flow-state");
1210
+ const marker = path6.join(dbDir, ".service-flow-state");
1037
1211
  const dangerous = /* @__PURE__ */ new Set([
1038
- path5.parse(dbDir).root,
1212
+ path6.parse(dbDir).root,
1039
1213
  "/tmp",
1040
- process.env.HOME ? path5.resolve(process.env.HOME) : "",
1214
+ process.env.HOME ? path6.resolve(process.env.HOME) : "",
1041
1215
  workspaceRoot
1042
1216
  ]);
1043
1217
  let ownsState;
1044
1218
  try {
1045
- ownsState = (await fs5.stat(marker)).isFile();
1219
+ ownsState = (await fs6.stat(marker)).isFile();
1046
1220
  } catch {
1047
1221
  ownsState = false;
1048
1222
  }
@@ -1050,7 +1224,7 @@ function createProgram() {
1050
1224
  throw new Error(
1051
1225
  `Refusing to recursively delete unowned or dangerous state directory: ${dbDir}. Use --db-only to remove only the database file.`
1052
1226
  );
1053
- await fs5.rm(dbDir, { recursive: true, force: true });
1227
+ await fs6.rm(dbDir, { recursive: true, force: true });
1054
1228
  }
1055
1229
  process.stdout.write("Cleaned service-flow state\n");
1056
1230
  })().catch(fail)