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.
- package/CHANGELOG.md +53 -0
- package/README.md +74 -33
- package/dist/app.d.ts +1 -1
- package/dist/app.js +67 -16
- package/dist/src/commands/BackfillCommand.js +15 -5
- package/dist/src/commands/ConfigCommand.d.ts +12 -0
- package/dist/src/commands/ConfigCommand.js +98 -0
- package/dist/src/commands/IndexCommand.js +1 -1
- package/dist/src/config-store.d.ts +53 -0
- package/dist/src/config-store.js +197 -0
- package/dist/src/config.d.ts +22 -8
- package/dist/src/config.js +103 -58
- package/dist/src/container.d.ts +1 -1
- package/dist/src/container.js +8 -7
- package/dist/src/db/index.d.ts +2 -3
- package/dist/src/db/index.js +6 -7
- package/dist/src/db/schema.d.ts +577 -0
- package/dist/src/db/schema.js +47 -0
- package/dist/src/help.js +28 -12
- package/dist/src/interfaces/IAstParser.d.ts +19 -0
- package/dist/src/interfaces/IEmbedder.d.ts +1 -0
- package/dist/src/project-selector.d.ts +14 -5
- package/dist/src/project-selector.js +74 -60
- package/dist/src/services/AstParserService.d.ts +7 -0
- package/dist/src/services/AstParserService.js +191 -28
- package/dist/src/services/DatabaseService.d.ts +11 -4
- package/dist/src/services/DatabaseService.js +107 -48
- package/dist/src/services/IndexerService.d.ts +9 -0
- package/dist/src/services/IndexerService.js +224 -60
- package/dist/src/services/LoggerService.d.ts +5 -1
- package/dist/src/services/LoggerService.js +8 -5
- package/dist/src/services/McpServer.d.ts +1 -0
- package/dist/src/services/McpServer.js +74 -4
- package/dist/src/services/OllamaEmbedder.d.ts +2 -0
- package/dist/src/services/OllamaEmbedder.js +29 -0
- package/dist/src/services/SearcherService.d.ts +13 -0
- package/dist/src/services/SearcherService.js +118 -22
- package/dist/src/setup.js +57 -62
- 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,7 +1,16 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface Project {
|
|
2
2
|
name: string;
|
|
3
3
|
path: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
async
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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 {
|
|
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
|
|
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: (
|
|
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 {};
|