codesearch-mcp 0.0.15-alpha → 0.0.17-alpha

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +74 -33
  3. package/dist/app.d.ts +1 -1
  4. package/dist/app.js +67 -16
  5. package/dist/src/commands/BackfillCommand.js +15 -5
  6. package/dist/src/commands/ConfigCommand.d.ts +12 -0
  7. package/dist/src/commands/ConfigCommand.js +98 -0
  8. package/dist/src/commands/IndexCommand.js +1 -1
  9. package/dist/src/config-store.d.ts +53 -0
  10. package/dist/src/config-store.js +197 -0
  11. package/dist/src/config.d.ts +22 -8
  12. package/dist/src/config.js +103 -58
  13. package/dist/src/container.d.ts +1 -1
  14. package/dist/src/container.js +8 -7
  15. package/dist/src/db/index.d.ts +2 -3
  16. package/dist/src/db/index.js +6 -7
  17. package/dist/src/db/schema.d.ts +577 -0
  18. package/dist/src/db/schema.js +47 -0
  19. package/dist/src/help.js +28 -12
  20. package/dist/src/interfaces/IAstParser.d.ts +19 -0
  21. package/dist/src/interfaces/IEmbedder.d.ts +1 -0
  22. package/dist/src/project-selector.d.ts +14 -5
  23. package/dist/src/project-selector.js +74 -60
  24. package/dist/src/services/AstParserService.d.ts +7 -0
  25. package/dist/src/services/AstParserService.js +191 -28
  26. package/dist/src/services/DatabaseService.d.ts +11 -4
  27. package/dist/src/services/DatabaseService.js +107 -48
  28. package/dist/src/services/IndexerService.d.ts +9 -0
  29. package/dist/src/services/IndexerService.js +224 -60
  30. package/dist/src/services/LoggerService.d.ts +5 -1
  31. package/dist/src/services/LoggerService.js +8 -5
  32. package/dist/src/services/McpServer.d.ts +1 -0
  33. package/dist/src/services/McpServer.js +74 -4
  34. package/dist/src/services/OllamaEmbedder.d.ts +2 -0
  35. package/dist/src/services/OllamaEmbedder.js +29 -0
  36. package/dist/src/services/SearcherService.d.ts +13 -0
  37. package/dist/src/services/SearcherService.js +118 -22
  38. package/dist/src/setup.js +57 -62
  39. package/package.json +4 -5
@@ -13,12 +13,31 @@ export interface SymbolEntry {
13
13
  kind: 'function' | 'class' | 'interface' | 'type' | 'variable' | 'method';
14
14
  signature: string | null;
15
15
  line: number | null;
16
+ endLine: number | null;
17
+ col: number | null;
18
+ }
19
+ export interface CallEdge {
20
+ project: string;
21
+ fromFile: string;
22
+ fromSymbol: string | null;
23
+ calleeName: string;
24
+ calleeFile: string;
25
+ calleeSymbol: string | null;
26
+ line: number | null;
16
27
  col: number | null;
17
28
  }
18
29
  export interface FileAnalysis {
19
30
  edges: DepEdge[];
20
31
  symbols: SymbolEntry[];
32
+ calls: CallEdge[];
21
33
  }
22
34
  export interface IAstParser {
35
+ /**
36
+ * @deprecated Use analyzeProject instead for project-wide context.
37
+ */
23
38
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
39
+ analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: {
40
+ skipDependencyResolution?: boolean;
41
+ }): Promise<Map<string, FileAnalysis>>;
42
+ clearCache(projectRoot?: string): void;
24
43
  }
@@ -1,4 +1,5 @@
1
1
  export interface IEmbedder {
2
2
  embed(text: string): Promise<number[]>;
3
3
  translate?(text: string, model: string): Promise<string>;
4
+ rerank?(query: string, documents: string[], topN: number): Promise<number[]>;
4
5
  }
