@optave/codegraph 1.1.0 → 1.4.1

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/config.js CHANGED
@@ -1,55 +1,103 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { debug } from './logger.js';
4
-
5
- export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
6
-
7
- export const DEFAULTS = {
8
- include: [],
9
- exclude: [],
10
- ignoreDirs: [],
11
- extensions: [],
12
- aliases: {},
13
- build: {
14
- incremental: true,
15
- dbPath: '.codegraph/graph.db'
16
- },
17
- query: {
18
- defaultDepth: 3,
19
- defaultLimit: 20
20
- }
21
- };
22
-
23
- /**
24
- * Load project configuration from a .codegraphrc.json or similar file.
25
- * Returns merged config with defaults.
26
- */
27
- export function loadConfig(cwd) {
28
- cwd = cwd || process.cwd();
29
- for (const name of CONFIG_FILES) {
30
- const filePath = path.join(cwd, name);
31
- if (fs.existsSync(filePath)) {
32
- try {
33
- const raw = fs.readFileSync(filePath, 'utf-8');
34
- const config = JSON.parse(raw);
35
- debug(`Loaded config from ${filePath}`);
36
- return mergeConfig(DEFAULTS, config);
37
- } catch (err) {
38
- debug(`Failed to parse config ${filePath}: ${err.message}`);
39
- }
40
- }
41
- }
42
- return { ...DEFAULTS };
43
- }
44
-
45
- function mergeConfig(defaults, overrides) {
46
- const result = { ...defaults };
47
- for (const [key, value] of Object.entries(overrides)) {
48
- if (value && typeof value === 'object' && !Array.isArray(value) && defaults[key] && typeof defaults[key] === 'object') {
49
- result[key] = { ...defaults[key], ...value };
50
- } else {
51
- result[key] = value;
52
- }
53
- }
54
- return result;
55
- }
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { debug, warn } from './logger.js';
5
+
6
+ export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];
7
+
8
+ export const DEFAULTS = {
9
+ include: [],
10
+ exclude: [],
11
+ ignoreDirs: [],
12
+ extensions: [],
13
+ aliases: {},
14
+ build: {
15
+ incremental: true,
16
+ dbPath: '.codegraph/graph.db',
17
+ },
18
+ query: {
19
+ defaultDepth: 3,
20
+ defaultLimit: 20,
21
+ },
22
+ embeddings: { model: 'minilm', llmProvider: null },
23
+ llm: { provider: null, model: null, baseUrl: null, apiKey: null, apiKeyCommand: null },
24
+ search: { defaultMinScore: 0.2, rrfK: 60, topK: 15 },
25
+ ci: { failOnCycles: false, impactThreshold: null },
26
+ };
27
+
28
+ /**
29
+ * Load project configuration from a .codegraphrc.json or similar file.
30
+ * Returns merged config with defaults.
31
+ */
32
+ export function loadConfig(cwd) {
33
+ cwd = cwd || process.cwd();
34
+ for (const name of CONFIG_FILES) {
35
+ const filePath = path.join(cwd, name);
36
+ if (fs.existsSync(filePath)) {
37
+ try {
38
+ const raw = fs.readFileSync(filePath, 'utf-8');
39
+ const config = JSON.parse(raw);
40
+ debug(`Loaded config from ${filePath}`);
41
+ return resolveSecrets(applyEnvOverrides(mergeConfig(DEFAULTS, config)));
42
+ } catch (err) {
43
+ debug(`Failed to parse config ${filePath}: ${err.message}`);
44
+ }
45
+ }
46
+ }
47
+ return resolveSecrets(applyEnvOverrides({ ...DEFAULTS }));
48
+ }
49
+
50
+ const ENV_LLM_MAP = {
51
+ CODEGRAPH_LLM_PROVIDER: 'provider',
52
+ CODEGRAPH_LLM_API_KEY: 'apiKey',
53
+ CODEGRAPH_LLM_MODEL: 'model',
54
+ };
55
+
56
+ export function applyEnvOverrides(config) {
57
+ for (const [envKey, field] of Object.entries(ENV_LLM_MAP)) {
58
+ if (process.env[envKey] !== undefined) {
59
+ config.llm[field] = process.env[envKey];
60
+ }
61
+ }
62
+ return config;
63
+ }
64
+
65
+ export function resolveSecrets(config) {
66
+ const cmd = config.llm.apiKeyCommand;
67
+ if (typeof cmd !== 'string' || cmd.trim() === '') return config;
68
+
69
+ const parts = cmd.trim().split(/\s+/);
70
+ const [executable, ...args] = parts;
71
+ try {
72
+ const result = execFileSync(executable, args, {
73
+ encoding: 'utf-8',
74
+ timeout: 10_000,
75
+ maxBuffer: 64 * 1024,
76
+ stdio: ['ignore', 'pipe', 'pipe'],
77
+ }).trim();
78
+ if (result) {
79
+ config.llm.apiKey = result;
80
+ }
81
+ } catch (err) {
82
+ warn(`apiKeyCommand failed: ${err.message}`);
83
+ }
84
+ return config;
85
+ }
86
+
87
+ function mergeConfig(defaults, overrides) {
88
+ const result = { ...defaults };
89
+ for (const [key, value] of Object.entries(overrides)) {
90
+ if (
91
+ value &&
92
+ typeof value === 'object' &&
93
+ !Array.isArray(value) &&
94
+ defaults[key] &&
95
+ typeof defaults[key] === 'object'
96
+ ) {
97
+ result[key] = { ...defaults[key], ...value };
98
+ } else {
99
+ result[key] = value;
100
+ }
101
+ }
102
+ return result;
103
+ }
package/src/constants.js CHANGED
@@ -1,28 +1,41 @@
1
- import path from 'path';
2
-
3
- export const IGNORE_DIRS = new Set([
4
- 'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.svelte-kit',
5
- 'coverage', '.codegraph', '__pycache__', '.tox', 'vendor', '.venv', 'venv',
6
- 'env', '.env'
7
- ]);
8
-
9
- export const EXTENSIONS = new Set([
10
- '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
11
- '.tf', '.hcl', '.py'
12
- ]);
13
-
14
- export function shouldIgnore(dirName) {
15
- return IGNORE_DIRS.has(dirName) || dirName.startsWith('.');
16
- }
17
-
18
- export function isSupportedFile(filePath) {
19
- return EXTENSIONS.has(path.extname(filePath));
20
- }
21
-
22
- /**
23
- * Normalize a file path to always use forward slashes.
24
- * Ensures cross-platform consistency in the SQLite database.
25
- */
26
- export function normalizePath(filePath) {
27
- return filePath.split(path.sep).join('/');
28
- }
1
+ import path from 'node:path';
2
+ import { SUPPORTED_EXTENSIONS } from './parser.js';
3
+
4
+ export const IGNORE_DIRS = new Set([
5
+ 'node_modules',
6
+ '.git',
7
+ 'dist',
8
+ 'build',
9
+ '.next',
10
+ '.nuxt',
11
+ '.svelte-kit',
12
+ 'coverage',
13
+ '.codegraph',
14
+ '__pycache__',
15
+ '.tox',
16
+ 'vendor',
17
+ '.venv',
18
+ 'venv',
19
+ 'env',
20
+ '.env',
21
+ ]);
22
+
23
+ // Re-export as an indirect binding to avoid TDZ in the circular
24
+ // parser.js constants.js import (no value read at evaluation time).
25
+ export { SUPPORTED_EXTENSIONS as EXTENSIONS };
26
+
27
+ export function shouldIgnore(dirName) {
28
+ return IGNORE_DIRS.has(dirName) || dirName.startsWith('.');
29
+ }
30
+
31
+ export function isSupportedFile(filePath) {
32
+ return SUPPORTED_EXTENSIONS.has(path.extname(filePath));
33
+ }
34
+
35
+ /**
36
+ * Normalize a file path to always use forward slashes.
37
+ * Ensures cross-platform consistency in the SQLite database.
38
+ */
39
+ export function normalizePath(filePath) {
40
+ return filePath.split(path.sep).join('/');
41
+ }
package/src/cycles.js CHANGED
@@ -1,104 +1,125 @@
1
- /**
2
- * Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
3
- * @param {object} db - Open SQLite database
4
- * @param {object} opts - { fileLevel: true }
5
- * @returns {string[][]} Array of cycles, each cycle is an array of file paths
6
- */
7
- export function findCycles(db, opts = {}) {
8
- const fileLevel = opts.fileLevel !== false;
9
-
10
- // Build adjacency list
11
- let edges;
12
- if (fileLevel) {
13
- edges = db.prepare(`
14
- SELECT DISTINCT n1.file AS source, n2.file AS target
15
- FROM edges e
16
- JOIN nodes n1 ON e.source_id = n1.id
17
- JOIN nodes n2 ON e.target_id = n2.id
18
- WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type')
19
- `).all();
20
- } else {
21
- edges = db.prepare(`
22
- SELECT DISTINCT
23
- (n1.name || '|' || n1.file) AS source,
24
- (n2.name || '|' || n2.file) AS target
25
- FROM edges e
26
- JOIN nodes n1 ON e.source_id = n1.id
27
- JOIN nodes n2 ON e.target_id = n2.id
28
- WHERE n1.kind IN ('function', 'method', 'class')
29
- AND n2.kind IN ('function', 'method', 'class')
30
- AND e.kind = 'calls'
31
- AND n1.id != n2.id
32
- `).all();
33
- }
34
-
35
- const graph = new Map();
36
- for (const { source, target } of edges) {
37
- if (!graph.has(source)) graph.set(source, []);
38
- graph.get(source).push(target);
39
- if (!graph.has(target)) graph.set(target, []);
40
- }
41
-
42
- // Tarjan's strongly connected components algorithm
43
- let index = 0;
44
- const stack = [];
45
- const onStack = new Set();
46
- const indices = new Map();
47
- const lowlinks = new Map();
48
- const sccs = [];
49
-
50
- function strongconnect(v) {
51
- indices.set(v, index);
52
- lowlinks.set(v, index);
53
- index++;
54
- stack.push(v);
55
- onStack.add(v);
56
-
57
- for (const w of (graph.get(v) || [])) {
58
- if (!indices.has(w)) {
59
- strongconnect(w);
60
- lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
61
- } else if (onStack.has(w)) {
62
- lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
63
- }
64
- }
65
-
66
- if (lowlinks.get(v) === indices.get(v)) {
67
- const scc = [];
68
- let w;
69
- do {
70
- w = stack.pop();
71
- onStack.delete(w);
72
- scc.push(w);
73
- } while (w !== v);
74
- if (scc.length > 1) sccs.push(scc);
75
- }
76
- }
77
-
78
- for (const node of graph.keys()) {
79
- if (!indices.has(node)) strongconnect(node);
80
- }
81
-
82
- return sccs;
83
- }
84
-
85
- /**
86
- * Format cycles for human-readable output.
87
- */
88
- export function formatCycles(cycles) {
89
- if (cycles.length === 0) {
90
- return 'No circular dependencies detected.';
91
- }
92
-
93
- const lines = [`Found ${cycles.length} circular dependency cycle(s):\n`];
94
- for (let i = 0; i < cycles.length; i++) {
95
- const cycle = cycles[i];
96
- lines.push(` Cycle ${i + 1} (${cycle.length} files):`);
97
- for (const file of cycle) {
98
- lines.push(` -> ${file}`);
99
- }
100
- lines.push(` -> ${cycle[0]} (back to start)`);
101
- lines.push('');
102
- }
103
- return lines.join('\n');
104
- }
1
+ import { loadNative } from './native.js';
2
+
3
+ /**
4
+ * Detect circular dependencies in the codebase using Tarjan's SCC algorithm.
5
+ * Dispatches to native Rust implementation when available, falls back to JS.
6
+ * @param {object} db - Open SQLite database
7
+ * @param {object} opts - { fileLevel: true }
8
+ * @returns {string[][]} Array of cycles, each cycle is an array of file paths
9
+ */
10
+ export function findCycles(db, opts = {}) {
11
+ const fileLevel = opts.fileLevel !== false;
12
+
13
+ // Build adjacency list from SQLite (stays in JS — only the algorithm can move to Rust)
14
+ let edges;
15
+ if (fileLevel) {
16
+ edges = db
17
+ .prepare(`
18
+ SELECT DISTINCT n1.file AS source, n2.file AS target
19
+ FROM edges e
20
+ JOIN nodes n1 ON e.source_id = n1.id
21
+ JOIN nodes n2 ON e.target_id = n2.id
22
+ WHERE n1.file != n2.file AND e.kind IN ('imports', 'imports-type')
23
+ `)
24
+ .all();
25
+ } else {
26
+ edges = db
27
+ .prepare(`
28
+ SELECT DISTINCT
29
+ (n1.name || '|' || n1.file) AS source,
30
+ (n2.name || '|' || n2.file) AS target
31
+ FROM edges e
32
+ JOIN nodes n1 ON e.source_id = n1.id
33
+ JOIN nodes n2 ON e.target_id = n2.id
34
+ WHERE n1.kind IN ('function', 'method', 'class')
35
+ AND n2.kind IN ('function', 'method', 'class')
36
+ AND e.kind = 'calls'
37
+ AND n1.id != n2.id
38
+ `)
39
+ .all();
40
+ }
41
+
42
+ // Try native Rust implementation
43
+ const native = loadNative();
44
+ if (native) {
45
+ return native.detectCycles(edges);
46
+ }
47
+
48
+ // Fallback: JS Tarjan
49
+ return findCyclesJS(edges);
50
+ }
51
+
52
+ /**
53
+ * Pure-JS Tarjan's SCC implementation.
54
+ */
55
+ export function findCyclesJS(edges) {
56
+ const graph = new Map();
57
+ for (const { source, target } of edges) {
58
+ if (!graph.has(source)) graph.set(source, []);
59
+ graph.get(source).push(target);
60
+ if (!graph.has(target)) graph.set(target, []);
61
+ }
62
+
63
+ // Tarjan's strongly connected components algorithm
64
+ let index = 0;
65
+ const stack = [];
66
+ const onStack = new Set();
67
+ const indices = new Map();
68
+ const lowlinks = new Map();
69
+ const sccs = [];
70
+
71
+ function strongconnect(v) {
72
+ indices.set(v, index);
73
+ lowlinks.set(v, index);
74
+ index++;
75
+ stack.push(v);
76
+ onStack.add(v);
77
+
78
+ for (const w of graph.get(v) || []) {
79
+ if (!indices.has(w)) {
80
+ strongconnect(w);
81
+ lowlinks.set(v, Math.min(lowlinks.get(v), lowlinks.get(w)));
82
+ } else if (onStack.has(w)) {
83
+ lowlinks.set(v, Math.min(lowlinks.get(v), indices.get(w)));
84
+ }
85
+ }
86
+
87
+ if (lowlinks.get(v) === indices.get(v)) {
88
+ const scc = [];
89
+ let w;
90
+ do {
91
+ w = stack.pop();
92
+ onStack.delete(w);
93
+ scc.push(w);
94
+ } while (w !== v);
95
+ if (scc.length > 1) sccs.push(scc);
96
+ }
97
+ }
98
+
99
+ for (const node of graph.keys()) {
100
+ if (!indices.has(node)) strongconnect(node);
101
+ }
102
+
103
+ return sccs;
104
+ }
105
+
106
+ /**
107
+ * Format cycles for human-readable output.
108
+ */
109
+ export function formatCycles(cycles) {
110
+ if (cycles.length === 0) {
111
+ return 'No circular dependencies detected.';
112
+ }
113
+
114
+ const lines = [`Found ${cycles.length} circular dependency cycle(s):\n`];
115
+ for (let i = 0; i < cycles.length; i++) {
116
+ const cycle = cycles[i];
117
+ lines.push(` Cycle ${i + 1} (${cycle.length} files):`);
118
+ for (const file of cycle) {
119
+ lines.push(` -> ${file}`);
120
+ }
121
+ lines.push(` -> ${cycle[0]} (back to start)`);
122
+ lines.push('');
123
+ }
124
+ return lines.join('\n');
125
+ }