code-graph-llm 1.5.0 → 2.0.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/AGENT_RULES.md ADDED
@@ -0,0 +1,24 @@
1
+ # AGENT OPERATIONAL PROTOCOL (STRICT)
2
+ > This protocol is MANDATORY for all LLM agents. Failure to update memory is a failure of the task.
3
+
4
+ ## 🧠 THE REFLECTION CYCLE
5
+ Every execution MUST follow this cycle:
6
+ 1. **PRE-TASK:** Read `PROJECT_REFLECTIONS.md` to identify existing pitfalls (OS, Deps, Logic).
7
+ 2. **EXECUTION:** Monitor for any "Learned Moments":
8
+ - **Task Failures:** Identify the root cause (e.g., "Win32 shell restrictions").
9
+ - **Sub-optimal Logic:** Identify better abstractions (e.g., "Prefer fsp over fs").
10
+ - **Environment Issues:** Identify missing dependencies or OS incompatibilities.
11
+ 3. **POST-TASK:** Immediately run `code-graph reflect <CAT> <LESSON>` for any discovery.
12
+ - **DO NOT** wait for the user to ask.
13
+ - **DO NOT** commit until `llm-code-graph.md` is regenerated.
14
+
15
+ ## ⚠️ CRITICAL MANDATES
16
+ - **OS [win32]:** Always prefix npm commands with `cmd /c`.
17
+ - **FILE [index.js]:** This is the core engine. Any change here requires immediate `npm test` validation.
18
+ - **DOCS:** `llm-code-graph.md` is the "Source of Truth" for your context. Keep it accurate.
19
+
20
+ ## 📝 REFLECTION CATEGORIES
21
+ - `LOGIC`: Code bugs, better patterns, or complex regex pitfalls.
22
+ - `ENV`: OS compatibility, shell behaviors, or CI/CD issues.
23
+ - `DEP`: Library bugs, version incompatibilities, or deprecations.
24
+ - `STYLE`: Naming conventions or project-specific architectural rules.
@@ -0,0 +1,14 @@
1
+ # PROJECT_REFLECTIONS & LESSONS LEARNED
2
+ > LLM AGENT MEMORY: READ BEFORE STARTING TASKS. UPDATE ON FAILURES.
3
+
4
+ ## ⚠️ CRITICAL PITFALLS
5
+ - [OS: win32] PowerShell script execution is disabled by default; use `cmd /c npm` for all npm commands to ensure compatibility.
6
+ - [LOGIC: index.js] `extractSymbolsAndInheritance` regex matches itself if TODO/FIXME strings are present in the code; ensure filters are in place.
7
+
8
+ ## 📦 DEPENDENCIES & ENVIRONMENT
9
+ - [DEP: chokidar] Watcher should be debounced by at least 500ms to avoid race conditions during rapid file saves.
10
+
11
+ ## ✅ BEST PRACTICES
12
+ - [STYLE] Always include dependency counts (↑N ↓M) in `llm-code-graph.md` to help prioritize architectural understanding.
13
+
14
+ - [TOOLING: 2026-04-15] Added reflect command to simplify LLM memory updates.
package/README.md CHANGED
@@ -1,11 +1,15 @@
1
- # CODE-GRAPH
1
+ # CODE-GRAPH (v2.0.0)
2
2
 
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.
3
+ A language-agnostic, ultra-compact codebase mapper and **agent memory system** designed specifically for LLM agents. It optimizes context and token usage while enabling agents to learn from their own mistakes across sessions.
4
+
5
+ ## 🚀 New in v2.0: Self-Learning Memory
6
+ - **Reflection System:** Agents can now persist "Lessons Learned" via `code-graph reflect`.
7
+ - **Operational Protocol:** Standardized `AGENT_RULES.md` ensures agents follow strict "Reflect-Act-Verify" cycles.
8
+ - **Architectural Weight:** Project maps now include dependency counts (↑N ↓M) and CORE entry-point markers.
9
+ - **Production Refactor:** Class-based service architecture with full `fs/promises` async support.
4
10
 
5
11
  ## Features
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.
12
+ - **Structural Knowledge Graph:** Captures `imports`, `requires`, `extends`, and `implements`.
9
13
  - **Smart Context Extraction:** Captures JSDoc, Python docstrings, and preceding comments.
10
14
  - **Signature Fallback:** Extracts function signatures (parameters/types) if documentation is missing.
11
15
  - **Recursive .gitignore Support:** Deeply respects both root and nested `.gitignore` files.
@@ -16,33 +20,41 @@ A language-agnostic, ultra-compact codebase mapper designed specifically for LLM
16
20
 
17
21
  ### 1. Install via NPM
18
22
  ```bash
19
- # Global installation for CLI use
20
23
  npm install -g code-graph-llm
21
-
22
- # Local project dependency
24
+ # OR
23
25
  npm install --save-dev code-graph-llm
24
26
  ```
25
27
 
26
- ### 2. Basic Usage
28
+ ### 2. Core Commands
27
29
  ```bash
28
30
  # Generate the llm-code-graph.md map
29
31
  code-graph generate
30
32
 
33
+ # Record a project reflection (Memory)
34
+ code-graph reflect <CATEGORY> "Lesson learned"
35
+ # Example: code-graph reflect ENV "Always use 'cmd /c npm' on Windows."
36
+
31
37
  # Start the live watcher for real-time updates
32
38
  code-graph watch
33
39
 
34
- # Install the Git pre-commit hook
40
+ # Install the Git pre-commit hook (Enforces Map & Memory sync)
35
41
  code-graph install-hook
36
42
  ```
37
43
 
38
- ## LLM Usage & Token Efficiency
44
+ ## 🧠 LLM Agent Strategy
39
45
 
40
- ### The "Read First" Strategy
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:
46
+ ### 1. The Mandatory Protocol
47
+ Instruct your agent to follow the **STRICT AGENT PROTOCOL** in `AGENT_RULES.md`. This ensures the agent:
48
+ 1. Reads `PROJECT_REFLECTIONS.md` before starting any task.
49
+ 2. Updates reflections after any failure or "learned moment."
50
+ 3. Regenerates the project map (`llm-code-graph.md`) after structural changes.
51
+
52
+ ### 2. The "Read First" Strategy
53
+ The `llm-code-graph.md` file provides a high-level map and structural graph for relational reasoning:
42
54
 
