code-graph-llm 1.6.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 +24 -0
- package/PROJECT_REFLECTIONS.md +14 -0
- package/README.md +32 -49
- package/index.js +287 -293
- package/llm-code-graph.md +8 -5
- package/package.json +1 -1
- package/test/index.test.js +51 -16
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
|
|
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
|
|
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.
|
|
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
|
|
44
|
+
## 🧠 LLM Agent Strategy
|
|
39
45
|
|
|
40
|
-
### The
|
|
41
|
-
Instruct your
|
|
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
|
-
|
|
54
|
-
> "Before acting, read `llm-code-graph.md`.
|
|
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
|
|
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
|
|
88
|
-
4. **
|
|
89
|
-
5. **
|
|
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,334 +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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
]
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (
|
|
146
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
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);
|
|
255
180
|
}
|
|
256
181
|
}
|
|
257
182
|
}
|
|
258
183
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
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;
|
|
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
|
+
}
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
+
}
|
|
313
274
|
}
|
|
314
275
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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);
|
|
320
302
|
}
|
|
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
303
|
}
|
|
326
304
|
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+
});
|
|
334
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,17 +1,20 @@
|
|
|
1
1
|
# CODE_GRAPH_MAP
|
|
2
|
-
>
|
|
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]
|
|
5
|
+
> Notation: syms: [Name [Signature/Context]], desc: File Summary, [TAG: Context]
|
|
5
6
|
|
|
6
|
-
- [CORE] index.js (↑
|
|
7
|
-
- syms: [
|
|
8
|
-
- test/index.test.js (↑10 ↓0)
|
|
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: []
|
|
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
package/test/index.test.js
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
CodeParser,
|
|
8
|
+
ProjectMapper,
|
|
9
|
+
ReflectionManager,
|
|
10
10
|
SUPPORTED_EXTENSIONS,
|
|
11
|
-
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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 } =
|
|
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
|
|
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
|
|
140
|
+
await new ProjectMapper(tempDir).generate();
|
|
106
141
|
|
|
107
|
-
const mapPath = path.join(tempDir,
|
|
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'));
|