code-graph-llm 1.2.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/README.md +20 -18
- package/index.js +139 -93
- package/llm-code-graph.md +26 -7
- package/package.json +1 -1
- package/test/index.test.js +59 -31
package/README.md
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
A language-agnostic, ultra-compact codebase mapper designed specifically for LLM agents to optimize context and token usage. It doesn't just list files; it provides a high-signal "map" of your project's architecture, including descriptions and signatures.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
|
-
- **
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
6
|
+
- **Structural Knowledge Graph:** Captures relationships between files and classes:
|
|
7
|
+
- **Dependencies:** Tracks `imports`, `requires`, and `includes` across files.
|
|
8
|
+
- **Inheritance:** Maps `extends`, `implements`, and class hierarchies.
|
|
9
|
+
- **Smart Context Extraction:** Captures JSDoc, Python docstrings, and preceding comments.
|
|
10
|
+
- **Signature Fallback:** Extracts function signatures (parameters/types) if documentation is missing.
|
|
11
|
+
- **Recursive .gitignore Support:** Deeply respects both root and nested `.gitignore` files.
|
|
12
|
+
- **Compact & Dense:** Optimized for LLM token efficiency with a dedicated `## GRAPH EDGES` section.
|
|
13
|
+
- **Language-Agnostic:** Support for JS/TS, Python, Go, Rust, Java, C#, C/C++, Swift, PHP, Ruby, Dart, and more.
|
|
12
14
|
|
|
13
15
|
## Installation
|
|
14
16
|
|
|
@@ -36,12 +38,16 @@ code-graph install-hook
|
|
|
36
38
|
## LLM Usage & Token Efficiency
|
|
37
39
|
|
|
38
40
|
### The "Read First" Strategy
|
|
39
|
-
Instruct your LLM agent to read `llm-code-graph.md` as its first step. The file
|
|
41
|
+
Instruct your LLM agent to read `llm-code-graph.md` as its first step. The file provides a high-level map and a structural graph for relational reasoning:
|
|
40
42
|
|
|
41
43
|
**Example Map Entry:**
|
|
42
44
|
```markdown
|
|
43
|
-
- src/auth.js | desc: Handles user authentication
|
|
45
|
+
- src/auth.js | desc: Handles user authentication.
|
|
44
46
|
- syms: [login [ (username, password) ], validateToken [ (token: string) ]]
|
|
47
|
+
|
|
48
|
+
## GRAPH EDGES
|
|
49
|
+
[src/auth.js] -> [imports] -> [jwt-library]
|
|
50
|
+
[AdminUser] -> [inherits] -> [BaseUser]
|
|
45
51
|
```
|
|
46
52
|
|
|
47
53
|
**Example System Prompt:**
|
|
@@ -76,13 +82,9 @@ fn main() {
|
|
|
76
82
|
```
|
|
77
83
|
|
|
78
84
|
## How it works
|
|
79
|
-
1. **File Scanning:** Recursively walks the directory, ignoring patterns in `.gitignore
|
|
80
|
-
2. **Context Extraction:** Scans for classes, functions, and variables.
|
|
81
|
-
3. **
|
|
82
|
-
4. **
|
|
83
|
-
5. **
|
|
84
|
-
|
|
85
|
-
## Publishing as a Package
|
|
86
|
-
To share your own version:
|
|
87
|
-
1. `npm login`
|
|
88
|
-
2. `npm publish --access public`
|
|
85
|
+
1. **File Scanning:** Recursively walks the directory, ignoring patterns in `.gitignore` (recursive).
|
|
86
|
+
2. **Context Extraction:** Scans for classes, functions, and variables while ignoring matches in comments.
|
|
87
|
+
3. **Graph Extraction:** Identifies `imports`, `requires`, `extends`, and `implements` to build a structural skeleton.
|
|
88
|
+
4. **Docstring Capture:** Captures preceding comments as descriptions.
|
|
89
|
+
5. **Signature Capture:** Fallback to declaration signatures (parameters) if docs are missing.
|
|
90
|
+
6. **Compilation:** Writes a single, minified `llm-code-graph.md` file with a dedicated `## GRAPH EDGES` section.
|
package/index.js
CHANGED
|
@@ -13,76 +13,93 @@ 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
|
|
17
|
-
|
|
16
|
+
// Types, Classes, Interfaces (Universal) with Inheritance support
|
|
17
|
+
// Captures: class Name extends Parent, interface Name implements Base
|
|
18
|
+
/\b(?:class|interface|type|struct|enum|protocol|extension|trait|module|namespace|object)\s+([a-zA-Z_]\w*)(?:\s+(?:extends|implements|:)\s+([a-zA-Z_]\w*(?:\s*,\s*[a-zA-Z_]\w*)*))?/g,
|
|
18
19
|
|
|
19
|
-
// Explicit Function Keywords
|
|
20
|
+
// Explicit Function Keywords
|
|
20
21
|
/\b(?:function|def|fn|func|fun|method|procedure|sub|routine)\s+([a-zA-Z_]\w*)/g,
|
|
21
22
|
|
|
22
|
-
// C-style
|
|
23
|
-
|
|
24
|
-
/\b(?:void|async|public|private|protected|static|virtual|override|readonly|int|float|double|char|bool|string|val|var|let|const|final)\s+([a-zA-Z_]\w*)(?=\s*\(|(?:\s*:\s*\w+)?\s*=>)/g,
|
|
23
|
+
// Method/Var Declarations (C-style, Java, C#, TS, Dart)
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
//
|
|
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
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const EDGE_REGEXES = [
|
|
31
|
+
// Imports/Includes (JS, TS, Python, Go, Rust, C++, Java, Dart)
|
|
32
|
+
/\b(?:import|from|include|require|using)\s*(?:[\(\s])\s*['"]?([@\w\.\/\-]+)['"]?/g,
|
|
33
|
+
// C-style includes
|
|
34
|
+
/#include\s+[<"]([\w\.\/\-]+)[>"]/g
|
|
36
35
|
];
|
|
37
36
|
|
|
38
37
|
export const SUPPORTED_EXTENSIONS = [
|
|
39
|
-
'.js', '.ts', '.jsx', '.tsx',
|
|
40
|
-
'.
|
|
41
|
-
'.
|
|
42
|
-
'.rb', '.php', '.swift', '.kt',
|
|
43
|
-
'.cs', '.dart', '.scala', '.m', '.mm'
|
|
38
|
+
'.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java',
|
|
39
|
+
'.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.php', '.swift',
|
|
40
|
+
'.kt', '.cs', '.dart', '.scala', '.m', '.mm'
|
|
44
41
|
];
|
|
45
42
|
|
|
46
|
-
export function getIgnores(cwd) {
|
|
47
|
-
const ig = ignore().add([
|
|
43
|
+
export function getIgnores(cwd, additionalLines = []) {
|
|
44
|
+
const ig = ignore().add([
|
|
45
|
+
'.git/', 'node_modules/', DEFAULT_MAP_FILE, 'package-lock.json',
|
|
46
|
+
'.idea/', 'build/', 'dist/', 'bin/', 'obj/', '.dart_tool/', '.pub-cache/', '.pub/'
|
|
47
|
+
]);
|
|
48
48
|
const ignorePath = path.join(cwd, IGNORE_FILE);
|
|
49
49
|
if (fs.existsSync(ignorePath)) {
|
|
50
50
|
ig.add(fs.readFileSync(ignorePath, 'utf8'));
|
|
51
51
|
}
|
|
52
|
+
if (additionalLines.length > 0) {
|
|
53
|
+
ig.add(additionalLines);
|
|
54
|
+
}
|
|
52
55
|
return ig;
|
|
53
56
|
}
|
|
54
57
|
|
|
55
|
-
export function
|
|
58
|
+
export function extractSymbolsAndInheritance(content) {
|
|
56
59
|
const symbols = [];
|
|
60
|
+
const inheritance = [];
|
|
61
|
+
|
|
62
|
+
// Create a version of content without comments to find symbols accurately
|
|
63
|
+
const noComments = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
64
|
+
|
|
57
65
|
for (const regex of SYMBOL_REGEXES) {
|
|
58
66
|
let match;
|
|
59
67
|
regex.lastIndex = 0;
|
|
60
|
-
while ((match = regex.exec(
|
|
68
|
+
while ((match = regex.exec(noComments)) !== null) {
|
|
61
69
|
if (match[1]) {
|
|
62
70
|
const symbolName = match[1];
|
|
63
|
-
if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield'].includes(symbolName)) continue;
|
|
71
|
+
if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield', 'const', 'new', 'let', 'var'].includes(symbolName)) continue;
|
|
72
|
+
|
|
73
|
+
// Capture inheritance if present (match[2])
|
|
74
|
+
if (match[2]) {
|
|
75
|
+
const parents = match[2].split(',').map(p => p.trim());
|
|
76
|
+
parents.forEach(parent => {
|
|
77
|
+
inheritance.push({ child: symbolName, parent });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// To find the comment, we need to find the position in the ORIGINAL content
|
|
82
|
+
const escapedName = symbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
83
|
+
const posRegex = new RegExp(`\\b${escapedName}\\b`, 'g');
|
|
84
|
+
let posMatch = posRegex.exec(content);
|
|
85
|
+
if (!posMatch) continue;
|
|
64
86
|
|
|
65
|
-
|
|
66
|
-
const linesBefore = content.substring(0, match.index).split('\n');
|
|
87
|
+
const linesBefore = content.substring(0, posMatch.index).split('\n');
|
|
67
88
|
let comment = '';
|
|
68
|
-
|
|
89
|
+
// Skip the current line where the symbol is defined
|
|
90
|
+
for (let i = linesBefore.length - 2; i >= 0; i--) {
|
|
69
91
|
const line = linesBefore[i].trim();
|
|
70
|
-
if (line.startsWith('//') || line.startsWith('*') || line.startsWith('"""') || line.startsWith('#')) {
|
|
92
|
+
if (line.startsWith('//') || line.startsWith('*') || line.startsWith('"""') || line.startsWith('#') || line.startsWith('/*')) {
|
|
71
93
|
const clean = line.replace(/[\/*#"]/g, '').trim();
|
|
72
94
|
if (clean) comment = clean + (comment ? ' ' + comment : '');
|
|
73
|
-
if (comment.length >
|
|
74
|
-
} else if (line === '' && comment === '')
|
|
75
|
-
|
|
76
|
-
} else {
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
95
|
+
if (comment.length > 100) break;
|
|
96
|
+
} else if (line === '' && comment === '') continue;
|
|
97
|
+
else break;
|
|
79
98
|
}
|
|
80
99
|
|
|
81
|
-
// 2. Backup: Extract Signature (Parameters/Type) if no comment
|
|
82
100
|
let context = comment;
|
|
83
101
|
if (!context) {
|
|
84
|
-
const remainingLine = content.substring(
|
|
85
|
-
// Match until the first opening brace or colon or end of line, but include balanced parentheses
|
|
102
|
+
const remainingLine = content.substring(posMatch.index + symbolName.length);
|
|
86
103
|
const sigMatch = remainingLine.match(/^\s*(\([^)]*\)|[^\n{;]*)/);
|
|
87
104
|
if (sigMatch && sigMatch[1].trim()) {
|
|
88
105
|
context = sigMatch[1].trim();
|
|
@@ -93,99 +110,136 @@ export function extractSymbols(content) {
|
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
|
-
return
|
|
113
|
+
return {
|
|
114
|
+
symbols: Array.from(new Set(symbols)).sort(),
|
|
115
|
+
inheritance: Array.from(new Set(inheritance.map(JSON.stringify))).map(JSON.parse)
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function extractEdges(content) {
|
|
120
|
+
const dependencies = new Set();
|
|
121
|
+
const noComments = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
|
|
122
|
+
for (const regex of EDGE_REGEXES) {
|
|
123
|
+
let match;
|
|
124
|
+
regex.lastIndex = 0;
|
|
125
|
+
while ((match = regex.exec(noComments)) !== null) {
|
|
126
|
+
if (match[1]) {
|
|
127
|
+
const dep = match[1];
|
|
128
|
+
if (dep.length > 1 && !['style', 'react', 'vue', 'flutter'].includes(dep.toLowerCase())) {
|
|
129
|
+
dependencies.add(dep);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Array.from(dependencies).sort();
|
|
97
135
|
}
|
|
98
136
|
|
|
99
|
-
|
|
100
|
-
const ig = getIgnores(cwd);
|
|
137
|
+
async function generate(cwd = process.cwd()) {
|
|
101
138
|
const files = [];
|
|
139
|
+
const allEdges = [];
|
|
140
|
+
|
|
141
|
+
function walk(dir, ig) {
|
|
142
|
+
let localIg = ig;
|
|
143
|
+
const localIgnorePath = path.join(dir, IGNORE_FILE);
|
|
144
|
+
if (fs.existsSync(localIgnorePath) && dir !== cwd) {
|
|
145
|
+
const content = fs.readFileSync(localIgnorePath, 'utf8');
|
|
146
|
+
const lines = content.split('\n').map(line => {
|
|
147
|
+
line = line.trim();
|
|
148
|
+
if (!line || line.startsWith('#')) return null;
|
|
149
|
+
const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
|
|
150
|
+
return relDir ? `${relDir}/${line}` : line;
|
|
151
|
+
}).filter(Boolean);
|
|
152
|
+
localIg = ignore().add(ig).add(lines);
|
|
153
|
+
}
|
|
102
154
|
|
|
103
|
-
function walk(dir) {
|
|
104
155
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
156
|
for (const entry of entries) {
|
|
106
157
|
const fullPath = path.join(dir, entry.name);
|
|
107
|
-
|
|
158
|
+
const relativePath = path.relative(cwd, fullPath);
|
|
108
159
|
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
109
160
|
const isDirectory = entry.isDirectory();
|
|
110
161
|
const checkPath = isDirectory ? `${normalizedPath}/` : normalizedPath;
|
|
111
162
|
|
|
112
|
-
if (
|
|
163
|
+
if (localIg.ignores(checkPath)) continue;
|
|
113
164
|
|
|
114
165
|
if (isDirectory) {
|
|
115
|
-
walk(fullPath);
|
|
166
|
+
walk(fullPath, localIg);
|
|
116
167
|
} else if (entry.isFile()) {
|
|
117
168
|
const ext = path.extname(entry.name);
|
|
118
169
|
if (SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
119
170
|
const content = fs.readFileSync(fullPath, 'utf8');
|
|
120
171
|
|
|
121
172
|
// Extract file-level description
|
|
122
|
-
const
|
|
173
|
+
const lines = content.split('\n');
|
|
123
174
|
let fileDesc = '';
|
|
124
|
-
for (
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
|
|
175
|
+
for (let i = 0; i < Math.min(10, lines.length); i++) {
|
|
176
|
+
const line = lines[i].trim();
|
|
177
|
+
if (line.startsWith('#!') || line === '') continue;
|
|
178
|
+
if (line.startsWith('//') || line.startsWith('#') || line.startsWith('/*')) {
|
|
179
|
+
fileDesc += line.replace(/[\/*#]/g, '').trim() + ' ';
|
|
180
|
+
} else {
|
|
181
|
+
break;
|
|
128
182
|
}
|
|
129
183
|
}
|
|
130
184
|
|
|
131
|
-
const symbols =
|
|
185
|
+
const { symbols, inheritance } = extractSymbolsAndInheritance(content);
|
|
186
|
+
const dependencies = extractEdges(content);
|
|
187
|
+
|
|
188
|
+
if (!fileDesc.trim() && symbols.length > 0) fileDesc = `Contains ${symbols.length} symbols.`;
|
|
132
189
|
|
|
133
|
-
// Backup: If no file description, provide a summary
|
|
134
|
-
if (!fileDesc.trim() && symbols.length > 0) {
|
|
135
|
-
fileDesc = `Contains ${symbols.length} symbols.`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
190
|
files.push({ path: normalizedPath, desc: fileDesc.trim(), symbols });
|
|
139
|
-
|
|
140
|
-
|
|
191
|
+
|
|
192
|
+
// Collect Edges
|
|
193
|
+
dependencies.forEach(dep => {
|
|
194
|
+
allEdges.push(`[${normalizedPath}] -> [imports] -> [${dep}]`);
|
|
195
|
+
});
|
|
196
|
+
inheritance.forEach(inh => {
|
|
197
|
+
allEdges.push(`[${inh.child}] -> [inherits] -> [${inh.parent}]`);
|
|
198
|
+
});
|
|
141
199
|
}
|
|
142
200
|
}
|
|
143
201
|
}
|
|
144
202
|
}
|
|
145
203
|
|
|
146
|
-
walk(cwd);
|
|
204
|
+
walk(cwd, getIgnores(cwd));
|
|
147
205
|
|
|
148
|
-
const
|
|
206
|
+
const nodesOutput = files.map(f => {
|
|
149
207
|
const descStr = f.desc ? ` | desc: ${f.desc.substring(0, 100)}` : '';
|
|
150
208
|
const symStr = f.symbols.length > 0 ? `\n - syms: [${f.symbols.join(', ')}]` : '';
|
|
151
209
|
return `- ${f.path}${descStr}${symStr}`;
|
|
152
210
|
}).join('\n');
|
|
153
211
|
|
|
212
|
+
const edgesOutput = allEdges.length > 0
|
|
213
|
+
? `\n\n## GRAPH EDGES\n${Array.from(new Set(allEdges)).sort().join('\n')}`
|
|
214
|
+
: '';
|
|
215
|
+
|
|
154
216
|
const header = `# CODE_GRAPH_MAP\n> LLM_ONLY: DO NOT EDIT. COMPACT PROJECT MAP.\n\n`;
|
|
155
|
-
fs.writeFileSync(path.join(cwd, DEFAULT_MAP_FILE), header +
|
|
217
|
+
fs.writeFileSync(path.join(cwd, DEFAULT_MAP_FILE), header + nodesOutput + edgesOutput);
|
|
156
218
|
console.log(`[Code-Graph] Updated ${DEFAULT_MAP_FILE}`);
|
|
157
219
|
}
|
|
158
220
|
|
|
221
|
+
export { generate };
|
|
222
|
+
|
|
159
223
|
export function watch(cwd = process.cwd()) {
|
|
160
224
|
console.log(`[Code-Graph] Watching for changes in ${cwd}...`);
|
|
161
|
-
|
|
162
|
-
|
|
225
|
+
let timeout;
|
|
226
|
+
const debouncedGenerate = () => {
|
|
227
|
+
clearTimeout(timeout);
|
|
228
|
+
timeout = setTimeout(() => generate(cwd), 500);
|
|
229
|
+
};
|
|
230
|
+
|
|
163
231
|
const watcher = chokidar.watch(cwd, {
|
|
164
232
|
ignored: (p) => {
|
|
165
233
|
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
|
-
}
|
|
234
|
+
// Watcher ignore is harder for recursive .gitignore without complexity
|
|
235
|
+
// We rely on the generate() call to skip them during walk
|
|
236
|
+
return false;
|
|
176
237
|
},
|
|
177
238
|
persistent: true,
|
|
178
239
|
ignoreInitial: true
|
|
179
240
|
});
|
|
180
241
|
|
|
181
|
-
let timeout;
|
|
182
|
-
const debouncedGenerate = () => {
|
|
183
|
-
clearTimeout(timeout);
|
|
184
|
-
timeout = setTimeout(() => generate(cwd), 500);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
242
|
watcher.on('all', (event, path) => {
|
|
188
|
-
console.log(`[Code-Graph] Change detected: ${event} on ${path}`);
|
|
189
243
|
debouncedGenerate();
|
|
190
244
|
});
|
|
191
245
|
}
|
|
@@ -196,10 +250,8 @@ export function installHook(cwd = process.cwd()) {
|
|
|
196
250
|
console.error('[Code-Graph] No .git directory found. Cannot install hook.');
|
|
197
251
|
return;
|
|
198
252
|
}
|
|
199
|
-
|
|
200
253
|
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
201
254
|
const hookContent = `#!/bin/sh\n# Code-Graph pre-commit hook\nnode "${__filename}" generate\ngit add "${DEFAULT_MAP_FILE}"\n`;
|
|
202
|
-
|
|
203
255
|
fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
204
256
|
console.log('[Code-Graph] Installed pre-commit hook.');
|
|
205
257
|
}
|
|
@@ -207,14 +259,8 @@ export function installHook(cwd = process.cwd()) {
|
|
|
207
259
|
if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith('index.js'))) {
|
|
208
260
|
const args = process.argv.slice(2);
|
|
209
261
|
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
|
-
}
|
|
262
|
+
if (command === 'generate') generate();
|
|
263
|
+
else if (command === 'watch') watch();
|
|
264
|
+
else if (command === 'install-hook') installHook();
|
|
265
|
+
else console.log('Usage: code-graph [generate|watch|install-hook]');
|
|
220
266
|
}
|
package/llm-code-graph.md
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
# CODE_GRAPH_MAP
|
|
2
2
|
> LLM_ONLY: DO NOT EDIT. COMPACT PROJECT MAP.
|
|
3
3
|
|
|
4
|
-
- .
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
- index.js | desc: Contains 7 symbols.
|
|
5
|
+
- syms: [SUPPORTED_EXTENSIONS [= [], extractSymbolsAndInheritance [(content)], generate [(cwd = process.cwd()], getIgnores [(cwd, additionalLines = [])], installHook [(cwd = process.cwd()], walk [(dir, ig)], watch [(cwd = process.cwd()]]
|
|
6
|
+
- test/index.test.js | desc: Contains 8 symbols.
|
|
7
|
+
- syms: [AdminUser [extends BaseUser], IRepository [implements IBase], MyWidget [: StatelessWidget], ignored [by subdir/.gitignore], included [.js'), 'function included()], noDocFunc [(arg1: string, arg2: number)], realFunction [()], testFunc [This is a test function]]
|
|
8
|
+
|
|
9
|
+
## GRAPH EDGES
|
|
10
|
+
[AdminUser] -> [inherits] -> [BaseUser]
|
|
11
|
+
[IRepository] -> [inherits] -> [IBase]
|
|
12
|
+
[MyWidget] -> [inherits] -> [StatelessWidget]
|
|
13
|
+
[index.js] -> [imports] -> [chokidar]
|
|
14
|
+
[index.js] -> [imports] -> [dependencies]
|
|
15
|
+
[index.js] -> [imports] -> [fs]
|
|
16
|
+
[index.js] -> [imports] -> [ignore]
|
|
17
|
+
[index.js] -> [imports] -> [new]
|
|
18
|
+
[index.js] -> [imports] -> [path]
|
|
19
|
+
[index.js] -> [imports] -> [url]
|
|
20
|
+
[test/index.test.js] -> [imports] -> [../index.js]
|
|
21
|
+
[test/index.test.js] -> [imports] -> [./local-file]
|
|
22
|
+
[test/index.test.js] -> [imports] -> [assert]
|
|
23
|
+
[test/index.test.js] -> [imports] -> [fs]
|
|
24
|
+
[test/index.test.js] -> [imports] -> [header.h]
|
|
25
|
+
[test/index.test.js] -> [imports] -> [node]
|
|
26
|
+
[test/index.test.js] -> [imports] -> [other-module]
|
|
27
|
+
[test/index.test.js] -> [imports] -> [path]
|
|
28
|
+
[test/index.test.js] -> [imports] -> [test]
|
|
29
|
+
[test/index.test.js] -> [imports] -> [url]
|
package/package.json
CHANGED
package/test/index.test.js
CHANGED
|
@@ -3,16 +3,12 @@ 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
|
+
extractSymbolsAndInheritance,
|
|
8
|
+
extractEdges,
|
|
14
9
|
getIgnores,
|
|
15
|
-
SUPPORTED_EXTENSIONS
|
|
10
|
+
SUPPORTED_EXTENSIONS,
|
|
11
|
+
generate
|
|
16
12
|
} from '../index.js';
|
|
17
13
|
|
|
18
14
|
test('extractSymbols - JS/TS Docstrings', () => {
|
|
@@ -22,7 +18,7 @@ test('extractSymbols - JS/TS Docstrings', () => {
|
|
|
22
18
|
*/
|
|
23
19
|
function testFunc(a, b) {}
|
|
24
20
|
`;
|
|
25
|
-
const symbols =
|
|
21
|
+
const { symbols } = extractSymbolsAndInheritance(code);
|
|
26
22
|
assert.ok(symbols.some(s => s.includes('testFunc') && s.includes('This is a test function')));
|
|
27
23
|
});
|
|
28
24
|
|
|
@@ -32,28 +28,43 @@ test('extractSymbols - Signature Fallback', () => {
|
|
|
32
28
|
return true;
|
|
33
29
|
}
|
|
34
30
|
`;
|
|
35
|
-
const symbols =
|
|
31
|
+
const { symbols } = extractSymbolsAndInheritance(code);
|
|
32
|
+
// Matches "noDocFunc [ (arg1: string, arg2: number)]"
|
|
36
33
|
assert.ok(symbols.some(s => s.includes('noDocFunc') && s.includes('arg1: string, arg2: number')));
|
|
37
34
|
});
|
|
38
35
|
|
|
39
|
-
test('extractSymbols -
|
|
36
|
+
test('extractSymbols - Flutter/Dart Noise Reduction', () => {
|
|
40
37
|
const code = `
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
Python docstring test
|
|
44
|
-
"""
|
|
45
|
-
pass
|
|
38
|
+
const SizedBox(height: 10);
|
|
39
|
+
void realFunction() {}
|
|
46
40
|
`;
|
|
47
|
-
const symbols =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
41
|
+
const { symbols } = extractSymbolsAndInheritance(code);
|
|
42
|
+
assert.ok(symbols.some(s => s.includes('realFunction')));
|
|
43
|
+
assert.ok(!symbols.some(s => s.includes('SizedBox')));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('extractInheritance - Class relationships', () => {
|
|
47
|
+
const code = `
|
|
48
|
+
class AdminUser extends BaseUser {}
|
|
49
|
+
interface IRepository implements IBase {}
|
|
50
|
+
class MyWidget : StatelessWidget {}
|
|
51
|
+
`;
|
|
52
|
+
const { inheritance } = extractSymbolsAndInheritance(code);
|
|
53
|
+
assert.ok(inheritance.some(i => i.child === 'AdminUser' && i.parent === 'BaseUser'));
|
|
54
|
+
assert.ok(inheritance.some(i => i.child === 'IRepository' && i.parent === 'IBase'));
|
|
55
|
+
assert.ok(inheritance.some(i => i.child === 'MyWidget' && i.parent === 'StatelessWidget'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('extractEdges - Imports and includes', () => {
|
|
59
|
+
const code = `
|
|
60
|
+
import { something } from './local-file';
|
|
61
|
+
const other = require('other-module');
|
|
62
|
+
#include "header.h"
|
|
54
63
|
`;
|
|
55
|
-
const
|
|
56
|
-
assert.ok(
|
|
64
|
+
const edges = extractEdges(code);
|
|
65
|
+
assert.ok(edges.includes('./local-file'));
|
|
66
|
+
assert.ok(edges.includes('other-module'));
|
|
67
|
+
assert.ok(edges.includes('header.h'));
|
|
57
68
|
});
|
|
58
69
|
|
|
59
70
|
test('getIgnores - Default Patterns', () => {
|
|
@@ -61,12 +72,29 @@ test('getIgnores - Default Patterns', () => {
|
|
|
61
72
|
assert.strictEqual(ig.ignores('.git/'), true);
|
|
62
73
|
assert.strictEqual(ig.ignores('node_modules/'), true);
|
|
63
74
|
assert.strictEqual(ig.ignores('.idea/'), true);
|
|
64
|
-
assert.strictEqual(ig.ignores('
|
|
75
|
+
assert.strictEqual(ig.ignores('.dart_tool/'), true);
|
|
65
76
|
});
|
|
66
77
|
|
|
67
|
-
test('
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
test('Recursive Ignore Simulation (Logic Check)', async () => {
|
|
79
|
+
const tempDir = path.join(process.cwd(), 'temp_test_dir');
|
|
80
|
+
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
|
|
81
|
+
fs.mkdirSync(tempDir);
|
|
82
|
+
|
|
83
|
+
const subDir = path.join(tempDir, 'subdir');
|
|
84
|
+
fs.mkdirSync(subDir);
|
|
85
|
+
|
|
86
|
+
// Create a file that should be ignored by subdir/.gitignore
|
|
87
|
+
fs.writeFileSync(path.join(subDir, 'ignored.js'), 'function ignored() {}');
|
|
88
|
+
fs.writeFileSync(path.join(subDir, 'included.js'), 'function included() {}');
|
|
89
|
+
fs.writeFileSync(path.join(subDir, '.gitignore'), 'ignored.js');
|
|
90
|
+
|
|
91
|
+
await generate(tempDir);
|
|
92
|
+
|
|
93
|
+
const mapPath = path.join(tempDir, 'llm-code-graph.md');
|
|
94
|
+
const mapContent = fs.readFileSync(mapPath, 'utf8');
|
|
95
|
+
|
|
96
|
+
assert.ok(mapContent.includes('included.js'));
|
|
97
|
+
assert.ok(!mapContent.includes('ignored.js'));
|
|
98
|
+
|
|
99
|
+
fs.rmSync(tempDir, { recursive: true });
|
|
72
100
|
});
|