43
55
  **Example Map Entry:**
44
56
  ```markdown
45
- - src/auth.js | desc: Handles user authentication.
57
+ - [CORE] src/auth.js (↑3 ↓5) [TODO: Add JWT rotation] | desc: Handles user authentication.
46
58
  - syms: [login [ (username, password) ], validateToken [ (token: string) ]]
47
59
 
48
60
  ## GRAPH EDGES
@@ -50,41 +62,12 @@ Instruct your LLM agent to read `llm-code-graph.md` as its first step. The file
50
62
  [AdminUser] -> [inherits] -> [BaseUser]
51
63
  ```
52
64
 
53
- **Example System Prompt:**
54
- > "Before acting, read `llm-code-graph.md`. It contains the project map, file descriptions, and function signatures. Use this to locate relevant logic instead of scanning the full codebase."
55
-
56
- ## Build Phase Integration
57
-
58
- ### 1. Java/Kotlin (Maven/Gradle)
59
- **Gradle (Groovy):**
60
- ```groovy
61
- task generateCodeGraph(type: Exec) {
62
- commandLine 'code-graph', 'generate'
63
- }
64
- compileJava.dependsOn generateCodeGraph
65
- ```
66
-
67
- ### 2. Python
68
- **Makefile:**
69
- ```makefile
70
- map:
71
- code-graph generate
72
- test: map
73
- pytest
74
- ```
75
-
76
- ### 3. Rust (build.rs)
77
- ```rust
78
- use std::process::Command;
79
- fn main() {
80
- Command::new("code-graph").arg("generate").status().unwrap();
81
- }
82
- ```
65
+ ### 3. Example System Prompt
66
+ > "Before acting, read `llm-code-graph.md`. Follow the protocol in `AGENT_RULES.md`. If you encounter a bug or an environment quirk, use the `code-graph reflect` tool to record the lesson in `PROJECT_REFLECTIONS.md`."
83
67
 
84
68
  ## How it works
85
- 1. **File Scanning:** Recursively walks the directory, ignoring patterns in `.gitignore` (recursive).
69
+ 1. **File Scanning:** Recursively walks the directory, ignoring patterns in `.gitignore`.
86
70
  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.
71
+ 3. **Graph Extraction:** Identifies `imports`, `requires`, `extends`, and `implements`.
72
+ 4. **Reflection Management:** Deduplicates and persists agent learning into a standardized Markdown format.
73
+ 5. **Compilation:** Writes a single, minified `llm-code-graph.md` file with a dedicated `## GRAPH EDGES` section.
package/index.js CHANGED
@@ -1,283 +1,328 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ /**
4
+ * @file index.js
5
+ * @description Compact, language-agnostic codebase mapper for LLM token efficiency.
6
+ * Features: Symbol extraction, dependency mapping, reflection logging, and git hooks.
7
+ */
8
+
3
9
  import fs from 'fs';
10
+ import { promises as fsp } from 'fs';
4
11
  import path from 'path';
5
12
  import { fileURLToPath } from 'url';
6
13
  import chokidar from 'chokidar';
7
14
  import ignore from 'ignore';
8
15
 
16
+ // --- Constants & Configuration ---
17
+
9
18
  const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- const IGNORE_FILE = '.gitignore';
