@optave/codegraph 2.0.0 → 2.1.1-dev.00f091c
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/README.md +74 -37
- package/package.json +10 -10
- package/src/builder.js +252 -38
- package/src/cli.js +44 -8
- package/src/config.js +1 -1
- package/src/db.js +4 -0
- package/src/embedder.js +3 -3
- package/src/extractors/csharp.js +248 -0
- package/src/extractors/go.js +172 -0
- package/src/extractors/hcl.js +73 -0
- package/src/extractors/helpers.js +10 -0
- package/src/extractors/index.js +9 -0
- package/src/extractors/java.js +230 -0
- package/src/extractors/javascript.js +414 -0
- package/src/extractors/php.js +243 -0
- package/src/extractors/python.js +150 -0
- package/src/extractors/ruby.js +188 -0
- package/src/extractors/rust.js +225 -0
- package/src/index.js +2 -0
- package/src/journal.js +109 -0
- package/src/mcp.js +47 -4
- package/src/parser.js +28 -1890
- package/src/queries.js +586 -4
- package/src/registry.js +24 -7
- package/src/resolve.js +4 -3
- package/src/watcher.js +25 -0
package/src/builder.js
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
import { EXTENSIONS, IGNORE_DIRS, normalizePath } from './constants.js';
|
|
6
7
|
import { initSchema, openDb } from './db.js';
|
|
8
|
+
import { readJournal, writeJournalHeader } from './journal.js';
|
|
7
9
|
import { debug, warn } from './logger.js';
|
|
8
10
|
import { getActiveEngine, parseFilesAuto } from './parser.js';
|
|
9
11
|
import { computeConfidence, resolveImportPath, resolveImportsBatch } from './resolve.js';
|
|
10
12
|
|
|
11
13
|
export { resolveImportPath } from './resolve.js';
|
|
12
14
|
|
|
15
|
+
const BUILTIN_RECEIVERS = new Set([
|
|
16
|
+
'console',
|
|
17
|
+
'Math',
|
|
18
|
+
'JSON',
|
|
19
|
+
'Object',
|
|
20
|
+
'Array',
|
|
21
|
+
'String',
|
|
22
|
+
'Number',
|
|
23
|
+
'Boolean',
|
|
24
|
+
'Date',
|
|
25
|
+
'RegExp',
|
|
26
|
+
'Map',
|
|
27
|
+
'Set',
|
|
28
|
+
'WeakMap',
|
|
29
|
+
'WeakSet',
|
|
30
|
+
'Promise',
|
|
31
|
+
'Symbol',
|
|
32
|
+
'Error',
|
|
33
|
+
'TypeError',
|
|
34
|
+
'RangeError',
|
|
35
|
+
'Proxy',
|
|
36
|
+
'Reflect',
|
|
37
|
+
'Intl',
|
|
38
|
+
'globalThis',
|
|
39
|
+
'window',
|
|
40
|
+
'document',
|
|
41
|
+
'process',
|
|
42
|
+
'Buffer',
|
|
43
|
+
'require',
|
|
44
|
+
]);
|
|
45
|
+
|
|
13
46
|
export function collectFiles(dir, files = [], config = {}, directories = null) {
|
|
14
47
|
const trackDirs = directories !== null;
|
|
15
48
|
let entries;
|
|
@@ -80,8 +113,24 @@ function fileHash(content) {
|
|
|
80
113
|
return createHash('md5').update(content).digest('hex');
|
|
81
114
|
}
|
|
82
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Stat a file, returning { mtimeMs, size } or null on error.
|
|
118
|
+
*/
|
|
119
|
+
function fileStat(filePath) {
|
|
120
|
+
try {
|
|
121
|
+
const s = fs.statSync(filePath);
|
|
122
|
+
return { mtimeMs: s.mtimeMs, size: s.size };
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
83
128
|
/**
|
|
84
129
|
* Determine which files have changed since last build.
|
|
130
|
+
* Three-tier cascade:
|
|
131
|
+
* Tier 0 — Journal: O(changed) when watcher was running
|
|
132
|
+
* Tier 1 — mtime+size: O(n) stats, O(changed) reads
|
|
133
|
+
* Tier 2 — Hash comparison: O(changed) reads (fallback from Tier 1)
|
|
85
134
|
*/
|
|
86
135
|
function getChangedFiles(db, allFiles, rootDir) {
|
|
87
136
|
// Check if file_hashes table exists
|
|
@@ -94,7 +143,6 @@ function getChangedFiles(db, allFiles, rootDir) {
|
|
|
94
143
|
}
|
|
95
144
|
|
|
96
145
|
if (!hasTable) {
|
|
97
|
-
// No hash table = first build, everything is new
|
|
98
146
|
return {
|
|
99
147
|
changed: allFiles.map((f) => ({ file: f })),
|
|
100
148
|
removed: [],
|
|
@@ -104,36 +152,140 @@ function getChangedFiles(db, allFiles, rootDir) {
|
|
|
104
152
|
|
|
105
153
|
const existing = new Map(
|
|
106
154
|
db
|
|
107
|
-
.prepare('SELECT file, hash FROM file_hashes')
|
|
155
|
+
.prepare('SELECT file, hash, mtime, size FROM file_hashes')
|
|
108
156
|
.all()
|
|
109
|
-
.map((r) => [r.file, r
|
|
157
|
+
.map((r) => [r.file, r]),
|
|
110
158
|
);
|
|
111
159
|
|
|
112
|
-
|
|
160
|
+
// Build set of current files for removal detection
|
|
113
161
|
const currentFiles = new Set();
|
|
162
|
+
for (const file of allFiles) {
|
|
163
|
+
currentFiles.add(normalizePath(path.relative(rootDir, file)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const removed = [];
|
|
167
|
+
for (const existingFile of existing.keys()) {
|
|
168
|
+
if (!currentFiles.has(existingFile)) {
|
|
169
|
+
removed.push(existingFile);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Tier 0: Journal ──────────────────────────────────────────────
|
|
174
|
+
const journal = readJournal(rootDir);
|
|
175
|
+
if (journal.valid) {
|
|
176
|
+
// Validate journal timestamp against DB — journal should be from after the last build
|
|
177
|
+
const dbMtimes = db.prepare('SELECT MAX(mtime) as latest FROM file_hashes').get();
|
|
178
|
+
const latestDbMtime = dbMtimes?.latest || 0;
|
|
179
|
+
|
|
180
|
+
// Empty journal = no watcher was running, fall to Tier 1 for safety
|
|
181
|
+
const hasJournalEntries = journal.changed.length > 0 || journal.removed.length > 0;
|
|
182
|
+
|
|
183
|
+
if (hasJournalEntries && journal.timestamp >= latestDbMtime) {
|
|
184
|
+
debug(
|
|
185
|
+
`Tier 0: journal valid, ${journal.changed.length} changed, ${journal.removed.length} removed`,
|
|
186
|
+
);
|
|
187
|
+
const changed = [];
|
|
188
|
+
|
|
189
|
+
for (const relPath of journal.changed) {
|
|
190
|
+
const absPath = path.join(rootDir, relPath);
|
|
191
|
+
const stat = fileStat(absPath);
|
|
192
|
+
if (!stat) continue;
|
|
193
|
+
|
|
194
|
+
let content;
|
|
195
|
+
try {
|
|
196
|
+
content = fs.readFileSync(absPath, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const hash = fileHash(content);
|
|
201
|
+
const record = existing.get(relPath);
|
|
202
|
+
if (!record || record.hash !== hash) {
|
|
203
|
+
changed.push({ file: absPath, content, hash, relPath, stat });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Merge journal removals with filesystem removals (dedup)
|
|
208
|
+
const removedSet = new Set(removed);
|
|
209
|
+
for (const relPath of journal.removed) {
|
|
210
|
+
if (existing.has(relPath)) removedSet.add(relPath);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { changed, removed: [...removedSet], isFullBuild: false };
|
|
214
|
+
}
|
|
215
|
+
debug(
|
|
216
|
+
`Tier 0: skipped (${hasJournalEntries ? 'timestamp stale' : 'no entries'}), falling to Tier 1`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Tier 1: mtime+size fast-path ─────────────────────────────────
|
|
221
|
+
const needsHash = []; // Files that failed mtime+size check
|
|
222
|
+
const skipped = []; // Files that passed mtime+size check
|
|
114
223
|
|
|
115
224
|
for (const file of allFiles) {
|
|
116
225
|
const relPath = normalizePath(path.relative(rootDir, file));
|
|
117
|
-
|
|
226
|
+
const record = existing.get(relPath);
|
|
227
|
+
|
|
228
|
+
if (!record) {
|
|
229
|
+
// New file — needs full read+hash
|
|
230
|
+
needsHash.push({ file, relPath });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const stat = fileStat(file);
|
|
235
|
+
if (!stat) continue;
|
|
236
|
+
|
|
237
|
+
const storedMtime = record.mtime || 0;
|
|
238
|
+
const storedSize = record.size || 0;
|
|
239
|
+
|
|
240
|
+
// size > 0 guard: pre-v4 rows have size=0, always fall through to hash
|
|
241
|
+
if (storedSize > 0 && Math.floor(stat.mtimeMs) === storedMtime && stat.size === storedSize) {
|
|
242
|
+
skipped.push(relPath);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
needsHash.push({ file, relPath, stat });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (needsHash.length > 0) {
|
|
250
|
+
debug(`Tier 1: ${skipped.length} skipped by mtime+size, ${needsHash.length} need hash check`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Tier 2: Hash comparison ──────────────────────────────────────
|
|
254
|
+
const changed = [];
|
|
118
255
|
|
|
256
|
+
for (const item of needsHash) {
|
|
119
257
|
let content;
|
|
120
258
|
try {
|
|
121
|
-
content = fs.readFileSync(file, 'utf-8');
|
|
259
|
+
content = fs.readFileSync(item.file, 'utf-8');
|
|
122
260
|
} catch {
|
|
123
261
|
continue;
|
|
124
262
|
}
|
|
125
263
|
const hash = fileHash(content);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
264
|
+
const stat = item.stat || fileStat(item.file);
|
|
265
|
+
const record = existing.get(item.relPath);
|
|
266
|
+
|
|
267
|
+
if (!record || record.hash !== hash) {
|
|
268
|
+
changed.push({ file: item.file, content, hash, relPath: item.relPath, stat });
|
|
269
|
+
} else if (stat) {
|
|
270
|
+
// Hash matches but mtime/size was stale — self-heal by updating stored metadata
|
|
271
|
+
changed.push({
|
|
272
|
+
file: item.file,
|
|
273
|
+
content,
|
|
274
|
+
hash,
|
|
275
|
+
relPath: item.relPath,
|
|
276
|
+
stat,
|
|
277
|
+
metadataOnly: true,
|
|
278
|
+
});
|
|
129
279
|
}
|
|
130
280
|
}
|
|
131
281
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
282
|
+
// Filter out metadata-only updates from the "changed" list for parsing,
|
|
283
|
+
// but keep them so the caller can update file_hashes
|
|
284
|
+
const parseChanged = changed.filter((c) => !c.metadataOnly);
|
|
285
|
+
if (needsHash.length > 0) {
|
|
286
|
+
debug(
|
|
287
|
+
`Tier 2: ${parseChanged.length} actually changed, ${changed.length - parseChanged.length} metadata-only`,
|
|
288
|
+
);
|
|
137
289
|
}
|
|
138
290
|
|
|
139
291
|
return { changed, removed, isFullBuild: false };
|
|
@@ -179,9 +331,33 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
179
331
|
? getChangedFiles(db, files, rootDir)
|
|
180
332
|
: { changed: files.map((f) => ({ file: f })), removed: [], isFullBuild: true };
|
|
181
333
|
|
|
182
|
-
|
|
334
|
+
// Separate metadata-only updates (mtime/size self-heal) from real changes
|
|
335
|
+
const parseChanges = changed.filter((c) => !c.metadataOnly);
|
|
336
|
+
const metadataUpdates = changed.filter((c) => c.metadataOnly);
|
|
337
|
+
|
|
338
|
+
if (!isFullBuild && parseChanges.length === 0 && removed.length === 0) {
|
|
339
|
+
// Still update metadata for self-healing even when no real changes
|
|
340
|
+
if (metadataUpdates.length > 0) {
|
|
341
|
+
try {
|
|
342
|
+
const healHash = db.prepare(
|
|
343
|
+
'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
|
|
344
|
+
);
|
|
345
|
+
const healTx = db.transaction(() => {
|
|
346
|
+
for (const item of metadataUpdates) {
|
|
347
|
+
const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
|
|
348
|
+
const size = item.stat ? item.stat.size : 0;
|
|
349
|
+
healHash.run(item.relPath, item.hash, mtime, size);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
healTx();
|
|
353
|
+
debug(`Self-healed mtime/size for ${metadataUpdates.length} files`);
|
|
354
|
+
} catch {
|
|
355
|
+
/* ignore heal errors */
|
|
356
|
+
}
|
|
357
|
+
}
|
|
183
358
|
console.log('No changes detected. Graph is up to date.');
|
|
184
359
|
db.close();
|
|
360
|
+
writeJournalHeader(rootDir, Date.now());
|
|
185
361
|
return;
|
|
186
362
|
}
|
|
187
363
|
|
|
@@ -190,7 +366,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
190
366
|
'PRAGMA foreign_keys = OFF; DELETE FROM node_metrics; DELETE FROM edges; DELETE FROM nodes; PRAGMA foreign_keys = ON;',
|
|
191
367
|
);
|
|
192
368
|
} else {
|
|
193
|
-
console.log(`Incremental: ${
|
|
369
|
+
console.log(`Incremental: ${parseChanges.length} changed, ${removed.length} removed`);
|
|
194
370
|
// Remove metrics/edges/nodes for changed and removed files
|
|
195
371
|
const deleteNodesForFile = db.prepare('DELETE FROM nodes WHERE file = ?');
|
|
196
372
|
const deleteEdgesForFile = db.prepare(`
|
|
@@ -205,7 +381,7 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
205
381
|
deleteMetricsForFile.run(relPath);
|
|
206
382
|
deleteNodesForFile.run(relPath);
|
|
207
383
|
}
|
|
208
|
-
for (const item of
|
|
384
|
+
for (const item of parseChanges) {
|
|
209
385
|
const relPath = item.relPath || normalizePath(path.relative(rootDir, item.file));
|
|
210
386
|
deleteEdgesForFile.run({ f: relPath });
|
|
211
387
|
deleteMetricsForFile.run(relPath);
|
|
@@ -223,11 +399,11 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
223
399
|
'INSERT INTO edges (source_id, target_id, kind, confidence, dynamic) VALUES (?, ?, ?, ?, ?)',
|
|
224
400
|
);
|
|
225
401
|
|
|
226
|
-
// Prepare hash upsert
|
|
402
|
+
// Prepare hash upsert (with size column from migration v4)
|
|
227
403
|
let upsertHash;
|
|
228
404
|
try {
|
|
229
405
|
upsertHash = db.prepare(
|
|
230
|
-
'INSERT OR REPLACE INTO file_hashes (file, hash, mtime) VALUES (?, ?, ?)',
|
|
406
|
+
'INSERT OR REPLACE INTO file_hashes (file, hash, mtime, size) VALUES (?, ?, ?, ?)',
|
|
231
407
|
);
|
|
232
408
|
} catch {
|
|
233
409
|
upsertHash = null;
|
|
@@ -245,17 +421,17 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
245
421
|
// We'll fill these in during the parse pass + edge pass
|
|
246
422
|
}
|
|
247
423
|
|
|
248
|
-
const filesToParse = isFullBuild ? files.map((f) => ({ file: f })) :
|
|
424
|
+
const filesToParse = isFullBuild ? files.map((f) => ({ file: f })) : parseChanges;
|
|
249
425
|
|
|
250
426
|
// ── Unified parse via parseFilesAuto ───────────────────────────────
|
|
251
427
|
const filePaths = filesToParse.map((item) => item.file);
|
|
252
428
|
const allSymbols = await parseFilesAuto(filePaths, rootDir, engineOpts);
|
|
253
429
|
|
|
254
|
-
// Build a
|
|
255
|
-
const
|
|
430
|
+
// Build a lookup from incremental data (changed items may carry pre-computed hashes + stats)
|
|
431
|
+
const precomputedData = new Map();
|
|
256
432
|
for (const item of filesToParse) {
|
|
257
|
-
if (item.
|
|
258
|
-
|
|
433
|
+
if (item.relPath) {
|
|
434
|
+
precomputedData.set(item.relPath, item);
|
|
259
435
|
}
|
|
260
436
|
}
|
|
261
437
|
|
|
@@ -271,11 +447,14 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
271
447
|
insertNode.run(exp.name, exp.kind, relPath, exp.line, null);
|
|
272
448
|
}
|
|
273
449
|
|
|
274
|
-
// Update file hash for incremental builds
|
|
450
|
+
// Update file hash with real mtime+size for incremental builds
|
|
275
451
|
if (upsertHash) {
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
278
|
-
|
|
452
|
+
const precomputed = precomputedData.get(relPath);
|
|
453
|
+
if (precomputed?.hash) {
|
|
454
|
+
const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
|
|
455
|
+
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
456
|
+
const size = stat ? stat.size : 0;
|
|
457
|
+
upsertHash.run(relPath, precomputed.hash, mtime, size);
|
|
279
458
|
} else {
|
|
280
459
|
const absPath = path.join(rootDir, relPath);
|
|
281
460
|
let code;
|
|
@@ -285,11 +464,23 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
285
464
|
code = null;
|
|
286
465
|
}
|
|
287
466
|
if (code !== null) {
|
|
288
|
-
|
|
467
|
+
const stat = fileStat(absPath);
|
|
468
|
+
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
469
|
+
const size = stat ? stat.size : 0;
|
|
470
|
+
upsertHash.run(relPath, fileHash(code), mtime, size);
|
|
289
471
|
}
|
|
290
472
|
}
|
|
291
473
|
}
|
|
292
474
|
}
|
|
475
|
+
|
|
476
|
+
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
477
|
+
if (upsertHash) {
|
|
478
|
+
for (const item of metadataUpdates) {
|
|
479
|
+
const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
|
|
480
|
+
const size = item.stat ? item.stat.size : 0;
|
|
481
|
+
upsertHash.run(item.relPath, item.hash, mtime, size);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
293
484
|
});
|
|
294
485
|
insertAll();
|
|
295
486
|
|
|
@@ -457,7 +648,9 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
457
648
|
}
|
|
458
649
|
|
|
459
650
|
// Call edges with confidence scoring — using pre-loaded lookup maps (N+1 fix)
|
|
651
|
+
const seenCallEdges = new Set();
|
|
460
652
|
for (const call of symbols.calls) {
|
|
653
|
+
if (call.receiver && BUILTIN_RECEIVERS.has(call.receiver)) continue;
|
|
461
654
|
let caller = null;
|
|
462
655
|
for (const def of symbols.definitions) {
|
|
463
656
|
if (def.line <= call.line) {
|
|
@@ -492,10 +685,18 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
492
685
|
);
|
|
493
686
|
if (methodCandidates.length > 0) {
|
|
494
687
|
targets = methodCandidates;
|
|
495
|
-
} else
|
|
496
|
-
|
|
497
|
-
|
|
688
|
+
} else if (
|
|
689
|
+
!call.receiver ||
|
|
690
|
+
call.receiver === 'this' ||
|
|
691
|
+
call.receiver === 'self' ||
|
|
692
|
+
call.receiver === 'super'
|
|
693
|
+
) {
|
|
694
|
+
// Scoped fallback — same-dir or parent-dir only, not global
|
|
695
|
+
targets = (nodesByName.get(call.name) || []).filter(
|
|
696
|
+
(n) => computeConfidence(relPath, n.file, null) >= 0.5,
|
|
697
|
+
);
|
|
498
698
|
}
|
|
699
|
+
// else: method call on a receiver — skip global fallback entirely
|
|
499
700
|
}
|
|
500
701
|
}
|
|
501
702
|
|
|
@@ -508,7 +709,9 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
508
709
|
}
|
|
509
710
|
|
|
510
711
|
for (const t of targets) {
|
|
511
|
-
|
|
712
|
+
const edgeKey = `${caller.id}|${t.id}`;
|
|
713
|
+
if (t.id !== caller.id && !seenCallEdges.has(edgeKey)) {
|
|
714
|
+
seenCallEdges.add(edgeKey);
|
|
512
715
|
const confidence = computeConfidence(relPath, t.file, importedFrom);
|
|
513
716
|
insertEdge.run(caller.id, t.id, 'calls', confidence, isDynamic);
|
|
514
717
|
edgeCount++;
|
|
@@ -581,10 +784,21 @@ export async function buildGraph(rootDir, opts = {}) {
|
|
|
581
784
|
console.log(`Stored in ${dbPath}`);
|
|
582
785
|
db.close();
|
|
583
786
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
787
|
+
// Write journal header after successful build
|
|
788
|
+
writeJournalHeader(rootDir, Date.now());
|
|
789
|
+
|
|
790
|
+
if (!opts.skipRegistry) {
|
|
791
|
+
const tmpDir = path.resolve(os.tmpdir());
|
|
792
|
+
const resolvedRoot = path.resolve(rootDir);
|
|
793
|
+
if (resolvedRoot.startsWith(tmpDir)) {
|
|
794
|
+
debug(`Skipping auto-registration for temp directory: ${resolvedRoot}`);
|
|
795
|
+
} else {
|
|
796
|
+
try {
|
|
797
|
+
const { registerRepo } = await import('./registry.js');
|
|
798
|
+
registerRepo(rootDir);
|
|
799
|
+
} catch (err) {
|
|
800
|
+
debug(`Auto-registration failed: ${err.message}`);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
589
803
|
}
|
|
590
804
|
}
|
package/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildEmbeddings, MODELS, search } from './embedder.js';
|
|
|
11
11
|
import { exportDOT, exportJSON, exportMermaid } from './export.js';
|
|
12
12
|
import { setVerbose } from './logger.js';
|
|
13
13
|
import {
|
|
14
|
+
context,
|
|
14
15
|
diffImpact,
|
|
15
16
|
fileDeps,
|
|
16
17
|
fnDeps,
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
impactAnalysis,
|
|
19
20
|
moduleMap,
|
|
20
21
|
queryName,
|
|
22
|
+
stats,
|
|
21
23
|
} from './queries.js';
|
|
22
24
|
import {
|
|
23
25
|
listRepos,
|
|
@@ -28,11 +30,14 @@ import {
|
|
|
28
30
|
} from './registry.js';
|
|
29
31
|
import { watchProject } from './watcher.js';
|
|
30
32
|
|
|
33
|
+
const __cliDir = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/i, '$1'));
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf-8'));
|
|
35
|
+
|
|
31
36
|
const program = new Command();
|
|
32
37
|
program
|
|
33
38
|
.name('codegraph')
|
|
34
39
|
.description('Local code dependency graph tool')
|
|
35
|
-
.version(
|
|
40
|
+
.version(pkg.version)
|
|
36
41
|
.option('-v, --verbose', 'Enable verbose/debug output')
|
|
37
42
|
.option('--engine <engine>', 'Parser engine: native, wasm, or auto (default: auto)', 'auto')
|
|
38
43
|
.hook('preAction', (thisCommand) => {
|
|
@@ -78,6 +83,15 @@ program
|
|
|
78
83
|
moduleMap(opts.db, parseInt(opts.limit, 10), { json: opts.json });
|
|
79
84
|
});
|
|
80
85
|
|
|
86
|
+
program
|
|
87
|
+
.command('stats')
|
|
88
|
+
.description('Show graph health overview: nodes, edges, languages, cycles, hotspots, embeddings')
|
|
89
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
90
|
+
.option('-j, --json', 'Output as JSON')
|
|
91
|
+
.action((opts) => {
|
|
92
|
+
stats(opts.db, { json: opts.json });
|
|
93
|
+
});
|
|
94
|
+
|
|
81
95
|
program
|
|
82
96
|
.command('deps <file>')
|
|
83
97
|
.description('Show what this file imports and what imports it')
|
|
@@ -117,6 +131,25 @@ program
|
|
|
117
131
|
});
|
|
118
132
|
});
|
|
119
133
|
|
|
134
|
+
program
|
|
135
|
+
.command('context <name>')
|
|
136
|
+
.description('Full context for a function: source, deps, callers, tests, signature')
|
|
137
|
+
.option('-d, --db <path>', 'Path to graph.db')
|
|
138
|
+
.option('--depth <n>', 'Include callee source up to N levels deep', '0')
|
|
139
|
+
.option('--no-source', 'Metadata only (skip source extraction)')
|
|
140
|
+
.option('--include-tests', 'Include test source code')
|
|
141
|
+
.option('-T, --no-tests', 'Exclude test files from callers')
|
|
142
|
+
.option('-j, --json', 'Output as JSON')
|
|
143
|
+
.action((name, opts) => {
|
|
144
|
+
context(name, opts.db, {
|
|
145
|
+
depth: parseInt(opts.depth, 10),
|
|
146
|
+
noSource: !opts.source,
|
|
147
|
+
noTests: !opts.tests,
|
|
148
|
+
includeTests: opts.includeTests,
|
|
149
|
+
json: opts.json,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
120
153
|
program
|
|
121
154
|
.command('diff-impact [ref]')
|
|
122
155
|
.description('Show impact of git changes (unstaged, staged, or vs a ref)')
|
|
@@ -214,6 +247,7 @@ registry
|
|
|
214
247
|
.description('List all registered repositories')
|
|
215
248
|
.option('-j, --json', 'Output as JSON')
|
|
216
249
|
.action((opts) => {
|
|
250
|
+
pruneRegistry();
|
|
217
251
|
const repos = listRepos();
|
|
218
252
|
if (opts.json) {
|
|
219
253
|
console.log(JSON.stringify(repos, null, 2));
|
|
@@ -257,14 +291,16 @@ registry
|
|
|
257
291
|
|
|
258
292
|
registry
|
|
259
293
|
.command('prune')
|
|
260
|
-
.description('Remove registry entries
|
|
261
|
-
.
|
|
262
|
-
|
|
294
|
+
.description('Remove stale registry entries (missing directories or idle beyond TTL)')
|
|
295
|
+
.option('--ttl <days>', 'Days of inactivity before pruning (default: 30)', '30')
|
|
296
|
+
.action((opts) => {
|
|
297
|
+
const pruned = pruneRegistry(undefined, parseInt(opts.ttl, 10));
|
|
263
298
|
if (pruned.length === 0) {
|
|
264
299
|
console.log('No stale entries found.');
|
|
265
300
|
} else {
|
|
266
301
|
for (const entry of pruned) {
|
|
267
|
-
|
|
302
|
+
const tag = entry.reason === 'expired' ? 'expired' : 'missing';
|
|
303
|
+
console.log(`Pruned "${entry.name}" (${entry.path}) [${tag}]`);
|
|
268
304
|
}
|
|
269
305
|
console.log(`\nRemoved ${pruned.length} stale ${pruned.length === 1 ? 'entry' : 'entries'}.`);
|
|
270
306
|
}
|
|
@@ -278,7 +314,7 @@ program
|
|
|
278
314
|
.action(() => {
|
|
279
315
|
console.log('\nAvailable embedding models:\n');
|
|
280
316
|
for (const [key, config] of Object.entries(MODELS)) {
|
|
281
|
-
const def = key === '
|
|
317
|
+
const def = key === 'jina-code' ? ' (default)' : '';
|
|
282
318
|
console.log(` ${key.padEnd(12)} ${String(config.dim).padStart(4)}d ${config.desc}${def}`);
|
|
283
319
|
}
|
|
284
320
|
console.log('\nUsage: codegraph embed --model <name>');
|
|
@@ -292,8 +328,8 @@ program
|
|
|
292
328
|
)
|
|
293
329
|
.option(
|
|
294
330
|
'-m, --model <name>',
|
|
295
|
-
'Embedding model: minilm
|
|
296
|
-
'
|
|
331
|
+
'Embedding model: minilm, jina-small, jina-base, jina-code (default), nomic, nomic-v1.5, bge-large. Run `codegraph models` for details',
|
|
332
|
+
'jina-code',
|
|
297
333
|
)
|
|
298
334
|
.action(async (dir, opts) => {
|
|
299
335
|
const root = path.resolve(dir || '.');
|
package/src/config.js
CHANGED
|
@@ -19,7 +19,7 @@ export const DEFAULTS = {
|
|
|
19
19
|
defaultDepth: 3,
|
|
20
20
|
defaultLimit: 20,
|
|
21
21
|
},
|
|
22
|
-
embeddings: { model: '
|
|
22
|
+
embeddings: { model: 'jina-code', llmProvider: null },
|
|
23
23
|
llm: { provider: null, model: null, baseUrl: null, apiKey: null, apiKeyCommand: null },
|
|
24
24
|
search: { defaultMinScore: 0.2, rrfK: 60, topK: 15 },
|
|
25
25
|
ci: { failOnCycles: false, impactThreshold: null },
|
package/src/db.js
CHANGED
package/src/embedder.js
CHANGED
|
@@ -55,7 +55,7 @@ export const MODELS = {
|
|
|
55
55
|
},
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
export const DEFAULT_MODEL = '
|
|
58
|
+
export const DEFAULT_MODEL = 'jina-code';
|
|
59
59
|
const BATCH_SIZE_MAP = {
|
|
60
60
|
minilm: 32,
|
|
61
61
|
'jina-small': 16,
|
|
@@ -173,10 +173,10 @@ function initEmbeddingsSchema(db) {
|
|
|
173
173
|
/**
|
|
174
174
|
* Build embeddings for all functions/methods/classes in the graph.
|
|
175
175
|
*/
|
|
176
|
-
export async function buildEmbeddings(rootDir, modelKey) {
|
|
176
|
+
export async function buildEmbeddings(rootDir, modelKey, customDbPath) {
|
|
177
177
|
// path already imported at top
|
|
178
178
|
// fs already imported at top
|
|
179
|
-
const dbPath = findDbPath(null);
|
|
179
|
+
const dbPath = customDbPath || findDbPath(null);
|
|
180
180
|
|
|
181
181
|
const db = new Database(dbPath);
|
|
182
182
|
initEmbeddingsSchema(db);
|