@optave/codegraph 2.0.0 → 2.1.1-dev.3c12b64
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 +58 -22
- package/package.json +10 -10
- package/src/builder.js +14 -5
- package/src/cli.js +24 -8
- package/src/config.js +1 -1
- package/src/embedder.js +3 -3
- package/src/extractors/csharp.js +243 -0
- package/src/extractors/go.js +167 -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 +227 -0
- package/src/extractors/javascript.js +396 -0
- package/src/extractors/php.js +237 -0
- package/src/extractors/python.js +143 -0
- package/src/extractors/ruby.js +185 -0
- package/src/extractors/rust.js +215 -0
- package/src/index.js +1 -0
- package/src/mcp.js +2 -1
- package/src/parser.js +27 -1890
- package/src/queries.js +190 -4
- package/src/registry.js +24 -7
- package/src/resolve.js +4 -3
package/src/queries.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { findCycles } from './cycles.js';
|
|
3
5
|
import { findDbPath, openReadonlyOrFail } from './db.js';
|
|
6
|
+
import { LANGUAGE_REGISTRY } from './parser.js';
|
|
4
7
|
|
|
5
8
|
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
|
|
6
9
|
function isTestFile(filePath) {
|
|
@@ -190,14 +193,14 @@ export function moduleMapData(customDbPath, limit = 20) {
|
|
|
190
193
|
const nodes = db
|
|
191
194
|
.prepare(`
|
|
192
195
|
SELECT n.*,
|
|
193
|
-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as out_edges,
|
|
194
|
-
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as in_edges
|
|
196
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id AND kind != 'contains') as out_edges,
|
|
197
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind != 'contains') as in_edges
|
|
195
198
|
FROM nodes n
|
|
196
199
|
WHERE n.kind = 'file'
|
|
197
200
|
AND n.file NOT LIKE '%.test.%'
|
|
198
201
|
AND n.file NOT LIKE '%.spec.%'
|
|
199
202
|
AND n.file NOT LIKE '%__test__%'
|
|
200
|
-
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id) DESC
|
|
203
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id AND kind != 'contains') DESC
|
|
201
204
|
LIMIT ?
|
|
202
205
|
`)
|
|
203
206
|
.all(limit);
|
|
@@ -451,9 +454,25 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
451
454
|
const dbPath = findDbPath(customDbPath);
|
|
452
455
|
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
453
456
|
|
|
457
|
+
// Verify we're in a git repository before running git diff
|
|
458
|
+
let checkDir = repoRoot;
|
|
459
|
+
let isGitRepo = false;
|
|
460
|
+
while (checkDir) {
|
|
461
|
+
if (fs.existsSync(path.join(checkDir, '.git'))) {
|
|
462
|
+
isGitRepo = true;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
const parent = path.dirname(checkDir);
|
|
466
|
+
if (parent === checkDir) break;
|
|
467
|
+
checkDir = parent;
|
|
468
|
+
}
|
|
469
|
+
if (!isGitRepo) {
|
|
470
|
+
db.close();
|
|
471
|
+
return { error: `Not a git repository: ${repoRoot}` };
|
|
472
|
+
}
|
|
473
|
+
|
|
454
474
|
let diffOutput;
|
|
455
475
|
try {
|
|
456
|
-
// FIX: Use execFileSync with array args to prevent shell injection
|
|
457
476
|
const args = opts.staged
|
|
458
477
|
? ['diff', '--cached', '--unified=0', '--no-color']
|
|
459
478
|
: ['diff', opts.ref || 'HEAD', '--unified=0', '--no-color'];
|
|
@@ -461,6 +480,7 @@ export function diffImpactData(customDbPath, opts = {}) {
|
|
|
461
480
|
cwd: repoRoot,
|
|
462
481
|
encoding: 'utf-8',
|
|
463
482
|
maxBuffer: 10 * 1024 * 1024,
|
|
483
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
464
484
|
});
|
|
465
485
|
} catch (e) {
|
|
466
486
|
db.close();
|
|
@@ -596,6 +616,172 @@ export function listFunctionsData(customDbPath, opts = {}) {
|
|
|
596
616
|
return { count: rows.length, functions: rows };
|
|
597
617
|
}
|
|
598
618
|
|
|
619
|
+
export function statsData(customDbPath) {
|
|
620
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
621
|
+
|
|
622
|
+
// Node breakdown by kind
|
|
623
|
+
const nodeRows = db.prepare('SELECT kind, COUNT(*) as c FROM nodes GROUP BY kind').all();
|
|
624
|
+
const nodesByKind = {};
|
|
625
|
+
let totalNodes = 0;
|
|
626
|
+
for (const r of nodeRows) {
|
|
627
|
+
nodesByKind[r.kind] = r.c;
|
|
628
|
+
totalNodes += r.c;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Edge breakdown by kind
|
|
632
|
+
const edgeRows = db.prepare('SELECT kind, COUNT(*) as c FROM edges GROUP BY kind').all();
|
|
633
|
+
const edgesByKind = {};
|
|
634
|
+
let totalEdges = 0;
|
|
635
|
+
for (const r of edgeRows) {
|
|
636
|
+
edgesByKind[r.kind] = r.c;
|
|
637
|
+
totalEdges += r.c;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// File/language distribution — map extensions via LANGUAGE_REGISTRY
|
|
641
|
+
const extToLang = new Map();
|
|
642
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
643
|
+
for (const ext of entry.extensions) {
|
|
644
|
+
extToLang.set(ext, entry.id);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const fileNodes = db.prepare("SELECT file FROM nodes WHERE kind = 'file'").all();
|
|
648
|
+
const byLanguage = {};
|
|
649
|
+
for (const row of fileNodes) {
|
|
650
|
+
const ext = path.extname(row.file).toLowerCase();
|
|
651
|
+
const lang = extToLang.get(ext) || 'other';
|
|
652
|
+
byLanguage[lang] = (byLanguage[lang] || 0) + 1;
|
|
653
|
+
}
|
|
654
|
+
const langCount = Object.keys(byLanguage).length;
|
|
655
|
+
|
|
656
|
+
// Cycles
|
|
657
|
+
const fileCycles = findCycles(db, { fileLevel: true });
|
|
658
|
+
const fnCycles = findCycles(db, { fileLevel: false });
|
|
659
|
+
|
|
660
|
+
// Top 5 coupling hotspots (fan-in + fan-out, file nodes)
|
|
661
|
+
const hotspotRows = db
|
|
662
|
+
.prepare(`
|
|
663
|
+
SELECT n.file,
|
|
664
|
+
(SELECT COUNT(*) FROM edges WHERE target_id = n.id) as fan_in,
|
|
665
|
+
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as fan_out
|
|
666
|
+
FROM nodes n
|
|
667
|
+
WHERE n.kind = 'file'
|
|
668
|
+
ORDER BY (SELECT COUNT(*) FROM edges WHERE target_id = n.id)
|
|
669
|
+
+ (SELECT COUNT(*) FROM edges WHERE source_id = n.id) DESC
|
|
670
|
+
LIMIT 5
|
|
671
|
+
`)
|
|
672
|
+
.all();
|
|
673
|
+
const hotspots = hotspotRows.map((r) => ({
|
|
674
|
+
file: r.file,
|
|
675
|
+
fanIn: r.fan_in,
|
|
676
|
+
fanOut: r.fan_out,
|
|
677
|
+
}));
|
|
678
|
+
|
|
679
|
+
// Embeddings metadata
|
|
680
|
+
let embeddings = null;
|
|
681
|
+
try {
|
|
682
|
+
const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get();
|
|
683
|
+
if (count && count.c > 0) {
|
|
684
|
+
const meta = {};
|
|
685
|
+
const metaRows = db.prepare('SELECT key, value FROM embedding_meta').all();
|
|
686
|
+
for (const r of metaRows) meta[r.key] = r.value;
|
|
687
|
+
embeddings = {
|
|
688
|
+
count: count.c,
|
|
689
|
+
model: meta.model || null,
|
|
690
|
+
dim: meta.dim ? parseInt(meta.dim, 10) : null,
|
|
691
|
+
builtAt: meta.built_at || null,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
/* embeddings table may not exist */
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
db.close();
|
|
699
|
+
return {
|
|
700
|
+
nodes: { total: totalNodes, byKind: nodesByKind },
|
|
701
|
+
edges: { total: totalEdges, byKind: edgesByKind },
|
|
702
|
+
files: { total: fileNodes.length, languages: langCount, byLanguage },
|
|
703
|
+
cycles: { fileLevel: fileCycles.length, functionLevel: fnCycles.length },
|
|
704
|
+
hotspots,
|
|
705
|
+
embeddings,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
export function stats(customDbPath, opts = {}) {
|
|
710
|
+
const data = statsData(customDbPath);
|
|
711
|
+
if (opts.json) {
|
|
712
|
+
console.log(JSON.stringify(data, null, 2));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// Human-readable output
|
|
717
|
+
console.log('\n# Codegraph Stats\n');
|
|
718
|
+
|
|
719
|
+
// Nodes
|
|
720
|
+
console.log(`Nodes: ${data.nodes.total} total`);
|
|
721
|
+
const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
|
|
722
|
+
const kindParts = kindEntries.map(([k, v]) => `${k} ${v}`);
|
|
723
|
+
// Print in rows of 3
|
|
724
|
+
for (let i = 0; i < kindParts.length; i += 3) {
|
|
725
|
+
const row = kindParts
|
|
726
|
+
.slice(i, i + 3)
|
|
727
|
+
.map((p) => p.padEnd(18))
|
|
728
|
+
.join('');
|
|
729
|
+
console.log(` ${row}`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Edges
|
|
733
|
+
console.log(`\nEdges: ${data.edges.total} total`);
|
|
734
|
+
const edgeEntries = Object.entries(data.edges.byKind).sort((a, b) => b[1] - a[1]);
|
|
735
|
+
const edgeParts = edgeEntries.map(([k, v]) => `${k} ${v}`);
|
|
736
|
+
for (let i = 0; i < edgeParts.length; i += 3) {
|
|
737
|
+
const row = edgeParts
|
|
738
|
+
.slice(i, i + 3)
|
|
739
|
+
.map((p) => p.padEnd(18))
|
|
740
|
+
.join('');
|
|
741
|
+
console.log(` ${row}`);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Files
|
|
745
|
+
console.log(`\nFiles: ${data.files.total} (${data.files.languages} languages)`);
|
|
746
|
+
const langEntries = Object.entries(data.files.byLanguage).sort((a, b) => b[1] - a[1]);
|
|
747
|
+
const langParts = langEntries.map(([k, v]) => `${k} ${v}`);
|
|
748
|
+
for (let i = 0; i < langParts.length; i += 3) {
|
|
749
|
+
const row = langParts
|
|
750
|
+
.slice(i, i + 3)
|
|
751
|
+
.map((p) => p.padEnd(18))
|
|
752
|
+
.join('');
|
|
753
|
+
console.log(` ${row}`);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Cycles
|
|
757
|
+
console.log(
|
|
758
|
+
`\nCycles: ${data.cycles.fileLevel} file-level, ${data.cycles.functionLevel} function-level`,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// Hotspots
|
|
762
|
+
if (data.hotspots.length > 0) {
|
|
763
|
+
console.log(`\nTop ${data.hotspots.length} coupling hotspots:`);
|
|
764
|
+
for (let i = 0; i < data.hotspots.length; i++) {
|
|
765
|
+
const h = data.hotspots[i];
|
|
766
|
+
console.log(
|
|
767
|
+
` ${String(i + 1).padStart(2)}. ${h.file.padEnd(35)} fan-in: ${String(h.fanIn).padStart(3)} fan-out: ${String(h.fanOut).padStart(3)}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Embeddings
|
|
773
|
+
if (data.embeddings) {
|
|
774
|
+
const e = data.embeddings;
|
|
775
|
+
console.log(
|
|
776
|
+
`\nEmbeddings: ${e.count} vectors (${e.model || 'unknown'}, ${e.dim || '?'}d) built ${e.builtAt || 'unknown'}`,
|
|
777
|
+
);
|
|
778
|
+
} else {
|
|
779
|
+
console.log('\nEmbeddings: not built');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
console.log();
|
|
783
|
+
}
|
|
784
|
+
|
|
599
785
|
// ─── Human-readable output (original formatting) ───────────────────────
|
|
600
786
|
|
|
601
787
|
export function queryName(name, customDbPath, opts = {}) {
|
package/src/registry.js
CHANGED
|
@@ -3,7 +3,11 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { debug, warn } from './logger.js';
|
|
5
5
|
|
|
6
|
-
export const REGISTRY_PATH =
|
|
6
|
+
export const REGISTRY_PATH =
|
|
7
|
+
process.env.CODEGRAPH_REGISTRY_PATH || path.join(os.homedir(), '.codegraph', 'registry.json');
|
|
8
|
+
|
|
9
|
+
/** Default TTL: entries not accessed within 30 days are pruned. */
|
|
10
|
+
export const DEFAULT_TTL_DAYS = 30;
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* Load the registry from disk.
|
|
@@ -69,10 +73,12 @@ export function registerRepo(rootDir, name, registryPath = REGISTRY_PATH) {
|
|
|
69
73
|
}
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
const now = new Date().toISOString();
|
|
72
77
|
registry.repos[repoName] = {
|
|
73
78
|
path: absRoot,
|
|
74
79
|
dbPath: path.join(absRoot, '.codegraph', 'graph.db'),
|
|
75
|
-
addedAt:
|
|
80
|
+
addedAt: registry.repos[repoName]?.addedAt || now,
|
|
81
|
+
lastAccessedAt: now,
|
|
76
82
|
};
|
|
77
83
|
|
|
78
84
|
saveRegistry(registry, registryPath);
|
|
@@ -102,6 +108,7 @@ export function listRepos(registryPath = REGISTRY_PATH) {
|
|
|
102
108
|
path: entry.path,
|
|
103
109
|
dbPath: entry.dbPath,
|
|
104
110
|
addedAt: entry.addedAt,
|
|
111
|
+
lastAccessedAt: entry.lastAccessedAt || entry.addedAt,
|
|
105
112
|
}))
|
|
106
113
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
107
114
|
}
|
|
@@ -118,21 +125,31 @@ export function resolveRepoDbPath(name, registryPath = REGISTRY_PATH) {
|
|
|
118
125
|
warn(`Registry: database missing for "${name}" at ${entry.dbPath}`);
|
|
119
126
|
return undefined;
|
|
120
127
|
}
|
|
128
|
+
// Touch lastAccessedAt on successful resolution
|
|
129
|
+
entry.lastAccessedAt = new Date().toISOString();
|
|
130
|
+
saveRegistry(registry, registryPath);
|
|
121
131
|
return entry.dbPath;
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
/**
|
|
125
|
-
* Remove registry entries whose repo directory no longer exists on disk
|
|
126
|
-
*
|
|
127
|
-
* Returns an array of `{ name, path }` for each pruned entry.
|
|
135
|
+
* Remove registry entries whose repo directory no longer exists on disk,
|
|
136
|
+
* or that haven't been accessed within `ttlDays` days.
|
|
137
|
+
* Returns an array of `{ name, path, reason }` for each pruned entry.
|
|
128
138
|
*/
|
|
129
|
-
export function pruneRegistry(registryPath = REGISTRY_PATH) {
|
|
139
|
+
export function pruneRegistry(registryPath = REGISTRY_PATH, ttlDays = DEFAULT_TTL_DAYS) {
|
|
130
140
|
const registry = loadRegistry(registryPath);
|
|
131
141
|
const pruned = [];
|
|
142
|
+
const cutoff = Date.now() - ttlDays * 24 * 60 * 60 * 1000;
|
|
132
143
|
|
|
133
144
|
for (const [name, entry] of Object.entries(registry.repos)) {
|
|
134
145
|
if (!fs.existsSync(entry.path)) {
|
|
135
|
-
pruned.push({ name, path: entry.path });
|
|
146
|
+
pruned.push({ name, path: entry.path, reason: 'missing' });
|
|
147
|
+
delete registry.repos[name];
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const lastAccess = Date.parse(entry.lastAccessedAt || entry.addedAt);
|
|
151
|
+
if (lastAccess < cutoff) {
|
|
152
|
+
pruned.push({ name, path: entry.path, reason: 'expired' });
|
|
136
153
|
delete registry.repos[name];
|
|
137
154
|
}
|
|
138
155
|
}
|
package/src/resolve.js
CHANGED
|
@@ -31,7 +31,7 @@ function resolveViaAlias(importSource, aliases, _rootDir) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
for (const [pattern, targets] of Object.entries(aliases.paths)) {
|
|
34
|
+
for (const [pattern, targets] of Object.entries(aliases.paths || {})) {
|
|
35
35
|
const prefix = pattern.replace(/\*$/, '');
|
|
36
36
|
if (!importSource.startsWith(prefix)) continue;
|
|
37
37
|
const rest = importSource.slice(prefix.length);
|
|
@@ -113,12 +113,13 @@ export function resolveImportPath(fromFile, importSource, rootDir, aliases) {
|
|
|
113
113
|
const native = loadNative();
|
|
114
114
|
if (native) {
|
|
115
115
|
try {
|
|
116
|
-
|
|
116
|
+
const result = native.resolveImport(
|
|
117
117
|
fromFile,
|
|
118
118
|
importSource,
|
|
119
119
|
rootDir,
|
|
120
120
|
convertAliasesForNative(aliases),
|
|
121
121
|
);
|
|
122
|
+
return normalizePath(path.normalize(result));
|
|
122
123
|
} catch {
|
|
123
124
|
// fall through to JS
|
|
124
125
|
}
|
|
@@ -158,7 +159,7 @@ export function resolveImportsBatch(inputs, rootDir, aliases) {
|
|
|
158
159
|
const results = native.resolveImports(nativeInputs, rootDir, convertAliasesForNative(aliases));
|
|
159
160
|
const map = new Map();
|
|
160
161
|
for (const r of results) {
|
|
161
|
-
map.set(`${r.fromFile}|${r.importSource}`, r.resolvedPath);
|
|
162
|
+
map.set(`${r.fromFile}|${r.importSource}`, normalizePath(path.normalize(r.resolvedPath)));
|
|
162
163
|
}
|
|
163
164
|
return map;
|
|
164
165
|
} catch {
|