@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/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 = path.join(os.homedir(), '.codegraph', 'registry.json');
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: new Date().toISOString(),
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
- * Only checks the repo directory (not the DB file — a missing DB is normal pre-build state).
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
- return native.resolveImport(
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 {