13
- const DEFAULT_MAP_FILE = 'llm-code-graph.md';
14
- const SYMBOL_REGEXES = [
15
- // Types, Classes, Interfaces (Universal) with Inheritance support
16
- /\b(?:class|interface|type|struct|enum|protocol|extension|trait|module|namespace|object)\s+([a-zA-Z_]\w*)(?:[^\n\S]*(?:extends|implements|:|(?:\())[^\n\S]*([a-zA-Z_]\w*(?:[^\n\S]*,\s*[a-zA-Z_]\w*)*)\)?)?/g,
17
-
18
- // Explicit Function Keywords
19
- /\b(?:function|def|fn|func|fun|method|procedure|sub|routine)\s+([a-zA-Z_]\w*)/g,
20
-
21
- // Method/Var Declarations (Java, Spring Boot, C-style)
22
- // Captures: public String askAi(String question), void realFunction()
23
- /\b(?:void|async|public|private|protected|static|final|native|synchronized|abstract|transient|volatile)\s+(?:[\w<>[\]]+\s+)?([a-zA-Z_]\w*)(?=\s*\([^)]*\)\s*(?:\{|=>|;|=))/g,
24
-
25
- // Spring/Java/Dart Annotations (Captures the annotation name as a prefix)
26
- /(@[a-zA-Z_]\w*(?:\([^)]*\))?)\s*(?:(?:public|private|protected|static|final|abstract|class|interface|enum|void|[\w<>[\]]+)\s+)+([a-zA-Z_]\w*)/g,
27
-
28
- // Exported symbols
29
- /\bexport\s+(?:default\s+)?(?:const|let|var|function|class|type|interface|enum|async|val)\s+([a-zA-Z_]\w*)/g
30
- ];
31
-
32
-
33
- const EDGE_REGEXES = [
34
- // Imports/Includes (JS, TS, Python, Go, Rust, C++, Java, Dart)
35
- /\b(?:import|from|include|require|using)\s*(?:[\(\s])\s*['"]?([@\w\.\/\-]+)['"]?/g,
36
- // C-style includes
37
- /#include\s+[<"]([\w\.\/\-]+)[>"]/g
38
- ];
39
-
40
- export const SUPPORTED_EXTENSIONS = [
41
- '.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java',
42
- '.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.php', '.swift',
43
- '.kt', '.cs', '.dart', '.scala', '.m', '.mm'
44
- ];
45
-
46
- export function getIgnores(cwd, additionalLines = []) {
47
- const ig = ignore().add([
48
- '.git/', 'node_modules/', DEFAULT_MAP_FILE, 'package-lock.json',
49
- '.idea/', 'build/', 'dist/', 'bin/', 'obj/', '.dart_tool/', '.pub-cache/', '.pub/'
50
- ]);
51
- const ignorePath = path.join(cwd, IGNORE_FILE);
52
- if (fs.existsSync(ignorePath)) {
53
- ig.add(fs.readFileSync(ignorePath, 'utf8'));
19
+ export const CONFIG = {
20
+ IGNORE_FILE: '.gitignore',
21
+ MAP_FILE: 'llm-code-graph.md',
22
+ REFLECTIONS_FILE: 'PROJECT_REFLECTIONS.md',
23
+ RULES_FILE: 'AGENT_RULES.md',
24
+ SUPPORTED_EXTENSIONS: [
25
+ '.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs', '.java',
26
+ '.cpp', '.c', '.h', '.hpp', '.cc', '.rb', '.php', '.swift',
27
+ '.kt', '.cs', '.dart', '.scala', '.m', '.mm'
28
+ ],
29
+ DEFAULT_IGNORES: [
30
+ '.git/', 'node_modules/', 'package-lock.json', '.idea/',
31
+ 'build/', 'dist/', 'bin/', 'obj/', '.dart_tool/', '.pub-cache/', '.pub/'
32
+ ]
33
+ };
34
+
35
+ export const SUPPORTED_EXTENSIONS = CONFIG.SUPPORTED_EXTENSIONS;
36
+
37
+ const REGEX = {
38
+ SYMBOLS: [
39
+ // Types, Classes, Interfaces with Inheritance
40
+ /\b(?:class|interface|type|struct|enum|protocol|extension|trait|module|namespace|object)\s+([a-zA-Z_]\w*)(?:[^\n\S]*(?:extends|implements|:|(?:\())[^\n\S]*([a-zA-Z_]\w*(?:[^\n\S]*,\s*[a-zA-Z_]\w*)*)\)?)?/g,
41
+ // Functions
42
+ /\b(?:function|def|fn|func|fun|method|procedure|sub|routine)\s+([a-zA-Z_]\w*)/g,
43
+ // Method/Var Declarations (C-style, Java)
44
+ /\b(?:void|async|public|private|protected|static|final|native|synchronized|abstract|transient|volatile)\s+(?:[\w<>[\]]+\s+)?([a-zA-Z_]\w*)(?=\s*\([^)]*\)\s*(?:\{|=>|;|=))/g,
45
+ // Annotations
46
+ /(@[a-zA-Z_]\w*(?:\([^)]*\))?)\s*(?:(?:public|private|protected|static|final|abstract|class|interface|enum|void|[\w<>[\]]+)\s+)+([a-zA-Z_]\w*)/g,
47
+ // Exports
48
+ /\bexport\s+(?:default\s+)?(?:const|let|var|function|class|type|interface|enum|async|val)\s+([a-zA-Z_]\w*)/g
49
+ ],
50
+ EDGES: [
51
+ /\b(?:import|from|include|require|using)\s*(?:[\(\s])\s*['"]?([@\w\.\/\-]+)['"]?/g,
52
+ /#include\s+[<"]([\w\.\/\-]+)[>"]/g
53
+ ],
54
+ TAGS: /\b(TODO|FIXME|BUG|DEPRECATED):?\s*(.*)/i,
55
+ KEYWORDS: new Set(['if', 'for', 'while', 'switch', 'return', 'await', 'yield', 'const', 'new', 'let', 'var', 'class', 'void', 'public', 'private', 'protected'])
56
+ };
57
+
58
+ // --- Core Services ---
59
+
60
+ /**
61
+ * Handles extraction of symbols, edges, and metadata from source code.
62
+ */
63
+ class CodeParser {
64
+ static extract(content) {
65
+ const noComments = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
66
+ const cleanContent = noComments.replace(/['"`](?:\\.|[^'"`])*['"`]/g, ''); // Remove strings for symbol extraction
67
+
68
+ const { symbols, inheritance } = this.extractSymbols(content, cleanContent);
69
+ const edges = this.extractEdges(noComments); // Edges need strings (import paths)
70
+ const tags = this.extractTags(content);
71
+
72
+ return { symbols, inheritance, edges, tags };
54
73
  }
55
- if (additionalLines.length > 0) {
56
- ig.add(additionalLines);
57
- }
58
- return ig;
59
- }
60
74
 
61
- export function extractSymbolsAndInheritance(content) {
62
- const symbols = [];
63
- const inheritance = [];
64
-
65
- // Create a version of content without comments AND strings to find symbols accurately
66
- const cleanContent = content
67
- .replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '')
68
- .replace(/['"`](?:\\.|[^'"`])*['"`]/g, '');
69
-
70
- for (const regex of SYMBOL_REGEXES) {
71
- let match;
72
- regex.lastIndex = 0;
73
- while ((match = regex.exec(cleanContent)) !== null) {
74
- if (match[1]) {
75
- // Handle the Annotation regex separately (it has 2 groups)
76
- let symbolName = match[1];
77
- let annotation = '';
75
+ static extractSymbols(original, clean) {
76
+ const symbols = new Set();
77
+ const inheritance = [];
78
+
79
+ for (const regex of REGEX.SYMBOLS) {
80
+ let match;
81
+ regex.lastIndex = 0;
82
+ while ((match = regex.exec(clean)) !== null) {
83
+ let name = match[1];
84
+ let annotation = name.startsWith('@') ? name : '';
85
+ if (annotation) name = match[2] || '';
78
86
 
79
- if (symbolName.startsWith('@')) {
80
- annotation = symbolName;
81
- symbolName = match[2] || '';
82
- if (!symbolName) continue;
83
- }
84
-
85
- if (['if', 'for', 'while', 'switch', 'return', 'await', 'yield', 'const', 'new', 'let', 'var', 'class', 'void', 'public', 'private', 'protected'].includes(symbolName)) continue;
87
+ if (!name || REGEX.KEYWORDS.has(name)) continue;
86
88
 
87
- // Capture inheritance if present (match[2] only for non-annotation regex)
88
89
  if (!annotation && match[2]) {
89
- const parents = match[2].split(',').map(p => p.trim());
90
- parents.forEach(parent => {
91
- inheritance.push({ child: symbolName, parent });
92
- });
90
+ match[2].split(',').forEach(p => inheritance.push({ child: name, parent: p.trim() }));
93
91
  }
94
92
 
95
- // To find the comment, we need to find the position in the ORIGINAL content
96
- const escapedName = symbolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
97
- const posRegex = new RegExp(`\\b${escapedName}\\b`, 'g');
98
- let posMatch = posRegex.exec(content);
99
- if (!posMatch) continue;
100
-
101
- const linesBefore = content.substring(0, posMatch.index).split('\n');
102
- let comment = '';
103
- // Skip the current line where the symbol is defined
104
- for (let i = linesBefore.length - 2; i >= 0; i--) {
105
- const line = linesBefore[i].trim();
106
- if (line.startsWith('//') || line.startsWith('*') || line.startsWith('"""') || line.startsWith('#') || line.startsWith('/*')) {
107
- const clean = line.replace(/[\/*#"]/g, '').trim();
108
- if (clean) comment = clean + (comment ? ' ' + comment : '');
109
- if (comment.length > 100) break;
110
- } else if (line === '' && comment === '') continue;
111
- else if (line.startsWith('@')) continue; // Skip annotations in comment search
112
- else break;
113
- }
114
-
115
- let context = comment;
116
- if (!context) {
117
- const remainingLine = content.substring(posMatch.index + symbolName.length);
118
- const sigMatch = remainingLine.match(/^\s*(\([^)]*\)|[^\n{;]*)/);
119
- if (sigMatch && sigMatch[1].trim()) {
120
- context = sigMatch[1].trim();
121
- }
122
- }
123
-
124
- const displaySymbol = annotation ? `${annotation} ${symbolName}` : symbolName;
125
- symbols.push(context ? `${displaySymbol} [${context}]` : displaySymbol);
93
+ const context = this.findSymbolContext(original, name);
94
+ const display = annotation ? `${annotation} ${name}` : name;
95
+ symbols.add(context ? `${display} [${context}]` : display);
126
96
  }
127
97
  }
98
+ return { symbols: Array.from(symbols).sort(), inheritance };
128
99
  }
129
- return {
130
- symbols: Array.from(new Set(symbols)).sort(),
131
- inheritance: Array.from(new Set(inheritance.map(JSON.stringify))).map(JSON.parse)
132
- };
133
- }
134
100
 
135
- export function extractEdges(content) {
136
- const dependencies = new Set();
137
- const noComments = content.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, '');
138
- for (const regex of EDGE_REGEXES) {
139
- let match;
140
- regex.lastIndex = 0;
141
- while ((match = regex.exec(noComments)) !== null) {
142
- if (match[1]) {
143
- const dep = match[1];
144
- // Filter out obvious noise, common library names, or keywords
145
- if (dep.length > 1 && !['style', 'react', 'vue', 'flutter', 'new', 'const', 'let', 'var', 'dependencies', 'from', 'import'].includes(dep.toLowerCase())) {
146
- dependencies.add(dep);
147
- }
101
+ static findSymbolContext(content, name) {
102
+ const pos = content.search(new RegExp(`\\b${name}\\b`));
103
+ if (pos === -1) return '';
104
+
105
+ const linesBefore = content.substring(0, pos).split('\n');
106
+ let comment = '';
107
+ for (let i = linesBefore.length - 2; i >= 0; i--) {
108
+ const line = linesBefore[i].trim();
109
+ if (/^(\/\/|\*|"""|#|\/\*)/.test(line)) {
110
+ const clean = line.replace(/[\/*#"]/g, '').trim();
111
+ if (clean) comment = clean + (comment ? ' ' + comment : '');
112
+ if (comment.length > 100) break;
113
+ } else if (line !== '' && !line.startsWith('@')) break;
114
+ }
115
+
116
+ if (!comment) {
117
+ const remaining = content.substring(pos + name.length);
118
+ const sigMatch = remaining.match(/^\s*(\([^)]*\)|[^\n{;]*)/);
119
+ return sigMatch?.[1].trim() || '';
120
+ }
121
+ return comment;
122
+ }
123
+
124
+ static extractEdges(clean) {
125
+ const deps = new Set();
126
+ for (const regex of REGEX.EDGES) {
127
+ let match;
128
+ regex.lastIndex = 0;
129
+ while ((match = regex.exec(clean)) !== null) {
130
+ if (match[1] && match[1].length > 1) deps.add(match[1]);
148
131
  }
149
132
  }
133
+ return Array.from(deps).sort();
134
+ }
135
+
136
+ static extractTags(content) {
137
+ const tags = new Set();
138
+ content.split('\n').forEach(line => {
139
+ const match = line.match(REGEX.TAGS);
140
+ if (match) tags.add(`${match[1]}: ${match[2].substring(0, 50).trim()}`);
141
+ });
142
+ return Array.from(tags);
150
143
  }
151
- return Array.from(dependencies).sort();
152
144
  }
153
145
 
154
- async function generate(cwd = process.cwd()) {
155
- const files = [];
156
- const allEdges = [];
157
-
158
- function walk(dir, ig) {
159
- let localIg = ig;
160
- const localIgnorePath = path.join(dir, IGNORE_FILE);
161
- if (fs.existsSync(localIgnorePath) && dir !== cwd) {
162
- const content = fs.readFileSync(localIgnorePath, 'utf8');
163
- const lines = content.split('\n').map(line => {
164
- line = line.trim();
165
- if (!line || line.startsWith('#')) return null;
166
- const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
167
- return relDir ? `${relDir}/${line}` : line;
168
- }).filter(Boolean);
169
- localIg = ignore().add(ig).add(lines);
146
+ /**
147
+ * Manages the project mapping and file generation.
148
+ */
149
+ class ProjectMapper {
150
+ constructor(cwd) {
151
+ this.cwd = cwd;
152
+ this.files = [];
153
+ this.allEdges = [];
154
+ this.incomingEdges = {};
155
+ }
156
+
157
+ getIgnores(dir, baseIg) {
158
+ const ig = ignore().add(baseIg);
159
+ const ignorePath = path.join(dir, CONFIG.IGNORE_FILE);
160
+ if (fs.existsSync(ignorePath)) {
161
+ ig.add(fs.readFileSync(ignorePath, 'utf8'));
170
162
  }
163
+ return ig;
164
+ }
171
165
 
172
- const entries = fs.readdirSync(dir, { withFileTypes: true });
166
+ async walk(dir, ig) {
167
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
168
+
173
169
  for (const entry of entries) {
174
170
  const fullPath = path.join(dir, entry.name);
175
- const relativePath = path.relative(cwd, fullPath);
176
- const normalizedPath = relativePath.replace(/\\/g, '/');
177
- const isDirectory = entry.isDirectory();
178
- const checkPath = isDirectory ? `${normalizedPath}/` : normalizedPath;
179
-
180
- if (localIg.ignores(checkPath)) continue;
181
-
182
- if (isDirectory) {
183
- walk(fullPath, localIg);
184
- } else if (entry.isFile()) {
185
- const ext = path.extname(entry.name);
186
- if (SUPPORTED_EXTENSIONS.includes(ext)) {
187
- const content = fs.readFileSync(fullPath, 'utf8');
188
-
189
- // Extract file-level description
190
- const lines = content.split('\n');
191
- let fileDesc = '';
192
- for (let i = 0; i < Math.min(10, lines.length); i++) {
193
- const line = lines[i].trim();
194
- if (line.startsWith('#!') || line === '') continue;
195
- if (line.startsWith('//') || line.startsWith('#') || line.startsWith('/*')) {
196
- fileDesc += line.replace(/[\/*#]/g, '').trim() + ' ';
197
- } else {
198
- break;
199
- }
200
- }
201
-
202
- const { symbols, inheritance } = extractSymbolsAndInheritance(content);
203
- const dependencies = extractEdges(content);
204
-
205
- if (!fileDesc.trim() && symbols.length > 0) fileDesc = `Contains ${symbols.length} symbols.`;
206
-
207
- files.push({ path: normalizedPath, desc: fileDesc.trim(), symbols });
208
-
209
- // Collect Edges
210
- dependencies.forEach(dep => {
211
- allEdges.push(`[${normalizedPath}] -> [imports] -> [${dep}]`);
212
- });
213
- inheritance.forEach(inh => {
214
- allEdges.push(`[${inh.child}] -> [inherits] -> [${inh.parent}]`);
215
- });
216
- }
171
+ const relPath = path.relative(this.cwd, fullPath).replace(/\\/g, '/');
172
+ const checkPath = entry.isDirectory() ? `${relPath}/` : relPath;
173
+
174
+ if (ig.ignores(checkPath)) continue;
175
+
176
+ if (entry.isDirectory()) {
177
+ await this.walk(fullPath, this.getIgnores(fullPath, ig));
178
+ } else if (CONFIG.SUPPORTED_EXTENSIONS.includes(path.extname(entry.name))) {
179
+ await this.processFile(fullPath, relPath);
217
180
  }
218
181
  }
219
182
  }
220
183
 
221
- walk(cwd, getIgnores(cwd));
184
+ async processFile(fullPath, relPath) {
185
+ const content = await fsp.readFile(fullPath, 'utf8');
186
+ const { symbols, inheritance, edges, tags } = CodeParser.extract(content);
222
187
 
223
- const nodesOutput = files.map(f => {
224
- const descStr = f.desc ? ` | desc: ${f.desc.substring(0, 100)}` : '';
225
- const symStr = f.symbols.length > 0 ? `\n - syms: [${f.symbols.join(', ')}]` : '';
226
- return `- ${f.path}${descStr}${symStr}`;
227
- }).join('\n');
188
+ const isCore = /^(index|main|app|server|cli)\./i.test(path.basename(relPath));
189
+ const fileObj = { path: relPath, symbols, tags, isCore, outCount: edges.length, desc: this.extractFileDesc(content, symbols.length) };
190
+
191
+ this.files.push(fileObj);
192
+ this.processEdges(relPath, edges, inheritance);
193
+ }
194
+
195
+ extractFileDesc(content, symCount) {
196
+ const lines = content.split('\n').slice(0, 10);
197
+ let desc = '';
198
+ for (const line of lines) {
199
+ if (/^(\/\/|#|\/\*)/.test(line.trim())) desc += line.replace(/[\/*#]/g, '').trim() + ' ';
200
+ else if (desc) break;
201
+ }
202
+ return desc.trim() || (symCount > 0 ? `Contains ${symCount} symbols.` : '');
203
+ }
204
+
205
+ processEdges(relPath, edges, inheritance) {
206
+ edges.forEach(dep => {
207
+ let target = dep;
208
+ if (dep.startsWith('.')) {
209
+ const resolved = path.normalize(path.join(path.dirname(relPath), dep)).replace(/\\/g, '/');
210
+ target = this.resolveExtension(resolved);
211
+ }
212
+ this.allEdges.push(`[${relPath}] -> [imports] -> [${target}]`);
213
+ this.incomingEdges[target] = (this.incomingEdges[target] || 0) + 1;
214
+ });
215
+ inheritance.forEach(inh => this.allEdges.push(`[${inh.child}] -> [inherits] -> [${inh.parent}]`));
216
+ }
228
217
 
229
- const edgesOutput = allEdges.length > 0
230
- ? `\n\n## GRAPH EDGES\n${Array.from(new Set(allEdges)).sort().join('\n')}`
231
- : '';
218
+ resolveExtension(target) {
219
+ if (path.extname(target)) return target;
220
+ for (const ext of CONFIG.SUPPORTED_EXTENSIONS) {
221
+ if (fs.existsSync(path.join(this.cwd, target + ext))) return target + ext;
222
+ }
223
+ return target;
224
+ }
225
+
226
+ async generate() {
227
+ console.log(`[Code-Graph] Mapping ${this.cwd}...`);
228
+ await this.walk(this.cwd, this.getIgnores(this.cwd, CONFIG.DEFAULT_IGNORES));
229
+
230
+ this.files.sort((a, b) => (b.isCore - a.isCore) || ((this.incomingEdges[b.path] || 0) - (this.incomingEdges[a.path] || 0)));
231
+
232
+ const output = this.formatOutput();
233
+ await fsp.writeFile(path.join(this.cwd, CONFIG.MAP_FILE), output);
234
+ console.log(`[Code-Graph] Updated ${CONFIG.MAP_FILE}`);
235
+ }
232
236
 
233
- const header = `# CODE_GRAPH_MAP\n> LLM_ONLY: DO NOT EDIT. COMPACT PROJECT MAP.\n\n`;
234
- fs.writeFileSync(path.join(cwd, DEFAULT_MAP_FILE), header + nodesOutput + edgesOutput);
235
- console.log(`[Code-Graph] Updated ${DEFAULT_MAP_FILE}`);
237
+ formatOutput() {
238
+ const header = `# CODE_GRAPH_MAP\n> MISSION: COMPACT PROJECT MAP FOR LLM AGENTS.\n> PROTOCOL: Follow AGENT_RULES.md | MEMORY: See PROJECT_REFLECTIONS.md\n> Legend: [CORE] Entry Point, (↑N) Outgoing Deps, (↓M) Incoming Dependents\n> Notation: syms: [Name [Signature/Context]], desc: File Summary, [TAG: Context]\n\n`;
239
+ const nodes = this.files.map(f => {
240
+ const inCount = this.incomingEdges[f.path] || 0;
241
+ const tags = f.tags.length ? ` [${f.tags.join(', ')}]` : '';
242
+ return `- ${f.isCore ? '[CORE] ' : ''}${f.path} (↑${f.outCount} ↓${inCount})${tags} | desc: ${f.desc.substring(0, 100)}\n - syms: [${f.symbols.join(', ')}]`;
243
+ }).join('\n');
244
+ const edges = this.allEdges.length ? `\n\n## GRAPH EDGES\n${Array.from(new Set(this.allEdges)).sort().join('\n')}` : '';
245
+ return header + nodes + edges;
246
+ }
236
247
  }
237
248
 
238
- export { generate };
239
-
240
- export function watch(cwd = process.cwd()) {
241
- console.log(`[Code-Graph] Watching for changes in ${cwd}...`);
242
- let timeout;
243
- const debouncedGenerate = () => {
244
- clearTimeout(timeout);
245
- timeout = setTimeout(() => generate(cwd), 500);
246
- };
247
-
248
- const watcher = chokidar.watch(cwd, {
249
- ignored: (p) => {
250
- if (p === cwd) return false;
251
- // Watcher ignore is harder for recursive .gitignore without complexity
252
- // We rely on the generate() call to skip them during walk
253
- return false;
254
- },
255
- persistent: true,
256
- ignoreInitial: true
257
- });
258
-
259
- watcher.on('all', (event, path) => {
260
- debouncedGenerate();
261
- });
249
+ /**
250
+ * Manages project reflections and lessons learned.
251
+ */
252
+ class ReflectionManager {
253
+ static async add(category, lesson) {
254
+ if (!lesson) return console.error('[Code-Graph] Usage: reflect <cat> <lesson>');
255
+
256
+ const filePath = path.join(process.cwd(), CONFIG.REFLECTIONS_FILE);
257
+ const header = `# PROJECT_REFLECTIONS & LESSONS LEARNED\n> LLM AGENT MEMORY: READ BEFORE STARTING TASKS. UPDATE ON FAILURES.\n`;
258
+ const entry = `- [${category.toUpperCase()}: ${new Date().toISOString().split('T')[0]}] ${lesson}`;
259
+
260
+ try {
261
+ let content = fs.existsSync(filePath) ? await fsp.readFile(filePath, 'utf8') : header;
262
+ if (!fs.existsSync(filePath)) await fsp.writeFile(filePath, header);
263
+
264
+ if (content.toLowerCase().includes(lesson.toLowerCase().trim())) {
265
+ return console.log('[Code-Graph] Reflection already exists.');
266
+ }
267
+
268
+ await fsp.appendFile(filePath, `\n${entry}`);
269
+ console.log(`[Code-Graph] Recorded reflection: ${lesson}`);
270
+ } catch (err) {
271
+ console.error(`[Code-Graph] Reflection failed: ${err.message}`);
272
+ }
273
+ }
262
274
  }
263
275
 
264
- export function installHook(cwd = process.cwd()) {
265
- const hooksDir = path.join(cwd, '.git', 'hooks');
266
- if (!fs.existsSync(hooksDir)) {
267
- console.error('[Code-Graph] No .git directory found. Cannot install hook.');
268
- return;
276
+ // --- CLI Entry Point ---
277
+
278
+ async function main() {
279
+ const [command, ...args] = process.argv.slice(2);
280
+ const cwd = process.cwd();
281
+
282
+ try {
283
+ switch (command || 'generate') {
284
+ case 'generate':
285
+ await new ProjectMapper(cwd).generate();
286
+ break;
287
+ case 'reflect':
288
+ await ReflectionManager.add(args[0], args.slice(1).join(' '));
289
+ break;
290
+ case 'install-hook':
291
+ await installGitHook(cwd);
292
+ break;
293
+ case 'watch':
294
+ startWatcher(cwd);
295
+ break;
296
+ default:
297
+ console.log('Usage: code-graph [generate|reflect|install-hook|watch]');
298
+ }
299
+ } catch (err) {
300
+ console.error(`[Code-Graph] Critical Error: ${err.message}`);
301
+ process.exit(1);
269
302
  }
270
- const hookPath = path.join(hooksDir, 'pre-commit');
271
- const hookContent = `#!/bin/sh\n# Code-Graph pre-commit hook\nnode "${__filename}" generate\ngit add "${DEFAULT_MAP_FILE}"\n`;
272
- fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
273
- console.log('[Code-Graph] Installed pre-commit hook.');
274
303
  }
275
304
 
276
- if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith('index.js'))) {
277
- const args = process.argv.slice(2);
278
- const command = args[0] || 'generate';
279
- if (command === 'generate') generate();
280
- else if (command === 'watch') watch();
281
- else if (command === 'install-hook') installHook();
282
- else console.log('Usage: code-graph [generate|watch|install-hook]');
305
+ async function installGitHook(cwd) {
306
+ const hookPath = path.join(cwd, '.git', 'hooks', 'pre-commit');
307
+ if (!fs.existsSync(path.dirname(hookPath))) return console.error('[Code-Graph] No .git directory found.');
308
+
309
+ const content = `#!/bin/sh\n# Code-Graph Sync\necho "[Code-Graph] Validating..."\nnode "${__filename}" generate\ngit add "${CONFIG.MAP_FILE}"\nif [ ! -z "$(git diff --cached --shortstat)" ]; then\n echo "[Code-Graph] Reminder: Run 'code-graph reflect' if this fix resolves a non-obvious bug."\nfi`;
310
+ await fsp.writeFile(hookPath, content, { mode: 0o755 });
311
+ console.log('[Code-Graph] Pre-commit hook installed.');
312
+ }
313
+
314
+ function startWatcher(cwd) {
315
+ console.log(`[Code-Graph] Watching ${cwd}...`);
316
+ let timer;
317
+ chokidar.watch(cwd, { ignoreInitial: true, ignored: [/node_modules/, /\.git/, new RegExp(CONFIG.MAP_FILE)] })
318
+ .on('all', () => {
319
+ clearTimeout(timer);
320
+ timer = setTimeout(() => new ProjectMapper(cwd).generate(), 1000);
321
+ });
283
322
  }
323
+
324
+ if (process.argv[1] && (process.argv[1] === __filename || process.argv[1].endsWith('index.js'))) {
325
+ main();
326
+ }
327
+
328
+ export { CodeParser, ProjectMapper, ReflectionManager };
package/llm-code-graph.md CHANGED
@@ -1,23 +1,28 @@
1
1
  # CODE_GRAPH_MAP
2
- > LLM_ONLY: DO NOT EDIT. COMPACT PROJECT MAP.
2
+ > MISSION: COMPACT PROJECT MAP FOR LLM AGENTS.
3
+ > PROTOCOL: Follow AGENT_RULES.md | MEMORY: See PROJECT_REFLECTIONS.md
4
+ > Legend: [CORE] Entry Point, (↑N) Outgoing Deps, (↓M) Incoming Dependents
5
+ > Notation: syms: [Name [Signature/Context]], desc: File Summary, [TAG: Context]
3
6
 
4
- - index.js | desc: Contains 5 symbols.
5
- - syms: [SUPPORTED_EXTENSIONS [= [], extractSymbolsAndInheritance [(content)], generate [(cwd = process.cwd()], getIgnores [(cwd, additionalLines = [])], walk [(dir, ig)]]
6
- - test/index.test.js
7
+ - [CORE] index.js (↑6 ↓1) [TODO: |FIXME|BUG|DEPRECATED):?\s*(.*)/i,, bug: ."\nfi`;] | desc: !usrbinenv node
8
+ - syms: [CONFIG [=], CodeParser [--- Core Services --- Handles extraction of symbols, edges, and metadata from source code.], ProjectMapper [Manages the project mapping and file generation.], ReflectionManager [Manages project reflections and lessons learned.], SUPPORTED_EXTENSIONS [: [], add [(context ? `${display} [${context}]` : display)], extract [(content)], installGitHook [(cwd)], main [|app|server|cli)\./i.test(path.basename(relPath))], processFile [(fullPath, relPath)], startWatcher [(cwd)], walk [(dir, ig)]]
9
+ - [CORE] test/index.test.js (↑10 ↓0) | desc:
10
+ - syms: []
7
11
 
8
12
  ## GRAPH EDGES
9
13
  [index.js] -> [imports] -> [chokidar]
10
14
  [index.js] -> [imports] -> [fs]
11
15
  [index.js] -> [imports] -> [ignore]
12
16
  [index.js] -> [imports] -> [path]
17
+ [index.js] -> [imports] -> [symbols]
13
18
  [index.js] -> [imports] -> [url]
14
- [test/index.test.js] -> [imports] -> [../index.js]
15
- [test/index.test.js] -> [imports] -> [./local-file]
16
19
  [test/index.test.js] -> [imports] -> [assert]
17
20
  [test/index.test.js] -> [imports] -> [fs]
18
21
  [test/index.test.js] -> [imports] -> [header.h]
22
+ [test/index.test.js] -> [imports] -> [index.js]
19
23
  [test/index.test.js] -> [imports] -> [node]
20
24
  [test/index.test.js] -> [imports] -> [other-module]
21
25
  [test/index.test.js] -> [imports] -> [path]
26
+ [test/index.test.js] -> [imports] -> [test/local-file]
22
27
  [test/index.test.js] -> [imports] -> [test]
23
28
  [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.5.0",
3
+ "version": "2.0.0",
4
4
  "description": "Compact, language-agnostic codebase mapper for LLM token efficiency.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -4,11 +4,11 @@ import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import {
7
- extractSymbolsAndInheritance,
8
- extractEdges,
9
- getIgnores,
7
+ CodeParser,
8
+ ProjectMapper,
9
+ ReflectionManager,
10
10
  SUPPORTED_EXTENSIONS,
11
- generate
11
+ CONFIG
12
12
  } from '../index.js';
13
13
 
14
14
  test('extractSymbols - JS/TS Docstrings', () => {
@@ -18,7 +18,7 @@ test('extractSymbols - JS/TS Docstrings', () => {
18
18
  */
19
19
  function testFunc(a, b) {}
20
20
  `;
21
- const { symbols } = extractSymbolsAndInheritance(code);
21
+ const { symbols } = CodeParser.extract(code);
22
22
  assert.ok(symbols.some(s => s.includes('testFunc') && s.includes('This is a test function')));
23
23
  });
24
24
 
@@ -28,8 +28,7 @@ test('extractSymbols - Signature Fallback', () => {
28
28
  return true;
29
29
  }
30
30
  `;
31
- const { symbols } = extractSymbolsAndInheritance(code);
32
- // Matches "noDocFunc [ (arg1: string, arg2: number)]"
31
+ const { symbols } = CodeParser.extract(code);
33
32
  assert.ok(symbols.some(s => s.includes('noDocFunc') && s.includes('arg1: string, arg2: number')));
34
33
  });
35
34
 
@@ -38,7 +37,7 @@ test('extractSymbols - Flutter/Dart Noise Reduction', () => {
38
37
  const SizedBox(height: 10);
39
38
  void realFunction() {}
40
39
  `;
41
- const { symbols } = extractSymbolsAndInheritance(code);
40
+ const { symbols } = CodeParser.extract(code);
42
41
  assert.ok(symbols.some(s => s.includes('realFunction')));
43
42
  assert.ok(!symbols.some(s => s.includes('SizedBox')));
44
43
  });
@@ -49,7 +48,7 @@ test('extractInheritance - Class relationships', () => {
49
48
  interface IRepository implements IBase {}
50
49
  class MyWidget : StatelessWidget {}
51
50
  `;
52
- const { inheritance } = extractSymbolsAndInheritance(code);
51
+ const { inheritance } = CodeParser.extract(code);
53
52
  assert.ok(inheritance.some(i => i.child === 'AdminUser' && i.parent === 'BaseUser'));
54
53
  assert.ok(inheritance.some(i => i.child === 'IRepository' && i.parent === 'IBase'));
55
54
  assert.ok(inheritance.some(i => i.child === 'MyWidget' && i.parent === 'StatelessWidget'));
@@ -61,7 +60,7 @@ test('extractEdges - Imports and includes', () => {
61
60
  const other = require('other-module');
62
61
  #include "header.h"
63
62
  `;
64
- const edges = extractEdges(code);
63
+ const { edges } = CodeParser.extract(code);
65
64
  assert.ok(edges.includes('./local-file'));
66
65
  assert.ok(edges.includes('other-module'));
67
66
  assert.ok(edges.includes('header.h'));
@@ -75,20 +74,57 @@ test('extractSymbols - Java/Spring Annotations', () => {
75
74
  public String hello() { return "hi"; }
76
75
  }
77
76
  `;
78
- const { symbols } = extractSymbolsAndInheritance(code);
77
+ const { symbols } = CodeParser.extract(code);
79
78
  assert.ok(symbols.some(s => s.includes('@RestController MyController')));
80
- // Note: Strings are stripped during extraction to avoid false positives
81
79
  assert.ok(symbols.some(s => s.includes('@GetMapping() hello')));
82
80
  });
83
81
 
84
82
  test('getIgnores - Default Patterns', () => {
85
- const ig = getIgnores(process.cwd());
83
+ const mapper = new ProjectMapper(process.cwd());
84
+ const ig = mapper.getIgnores(process.cwd(), CONFIG.DEFAULT_IGNORES);
86
85
  assert.strictEqual(ig.ignores('.git/'), true);
87
86
  assert.strictEqual(ig.ignores('node_modules/'), true);
88
87
  assert.strictEqual(ig.ignores('.idea/'), true);
89
88
  assert.strictEqual(ig.ignores('.dart_tool/'), true);
90
89
  });
91
90
 
91
+ test('ReflectionManager - Add and Deduplicate', async () => {
92
+ const tempReflectFile = path.join(process.cwd(), CONFIG.REFLECTIONS_FILE);
93
+ const backupExists = fs.existsSync(tempReflectFile);
94
+ let backupContent = '';
95
+ if (backupExists) backupContent = fs.readFileSync(tempReflectFile, 'utf8');
96
+
97
+ // Test Initial Add
98
+ const lesson = "Unique test lesson for reflection";
99
+ await ReflectionManager.add('TEST', lesson);
100
+ const content = fs.readFileSync(tempReflectFile, 'utf8');
101
+ assert.ok(content.includes(lesson));
102
+
103
+ // Test Deduplication
104
+ const logSpy = [];
105
+ const originalLog = console.log;
106
+ console.log = (msg) => logSpy.push(msg);
107
+
108
+ await ReflectionManager.add('TEST', lesson);
109
+
110
+ console.log = originalLog;
111
+ assert.ok(logSpy.includes('[Code-Graph] Reflection already exists.'));
112
+
113
+ // Restore
114
+ if (backupExists) fs.writeFileSync(tempReflectFile, backupContent);
115
+ else fs.unlinkSync(tempReflectFile);
116
+ });
117
+
118
+ test('ProjectMapper - Format Output Header', () => {
119
+ const mapper = new ProjectMapper(process.cwd());
120
+ mapper.files = [{ path: 'test.js', symbols: [], tags: [], isCore: true, outCount: 0, desc: 'test' }];
121
+ const output = mapper.formatOutput();
122
+
123
+ assert.ok(output.includes('MISSION: COMPACT PROJECT MAP FOR LLM AGENTS.'));
124
+ assert.ok(output.includes('PROTOCOL: Follow AGENT_RULES.md'));
125
+ assert.ok(output.includes('MEMORY: See PROJECT_REFLECTIONS.md'));
126
+ });
127
+
92
128
  test('Recursive Ignore Simulation (Logic Check)', async () => {
93
129
  const tempDir = path.join(process.cwd(), 'temp_test_dir');
94
130
  if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true });
@@ -97,14 +133,13 @@ test('Recursive Ignore Simulation (Logic Check)', async () => {
97
133
  const subDir = path.join(tempDir, 'subdir');
98
134
  fs.mkdirSync(subDir);
99
135
 
100
- // Create a file that should be ignored by subdir/.gitignore
101
136
  fs.writeFileSync(path.join(subDir, 'ignored.js'), 'function ignored() {}');
102
137
  fs.writeFileSync(path.join(subDir, 'included.js'), 'function included() {}');
103
138
  fs.writeFileSync(path.join(subDir, '.gitignore'), 'ignored.js');
104
139
 
105
- await generate(tempDir);
140
+ await new ProjectMapper(tempDir).generate();
106
141
 
107
- const mapPath = path.join(tempDir, 'llm-code-graph.md');
142
+ const mapPath = path.join(tempDir, CONFIG.MAP_FILE);
108
143
  const mapContent = fs.readFileSync(mapPath, 'utf8');
109
144
 
110
145
  assert.ok(mapContent.includes('included.js'));