code-graph-llm 1.2.0 → 1.3.0
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 +2 -0
- package/index.js +58 -80
- package/package.json +1 -1
- package/test/index.test.js +31 -28
package/README.md
CHANGED
|
@@ -5,6 +5,8 @@ A language-agnostic, ultra-compact codebase mapper designed specifically for LLM
|
|
|
5
5
|
## Features
|
|
6
6
|
- **Smart Context Extraction:** Captures JSDoc, Python docstrings, and preceding comments for files and symbols.
|
|
7
7
|
- **Signature Fallback:** Automatically extracts function signatures (parameters/types) if documentation is missing.
|
|
8
|
+
- **Recursive .gitignore Support:** Deeply respects both root and nested `.gitignore` files across the entire project structure.
|
|
9
|
+
- **Smart Flutter/Dart Support:** Optimized to reduce noise by filtering out common widget instantiations while capturing real functional declarations.
|
|
8
10
|
- **Compact & Dense:** Optimized for LLM token efficiency, replacing expensive recursive file scans.
|
|
9
11
|
- **Language-Agnostic:** Optimized regex support for JS/TS, Python, Go, Rust, Java, C#, C/C++, Swift, PHP, Ruby, Dart, and more.
|
|
10
12
|
- **Recursive Ignore Logic:** Deeply respects `.gitignore` and standard excludes (`node_modules`, `.git`).
|
package/index.js
CHANGED
|
@@ -13,42 +13,38 @@ const IGNORE_FILE = '.gitignore';
|
|
|
13
13
|
const DEFAULT_MAP_FILE = 'llm-code-graph.md';
|
|
14
14
|
|
|
15
15
|
const SYMBOL_REGEXES = [
|
|
16
|
-
// Types, Classes, Interfaces
|
|
16
|
+
// Types, Classes, Interfaces (Universal)
|
|
17
17
|
/\b(?:class|interface|type|struct|enum|protocol|extension|trait|module|namespace|object)\s+([a-zA-Z_]\w*)/g,
|
|
18
18
|
|
|
19
|
-
// Explicit Function Keywords
|
|
19
|
+
// Explicit Function Keywords
|
|
20
20
|
/\b(?:function|def|fn|func|fun|method|procedure|sub|routine)\s+([a-zA-Z_]\w*)/g,
|
|
21
21
|
|
|
22
|
-
// C-style
|
|
23
|
-
//
|
|
24
|
-
/\b(?:void|async|public|private|protected|static|virtual|override|readonly|int|float|double|char|bool|string|val|var|let|
|
|
22
|
+
// Method/Var Declarations (C-style, Java, C#, TS, Dart)
|
|
23
|
+
// Refined to require a variable/function name followed by a declaration signal
|
|
24
|
+
/\b(?:void|async|public|private|protected|static|virtual|override|readonly|int|float|double|char|bool|string|val|var|let|final)\s+([a-zA-Z_]\w*)(?=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*(?:\{|=>|;|=))/g,
|
|
25
25
|
|
|
26
|
-
// Exported symbols
|
|
27
|
-
/\bexport\s+(?:default\s+)?(?:const|let|var|function|class|type|interface|enum|async|val)\s+([a-zA-Z_]\w*)/g
|
|
28
|
-
|
|
29
|
-
// Ruby: def name, class Name, module Name (defs covered by Explicit Function Keywords)
|
|
30
|
-
|
|
31
|
-
// PHP: class Name, interface Name, trait Name, function Name
|
|
32
|
-
|
|
33
|
-
// Swift: func name, class Name, struct Name, protocol Name, extension Name
|
|
34
|
-
|
|
35
|
-
// Dart: class Name, void name, var name (void/var covered by C-style pattern)
|
|
26
|
+
// Exported symbols
|
|
27
|
+
/\bexport\s+(?:default\s+)?(?:const|let|var|function|class|type|interface|enum|async|val)\s+([a-zA-Z_]\w*)/g
|
|
36
28
|
];
|
|
37
29
|
|
|
38
30
|
export const SUPPORTED_EXTENSIONS = [
|
|
39
|
-
'.js', '.ts', '.jsx', '.tsx',
|
|
40
|
-
'.
|
|
41
|
-
'.
|
|
42
|
-
'.rb', '.php', '.swift', '.kt',
|
|
43
|
-
'.cs', '.dart', '.scala', '.m', '.mm'
|
|
31
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java',
|
|
32
|
+
'.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.php', '.swift',
|
|
33
|
+
'.kt', '.cs', '.dart', '.scala', '.m', '.mm'
|
|
44
34
|
];
|
|
45
35
|
|
|
46
|
-
export function getIgnores(cwd) {
|
|
47
|
-
const ig = ignore().add([
|
|
36
|
+
export function getIgnores(cwd, additionalLines = []) {
|
|
37
|
+
const ig = ignore().add([
|
|
38
|
+
'.git/', 'node_modules/', DEFAULT_MAP_FILE, 'package-lock.json',
|
|
39
|
+
'.idea/', 'build/', 'dist/', 'bin/', 'obj/', '.dart_tool/', '.pub-cache/', '.pub/'
|
|
40
|
+
]);
|
|
48
41
|
const ignorePath = path.join(cwd, IGNORE_FILE);
|
|
49
42
|
if (fs.existsSync(ignorePath)) {
|
|
50
43
|
ig.add(fs.readFileSync(ignorePath, 'utf8'));
|
|
51
44
|
}
|
|
45
|
+
if (additionalLines.length > 0) {
|
|
46
|
+
ig.add(additionalLines);
|
|
47
|
+
}
|
|
52
48
|
return ig;
|
|
53
49
|
}
|
|
54
50
|
|
|
@@ -60,9 +56,8 @@ export function extractSymbols(content) {
|
|
|
60
56
|
while ((match = regex.exec(content)) !== null) {
|
|
61
57
|
if (match[1]) {
|
|
62
58
|
const symbolName = match[1];
|
|
63
|
-
if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield'].includes(symbolName)) continue;
|
|
59
|
+
if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield', 'const', 'new'].includes(symbolName)) continue;
|
|
64
60
|
|
|
65
|
-
// 1. Extract preceding comment/docstring
|
|
66
61
|
const linesBefore = content.substring(0, match.index).split('\n');
|
|
67
62
|
let comment = '';
|
|
68
63
|
for (let i = linesBefore.length - 1; i >= 0; i--) {
|
|
@@ -71,18 +66,13 @@ export function extractSymbols(content) {
|
|
|
71
66
|
const clean = line.replace(/[\/*#"]/g, '').trim();
|
|
72
67
|
if (clean) comment = clean + (comment ? ' ' + comment : '');
|
|
73
68
|
if (comment.length > 80) break;
|
|
74
|
-
} else if (line === '' && comment === '')
|
|
75
|
-
|
|
76
|
-
} else {
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
69
|
+
} else if (line === '' && comment === '') continue;
|
|
70
|
+
else break;
|
|
79
71
|
}
|
|
80
72
|
|
|
81
|
-
// 2. Backup: Extract Signature (Parameters/Type) if no comment
|
|
82
73
|
let context = comment;
|
|
83
74
|
if (!context) {
|
|
84
75
|
const remainingLine = content.substring(match.index + match[0].length);
|
|
85
|
-
// Match until the first opening brace or colon or end of line, but include balanced parentheses
|
|
86
76
|
const sigMatch = remainingLine.match(/^\s*(\([^)]*\)|[^\n{;]*)/);
|
|
87
77
|
if (sigMatch && sigMatch[1].trim()) {
|
|
88
78
|
context = sigMatch[1].trim();
|
|
@@ -96,29 +86,41 @@ export function extractSymbols(content) {
|
|
|
96
86
|
return Array.from(new Set(symbols)).sort();
|
|
97
87
|
}
|
|
98
88
|
|
|
99
|
-
|
|
100
|
-
const ig = getIgnores(cwd);
|
|
89
|
+
async function generate(cwd = process.cwd()) {
|
|
101
90
|
const files = [];
|
|
102
91
|
|
|
103
|
-
function walk(dir) {
|
|
92
|
+
function walk(dir, ig) {
|
|
93
|
+
// Check for local .gitignore and create a new scoped ignore object if found
|
|
94
|
+
let localIg = ig;
|
|
95
|
+
const localIgnorePath = path.join(dir, IGNORE_FILE);
|
|
96
|
+
if (fs.existsSync(localIgnorePath) && dir !== cwd) {
|
|
97
|
+
const content = fs.readFileSync(localIgnorePath, 'utf8');
|
|
98
|
+
const lines = content.split('\n').map(line => {
|
|
99
|
+
line = line.trim();
|
|
100
|
+
if (!line || line.startsWith('#')) return null;
|
|
101
|
+
// Rules in sub-gitignore are relative to that directory
|
|
102
|
+
const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
|
|
103
|
+
return relDir ? `${relDir}/${line}` : line;
|
|
104
|
+
}).filter(Boolean);
|
|
105
|
+
localIg = ignore().add(ig).add(lines);
|
|
106
|
+
}
|
|
107
|
+
|
|
104
108
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
109
|
for (const entry of entries) {
|
|
106
110
|
const fullPath = path.join(dir, entry.name);
|
|
107
|
-
|
|
111
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
108
112
|
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
109
113
|
const isDirectory = entry.isDirectory();
|
|
110
114
|
const checkPath = isDirectory ? `${normalizedPath}/` : normalizedPath;
|
|
111
115
|
|
|
112
|
-
if (
|
|
116
|
+
if (localIg.ignores(checkPath)) continue;
|
|
113
117
|
|
|
114
118
|
if (isDirectory) {
|
|
115
|
-
walk(fullPath);
|
|
119
|
+
walk(fullPath, localIg);
|
|
116
120
|
} else if (entry.isFile()) {
|
|
117
121
|
const ext = path.extname(entry.name);
|
|
118
122
|
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
119
123
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
120
|
-
|
|
121
|
-
// Extract file-level description
|
|
122
124
|
const firstLines = content.split('\n').slice(0, 5);
|
|
123
125
|
let fileDesc = '';
|
|
124
126
|
for (const line of firstLines) {
|
|
@@ -127,23 +129,15 @@ export async function generate(cwd = process.cwd()) {
|
|
|
127
129
|
fileDesc += trimmed.replace(/[\/*#]/g, '').trim() + ' ';
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
|
-
|
|
131
132
|
const symbols = extractSymbols(content);
|
|
132
|
-
|
|
133
|
-
// Backup: If no file description, provide a summary
|
|
134
|
-
if (!fileDesc.trim() && symbols.length > 0) {
|
|
135
|
-
fileDesc = `Contains ${symbols.length} symbols.`;
|
|
136
|
-
}
|
|
137
|
-
|
|
133
|
+
if (!fileDesc.trim() && symbols.length > 0) fileDesc = `Contains ${symbols.length} symbols.`;
|
|
138
134
|
files.push({ path: normalizedPath, desc: fileDesc.trim(), symbols });
|
|
139
|
-
} else {
|
|
140
|
-
files.push({ path: normalizedPath, desc: '', symbols: [] });
|
|
141
135
|
}
|
|
142
136
|
}
|
|
143
137
|
}
|
|
144
138
|
}
|
|
145
139
|
|
|
146
|
-
walk(cwd);
|
|
140
|
+
walk(cwd, getIgnores(cwd));
|
|
147
141
|
|
|
148
142
|
const output = files.map(f => {
|
|
149
143
|
const descStr = f.desc ? ` | desc: ${f.desc.substring(0, 100)}` : '';
|
|
@@ -156,36 +150,28 @@ export async function generate(cwd = process.cwd()) {
|
|
|
156
150
|
console.log(`[Code-Graph] Updated ${DEFAULT_MAP_FILE}`);
|
|
157
151
|
}
|
|
158
152
|
|
|
153
|
+
export { generate };
|
|
154
|
+
|
|
159
155
|
export function watch(cwd = process.cwd()) {
|
|
160
156
|
console.log(`[Code-Graph] Watching for changes in ${cwd}...`);
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
let timeout;
|
|
158
|
+
const debouncedGenerate = () => {
|
|
159
|
+
clearTimeout(timeout);
|
|
160
|
+
timeout = setTimeout(() => generate(cwd), 500);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
163
|
const watcher = chokidar.watch(cwd, {
|
|
164
164
|
ignored: (p) => {
|
|
165
165
|
if (p === cwd) return false;
|
|
166
|
-
|
|
167
|
-
// We
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const stats = fs.statSync(p);
|
|
171
|
-
const checkPath = stats.isDirectory() ? `${rel}/` : rel;
|
|
172
|
-
return ig.ignores(checkPath);
|
|
173
|
-
} catch (e) {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
166
|
+
// Watcher ignore is harder for recursive .gitignore without complexity
|
|
167
|
+
// We rely on the generate() call to skip them during walk
|
|
168
|
+
return false;
|
|
176
169
|
},
|
|
177
170
|
persistent: true,
|
|
178
171
|
ignoreInitial: true
|
|
179
172
|
});
|
|
180
173
|
|
|
181
|
-
let timeout;
|
|
182
|
-
const debouncedGenerate = () => {
|
|
183
|
-
clearTimeout(timeout);
|
|
184
|
-
timeout = setTimeout(() => generate(cwd), 500);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
174
|
watcher.on('all', (event, path) => {
|
|
188
|
-
console.log(`[Code-Graph] Change detected: ${event} on ${path}`);
|
|
189
175
|
debouncedGenerate();
|
|
190
176
|
});
|
|
191
177
|
}
|
|
@@ -196,10 +182,8 @@ export function installHook(cwd = process.cwd()) {
|
|
|
196
182
|
console.error('[Code-Graph] No .git directory found. Cannot install hook.');
|
|
197
183
|
return;
|
|
198
184
|
}
|
|
199
|
-
|
|
200
185
|
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
201
186
|
const hookContent = `#!/bin/sh\n# Code-Graph pre-commit hook\nnode "${__filename}" generate\ngit add "${DEFAULT_MAP_FILE}"\n`;
|
|
202
|
-
|
|
203
187
|
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
204
188
|
console.log('[Code-Graph] Installed pre-commit hook.');
|
|
205
189
|
}
|
|
@@ -207,14 +191,8 @@ export function installHook(cwd = process.cwd()) {
|
|
|
207
191
|
if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith('index.js'))) {
|
|
208
192
|
const args = process.argv.slice(2);
|
|
209
193
|
const command = args[0] || 'generate';
|
|
210
|
-
|
|
211
|
-
if (command === '
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
watch();
|
|
215
|
-
} else if (command === 'install-hook') {
|
|
216
|
-
installHook();
|
|
217
|
-
} else {
|
|
218
|
-
console.log('Usage: code-graph [generate|watch|install-hook]');
|
|
219
|
-
}
|
|
194
|
+
if (command === 'generate') generate();
|
|
195
|
+
else if (command === 'watch') watch();
|
|
196
|
+
else if (command === 'install-hook') installHook();
|
|
197
|
+
else console.log('Usage: code-graph [generate|watch|install-hook]');
|
|
220
198
|
}
|
package/package.json
CHANGED
package/test/index.test.js
CHANGED
|
@@ -3,16 +3,11 @@ import test from 'node:test';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
|
|
7
|
-
// Import the core functions by reading the file and eval-ing or refactoring.
|
|
8
|
-
// For simplicity in this environment, I will redefine the core logic in the test
|
|
9
|
-
// or point to the index.js if it was exported.
|
|
10
|
-
// Since index.js is a CLI, I'll extract the logic into a testable state.
|
|
11
|
-
|
|
12
6
|
import {
|
|
13
7
|
extractSymbols,
|
|
14
8
|
getIgnores,
|
|
15
|
-
SUPPORTED_EXTENSIONS
|
|
9
|
+
SUPPORTED_EXTENSIONS,
|
|
10
|
+
generate
|
|
16
11
|
} from '../index.js';
|
|
17
12
|
|
|
18
13
|
test('extractSymbols - JS/TS Docstrings', () => {
|
|
@@ -33,27 +28,18 @@ test('extractSymbols - Signature Fallback', () => {
|
|
|
33
28
|
}
|
|
34
29
|
`;
|
|
35
30
|
const symbols = extractSymbols(code);
|
|
31
|
+
// Matches "noDocFunc [(arg1: string, arg2: number)]"
|
|
36
32
|
assert.ok(symbols.some(s => s.includes('noDocFunc') && s.includes('arg1: string, arg2: number')));
|
|
37
33
|
});
|
|
38
34
|
|
|
39
|
-
test('extractSymbols -
|
|
35
|
+
test('extractSymbols - Flutter/Dart Noise Reduction', () => {
|
|
40
36
|
const code = `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Python docstring test
|
|
44
|
-
"""
|
|
45
|
-
pass
|
|
37
|
+
const SizedBox(height: 10);
|
|
38
|
+
void realFunction() {}
|
|
46
39
|
`;
|
|
47
40
|
const symbols = extractSymbols(code);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const codeWithComment = `
|
|
51
|
-
# This is a python comment
|
|
52
|
-
def py_func_2(x):
|
|
53
|
-
pass
|
|
54
|
-
`;
|
|
55
|
-
const symbols2 = extractSymbols(codeWithComment);
|
|
56
|
-
assert.ok(symbols2.some(s => s.includes('py_func_2') && s.includes('This is a python comment')));
|
|
41
|
+
assert.ok(symbols.some(s => s.includes('realFunction')));
|
|
42
|
+
assert.ok(!symbols.some(s => s.includes('SizedBox')));
|
|
57
43
|
});
|
|
58
44
|
|
|
59
45
|
test('getIgnores - Default Patterns', () => {
|
|
@@ -61,12 +47,29 @@ test('getIgnores - Default Patterns', () => {
|
|
|
61
47
|
assert.strictEqual(ig.ignores('.git/'), true);
|
|
62
48
|
assert.strictEqual(ig.ignores('node_modules/'), true);
|
|
63
49
|
assert.strictEqual(ig.ignores('.idea/'), true);
|
|
64
|
-
assert.strictEqual(ig.ignores('
|
|
50
|
+
assert.strictEqual(ig.ignores('.dart_tool/'), true);
|
|
65
51
|
});
|
|
66
52
|
|
|
67
|
-
test('
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
53
|
+
test('Recursive Ignore Simulation (Logic Check)', async () => {
|
|
54
|
+
const tempDir = path.join(process.cwd(), 'temp_test_dir');
|
|
55
|
+
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
56
|
+
fs.mkdirSync(tempDir);
|
|
57
|
+
|
|
58
|
+
const subDir = path.join(tempDir, 'subdir');
|
|
59
|
+
fs.mkdirSync(subDir);
|
|
60
|
+
|
|
61
|
+
// Create a file that should be ignored by subdir/.gitignore
|
|
62
|
+
fs.writeFileSync(path.join(subDir, 'ignored.js'), 'function ignored() {}');
|
|
63
|
+
fs.writeFileSync(path.join(subDir, 'included.js'), 'function included() {}');
|
|
64
|
+
fs.writeFileSync(path.join(subDir, '.gitignore'), 'ignored.js');
|
|
65
|
+
|
|
66
|
+
await generate(tempDir);
|
|
67
|
+
|
|
68
|
+
const mapPath = path.join(tempDir, 'llm-code-graph.md');
|
|
69
|
+
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
70
|
+
|
|
71
|
+
assert.ok(mapContent.includes('included.js'));
|
|
72
|
+
assert.ok(!mapContent.includes('ignored.js'));
|
|
73
|
+
|
|
74
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
72
75
|
});
|