code-graph-llm 1.6.0 → 2.1.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,20 @@
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
+ ## 🧠 THE REFLECTION CYCLE
4
+ Every execution SHOULD follow this cycle:
5
+ 1. **PRE-TASK:** Read \`PROJECT_REFLECTIONS.md\` to identify existing pitfalls.
6
+ 2. **EXECUTION:** Monitor for "Learned Moments" (failures, unexpected behaviors, or non-obvious fixes).
7
+ 3. **POST-TASK:** If a significant lesson was learned, run \`code-graph reflect <CAT> <LESSON>\`.
8
+ - **NOTE:** Routine, expected changes do not require reflections. Focus on high-signal context.
9
+
10
+
11
+ ## ⚠️ CRITICAL MANDATES
12
+ - **OS [win32]:** Always prefix npm commands with `cmd /c`.
13
+ - **FILE [index.js]:** This is the core engine. Any change here requires immediate `npm test` validation.
14
+ - **DOCS:** `llm-code-graph.md` is the "Source of Truth" for your context. Keep it accurate.
15
+
16
+ ## 📝 REFLECTION CATEGORIES
17
+ - `LOGIC`: Code bugs, better patterns, or complex regex pitfalls.
18
+ - `ENV`: OS compatibility, shell behaviors, or CI/CD issues.
19
+ - `DEP`: Library bugs, version incompatibilities, or deprecations.
20
+ - `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,44 @@ 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
30
+ # Initialize Agent Rules and Reflection files (Scaffolding)
31
+ code-graph init
32
+
28
33
  # Generate the llm-code-graph.md map
29
34
  code-graph generate
30
35
 
36
+ # Record a project reflection (Memory)
37
+ code-graph reflect <CATEGORY> "Lesson learned"
38
+ # Example: code-graph reflect ENV "Always use 'cmd /c npm' on Windows."
39
+
31
40
  # Start the live watcher for real-time updates
32
41
  code-graph watch
33
42
 
34
- # Install the Git pre-commit hook
43
+ # Install the Git pre-commit hook (Enforces Map & Memory sync)
35
44
  code-graph install-hook
36
45
  ```
37
46
 
38
- ## LLM Usage & Token Efficiency
47
+ ## 🧠 LLM Agent Strategy
48
+
49
+ ### 1. The Mandatory Protocol
50
+ Instruct your agent to follow the **STRICT AGENT PROTOCOL** in `AGENT_RULES.md`. This ensures the agent:
51
+ 1. Reads `PROJECT_REFLECTIONS.md` before starting any task.
52
+ 2. Updates reflections after any failure or "learned moment."
53
+ 3. Regenerates the project map (`llm-code-graph.md`) after structural changes.
39
54
 
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:
55
+ ### 2. The "Read First" Strategy
56
+ The `llm-code-graph.md` file provides a high-level map and structural graph for relational reasoning:
42
57
 
43
58
  **Example Map Entry:**
44
59
  ```markdown
45
- - src/auth.js | desc: Handles user authentication.
60
+ - [CORE] src/auth.js (↑3 ↓5) [TODO: Add JWT rotation] | desc: Handles user authentication.
46
61
  - syms: [login [ (username, password) ], validateToken [ (token: string) ]]
47
62
 
48
63
  ## GRAPH EDGES
@@ -50,41 +65,12 @@ Instruct your LLM agent to read `llm-code-graph.md` as its first step. The file
50
65
  [AdminUser] -> [inherits] -> [BaseUser]
51
66
  ```
52
67
 
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
- ```
68
+ ### 3. Example System Prompt
69
+ > "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
70
 
84
71
  ## How it works
85
- 1. **File Scanning:** Recursively walks the directory, ignoring patterns in `.gitignore` (recursive).
72
+ 1. **File Scanning:** Recursively walks the directory, ignoring patterns in `.gitignore`.
86
73
  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.
74
+ 3. **Graph Extraction:** Identifies `imports`, `requires`, `extends`, and `implements`.
75
+ 4. **Reflection Management:** Deduplicates and persists agent learning into a standardized Markdown format.
76
+ 5. **Compilation:** Writes a single, minified `llm-code-graph.md` file with a dedicated `## GRAPH EDGES` section.
package/index.js CHANGED
@@ -1,334 +1,393 @@
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
- });
93
- }
94
-
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;
90
+ match[2].split(',').forEach(p => inheritance.push({ child: name, parent: p.trim() }));
113
91
  }
