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 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
- - **Smart Context Extraction:** Captures JSDoc, Python docstrings, and preceding comments for files and symbols.
7
- - **Signature Fallback:** Automatically extracts function signatures (parameters/types) if documentation is missing.
8
- - **Compact & Dense:** Optimized for LLM token efficiency, replacing expensive recursive file scans.
9
- - **Language-Agnostic:** Optimized regex support for JS/TS, Python, Go, Rust, Java, C#, C/C++, Swift, PHP, Ruby, Dart, and more.
10
- - **Recursive Ignore Logic:** Deeply respects `.gitignore` and standard excludes (`node_modules`, `.git`).
11
- - **Live Sync:** Continuous background updates or Git pre-commit hooks.
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 uses a dense format that provides immediate architectural context:
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 and JWT validation.
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. **Docstring Capture:** If a symbol has a preceding comment (`//`, `/**`, `#`, `"""`), it's captured as a description.
82
- 4. **Signature Capture:** If no comment is found, it captures the declaration signature (parameters) as a fallback.
83
- 5. **Compilation:** Writes a single, minified `llm-code-graph.md` file designed for machine consumption.
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, and Containers (Universal)
17
- /\b(?:class|interface|type|struct|enum|protocol|extension|trait|module|namespace|object)\s+([a-zA-Z_]\w*)/g,
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 (JS, Python, Go, Rust, Ruby, PHP, Swift, Kotlin, Dart)
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 / Java / C# / TypeScript Method Patterns
23
- // Matches: ReturnType Name(...) or AccessModifier Name(...)
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 (JS/TS specific but captures named exports)
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
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
- '.py', '.go', '.rs', '.java',
41
- '.cpp', '.c', '.h', '.hpp', '.cc',
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(['.git/', 'node_modules/', DEFAULT_MAP_FILE, 'package-lock.json', '.idea/', 'build/', 'dist/', 'bin/', 'obj/']);
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 extractSymbols(content) {
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(content)) !== null) {
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
- // 1. Extract preceding comment/docstring
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
- for (let i = linesBefore.length - 1; i >= 0; i--) {
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 > 80) break;
74
- } else if (line === '' && comment === '') {
75
- continue;
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(match.index + match[0].length);
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 Array.from(new Set(symbols)).sort();
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
- export async function generate(cwd = process.cwd()) {
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
- let relativePath = path.relative(cwd, fullPath);
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 (ig.ignores(checkPath)) continue;
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 firstLines = content.split('\n').slice(0, 5);
173
+ const lines = content.split('\n');
123
174
  let fileDesc = '';
124
- for (const line of firstLines) {
125
- const trimmed = line.trim();
126
- if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('/*')) {
127
- fileDesc += trimmed.replace(/[\/*#]/g, '').trim() + ' ';
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 = extractSymbols(content);
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
- } else {
140
- files.push({ path: normalizedPath, desc: '', symbols: [] });
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 output = files.map(f => {
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 + output);
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
- const ig = getIgnores(cwd);
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
- const rel = path.relative(cwd, p).replace(/\\/g, '/');
167
- // We must check if p is a directory to append the trailing slash
168
- // Since chokidar's ignore function is synchronous, we use fs.statSync
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 === 'generate') {
212
- generate();
213
- } else if (command === 'watch') {
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
- - .gitignore
5
- - index.js | desc: !usrbinenv node
6
- - syms: [Name [Dart:], Name [PHP: class Name, interface Name, trait Name,], Name [PHP: class Name, interface Name,], Name [PHP: class Name,], Name [PHP:], Name [Ruby: def name, class Name,], Name [Ruby: def name,], Name [Swift: func name, class Name, struct Name, protocol Name,], Name [Swift: func name, class Name, struct Name,], Name [Swift: func name, class Name,], Name [Swift: func name,], SUPPORTED_EXTENSIONS [= [], extractSymbols [(content)], function [generate(cwd = process.cwd())], generate [(cwd = process.cwd()], getIgnores [(cwd)], installHook [(cwd = process.cwd()], is [We must check if p is a directory to append the trailing slash Since chokidar's ignore], name [Dart: class Name, void name,], name [Ruby:], name [Swift:], walk [(dir)], watch [(cwd = process.cwd()]]
7
- - package.json
8
- - README.md
9
- - test/index.test.js | desc: Contains 5 symbols.
10
- - syms: [noDocFunc [(arg1: string, arg2: number)], py_func [(x)], py_func [Note: Current regex captures '], py_func_2 [This is a python comment], testFunc [This is a test function]]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-llm",
3
- "version": "1.2.0",
3
+ "version": "1.4.1",
4
4
  "description": "Compact, language-agnostic codebase mapper for LLM token efficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -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
- extractSymbols,
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 = extractSymbols(code);
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 = extractSymbols(code);
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 - Python Docstrings', () => {
36
+ test('extractSymbols - Flutter/Dart Noise Reduction', () => {
40
37
  const code = `
41
- def py_func(x):
42
- """
43
- Python docstring test
44
- """
45
- pass
38
+ const SizedBox(height: 10);
39
+ void realFunction() {}
46
40
  `;
47
- const symbols = extractSymbols(code);
48
- // Note: Current regex captures 'def py_func'. Docstring is captured if it's ABOVE the def.
49
- // Let's test the comment above pattern which is common for our current extractor.
50
- const codeWithComment = `
51
- # This is a python comment
52
- def py_func_2(x):
53
- pass
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 symbols2 = extractSymbols(codeWithComment);
56
- assert.ok(symbols2.some(s => s.includes('py_func_2') && s.includes('This is a python comment')));
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('src/main.js'), false);
75
+ assert.strictEqual(ig.ignores('.dart_tool/'), true);
65
76
  });
66
77
 
67
- test('Supported Extensions', () => {
68
- assert.ok(SUPPORTED_EXTENSIONS.includes('.js'));
69
- assert.ok(SUPPORTED_EXTENSIONS.includes('.py'));
70
- assert.ok(SUPPORTED_EXTENSIONS.includes('.go'));
71
- assert.ok(SUPPORTED_EXTENSIONS.includes('.rs'));
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
  });