|
1 | 1 | import { execFileSync } from 'node:child_process'; |
2 | 2 | import fs from 'node:fs'; |
3 | 3 | import path from 'node:path'; |
| 4 | +import { findCycles } from './cycles.js'; |
4 | 5 | import { findDbPath, openReadonlyOrFail } from './db.js'; |
| 6 | +import { LANGUAGE_REGISTRY } from './parser.js'; |
5 | 7 |
|
6 | 8 | const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./; |
7 | 9 | function isTestFile(filePath) { |
@@ -191,14 +193,14 @@ export function moduleMapData(customDbPath, limit = 20) { |
191 | 193 | const nodes = db |
192 | 194 | .prepare(` |
193 | 195 | 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 |
196 | 198 | FROM nodes n |
197 | 199 | WHERE n.kind = 'file' |
198 | 200 | AND n.file NOT LIKE '%.test.%' |
199 | 201 | AND n.file NOT LIKE '%.spec.%' |
200 | 202 | 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 |
202 | 204 | LIMIT ? |
203 | 205 | `) |
204 | 206 | .all(limit); |
@@ -614,6 +616,172 @@ export function listFunctionsData(customDbPath, opts = {}) { |
614 | 616 | return { count: rows.length, functions: rows }; |
615 | 617 | } |
616 | 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 | + |
617 | 785 | // ─── Human-readable output (original formatting) ─────────────────────── |
618 | 786 |
|
619 | 787 | export function queryName(name, customDbPath, opts = {}) { |
|
0 commit comments