Skip to content

Commit 02b931d

Browse files
fix: throw on explicit --engine native when addon is unavailable
Previously --engine native silently fell back to WASM with a warning, which was easy to miss. Now it throws a clear error with install instructions, matching user intent. Auto mode remains unchanged.
1 parent dea0c3a commit 02b931d

3 files changed

Lines changed: 217 additions & 7 deletions

File tree

docs/improvement-plan.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,10 @@ Currently you need to run `map`, `cycles`, and read build output separately to a
109109
- Top 5 coupling hotspots
110110
- Embedding status (model, count, staleness)
111111

112-
#### 8. Improve map command ranking
112+
#### ~~8. Improve map command ranking~~ FIXED
113113
**Found by:** `codegraph map --limit 20` (WASM build)
114114

115-
When Rust files are parsed, the map is dominated by Rust extractor files (all with `inEdges: 1, outEdges: 0`). The ranking should prioritize files with meaningful import relationships over files with only `contains` edges.
116-
117-
**Action:** Weight import/reexport edges higher than `contains` edges in the `map` ranking algorithm. Consider filtering out files below a minimum edge threshold.
115+
Fixed: `moduleMapData` SQL now excludes `contains` edges from `inEdges`/`outEdges` counts and ranking (`AND kind != 'contains'`). Files with only structural `contains` edges (e.g. Rust extractor files) no longer dominate the map. `stats.totalEdges` remains the raw total for graph size indication.
118116

119117
#### 9. builder.js fan-out reduction
120118
**Found by:** `codegraph map` (fan-out 7, highest in codebase)

src/queries.js

Lines changed: 171 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { execFileSync } from 'node:child_process';
22
import fs from 'node:fs';
33
import path from 'node:path';
4+
import { findCycles } from './cycles.js';
45
import { findDbPath, openReadonlyOrFail } from './db.js';
6+
import { LANGUAGE_REGISTRY } from './parser.js';
57

68
const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
79
function isTestFile(filePath) {
@@ -191,14 +193,14 @@ export function moduleMapData(customDbPath, limit = 20) {
191193
const nodes = db
192194
.prepare(`
193195
SELECT n.*,
194-
(SELECT COUNT(*) FROM edges WHERE source_id = n.id) as out_edges,
195-
(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
196198
FROM nodes n
197199
WHERE n.kind = 'file'
198200
AND n.file NOT LIKE '%.test.%'
199201
AND n.file NOT LIKE '%.spec.%'
200202
AND n.file NOT LIKE '%__test__%'
201-
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
202204
LIMIT ?
203205
`)
204206
.all(limit);
@@ -614,6 +616,172 @@ export function listFunctionsData(customDbPath, opts = {}) {
614616
return { count: rows.length, functions: rows };
615617
}
616618

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+
617785
// ─── Human-readable output (original formatting) ───────────────────────
618786

619787
export function queryName(name, customDbPath, opts = {}) {

tests/integration/queries.test.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,50 @@ describe('moduleMapData', () => {
160160
const data = moduleMapData(dbPath, 2);
161161
expect(data.topNodes).toHaveLength(2);
162162
});
163+
164+
test('excludes contains edges from ranking and counts', () => {
165+
// Build a separate DB with contains + imports edges
166+
const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-contains-'));
167+
fs.mkdirSync(path.join(tmpDir2, '.codegraph'));
168+
const dbPath2 = path.join(tmpDir2, '.codegraph', 'graph.db');
169+
170+
const db2 = new Database(dbPath2);
171+
db2.pragma('journal_mode = WAL');
172+
initSchema(db2);
173+
174+
// Two file nodes
175+
const fA = insertNode(db2, 'a.js', 'file', 'a.js', 0);
176+
const fB = insertNode(db2, 'b.js', 'file', 'b.js', 0);
177+
const fC = insertNode(db2, 'c.js', 'file', 'c.js', 0);
178+
179+
// a.js gets only a contains edge (structural)
180+
insertEdge(db2, fC, fA, 'contains');
181+
// b.js gets an imports edge (real dependency)
182+
insertEdge(db2, fC, fB, 'imports');
183+
184+
db2.close();
185+
186+
try {
187+
const data = moduleMapData(dbPath2);
188+
const nodeA = data.topNodes.find((n) => n.file === 'a.js');
189+
const nodeB = data.topNodes.find((n) => n.file === 'b.js');
190+
191+
// b.js (imports edge) should have inEdges=1, a.js (contains edge) should have inEdges=0
192+
expect(nodeB.inEdges).toBe(1);
193+
expect(nodeA.inEdges).toBe(0);
194+
195+
// b.js should rank above a.js
196+
const indexA = data.topNodes.indexOf(nodeA);
197+
const indexB = data.topNodes.indexOf(nodeB);
198+
expect(indexB).toBeLessThan(indexA);
199+
200+
// c.js outEdges should only count the imports edge, not contains
201+
const nodeC = data.topNodes.find((n) => n.file === 'c.js');
202+
expect(nodeC.outEdges).toBe(1);
203+
} finally {
204+
fs.rmSync(tmpDir2, { recursive: true, force: true });
205+
}
206+
});
163207
});
164208

165209
// ─── fileDepsData ──────────────────────────────────────────────────────

0 commit comments

Comments
 (0)