@@ -1,7 +1,16 @@
1
- export declare function selectProjects(allProjects: {
1
+ export interface Project {
2
2
  name: string;
3
3
  path: string;
4
- }[], workspaceDir: string, forceSelect: boolean): Promise<{
5
- name: string;
6
- path: string;
7
- }[]>;
4
+ }
5
+ export declare class ProjectSelector {
6
+ private readonly workspaceDir;
7
+ private included;
8
+ private skipped;
9
+ constructor(workspaceDir: string);
10
+ select(allProjects: Project[], forceSelect: boolean): Promise<Project[]>;
11
+ private printAlreadySelected;
12
+ private promptForSelection;
13
+ private promptForSkip;
14
+ private load;
15
+ private save;
16
+ }
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.selectProjects = selectProjects;
36
+ exports.ProjectSelector = void 0;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const prompts_1 = require("@inquirer/prompts");
@@ -65,76 +65,90 @@ async function saveLines(filePath, names) {
65
65
  }
66
66
  }
67
67
  }
68
- async function loadConfig(workspaceDir) {
69
- const [included, skipped] = await Promise.all([
70
- loadLines(path.join(workspaceDir, '.ragprojects')),
71
- loadLines(path.join(workspaceDir, '.ragskip')),
72
- ]);
73
- return { included, skipped };
74
- }
75
- async function saveConfig(workspaceDir, config) {
76
- await Promise.all([
77
- saveLines(path.join(workspaceDir, '.ragprojects'), config.included),
78
- saveLines(path.join(workspaceDir, '.ragskip'), config.skipped),
79
- ]);
80
- }
81
- async function selectProjects(allProjects, workspaceDir, forceSelect) {
82
- const config = await loadConfig(workspaceDir);
83
- const newProjects = allProjects.filter(p => !config.included.has(p.name) && !config.skipped.has(p.name));
84
- // Nothing to ask — return previously saved selection silently
85
- if (newProjects.length === 0 && !forceSelect) {
86
- const selected = allProjects.filter(p => config.included.has(p.name));
87
- if (selected.length === 0) {
88
- console.log('No projects selected. Run with --select to choose projects.');
68
+ class ProjectSelector {
69
+ workspaceDir;
70
+ included = new Set();
71
+ skipped = new Set();
72
+ constructor(workspaceDir) {
73
+ this.workspaceDir = workspaceDir;
74
+ }
75
+ async select(allProjects, forceSelect) {
76
+ await this.load();
77
+ const newProjects = allProjects.filter(p => !this.included.has(p.name) && !this.skipped.has(p.name));
78
+ const previouslySelected = allProjects.filter(p => this.included.has(p.name));
79
+ // Nothing new and something already selected — return silently
80
+ if (newProjects.length === 0 && !forceSelect && previouslySelected.length > 0) {
81
+ return previouslySelected;
89
82
  }
83
+ // No previous selection — treat as implicit --select so the user picks projects now
84
+ const autoSelect = !forceSelect && previouslySelected.length === 0;
85
+ // In --select / auto-select mode show all non-skipped projects; otherwise only new ones
86
+ const toShow = (forceSelect || autoSelect)
87
+ ? allProjects.filter(p => !this.skipped.has(p.name))
88
+ : newProjects;
89
+ if (toShow.length === 0) {
90
+ console.log('All discovered projects are marked as skipped. Run with --select to reconfigure.');
91
+ return [];
92
+ }
93
+ if (!forceSelect && !autoSelect)
94
+ this.printAlreadySelected(allProjects);
95
+ await this.promptForSelection(toShow, forceSelect || autoSelect, newProjects.length);
96
+ await this.promptForSkip(toShow);
97
+ await this.save();
98
+ const selected = allProjects.filter(p => this.included.has(p.name));
99
+ console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
90
100
  return selected;
91
101
  }
92
- // In --select mode show all non-skipped projects; otherwise only new ones
93
- const toShow = forceSelect
94
- ? allProjects.filter(p => !config.skipped.has(p.name))
95
- : newProjects;
96
- const alreadyIncluded = allProjects.filter(p => config.included.has(p.name));
97
- if (!forceSelect && alreadyIncluded.length > 0) {
98
- console.log(`Already selected (will be re-indexed): ${alreadyIncluded.map(p => p.name).join(', ')}`);
99
- console.log('Run with --select to change the full selection.\n');
100
- }
101
- const message = forceSelect
102
- ? 'Select projects to index'
103
- : `Found ${newProjects.length} new project(s) — select which to add`;
104
- // Checkbox: pre-check previously included projects
105
- const chosen = await (0, prompts_1.checkbox)({
106
- message,
107
- choices: toShow.map(p => ({
108
- name: p.name,
109
- value: p.name,
110
- checked: config.included.has(p.name),
111
- })),
112
- pageSize: 15,
113
- });
114
- const chosenSet = new Set(chosen);
115
- // Update included
116
- for (const p of toShow) {
117
- if (chosenSet.has(p.name)) {
118
- config.included.add(p.name);
119
- config.skipped.delete(p.name);
102
+ printAlreadySelected(allProjects) {
103
+ const alreadyIncluded = allProjects.filter(p => this.included.has(p.name));
104
+ if (alreadyIncluded.length > 0) {
105
+ console.log(`Already selected (will be re-indexed): ${alreadyIncluded.map(p => p.name).join(', ')}`);
106
+ console.log('Run with --select to change the full selection.\n');
120
107
  }
121
- else {
122
- config.included.delete(p.name);
108
+ }
109
+ async promptForSelection(toShow, showAll, newCount) {
110
+ const message = showAll
111
+ ? 'Select projects to index'
112
+ : `Found ${newCount} new project(s) — select which to add`;
113
+ const chosen = await (0, prompts_1.checkbox)({
114
+ message,
115
+ choices: toShow.map(p => ({ name: p.name, value: p.name, checked: this.included.has(p.name) })),
116
+ pageSize: 15,
117
+ });
118
+ const chosenSet = new Set(chosen);
119
+ for (const p of toShow) {
120
+ if (chosenSet.has(p.name)) {
121
+ this.included.add(p.name);
122
+ this.skipped.delete(p.name);
123
+ }
124
+ else {
125
+ this.included.delete(p.name);
126
+ }
123
127
  }
124
128
  }
125
- // Ask which unchecked projects to always skip
126
- const unchecked = toShow.filter(p => !chosenSet.has(p.name) && !config.skipped.has(p.name));
127
- if (unchecked.length > 0) {
129
+ async promptForSkip(toShow) {
130
+ const unchecked = toShow.filter(p => !this.included.has(p.name) && !this.skipped.has(p.name));
131
+ if (unchecked.length === 0)
132
+ return;
128
133
  const toSkip = await (0, prompts_1.checkbox)({
129
134
  message: 'Always skip (never ask again)?',
130
135
  choices: unchecked.map(p => ({ name: p.name, value: p.name, checked: false })),
131
136
  pageSize: 15,
132
137
  });
133
138
  for (const name of toSkip)
134
- config.skipped.add(name);
139
+ this.skipped.add(name);
140
+ }
141
+ async load() {
142
+ [this.included, this.skipped] = await Promise.all([
143
+ loadLines(path.join(this.workspaceDir, '.ragprojects')),
144
+ loadLines(path.join(this.workspaceDir, '.ragskip')),
145
+ ]);
146
+ }
147
+ async save() {
148
+ await Promise.all([
149
+ saveLines(path.join(this.workspaceDir, '.ragprojects'), this.included),
150
+ saveLines(path.join(this.workspaceDir, '.ragskip'), this.skipped),
151
+ ]);
135
152
  }
136
- await saveConfig(workspaceDir, config);
137
- const selected = allProjects.filter(p => config.included.has(p.name));
138
- console.log(`\nSelected: ${selected.map(p => p.name).join(', ') || '(none)'}`);
139
- return selected;
140
153
  }
154
+ exports.ProjectSelector = ProjectSelector;
@@ -4,11 +4,18 @@ export declare class AstParserService implements IAstParser {
4
4
  private logger;
5
5
  private morphProjects;
6
6
  constructor(logger?: LoggerService);
7
+ analyzeProject(filePaths: string[], serviceName: string, projectRoot?: string, opts?: {
8
+ skipDependencyResolution?: boolean;
9
+ }): Promise<Map<string, FileAnalysis>>;
10
+ clearCache(projectRoot?: string): void;
7
11
  analyzeFile(filePath: string, serviceName: string, projectRoot?: string): Promise<FileAnalysis>;
8
12
  extractImportEdges(filePath: string, serviceName: string, projectRoot?: string): Promise<DepEdge[]>;
13
+ private createMorphProject;
9
14
  private getMorphProject;
10
15
  private extractEdges;
11
16
  private resolveFromSymbol;
12
17
  private buildUsageMap;
13
18
  private extractSymbols;
19
+ private extractCalls;
20
+ private getContainerName;
14
21
  }
@@ -51,9 +51,64 @@ class AstParserService {
51
51
  this.logger = logger;
52
52
  }
53
53
  // ── public interface ────────────────────────────────────────────────────────
54
+ async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
55
+ const t0 = Date.now();
56
+ const skipDependencyResolution = opts.skipDependencyResolution ?? false;
57
+ this.logger.debug('AstParser: analyzeProject start', { serviceName, projectRoot, files: filePaths.length, skipDependencyResolution });
58
+ const analysisMap = new Map();
59
+ const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution);
60
+ for (const filePath of filePaths) {
61
+ if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
62
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
63
+ continue;
64
+ }
65
+ try {
66
+ morphProject.addSourceFileAtPath(filePath);
67
+ }
68
+ catch (err) {
69
+ this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
70
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
71
+ }
72
+ }
73
+ for (const sourceFile of morphProject.getSourceFiles()) {
74
+ const filePath = sourceFile.getFilePath();
75
+ try {
76
+ const edges = this.extractEdges(sourceFile, filePath, serviceName);
77
+ const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
78
+ const calls = this.extractCalls(sourceFile, filePath, serviceName);
79
+ analysisMap.set(filePath, { edges, symbols, calls });
80
+ }
81
+ catch (err) {
82
+ this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
83
+ analysisMap.set(filePath, { edges: [], symbols: [], calls: [] });
84
+ }
85
+ }
86
+ // Release source file memory before returning
87
+ for (const sourceFile of morphProject.getSourceFiles()) {
88
+ morphProject.removeSourceFile(sourceFile);
89
+ }
90
+ this.logger.debug('AstParser: analyzeProject done', {
91
+ serviceName, projectRoot, total: filePaths.length, parsed: analysisMap.size, skipDependencyResolution, ms: Date.now() - t0,
92
+ });
93
+ return analysisMap;
94
+ }
95
+ clearCache(projectRoot) {
96
+ if (projectRoot) {
97
+ const project = this.morphProjects.get(projectRoot);
98
+ if (project) {
99
+ this.morphProjects.delete(projectRoot);
100
+ }
101
+ }
102
+ else {
103
+ this.morphProjects.clear();
104
+ }
105
+ }
54
106
  async analyzeFile(filePath, serviceName, projectRoot = path.dirname(filePath)) {
107
+ const t0 = Date.now();
108
+ this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
55
109
  if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
56
- return { edges: [], symbols: [] };
110
+ this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
111
+ return { edges: [], symbols: [], calls: [] };
57
112
  }
58
113
  const morphProject = this.getMorphProject(projectRoot);
59
114
  const existing = morphProject.getSourceFile(filePath);
@@ -63,13 +118,16 @@ class AstParserService {
63
118
  try {
64
119
  sourceFile = morphProject.addSourceFileAtPath(filePath);
65
120
  }
66
- catch {
67
- return { edges: [], symbols: [] };
121
+ catch (err) {
122
+ this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
123
+ return { edges: [], symbols: [], calls: [] };
68
124
  }
69
125
  const edges = this.extractEdges(sourceFile, filePath, serviceName);
70
126
  const symbols = this.extractSymbols(sourceFile, filePath, serviceName);
127
+ const calls = this.extractCalls(sourceFile, filePath, serviceName);
71
128
  morphProject.removeSourceFile(sourceFile);
72
- return { edges, symbols };
129
+ this.logger.debug('AstParser: analyzeFile done', { filePath, edges: edges.length, symbols: symbols.length, calls: calls.length, ms: Date.now() - t0 });
130
+ return { edges, symbols, calls };
73
131
  }
74
132
  // Kept for test compatibility (extract-import-edges.test.ts).
75
133
  async extractImportEdges(filePath, serviceName, projectRoot) {
@@ -77,46 +135,57 @@ class AstParserService {
77
135
  return edges;
78
136
  }
79
137
  // ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
80
- getMorphProject(projectRoot) {
81
- if (this.morphProjects.has(projectRoot)) {
82
- // Move to end = most recently used
83
- const cached = this.morphProjects.get(projectRoot);
84
- this.morphProjects.delete(projectRoot);
85
- this.morphProjects.set(projectRoot, cached);
86
- return cached;
87
- }
88
- if (this.morphProjects.size >= MORPH_CACHE_MAX) {
89
- // Evict least recently used (first entry in insertion-order Map)
90
- const lruKey = this.morphProjects.keys().next().value;
91
- this.morphProjects.delete(lruKey);
92
- this.logger.debug('AstParser: evicted LRU project', { lruKey });
93
- }
138
+ createMorphProject(projectRoot, skipFileDependencyResolution = false) {
139
+ const t0 = Date.now();
94
140
  const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
95
141
  const hasTsconfig = fs.existsSync(tsconfigPath);
96
- this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig });
142
+ this.logger.debug('AstParser: tsconfig lookup', { projectRoot, found: hasTsconfig, skipFileDependencyResolution });
97
143
  let project;
98
144
  try {
99
145
  const opts = hasTsconfig
100
- ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution: false }
101
- : FALLBACK_OPTS;
146
+ ? { tsConfigFilePath: tsconfigPath, skipAddingFilesFromTsConfig: true, skipFileDependencyResolution }
147
+ : { ...FALLBACK_OPTS, skipFileDependencyResolution };
102
148
  project = new ts_morph_1.Project(opts);
103
149
  if (hasTsconfig) {
104
150
  const co = project.getCompilerOptions();
105
151
  const pathsMap = co.paths ?? {};
106
152
  const pathCount = Object.keys(pathsMap).length;
107
- this.logger.debug('AstParser: tsconfig loaded', { pathCount });
153
+ this.logger.debug('AstParser: tsconfig loaded', { pathCount, ms: Date.now() - t0 });
108
154
  if (pathCount > 0) {
109
155
  this.logger.debug('AstParser: path aliases', { aliases: Object.keys(pathsMap) });
110
156
  }
111
157
  }
158
+ else {
159
+ this.logger.debug('AstParser: MorphProject created (fallback opts)', { ms: Date.now() - t0 });
160
+ }
112
161
  }
113
162
  catch (err) {
114
163
  this.logger.warn('AstParser: tsconfig load failed, using fallback', {
115
164
  tsconfigPath,
116
165
  reason: err.message,
166
+ ms: Date.now() - t0,
117
167
  });
118
168
  project = new ts_morph_1.Project(FALLBACK_OPTS);
119
169
  }
170
+ return project;
171
+ }
172
+ getMorphProject(projectRoot) {
173
+ if (this.morphProjects.has(projectRoot)) {
174
+ // Move to end = most recently used
175
+ const cached = this.morphProjects.get(projectRoot);
176
+ this.morphProjects.delete(projectRoot);
177
+ this.morphProjects.set(projectRoot, cached);
178
+ this.logger.debug('AstParser: getMorphProject cache hit', { projectRoot, cacheSize: this.morphProjects.size });
179
+ return cached;
180
+ }
181
+ if (this.morphProjects.size >= MORPH_CACHE_MAX) {
182
+ // Evict least recently used (first entry in insertion-order Map)
183
+ const lruKey = this.morphProjects.keys().next().value;
184
+ this.morphProjects.delete(lruKey);
185
+ this.logger.debug('AstParser: evicted LRU project', { lruKey });
186
+ }
187
+ this.logger.debug('AstParser: getMorphProject cache miss', { projectRoot, cacheSize: this.morphProjects.size });
188
+ const project = this.createMorphProject(projectRoot);
120
189
  this.morphProjects.set(projectRoot, project);
121
190
  return project;
122
191
  }
@@ -223,7 +292,6 @@ class AstParserService {
223
292
  // ── private: symbol extraction ─────────────────────────────────────────────
224
293
  extractSymbols(sourceFile, filePath, serviceName) {
225
294
  const symbols = [];
226
- // Find the first line matching the keyword — skips leading decorators.
227
295
  const sig = (text, keyword) => {
228
296
  const found = text.split('\n').find(l => keyword.test(l));
229
297
  return found?.trim() ?? text.split('\n')[0].trim();
@@ -239,7 +307,11 @@ class AstParserService {
239
307
  continue;
240
308
  const nameNode = cls.getNameNode();
241
309
  const { line, col } = nameNode ? getPos(nameNode) : { line: cls.getStartLineNumber(), col: 1 };
242
- symbols.push({ project: serviceName, filePath, name, kind: 'class', signature: sig(cls.getText(), /\bclass\s/), line, col });
310
+ symbols.push({
311
+ project: serviceName, filePath, name, kind: 'class',
312
+ signature: sig(cls.getText(), /\bclass\s/),
313
+ line, endLine: cls.getEndLineNumber(), col,
314
+ });
243
315
  }
244
316
  for (const fn of sourceFile.getFunctions()) {
245
317
  const name = fn.getName();
@@ -247,23 +319,114 @@ class AstParserService {
247
319
  continue;
248
320
  const nameNode = fn.getNameNode();
249
321
  const { line, col } = nameNode ? getPos(nameNode) : { line: fn.getStartLineNumber(), col: 1 };
250
- symbols.push({ project: serviceName, filePath, name, kind: 'function', signature: sig(fn.getText(), /\bfunction\s/), line, col });
322
+ symbols.push({
323
+ project: serviceName, filePath, name, kind: 'function',
324
+ signature: sig(fn.getText(), /\bfunction\s/),
325
+ line, endLine: fn.getEndLineNumber(), col,
326
+ });
251
327
  }
252
328
  for (const iface of sourceFile.getInterfaces()) {
253
329
  const { line, col } = getPos(iface.getNameNode());
254
- symbols.push({ project: serviceName, filePath, name: iface.getName(), kind: 'interface', signature: sig(iface.getText(), /\binterface\s/), line, col });
330
+ symbols.push({
331
+ project: serviceName, filePath, name: iface.getName(), kind: 'interface',
332
+ signature: sig(iface.getText(), /\binterface\s/),
333
+ line, endLine: iface.getEndLineNumber(), col,
334
+ });
255
335
  }
256
336
  for (const ta of sourceFile.getTypeAliases()) {
257
337
  const { line, col } = getPos(ta.getNameNode());
258
- symbols.push({ project: serviceName, filePath, name: ta.getName(), kind: 'type', signature: sig(ta.getText(), /\btype\s/), line, col });
338
+ symbols.push({
339
+ project: serviceName, filePath, name: ta.getName(), kind: 'type',
340
+ signature: sig(ta.getText(), /\btype\s/),
341
+ line, endLine: ta.getEndLineNumber(), col,
342
+ });
259
343
  }
260
344
  for (const stmt of sourceFile.getVariableStatements()) {
261
345
  for (const decl of stmt.getDeclarations()) {
262
346
  const { line, col } = getPos(decl.getNameNode());
263
- symbols.push({ project: serviceName, filePath, name: decl.getName(), kind: 'variable', signature: sig(stmt.getText(), /\b(const|let|var)\s/), line, col });
347
+ symbols.push({
348
+ project: serviceName, filePath, name: decl.getName(), kind: 'variable',
349
+ signature: sig(stmt.getText(), /\b(const|let|var)\s/),
350
+ line, endLine: stmt.getEndLineNumber(), col,
351
+ });
264
352
  }
265
353
  }
266
354
  return symbols;
267
355
  }
356
+ // ── private: call graph extraction ────────────────────────────────────────
357
+ extractCalls(sourceFile, filePath, serviceName) {
358
+ const calls = [];
359
+ for (const callExpr of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
360
+ const expr = callExpr.getExpression();
361
+ let calleeName = null;
362
+ let calleeFile = null;
363
+ let calleeSymbol = null;
364
+ try {
365
+ if (ts_morph_1.Node.isPropertyAccessExpression(expr)) {
366
+ calleeName = expr.getName();
367
+ const sym = expr.getNameNode().getSymbol();
368
+ const decl = sym?.getDeclarations()?.[0];
369
+ const declPath = decl?.getSourceFile().getFilePath();
370
+ if (declPath && !declPath.includes('/node_modules/') && declPath !== filePath) {
371
+ calleeFile = declPath;
372
+ calleeSymbol = calleeName;
373
+ }
374
+ }
375
+ else if (ts_morph_1.Node.isIdentifier(expr)) {
376
+ calleeName = expr.getText();
377
+ const sym = expr.getSymbol();
378
+ if (sym) {
379
+ // Imported identifiers resolve to ImportSpecifier (same file); follow the alias.
380
+ const firstDecl = sym.getDeclarations()?.[0];
381
+ const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl)
382
+ ? (sym.getAliasedSymbol() ?? sym)
383
+ : sym;
384
+ const decl = resolved.getDeclarations()?.[0];
385
+ const declPath = decl?.getSourceFile().getFilePath();
386
+ if (declPath && !declPath.includes('/node_modules/') && declPath !== filePath) {
387
+ calleeFile = declPath;
388
+ calleeSymbol = calleeName;
389
+ }
390
+ }
391
+ }
392
+ }
393
+ catch {
394
+ // type resolution failures are normal — skip this call
395
+ }
396
+ if (!calleeName || !calleeFile)
397
+ continue;
398
+ const fromSymbol = this.getContainerName(callExpr);
399
+ const pos = sourceFile.compilerNode
400
+ .getLineAndCharacterOfPosition(callExpr.getStart());
401
+ calls.push({
402
+ project: serviceName,
403
+ fromFile: filePath,
404
+ fromSymbol,
405
+ calleeName,
406
+ calleeFile,
407
+ calleeSymbol,
408
+ line: pos.line + 1,
409
+ col: pos.character + 1,
410
+ });
411
+ }
412
+ return calls;
413
+ }
414
+ getContainerName(node) {
415
+ let current = node.getParent();
416
+ while (current) {
417
+ if (ts_morph_1.Node.isMethodDeclaration(current) || ts_morph_1.Node.isFunctionDeclaration(current)) {
418
+ return current.getName() ?? null;
419
+ }
420
+ if (ts_morph_1.Node.isArrowFunction(current) || ts_morph_1.Node.isFunctionExpression(current)) {
421
+ const parent = current.getParent();
422
+ if (ts_morph_1.Node.isVariableDeclaration(parent))
423
+ return parent.getName();
424
+ if (ts_morph_1.Node.isPropertyDeclaration(parent))
425
+ return parent.getName();
426
+ }
427
+ current = current.getParent();
428
+ }
429
+ return null;
430
+ }
268
431
  }
269
432
  exports.AstParserService = AstParserService;
@@ -1,14 +1,21 @@
1
- import { EntityManager, EntityTarget, ObjectLiteral, Repository } from 'typeorm';
1
+ import type { NodePgDatabase, NodePgTransaction } from 'drizzle-orm/node-postgres';
2
+ import type { ExtractTablesWithRelations } from 'drizzle-orm/relations';
2
3
  import type { Config } from '../config';
4
+ import * as schema from '../db/schema';
5
+ type Schema = typeof schema;
6
+ export type DrizzleDb = NodePgDatabase<Schema>;
7
+ export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
3
8
  export declare class DatabaseService {
4
9
  private config;
5
- private dataSource;
10
+ private pool;
11
+ private _db;
6
12
  constructor(config: Config);
13
+ get orm(): DrizzleDb;
7
14
  query<T = any>(sql: string, params?: any[]): Promise<T>;
8
- transaction<T>(fn: (manager: EntityManager) => Promise<T>): Promise<T>;
9
- getRepository<T extends ObjectLiteral>(entity: EntityTarget<T>): Repository<T>;
15
+ transaction<T>(fn: (tx: DrizzleTx) => Promise<T>): Promise<T>;
10
16
  destroy(): Promise<void>;
11
17
  initDb(): Promise<void>;
12
18
  initDbWithSchema(): Promise<void>;
13
19
  private ensureDatabaseExists;
14
20
  }
21
+ export {};