114
92
 
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
- const incomingEdges = {}; // Map of projectPath -> count of incoming edges
158
-
159
- function walk(dir, ig) {
160
- let localIg = ig;
161
- const localIgnorePath = path.join(dir, IGNORE_FILE);
162
- if (fs.existsSync(localIgnorePath) && dir !== cwd) {
163
- const content = fs.readFileSync(localIgnorePath, 'utf8');
164
- const lines = content.split('\n').map(line => {
165
- line = line.trim();
166
- if (!line || line.startsWith('#')) return null;
167
- const relDir = path.relative(cwd, dir).replace(/\\/g, '/');
168
- return relDir ? `${relDir}/${line}` : line;
169
- }).filter(Boolean);
170
- 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'));
171
162
  }
163
+ return ig;
164
+ }
172
165
 
173
- const entries = fs.readdirSync(dir, { withFileTypes: true });
166
+ async walk(dir, ig) {
167
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
168
+
174
169
  for (const entry of entries) {
175
170
  const fullPath = path.join(dir, entry.name);
176
- const relativePath = path.relative(cwd, fullPath);
177
- const normalizedPath = relativePath.replace(/\\/g, '/');
178
- const isDirectory = entry.isDirectory();
179
- const checkPath = isDirectory ? `${normalizedPath}/` : normalizedPath;
180
-
181
- if (localIg.ignores(checkPath)) continue;
182
-
183
- if (isDirectory) {
184
- walk(fullPath, localIg);
185
- } else if (entry.isFile()) {
186
- const ext = path.extname(entry.name);
187
- if (SUPPORTED_EXTENSIONS.includes(ext)) {
188
- const content = fs.readFileSync(fullPath, 'utf8');
189
-
190
- // Extract file-level description and tags
191
- const lines = content.split('\n');
192
- let fileDesc = '';
193
- const tags = [];
194
- for (let i = 0; i < lines.length; i++) {
195
- const line = lines[i].trim();
196
- // Surface actionable tags
197
- const tagMatch = line.match(/\b(TODO|FIXME|BUG|DEPRECATED):?\s*(.*)/i);
198
- if (tagMatch) {
199
- tags.push(`${tagMatch[1]}: ${tagMatch[2].substring(0, 50).trim()}`);
200
- }
201
-
202
- if (i < 10) {
203
- if (line.startsWith('#!') || line === '') continue;
204
- if (line.startsWith('//') || line.startsWith('#') || line.startsWith('/*')) {
205
- fileDesc += line.replace(/[\/*#]/g, '').trim() + ' ';
206
- } else if (fileDesc) {
207
- // Stop if we hit non-comment code
208
- break;
209
- }
210
- }
211
- }
212
-
213
- const { symbols, inheritance } = extractSymbolsAndInheritance(content);
214
- const dependencies = extractEdges(content);
215
-
216
- if (!fileDesc.trim() && symbols.length > 0) fileDesc = `Contains ${symbols.length} symbols.`;
217
-
218
- // Identify core entry points
219
- const isCore = /^(index|main|app|server|cli)\.(js|ts|py|go|rs|java|cpp)$/i.test(entry.name);
220
-
221
- const fileObj = {
222
- path: normalizedPath,
223
- desc: fileDesc.trim(),
224
- symbols,
225
- tags: Array.from(new Set(tags)),
226
- isCore,
227
- outCount: dependencies.length
228
- };
229
- files.push(fileObj);
230
-
231
- // Collect Edges and track incoming
232
- dependencies.forEach(dep => {
233
- let target = dep;
234
- if (dep.startsWith('.')) {
235
- // Resolve relative path to project-relative
236
- const resolved = path.normalize(path.join(path.dirname(normalizedPath), dep));
237
- target = resolved.replace(/\\/g, '/');
238
- // Basic extension guesser
239
- if (!path.extname(target)) {
240
- for (const ex of SUPPORTED_EXTENSIONS) {
241
- if (fs.existsSync(path.join(cwd, target + ex))) {
242
- target += ex;
243
- break;
244
- }
245
- }
246
- }
247
- }
248
- allEdges.push(`[${normalizedPath}] -> [imports] -> [${target}]`);
249
- incomingEdges[target] = (incomingEdges[target] || 0) + 1;
250
- });
251
- inheritance.forEach(inh => {
252
- allEdges.push(`[${inh.child}] -> [inherits] -> [${inh.parent}]`);
253
- });
254
- }
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);
180
+ }
181
+ }
182
+ }
183
+
184
+ async processFile(fullPath, relPath) {
185
+ const content = await fsp.readFile(fullPath, 'utf8');
186
+ const { symbols, inheritance, edges, tags } = CodeParser.extract(content);
187
+
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);
255
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
+ }
217
+
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;
256
222
  }
223
+ return target;
257
224
  }
258
225
 
259
- walk(cwd, getIgnores(cwd));
260
-
261
- // Sort: Core files first, then by incoming edges (importance)
262
- files.sort((a, b) => {
263
- if (a.isCore && !b.isCore) return -1;
264
- if (!a.isCore && b.isCore) return 1;
265
- const inA = incomingEdges[a.path] || 0;
266
- const inB = incomingEdges[b.path] || 0;
267
- return inB - inA;
268
- });
269
-
270
- const nodesOutput = files.map(f => {
271
- const inCount = incomingEdges[f.path] || 0;
272
- const coreMark = f.isCore ? '[CORE] ' : '';
273
- const stats = `(↑${f.outCount} ↓${inCount})`;
274
- const tagStr = f.tags.length > 0 ? ` [${f.tags.join(', ')}]` : '';
275
- const descStr = f.desc ? ` | desc: ${f.desc.substring(0, 100)}` : '';
276
- const symStr = f.symbols.length > 0 ? `\n - syms: [${f.symbols.join(', ')}]` : '';
277
- return `- ${coreMark}${f.path} ${stats}${tagStr}${descStr}${symStr}`;
278
- }).join('\n');
279
-
280
- const edgesOutput = allEdges.length > 0
281
- ? `\n\n## GRAPH EDGES\n${Array.from(new Set(allEdges)).sort().join('\n')}`
282
- : '';
283
-
284
- const header = `# CODE_GRAPH_MAP\n> LLM_ONLY: DO NOT EDIT. COMPACT PROJECT MAP.\n> Legend: [CORE] Entry Point, (↑N) Outgoing Deps, (↓M) Incoming Dependents\n> Notation: syms: [Name [Signature/Context]], desc: File Summary, [TAG: Context] Actionable items\n\n`;
285
- fs.writeFileSync(path.join(cwd, DEFAULT_MAP_FILE), header + nodesOutput + edgesOutput);
286
- console.log(`[Code-Graph] Updated ${DEFAULT_MAP_FILE}`);
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
+ }
236
+
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
+ }
287
247
  }
288
248
 
289
- export { generate };
290
-
291
- export function watch(cwd = process.cwd()) {
292
- console.log(`[Code-Graph] Watching for changes in ${cwd}...`);
293
- let timeout;
294
- const debouncedGenerate = () => {
295
- clearTimeout(timeout);
296
- timeout = setTimeout(() => generate(cwd), 500);
297
- };
298
-
299
- const watcher = chokidar.watch(cwd, {
300
- ignored: (p) => {
301
- if (p === cwd) return false;
302
- // Watcher ignore is harder for recursive .gitignore without complexity
303
- // We rely on the generate() call to skip them during walk
304
- return false;
305
- },
306
- persistent: true,
307
- ignoreInitial: true
308
- });
309
-
310
- watcher.on('all', (event, path) => {
311
- debouncedGenerate();
312
- });
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
+ }
274
+ }
275
+
276
+ /**
277
+ * Scaffolds the initial agent-agnostic rule and reflection files.
278
+ */
279
+ class ProjectInitializer {
280
+ static async init(cwd) {
281
+ const rulesPath = path.join(cwd, CONFIG.RULES_FILE);
282
+ const reflectPath = path.join(cwd, CONFIG.REFLECTIONS_FILE);
283
+
284
+ const rulesContent = `# AGENT OPERATIONAL PROTOCOL (STRICT)
285
+ > This protocol is MANDATORY for all LLM agents. Failure to update memory is a failure of the task.
286
+
287
+ ## 🧠 THE REFLECTION CYCLE
288
+ 1. **PRE-TASK:** Read \`${CONFIG.REFLECTIONS_FILE}\` to identify existing pitfalls.
289
+ 2. **EXECUTION:** Monitor for "Learned Moments" (failures, corrections, or discoveries).
290
+ 3. **POST-TASK:** Immediately run \`code-graph reflect <CAT> <LESSON>\` for any discovery.
291
+
292
+ ## 📝 REFLECTION CATEGORIES
293
+ - \`LOGIC\`: Code bugs or complex regex pitfalls.
294
+ - \`ENV\`: OS compatibility or shell behaviors.
295
+ - \`DEP\`: Library bugs or version deprecations.
296
+ - \`STYLE\`: Project-specific architectural rules.
297
+ `;
298
+
299
+ const reflectContent = `# PROJECT_REFLECTIONS & LESSONS LEARNED\n> LLM AGENT MEMORY: READ BEFORE STARTING TASKS. UPDATE ON FAILURES.\n`;
300
+
301
+ try {
302
+ if (!fs.existsSync(rulesPath)) {
303
+ await fsp.writeFile(rulesPath, rulesContent);
304
+ console.log(`[Code-Graph] Initialized ${CONFIG.RULES_FILE}`);
305
+ }
306
+ if (!fs.existsSync(reflectPath)) {
307
+ await fsp.writeFile(reflectPath, reflectContent);
308
+ console.log(`[Code-Graph] Initialized ${CONFIG.REFLECTIONS_FILE}`);
309
+ }
310
+ } catch (err) {
311
+ console.error(`[Code-Graph] Initialization failed: ${err.message}`);
312
+ }
313
+ }
313
314
  }
314
315
 
315
- export function installHook(cwd = process.cwd()) {
316
- const hooksDir = path.join(cwd, '.git', 'hooks');
317
- if (!fs.existsSync(hooksDir)) {
318
- console.error('[Code-Graph] No .git directory found. Cannot install hook.');
319
- return;
316
+ // --- CLI Entry Point ---
317
+
318
+ async function main() {
319
+ const [command, ...args] = process.argv.slice(2);
320
+ const cwd = process.cwd();
321
+
322
+ try {
323
+ switch (command || 'generate') {
324
+ case 'generate':
325
+ await new ProjectMapper(cwd).generate();
326
+ break;
327
+ case 'init':
328
+ await ProjectInitializer.init(cwd);
329
+ break;
330
+ case 'reflect':
331
+ await ReflectionManager.add(args[0], args.slice(1).join(' '));
332
+ break;
333
+ case 'install-hook':
334
+ await ProjectInitializer.init(cwd);
335
+ await installGitHook(cwd);
336
+ break;
337
+ case 'watch':
338
+ startWatcher(cwd);
339
+ break;
340
+ default:
341
+ console.log('Usage: code-graph [generate|init|reflect|install-hook|watch]');
342
+ }
343
+ } catch (err) {
344
+ console.error(`[Code-Graph] Critical Error: ${err.message}`);
345
+ process.exit(1);
320
346
  }
321
- const hookPath = path.join(hooksDir, 'pre-commit');
322
- const hookContent = `#!/bin/sh\n# Code-Graph pre-commit hook\nnode "${__filename}" generate\ngit add "${DEFAULT_MAP_FILE}"\n`;
323
- fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
324
- console.log('[Code-Graph] Installed pre-commit hook.');
325
347
  }
326
348
 
327
- if (process.argv[1] && (process.argv[1] === fileURLToPath(import.meta.url) || process.argv[1].endsWith('index.js'))) {
328
- const args = process.argv.slice(2);
329
- const command = args[0] || 'generate';
330
- if (command === 'generate') generate();
331
- else if (command === 'watch') watch();
332
- else if (command === 'install-hook') installHook();
333
- else console.log('Usage: code-graph [generate|watch|install-hook]');
349
+ async function installGitHook(cwd) {
350
+ const hookPath = path.join(cwd, '.git', 'hooks', 'pre-commit');
351
+ if (!fs.existsSync(path.dirname(hookPath))) return console.error('[Code-Graph] No .git directory found.');
352
+
353
+ const content = `#!/bin/sh
354
+ # Code-Graph Advisory: Map Sync & Reflection Reminder
355
+ echo "[Code-Graph] Validating commit..."
356
+
357
+ # 1. Regenerate Map
358
+ node "${__filename}" generate
359
+ git add "${CONFIG.MAP_FILE}"
360
+
361
+ # 2. Reflection Advisory
362
+ # Notify if code changed but reflections didn't (Soft Nudge)
363
+ CODE_CHANGES=$(git diff --cached --name-only | grep -E "\\.(${CONFIG.SUPPORTED_EXTENSIONS.map(e => e.slice(1)).join('|')})$")
364
+ REFLECT_CHANGES=$(git diff --cached --name-only | grep "${CONFIG.REFLECTIONS_FILE}")
365
+
366
+ if [ ! -z "$CODE_CHANGES" ] && [ -z "$REFLECT_CHANGES" ]; then
367
+ echo "--------------------------------------------------------"
368
+ echo "ℹ️ [Code-Graph] ADVISORY: Reflection Check"
369
+ echo "Significant code changes detected without a reflection."
370
+ echo "If you learned something new or fixed a non-obvious bug,"
371
+ echo "run 'code-graph reflect LOGIC <lesson>' before committing."
372
+ echo "--------------------------------------------------------"
373
+ fi
374
+ `;
375
+ await fsp.writeFile(hookPath, content, { mode: 0o755 });
376
+ console.log('[Code-Graph] Pre-commit Advisory installed (Soft Enforcement).');
377
+ }
378
+
379
+ function startWatcher(cwd) {
380
+ console.log(`[Code-Graph] Watching ${cwd}...`);
381
+ let timer;
382
+ chokidar.watch(cwd, { ignoreInitial: true, ignored: [/node_modules/, /\.git/, new RegExp(CONFIG.MAP_FILE)] })
383
+ .on('all', () => {
384
+ clearTimeout(timer);
385
+ timer = setTimeout(() => new ProjectMapper(cwd).generate(), 1000);
386
+ });
334
387
  }
388
+
389
+ if (process.argv[1] && (process.argv[1] === __filename || process.argv[1].endsWith('index.js'))) {
390
+ main();
391
+ }
392
+
393
+ export { CodeParser, ProjectMapper, ReflectionManager };
package/llm-code-graph.md CHANGED
@@ -1,17 +1,20 @@
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
3
4
  > Legend: [CORE] Entry Point, (↑N) Outgoing Deps, (↓M) Incoming Dependents
4
- > Notation: syms: [Name [Signature/Context]], desc: File Summary, [TAG: Context] Actionable items
5
+ > Notation: syms: [Name [Signature/Context]], desc: File Summary, [TAG: Context]
5
6
 
6
- - [CORE] index.js (↑5 ↓1) [TODO: |FIXME|BUG|DEPRECATED):?\s*(.*)/i);] | desc: Contains 5 symbols.
7
- - syms: [SUPPORTED_EXTENSIONS [= [], extractSymbolsAndInheritance [(content)], generate [(cwd = process.cwd()], getIgnores [(cwd, additionalLines = [])], walk [(dir, ig)]]
8
- - test/index.test.js (↑10 ↓0)
7
+ - [CORE] index.js (↑6 ↓1) [TODO: |FIXME|BUG|DEPRECATED):?\s*(.*)/i,, bug: s or complex regex pitfalls., bug: s or version deprecations., bug: ,"] | desc: !usrbinenv node
8
+ - syms: [CONFIG [=], CodeParser [--- Core Services --- Handles extraction of symbols, edges, and metadata from source code.], ProjectInitializer [Scaffolds the initial agent-agnostic rule and reflection files.], ProjectMapper [Manages the project mapping and file generation.], ReflectionManager [Manages project reflections and lessons learned.], SUPPORTED_EXTENSIONS [: [], add [(context ? `${display} [${context}]` : display)], extract [(content)], init [(cwd)], installGitHook [(cwd)], main [|app|server|cli)\./i.test(path.basename(relPath))], processFile [(fullPath, relPath)], walk [(dir, ig)]]
9
+ - [CORE] test/index.test.js (↑10 ↓0) | desc:
10
+ - syms: []
9
11
 
10
12
  ## GRAPH EDGES
11
13
  [index.js] -> [imports] -> [chokidar]
12
14
  [index.js] -> [imports] -> [fs]
13
15
  [index.js] -> [imports] -> [ignore]
14
16
  [index.js] -> [imports] -> [path]
17
+ [index.js] -> [imports] -> [symbols]
15
18
  [index.js] -> [imports] -> [url]
16
19
  [test/index.test.js] -> [imports] -> [assert]
17
20
  [test/index.test.js] -> [imports] -> [fs]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-graph-llm",
3
- "version": "1.6.0",
3
+ "version": "2.1.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'));