@optave/codegraph 1.1.0
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/LICENSE +190 -0
- package/README.md +311 -0
- package/grammars/tree-sitter-hcl.wasm +0 -0
- package/grammars/tree-sitter-javascript.wasm +0 -0
- package/grammars/tree-sitter-python.wasm +0 -0
- package/grammars/tree-sitter-tsx.wasm +0 -0
- package/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +69 -0
- package/src/builder.js +547 -0
- package/src/cli.js +224 -0
- package/src/config.js +55 -0
- package/src/constants.js +28 -0
- package/src/cycles.js +104 -0
- package/src/db.js +117 -0
- package/src/embedder.js +330 -0
- package/src/export.js +138 -0
- package/src/index.js +39 -0
- package/src/logger.js +20 -0
- package/src/mcp.js +139 -0
- package/src/parser.js +573 -0
- package/src/queries.js +616 -0
- package/src/watcher.js +213 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { buildGraph } from './builder.js';
|
|
5
|
+
import { queryName, impactAnalysis, moduleMap, fileDeps, fnDeps, fnImpact, diffImpact } from './queries.js';
|
|
6
|
+
import { buildEmbeddings, search, MODELS } from './embedder.js';
|
|
7
|
+
import { watchProject } from './watcher.js';
|
|
8
|
+
import { exportDOT, exportMermaid, exportJSON } from './export.js';
|
|
9
|
+
import { findCycles, formatCycles } from './cycles.js';
|
|
10
|
+
import { findDbPath } from './db.js';
|
|
11
|
+
import { setVerbose } from './logger.js';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import Database from 'better-sqlite3';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
program
|
|
18
|
+
.name('codegraph')
|
|
19
|
+
.description('Local code dependency graph tool')
|
|
20
|
+
.version('1.1.0')
|
|
21
|
+
.option('-v, --verbose', 'Enable verbose/debug output')
|
|
22
|
+
.hook('preAction', (thisCommand) => {
|
|
23
|
+
const opts = thisCommand.opts();
|
|
24
|
+
if (opts.verbose) setVerbose(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('build [dir]')
|
|
29
|
+
.description('Parse repo and build graph in .codegraph/graph.db')
|
|
30
|
+
.option('--no-incremental', 'Force full rebuild (ignore file hashes)')
|
|
31
|
+
.action(async (dir, opts) => {
|
|
32
|
+
const root = path.resolve(dir || '.');
|
|
33
|
+
await buildGraph(root, { incremental: opts.incremental });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('query <name>')
|
|
38
|
+
.description('Find a function/class, show callers and callees')
|
|
39
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
40
|
+
.option('-j, --json', 'Output as JSON')
|
|
41
|
+
.action((name, opts) => {
|
|
42
|
+
queryName(name, opts.db, { json: opts.json });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command('impact <file>')
|
|
47
|
+
.description('Show what depends on this file (transitive)')
|
|
48
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
49
|
+
.option('-j, --json', 'Output as JSON')
|
|
50
|
+
.action((file, opts) => {
|
|
51
|
+
impactAnalysis(file, opts.db, { json: opts.json });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command('map')
|
|
56
|
+
.description('High-level module overview with most-connected nodes')
|
|
57
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
58
|
+
.option('-n, --limit <number>', 'Number of top nodes', '20')
|
|
59
|
+
.option('-j, --json', 'Output as JSON')
|
|
60
|
+
.action((opts) => {
|
|
61
|
+
moduleMap(opts.db, parseInt(opts.limit), { json: opts.json });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command('deps <file>')
|
|
66
|
+
.description('Show what this file imports and what imports it')
|
|
67
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
68
|
+
.option('-j, --json', 'Output as JSON')
|
|
69
|
+
.action((file, opts) => {
|
|
70
|
+
fileDeps(file, opts.db, { json: opts.json });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
program
|
|
74
|
+
.command('fn <name>')
|
|
75
|
+
.description('Function-level dependencies: callers, callees, and transitive call chain')
|
|
76
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
77
|
+
.option('--depth <n>', 'Transitive caller depth', '3')
|
|
78
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
79
|
+
.option('-j, --json', 'Output as JSON')
|
|
80
|
+
.action((name, opts) => {
|
|
81
|
+
fnDeps(name, opts.db, { depth: parseInt(opts.depth), noTests: !opts.tests, json: opts.json });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
program
|
|
85
|
+
.command('fn-impact <name>')
|
|
86
|
+
.description('Function-level impact: what functions break if this one changes')
|
|
87
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
88
|
+
.option('--depth <n>', 'Max transitive depth', '5')
|
|
89
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
90
|
+
.option('-j, --json', 'Output as JSON')
|
|
91
|
+
.action((name, opts) => {
|
|
92
|
+
fnImpact(name, opts.db, { depth: parseInt(opts.depth), noTests: !opts.tests, json: opts.json });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
program
|
|
96
|
+
.command('diff-impact [ref]')
|
|
97
|
+
.description('Show impact of git changes (unstaged, staged, or vs a ref)')
|
|
98
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
99
|
+
.option('--staged', 'Analyze staged changes instead of unstaged')
|
|
100
|
+
.option('--depth <n>', 'Max transitive caller depth', '3')
|
|
101
|
+
.option('-T, --no-tests', 'Exclude test/spec files from results')
|
|
102
|
+
.option('-j, --json', 'Output as JSON')
|
|
103
|
+
.action((ref, opts) => {
|
|
104
|
+
diffImpact(opts.db, { ref, staged: opts.staged, depth: parseInt(opts.depth), noTests: !opts.tests, json: opts.json });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ─── New commands ────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command('export')
|
|
111
|
+
.description('Export dependency graph as DOT (Graphviz), Mermaid, or JSON')
|
|
112
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
113
|
+
.option('-f, --format <format>', 'Output format: dot, mermaid, json', 'dot')
|
|
114
|
+
.option('--functions', 'Function-level graph instead of file-level')
|
|
115
|
+
.option('-o, --output <file>', 'Write to file instead of stdout')
|
|
116
|
+
.action((opts) => {
|
|
117
|
+
const db = new Database(findDbPath(opts.db), { readonly: true });
|
|
118
|
+
const exportOpts = { fileLevel: !opts.functions };
|
|
119
|
+
|
|
120
|
+
let output;
|
|
121
|
+
switch (opts.format) {
|
|
122
|
+
case 'mermaid':
|
|
123
|
+
output = exportMermaid(db, exportOpts);
|
|
124
|
+
break;
|
|
125
|
+
case 'json':
|
|
126
|
+
output = JSON.stringify(exportJSON(db), null, 2);
|
|
127
|
+
break;
|
|
128
|
+
case 'dot':
|
|
129
|
+
default:
|
|
130
|
+
output = exportDOT(db, exportOpts);
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
db.close();
|
|
135
|
+
|
|
136
|
+
if (opts.output) {
|
|
137
|
+
fs.writeFileSync(opts.output, output, 'utf-8');
|
|
138
|
+
console.log(`Exported ${opts.format} to ${opts.output}`);
|
|
139
|
+
} else {
|
|
140
|
+
console.log(output);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
program
|
|
145
|
+
.command('cycles')
|
|
146
|
+
.description('Detect circular dependencies in the codebase')
|
|
147
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
148
|
+
.option('--functions', 'Function-level cycle detection')
|
|
149
|
+
.option('-j, --json', 'Output as JSON')
|
|
150
|
+
.action((opts) => {
|
|
151
|
+
const db = new Database(findDbPath(opts.db), { readonly: true });
|
|
152
|
+
const cycles = findCycles(db, { fileLevel: !opts.functions });
|
|
153
|
+
db.close();
|
|
154
|
+
|
|
155
|
+
if (opts.json) {
|
|
156
|
+
console.log(JSON.stringify({ cycles, count: cycles.length }, null, 2));
|
|
157
|
+
} else {
|
|
158
|
+
console.log(formatCycles(cycles));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
program
|
|
163
|
+
.command('mcp')
|
|
164
|
+
.description('Start MCP (Model Context Protocol) server for AI assistant integration')
|
|
165
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
166
|
+
.action(async (opts) => {
|
|
167
|
+
const { startMCPServer } = await import('./mcp.js');
|
|
168
|
+
await startMCPServer(opts.db);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ─── Embedding commands ─────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
program
|
|
174
|
+
.command('models')
|
|
175
|
+
.description('List available embedding models')
|
|
176
|
+
.action(() => {
|
|
177
|
+
console.log('\nAvailable embedding models:\n');
|
|
178
|
+
for (const [key, config] of Object.entries(MODELS)) {
|
|
179
|
+
const def = key === 'minilm' ? ' (default)' : '';
|
|
180
|
+
console.log(` ${key.padEnd(12)} ${String(config.dim).padStart(4)}d ${config.desc}${def}`);
|
|
181
|
+
}
|
|
182
|
+
console.log('\nUsage: codegraph embed --model <name>');
|
|
183
|
+
console.log(' codegraph search "query" --model <name>\n');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
program
|
|
187
|
+
.command('embed [dir]')
|
|
188
|
+
.description('Build semantic embeddings for all functions/methods/classes (requires prior `build`)')
|
|
189
|
+
.option('-m, --model <name>', 'Embedding model: minilm (default), jina-small, jina-base, nomic. Run `codegraph models` for details', 'minilm')
|
|
190
|
+
.action(async (dir, opts) => {
|
|
191
|
+
const root = path.resolve(dir || '.');
|
|
192
|
+
await buildEmbeddings(root, opts.model);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
program
|
|
196
|
+
.command('search <query>')
|
|
197
|
+
.description('Semantic search: find functions by natural language description')
|
|
198
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
199
|
+
.option('-m, --model <name>', 'Override embedding model (auto-detects from DB)')
|
|
200
|
+
.option('-n, --limit <number>', 'Max results', '15')
|
|
201
|
+
.option('-T, --no-tests', 'Exclude test/spec files')
|
|
202
|
+
.option('--min-score <score>', 'Minimum similarity threshold', '0.2')
|
|
203
|
+
.option('-k, --kind <kind>', 'Filter by kind: function, method, class')
|
|
204
|
+
.option('--file <pattern>', 'Filter by file path pattern')
|
|
205
|
+
.action(async (query, opts) => {
|
|
206
|
+
await search(query, opts.db, {
|
|
207
|
+
limit: parseInt(opts.limit),
|
|
208
|
+
noTests: !opts.tests,
|
|
209
|
+
minScore: parseFloat(opts.minScore),
|
|
210
|
+
model: opts.model,
|
|
211
|
+
kind: opts.kind,
|
|
212
|
+
filePattern: opts.file
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
program
|
|
217
|
+
.command('watch [dir]')
|
|
218
|
+
.description('Watch project for file changes and incrementally update the graph')
|
|
219
|
+
.action(async (dir) => {
|
|
220
|
+
const root = path.resolve(dir || '.');
|
|
221
|
+
await watchProject(root);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
program.parse();
|
package/src/config.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { debug } from './logger.js';
|
|
4
|
+
|
|
5
|
+
export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
|
|
6
|
+
|
|
7
|
+
export const DEFAULTS = {
|
|
8
|
+
include: [],
|
|
9
|
+
exclude: [],
|
|
10
|
+
ignoreDirs: [],
|
|
11
|
+
extensions: [],
|
|
12
|
+
aliases: {},
|
|
13
|
+
build: {
|
|
14
|
+
incremental: true,
|
|
15
|
+
dbPath: '.codegraph/graph.db'
|
|
16
|
+
},
|
|
17
|
+
query: {
|
|
18
|
+
defaultDepth: 3,
|
|
19
|
+
defaultLimit: 20
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load project configuration from a .codegraphrc.json or similar file.
|
|
25
|
+
* Returns merged config with defaults.
|
|
26
|
+
*/
|
|
27
|
+
export function loadConfig(cwd) {
|
|
28
|
+
cwd = cwd || process.cwd();
|
|
29
|
+
for (const name of CONFIG_FILES) {
|
|
30
|
+
const filePath = path.join(cwd, name);
|
|
31
|
+
if (fs.existsSync(filePath)) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
34
|
+
const config = JSON.parse(raw);
|
|
35
|
+
debug(`Loaded config from ${filePath}`);
|
|
36
|
+
return mergeConfig(DEFAULTS, config);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
debug(`Failed to parse config ${filePath}: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { ...DEFAULTS };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function mergeConfig(defaults, overrides) {
|
|
46
|
+
const result = { ...defaults };
|
|
47
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
48
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && defaults[key] && typeof defaults[key] === 'object') {
|
|
49
|
+
result[key] = { ...defaults[key], ...value };
|
|
50
|
+
} else {
|
|
51
|
+
result[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
export const IGNORE_DIRS = new Set([
|
|
4
|
+
'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.svelte-kit',
|
|
5
|
+
'coverage', '.codegraph', '__pycache__', '.tox', 'vendor', '.venv', 'venv',
|
|
6
|
+
'env', '.env'
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const EXTENSIONS = new Set([
|
|
10
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
11
|
+
'.tf', '.hcl', '.py'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
export function shouldIgnore(dirName) {
|
|
15
|
+
return IGNORE_DIRS.has(dirName) || dirName.startsWith('.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isSupportedFile(filePath) {
|
|
19
|
+
return EXTENSIONS.has(path.extname(filePath));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a file path to always use forward slashes.
|
|
24
|
+
* Ensures cross-platform consistency in the SQLite database.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizePath(filePath) {
|
|
27
|
+
return filePath.split(path.sep).join('/');
|
|
28
|
+
}
|
package/src/cycles.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
|
|
3
|
+
* @param {object} db - Open SQLite database
|
|
4
|
+
* @param {object} opts - { fileLevel: true }
|
|
5
|
+
* @returns {string[][]} Array of cycles, each cycle is an array of file paths
|
|
6
|
+
*/
|
|
7
|
+
export function findCycles(db, opts = {}) {
|
|
8
|
+
const fileLevel = opts.fileLevel !== false;
|
|
9
|
+
|
|
10
|
+
// Build adjacency list
|
|
11
|
+
let edges;
|
|
12
|
+
if (fileLevel) {
|
|
13
|
+
edges = db.prepare(`
|
|
14
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
15
|
+
FROM edges e
|
|
16
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
17
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
18
|
+
WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type')
|
|
19
|
+
`).all();
|
|
20
|
+
} else {
|
|
21
|
+
edges = db.prepare(`
|
|
22
|
+
SELECT DISTINCT
|
|
23
|
+
(n1.name || '|' || n1.file) AS source,
|
|
24
|
+
(n2.name || '|' || n2.file) AS target
|
|
25
|
+
FROM edges e
|
|
26
|
+
JOIN nodes n1 ON e.source_id = n1.id
|
|
27
|
+
JOIN nodes n2 ON e.target_id = n2.id
|
|
28
|
+
WHERE n1.kind IN ('function', 'method', 'class')
|
|
29
|
+
AND n2.kind IN ('function', 'method', 'class')
|
|
30
|
+
AND e.kind = 'calls'
|
|
31
|
+
AND n1.id != n2.id
|
|
32
|
+
`).all();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const graph = new Map();
|
|
36
|
+
for (const { source, target } of edges) {
|
|
37
|
+
if (!graph.has(source)) graph.set(source, []);
|
|
38
|
+
graph.get(source).push(target);
|
|
39
|
+
if (!graph.has(target)) graph.set(target, []);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Tarjan's strongly connected components algorithm
|
|
43
|
+
let index = 0;
|
|
44
|
+
const stack = [];
|
|
45
|
+
const onStack = new Set();
|
|
46
|
+
const indices = new Map();
|
|
47
|
+
const lowlinks = new Map();
|
|
48
|
+
const sccs = [];
|
|
49
|
+
|
|
50
|
+
function strongconnect(v) {
|
|
51
|
+
indices.set(v, index);
|
|
52
|
+
lowlinks.set(v, index);
|
|
53
|
+
index++;
|
|
54
|
+
stack.push(v);
|
|
55
|
+
onStack.add(v);
|
|
56
|
+
|
|
57
|
+
for (const w of (graph.get(v) || [])) {
|
|
58
|
+
if (!indices.has(w)) {
|
|
59
|
+
strongconnect(w);
|
|
60
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
|
|
61
|
+
} else if (onStack.has(w)) {
|
|
62
|
+
lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (lowlinks.get(v) === indices.get(v)) {
|
|
67
|
+
const scc = [];
|
|
68
|
+
let w;
|
|
69
|
+
do {
|
|
70
|
+
w = stack.pop();
|
|
71
|
+
onStack.delete(w);
|
|
72
|
+
scc.push(w);
|
|
73
|
+
} while (w !== v);
|
|
74
|
+
if (scc.length > 1) sccs.push(scc);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const node of graph.keys()) {
|
|
79
|
+
if (!indices.has(node)) strongconnect(node);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return sccs;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Format cycles for human-readable output.
|
|
87
|
+
*/
|
|
88
|
+
export function formatCycles(cycles) {
|
|
89
|
+
if (cycles.length === 0) {
|
|
90
|
+
return 'No circular dependencies detected.';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = [`Found ${cycles.length} circular dependency cycle(s):\n`];
|
|
94
|
+
for (let i = 0; i < cycles.length; i++) {
|
|
95
|
+
const cycle = cycles[i];
|
|
96
|
+
lines.push(` Cycle ${i + 1} (${cycle.length} files):`);
|
|
97
|
+
for (const file of cycle) {
|
|
98
|
+
lines.push(` -> ${file}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push(` -> ${cycle[0]} (back to start)`);
|
|
101
|
+
lines.push('');
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
}
|
package/src/db.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import { warn, debug, info } from './logger.js';
|
|
5
|
+
|
|
6
|
+
// ─── Schema Migrations ─────────────────────────────────────────────────
|
|
7
|
+
export const MIGRATIONS = [
|
|
8
|
+
{
|
|
9
|
+
version: 1,
|
|
10
|
+
up: `
|
|
11
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
name TEXT NOT NULL,
|
|
14
|
+
kind TEXT NOT NULL,
|
|
15
|
+
file TEXT NOT NULL,
|
|
16
|
+
line INTEGER,
|
|
17
|
+
end_line INTEGER,
|
|
18
|
+
UNIQUE(name, kind, file, line)
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
21
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
22
|
+
source_id INTEGER NOT NULL,
|
|
23
|
+
target_id INTEGER NOT NULL,
|
|
24
|
+
kind TEXT NOT NULL,
|
|
25
|
+
confidence REAL DEFAULT 1.0,
|
|
26
|
+
dynamic INTEGER DEFAULT 0,
|
|
27
|
+
FOREIGN KEY(source_id) REFERENCES nodes(id),
|
|
28
|
+
FOREIGN KEY(target_id) REFERENCES nodes(id)
|
|
29
|
+
);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
|
|
34
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
|
|
35
|
+
CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind);
|
|
36
|
+
`
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
version: 2,
|
|
40
|
+
up: `
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_name_kind_file ON nodes(name, kind, file);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_file_kind ON nodes(file, kind);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source_kind ON edges(source_id, kind);
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target_kind ON edges(target_id, kind);
|
|
45
|
+
`
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
version: 3,
|
|
49
|
+
up: `
|
|
50
|
+
CREATE TABLE IF NOT EXISTS file_hashes (
|
|
51
|
+
file TEXT PRIMARY KEY,
|
|
52
|
+
hash TEXT NOT NULL,
|
|
53
|
+
mtime INTEGER NOT NULL
|
|
54
|
+
);
|
|
55
|
+
`
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function openDb(dbPath) {
|
|
60
|
+
const dir = path.dirname(dbPath);
|
|
61
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
const db = new Database(dbPath);
|
|
63
|
+
db.pragma('journal_mode = WAL');
|
|
64
|
+
return db;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function initSchema(db) {
|
|
68
|
+
db.exec(`CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL DEFAULT 0)`);
|
|
69
|
+
|
|
70
|
+
const row = db.prepare('SELECT version FROM schema_version').get();
|
|
71
|
+
let currentVersion = row ? row.version : 0;
|
|
72
|
+
|
|
73
|
+
if (!row) {
|
|
74
|
+
db.prepare('INSERT INTO schema_version (version) VALUES (0)').run();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
for (const migration of MIGRATIONS) {
|
|
78
|
+
if (migration.version > currentVersion) {
|
|
79
|
+
debug(`Running migration v${migration.version}`);
|
|
80
|
+
db.exec(migration.up);
|
|
81
|
+
db.prepare('UPDATE schema_version SET version = ?').run(migration.version);
|
|
82
|
+
currentVersion = migration.version;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try { db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER'); } catch { /* already exists */ }
|
|
87
|
+
try { db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0'); } catch { /* already exists */ }
|
|
88
|
+
try { db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0'); } catch { /* already exists */ }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function findDbPath(customPath) {
|
|
92
|
+
if (customPath) return path.resolve(customPath);
|
|
93
|
+
let dir = process.cwd();
|
|
94
|
+
while (true) {
|
|
95
|
+
const candidate = path.join(dir, '.codegraph', 'graph.db');
|
|
96
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
97
|
+
const parent = path.dirname(dir);
|
|
98
|
+
if (parent === dir) break;
|
|
99
|
+
dir = parent;
|
|
100
|
+
}
|
|
101
|
+
return path.join(process.cwd(), '.codegraph', 'graph.db');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Open a database in readonly mode, with a user-friendly error if the DB doesn't exist.
|
|
106
|
+
*/
|
|
107
|
+
export function openReadonlyOrFail(customPath) {
|
|
108
|
+
const dbPath = findDbPath(customPath);
|
|
109
|
+
if (!fs.existsSync(dbPath)) {
|
|
110
|
+
console.error(
|
|
111
|
+
`No codegraph database found at ${dbPath}.\n` +
|
|
112
|
+
`Run "codegraph build" first to analyze your codebase.`
|
|
113
|
+
);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
return new Database(dbPath, { readonly: true });
|
|
117
|
+
}
|