@sdsrs/code-graph 0.7.13 → 0.7.15
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/pre-edit-guide.js +44 -1
- package/claude-plugin/scripts/pre-edit-guide.test.js +161 -0
- package/claude-plugin/scripts/user-prompt-context.js +92 -75
- package/claude-plugin/scripts/user-prompt-context.test.js +466 -0
- package/claude-plugin/skills/explore.md +22 -0
- package/claude-plugin/skills/index.md +24 -0
- package/package.json +6 -6
- package/claude-plugin/commands/impact.md +0 -9
- package/claude-plugin/commands/rebuild.md +0 -7
- package/claude-plugin/commands/status.md +0 -5
- package/claude-plugin/commands/trace.md +0 -12
- package/claude-plugin/commands/understand.md +0 -12
- package/claude-plugin/skills/code-navigation.md +0 -20
|
@@ -47,13 +47,56 @@ for (const pat of fnPatterns) {
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
// Fallback: if old_string is inside a function body (not a definition),
|
|
51
|
+
// extract a unique identifier from the code and grep for it to find the containing function
|
|
52
|
+
if (!symbol || symbol.length < 3) {
|
|
53
|
+
const filePath = (input.tool_input && input.tool_input.file_path) || '';
|
|
54
|
+
if (filePath && oldStr.length >= 10) {
|
|
55
|
+
try {
|
|
56
|
+
// Extract identifiers from old_string, try the most specific one first
|
|
57
|
+
const identifiers = (oldStr.match(/\b([a-z]\w*(?:_\w+)+|[a-z]\w*(?:[A-Z]\w*)+|[A-Z]\w+\.\w+|[A-Z]\w+::\w+)\b/g) || [])
|
|
58
|
+
.filter(id => id.length >= 6);
|
|
59
|
+
const skipWords = new Set(['return', 'function', 'default', 'require', 'module', 'exports', 'import', 'console']);
|
|
60
|
+
// Sort by length descending (longer = more specific = fewer matches)
|
|
61
|
+
const candidates = [...new Set(identifiers)]
|
|
62
|
+
.filter(id => !skipWords.has(id.toLowerCase()))
|
|
63
|
+
.sort((a, b) => b.length - a.length);
|
|
64
|
+
for (const candidate of candidates.slice(0, 5)) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = execFileSync('code-graph-mcp', ['grep', candidate, filePath, '--json'], {
|
|
67
|
+
cwd, timeout: 2000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
const grepResult = JSON.parse(raw);
|
|
70
|
+
// Pick this candidate if it has few matches (precise location)
|
|
71
|
+
const withContainer = (grepResult || []).filter(m => m.container && m.container.name);
|
|
72
|
+
if (withContainer.length > 0 && withContainer.length <= 5) {
|
|
73
|
+
// If multiple containers, vote for the most common one
|
|
74
|
+
const votes = {};
|
|
75
|
+
for (const m of withContainer) {
|
|
76
|
+
const cn = m.container.name;
|
|
77
|
+
votes[cn] = (votes[cn] || 0) + 1;
|
|
78
|
+
}
|
|
79
|
+
const best = Object.entries(votes).sort((a, b) => b[1] - a[1])[0][0];
|
|
80
|
+
symbol = best.includes('.') ? best.split('.').pop() : best.includes('::') ? best.split('::').pop() : best;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
} catch { /* try next candidate */ }
|
|
84
|
+
}
|
|
85
|
+
} catch { /* grep failed or no match — fall through */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
50
89
|
if (!symbol || symbol.length < 3) process.exit(0);
|
|
51
90
|
|
|
52
91
|
// Skip common patterns that aren't real function names
|
|
53
|
-
if (
|
|
92
|
+
if (isCommonKeyword(symbol)) {
|
|
54
93
|
process.exit(0);
|
|
55
94
|
}
|
|
56
95
|
|
|
96
|
+
function isCommonKeyword(s) {
|
|
97
|
+
return /^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(s);
|
|
98
|
+
}
|
|
99
|
+
|
|
57
100
|
// --- Per-symbol cooldown: 2 minutes ---
|
|
58
101
|
const cooldownFile = path.join(os.tmpdir(), `.cg-impact-${symbol}`);
|
|
59
102
|
try {
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
|
|
5
|
+
// Pre-edit-guide.js is a script with side effects (reads stdin, checks db).
|
|
6
|
+
// We test its PATTERNS directly without requiring the module.
|
|
7
|
+
|
|
8
|
+
// --- Function signature patterns (copied from pre-edit-guide.js) ---
|
|
9
|
+
const fnPatterns = [
|
|
10
|
+
/(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/, // Rust
|
|
11
|
+
/(?:export\s+)?(?:async\s+)?function\s+(\w+)/, // JS/TS
|
|
12
|
+
/(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|_)\s*=>/, // JS arrow
|
|
13
|
+
/(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{/, // JS method / Go func
|
|
14
|
+
/def\s+(\w+)/, // Python/Ruby
|
|
15
|
+
/func\s+(\w+)/, // Go/Swift
|
|
16
|
+
/(?:public|private|protected|static|override|virtual|abstract|internal)\s+\S+\s+(\w+)\s*\(/, // Java/C#/Kotlin
|
|
17
|
+
/(?:public\s+)?function\s+(\w+)/, // PHP
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function extractFunctionName(code) {
|
|
21
|
+
for (const pat of fnPatterns) {
|
|
22
|
+
const m = code.match(pat);
|
|
23
|
+
if (m) return m[1] || m[2];
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isCommonKeyword(s) {
|
|
29
|
+
return /^(if|for|while|switch|catch|else|return|new|get|set|try)$/i.test(s);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Rust ────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
test('fn-extract: Rust pub fn', () => {
|
|
35
|
+
assert.equal(extractFunctionName('pub fn parse_code(input: &str) -> Vec<Node> {'), 'parse_code');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('fn-extract: Rust pub async fn', () => {
|
|
39
|
+
assert.equal(extractFunctionName('pub async fn handle_message(&self, msg: &str) -> Result<()> {'), 'handle_message');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('fn-extract: Rust fn (no pub)', () => {
|
|
43
|
+
assert.equal(extractFunctionName('fn helper_func(x: i32) -> i32 {'), 'helper_func');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── JavaScript/TypeScript ───────────────────────────────
|
|
47
|
+
|
|
48
|
+
test('fn-extract: JS function', () => {
|
|
49
|
+
assert.equal(extractFunctionName('function handleRequest(req, res) {'), 'handleRequest');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('fn-extract: JS export function', () => {
|
|
53
|
+
assert.equal(extractFunctionName('export function processData(input) {'), 'processData');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('fn-extract: JS async function', () => {
|
|
57
|
+
assert.equal(extractFunctionName('async function fetchData(url) {'), 'fetchData');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('fn-extract: JS export async function', () => {
|
|
61
|
+
assert.equal(extractFunctionName('export async function loadConfig(path) {'), 'loadConfig');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('fn-extract: JS arrow function (const)', () => {
|
|
65
|
+
assert.equal(extractFunctionName('const handleError = (err) => {'), 'handleError');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('fn-extract: JS arrow function (async)', () => {
|
|
69
|
+
assert.equal(extractFunctionName('const fetchUser = async (id) => {'), 'fetchUser');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('fn-extract: JS method', () => {
|
|
73
|
+
assert.equal(extractFunctionName(' handleMessage(msg) {'), 'handleMessage');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// ── Python ──────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
test('fn-extract: Python def', () => {
|
|
79
|
+
assert.equal(extractFunctionName('def process_data(self, items):'), 'process_data');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('fn-extract: Python async def', () => {
|
|
83
|
+
assert.equal(extractFunctionName('async def fetch_data(url):'), 'fetch_data');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ── Go ──────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
test('fn-extract: Go func', () => {
|
|
89
|
+
assert.equal(extractFunctionName('func HandleRequest(w http.ResponseWriter, r *http.Request) {'), 'HandleRequest');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── Java/C#/Kotlin ──────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
test('fn-extract: Java public method', () => {
|
|
95
|
+
assert.equal(extractFunctionName('public void processItem(Item item) {'), 'processItem');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('fn-extract: Java private method', () => {
|
|
99
|
+
assert.equal(extractFunctionName('private String formatOutput(Data data) {'), 'formatOutput');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('fn-extract: C# static method', () => {
|
|
103
|
+
assert.equal(extractFunctionName('static int CalculateTotal(List<int> items) {'), 'CalculateTotal');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ── PHP ─────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
test('fn-extract: PHP function', () => {
|
|
109
|
+
assert.equal(extractFunctionName('function handleUpload($file) {'), 'handleUpload');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('fn-extract: PHP public function', () => {
|
|
113
|
+
assert.equal(extractFunctionName('public function getUser($id) {'), 'getUser');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ── Ruby ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
test('fn-extract: Ruby def', () => {
|
|
119
|
+
assert.equal(extractFunctionName('def process_request(params)'), 'process_request');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── Keyword filter ──────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
test('keyword-filter: common keywords rejected', () => {
|
|
125
|
+
for (const kw of ['if', 'for', 'while', 'switch', 'catch', 'else', 'return', 'new', 'get', 'set', 'try']) {
|
|
126
|
+
assert.ok(isCommonKeyword(kw), `"${kw}" should be rejected`);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('keyword-filter: real function names pass', () => {
|
|
131
|
+
for (const name of ['parse_code', 'handleMessage', 'process_data', 'fetchUser']) {
|
|
132
|
+
assert.ok(!isCommonKeyword(name), `"${name}" should pass`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ── No false positives ──────────────────────────────────
|
|
137
|
+
|
|
138
|
+
test('fn-extract: plain code body returns null', () => {
|
|
139
|
+
assert.equal(extractFunctionName('let x = 42;\nreturn x + 1;'), null);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('fn-extract: comment returns null', () => {
|
|
143
|
+
assert.equal(extractFunctionName('// This is a comment about the function'), null);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('fn-extract: short strings return null', () => {
|
|
147
|
+
assert.equal(extractFunctionName('x = 1'), null);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ── Pattern consistency check ───────────────────────────
|
|
151
|
+
// Verify fnPatterns in this test match what's in pre-edit-guide.js
|
|
152
|
+
|
|
153
|
+
test('pattern-sync: fnPatterns count matches source', () => {
|
|
154
|
+
const fs = require('node:fs');
|
|
155
|
+
const path = require('node:path');
|
|
156
|
+
const source = fs.readFileSync(path.join(__dirname, 'pre-edit-guide.js'), 'utf8');
|
|
157
|
+
// Count regex pattern lines in the fnPatterns array (lines containing // Language comment)
|
|
158
|
+
const sourcePatternCount = (source.match(/\/\/\s*(Rust|JS|Python|Go|Java|C#|PHP|Ruby|Swift|Kotlin)/g) || []).length;
|
|
159
|
+
assert.ok(fnPatterns.length === 8, `Expected 8 patterns, got ${fnPatterns.length}`);
|
|
160
|
+
assert.ok(sourcePatternCount >= 7, `Source should have >= 7 language comments, found ${sourcePatternCount}`);
|
|
161
|
+
});
|
|
@@ -69,7 +69,7 @@ const cwd = process.cwd();
|
|
|
69
69
|
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
70
70
|
if (!fs.existsSync(dbPath)) process.exit(0);
|
|
71
71
|
|
|
72
|
-
// ---
|
|
72
|
+
// --- Pure logic (exported for testing) ---
|
|
73
73
|
|
|
74
74
|
const STOP_WORDS = new Set([
|
|
75
75
|
'this', 'that', 'with', 'from', 'what', 'when', 'which', 'there',
|
|
@@ -79,93 +79,110 @@ const STOP_WORDS = new Set([
|
|
|
79
79
|
'being', 'through', 'default', 'function', 'method', 'class',
|
|
80
80
|
]);
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
const PLAIN_WORD_EXCLUDE = /^(possible|together|actually|something|different|important|following|available|necessary|currently|implement|operation|otherwise|beginning|knowledge|attention|according|certainly|sometimes|direction|recommend|structure|describe|question|complete|generate|anything|continue|consider|response|approach|happened|recently|probably|expected|previous|original|specific|directly|received|required|supposed|separate|designed|finished|provided|included|prepared|combined|properly|remember|whatever|although|document|handling|existing|everyone|standard|research|personal|relative|absolute|practice|language|thousand|national|evidence|refactor|understand|validate|analysis|debugging|configure|improving|resolving|creating|building|checking|updating|removing|changing|searching|cleaning|optimize|migration|overview|introduce|reviewing|thinking|managing|starting|yourself|features|problems|breaking|requires|argument|settings|includes|examples|comments|patterns|tutorial|concepts|supports|priority|organize|scenario|tracking|internal|external|abstract|concrete|strategy|evaluate|diagnose|platform|variable|optional|multiple)$/;
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
const trimmed =
|
|
86
|
-
if (/^(yes|no|ok|commit|push|y|n|done|thanks|thank you|继续|确认|好的|好|是的|不|可以|行|对|提交|推送|没问题|谢谢|发布|更新|编译|安装|卸载|重启|重连|清理)\s*[.!?。!?]?\s*$/i.test(trimmed))
|
|
87
|
-
|
|
84
|
+
function shouldSkip(msg) {
|
|
85
|
+
const trimmed = msg.trim();
|
|
86
|
+
if (/^(yes|no|ok|commit|push|y|n|done|thanks|thank you|继续|确认|好的|好|是的|不|可以|行|对|提交|推送|没问题|谢谢|发布|更新|编译|安装|卸载|重启|重连|清理)\s*[.!?。!?]?\s*$/i.test(trimmed)) return 'simple';
|
|
87
|
+
if (/^(修复|实施|执行|开始|按|实测|进入|用|重新)/.test(trimmed) && !/[a-zA-Z_]{3,}/.test(trimmed)) return 'action-only';
|
|
88
|
+
return false;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
|
|
91
|
+
function extractFilePaths(msg) {
|
|
92
|
+
return (msg.match(/(?:src|lib|test|pkg|cmd|internal|app|components?)\/[\w/.-]+/g) || []).slice(0, 2);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
function extractSymbols(msg) {
|
|
96
|
+
const candidates = (msg.match(/\b(?:[A-Z]\w*(?:(?:::|\.)\w+)+|[a-z]\w*(?:_\w+){1,}|[a-z]\w*(?:[A-Z]\w*)+|[A-Z][a-z]+(?:[A-Z][a-z]+)+)\b/g) || [])
|
|
97
|
+
.filter(s => s.length > 4)
|
|
98
|
+
.filter(s => !STOP_WORDS.has(s.toLowerCase()))
|
|
99
|
+
.slice(0, 3);
|
|
100
|
+
|
|
101
|
+
if (candidates.length === 0) {
|
|
102
|
+
const backtickSymbols = (msg.match(/`([a-zA-Z_]\w{2,})`/g) || [])
|
|
103
|
+
.map(s => s.replace(/`/g, ''))
|
|
104
|
+
.filter(s => s.length >= 3 && !STOP_WORDS.has(s.toLowerCase()));
|
|
105
|
+
candidates.push(...backtickSymbols.slice(0, 3));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let lowConfidence = false;
|
|
109
|
+
if (candidates.length === 0) {
|
|
110
|
+
const plain = (msg.match(/\b[a-z][a-z]{7,}\b/g) || [])
|
|
111
|
+
.filter(s => !STOP_WORDS.has(s))
|
|
112
|
+
.filter(s => !PLAIN_WORD_EXCLUDE.test(s));
|
|
113
|
+
candidates.push(...plain.slice(0, 2));
|
|
114
|
+
if (candidates.length > 0) lowConfidence = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { symbols: candidates, lowConfidence };
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const hasIntent = intentImpact || intentModify || intentUnderstand || intentCallgraph || intentSearch;
|
|
125
|
-
if (!hasIntent && !hasQualifiedSymbol && filePaths.length === 0) {
|
|
126
|
-
process.exit(0);
|
|
120
|
+
function detectIntents(msg) {
|
|
121
|
+
return {
|
|
122
|
+
impact: /(?:impact|影响|修改前|改之前|blast radius|before (?:edit|chang|modif)|risk|风险|改动范围|波及|问题在|bug|干扰|冲突|卡)/i.test(msg),
|
|
123
|
+
modify: /(?:改(?!变)|修改|修复|重构|优化|简化|精简|适配|统一|修正|调整|去掉|整理|清理|解耦|更新|\brefactor\b|\bchange\b|\brename\b|\bfix\b|移动|\bmove\b|删(?!除文件)|\bremove\b|替换|\breplace\b|\bupdate\b|升级|\bmigrate\b|迁移|拆分|\bsplit\b|合并|\bmerge\b|提取|\bextract\b|改成|改为|换成|转为|异步|同步)/i.test(msg),
|
|
124
|
+
implement: /(?:\badd\b|\bimplement\b|\bcreate\b|\bbuild\b|\bwrite\b|新增|添加|实现|创建|编写|开发|增加|加上|加个|写|做个|搭建|补充|引入|支持|封装|接入|对接|配置)/i.test(msg),
|
|
125
|
+
understand: /(?:how does|怎么工作|怎么实现|怎么做|什么|理解|看看|看一下|了解|分析|explain|understand|架构|architecture|structure|overview|模块|概览|干什么|做什么|工作原理|逻辑|机制|流程|功能|结合度|效率|评估|调研|是什么|有什么|能用不|高效不|达标|起作用|科学|深入思考|源码|检查|审核|审查|验证|诊断)/i.test(msg),
|
|
126
|
+
callgraph: /(?:who calls|what calls|调用|call(?:graph|er|ee)|trace|链路|追踪|谁调|被谁调|调了谁|上下游|依赖关系|触发|路径|覆盖|介入)/i.test(msg),
|
|
127
|
+
search: /(?:where is|在哪|find|search|搜索|找|locate|哪里用|哪里定义|定义在|实现在|处理没|在源码|加不加)/i.test(msg),
|
|
128
|
+
};
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
callgraph: '[code-graph:callgraph] Call relationships:',
|
|
134
|
-
search: '[code-graph:search] Relevant code:',
|
|
135
|
-
};
|
|
131
|
+
function determineQueryType(intents, symbols, filePaths, isCoolingDownFn) {
|
|
132
|
+
const hasStrict = symbols.symbols.length > 0 && !symbols.lowConfidence;
|
|
133
|
+
const hasQualified = symbols.symbols.some(s => s.includes('::'));
|
|
134
|
+
const hasAny = intents.impact || intents.modify || intents.implement || intents.understand || intents.callgraph || intents.search;
|
|
136
135
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
} else if (filePaths.length > 0 && (intentUnderstand || !hasIntent) && !isCoolingDown('overview')) {
|
|
150
|
-
queryType = 'overview';
|
|
151
|
-
const dir = filePaths[0].replace(/\/[^/]+$/, '/');
|
|
152
|
-
result = run('code-graph-mcp', ['overview', dir]);
|
|
153
|
-
} else if ((intentSearch || hasQualifiedSymbol) && symbolCandidates.length > 0 && !isCoolingDown('search')) {
|
|
154
|
-
queryType = 'search';
|
|
155
|
-
result = run('code-graph-mcp', ['search', symbolCandidates[0], '--limit', '8']);
|
|
156
|
-
} else if (intentUnderstand && symbolCandidates.length > 0 && !isCoolingDown('search')) {
|
|
157
|
-
queryType = 'search';
|
|
158
|
-
result = run('code-graph-mcp', ['search', symbolCandidates[0], '--limit', '8']);
|
|
159
|
-
}
|
|
160
|
-
} catch {
|
|
161
|
-
process.exit(0);
|
|
136
|
+
// Gate: need intent, qualified symbol, file path, or any symbol
|
|
137
|
+
if (!hasAny && !hasQualified && filePaths.length === 0 && symbols.symbols.length === 0) return null;
|
|
138
|
+
|
|
139
|
+
const cd = isCoolingDownFn || (() => false);
|
|
140
|
+
|
|
141
|
+
if ((intents.impact || intents.modify) && hasStrict && !cd('impact')) return { type: 'impact', symbol: symbols.symbols[0] };
|
|
142
|
+
if (intents.callgraph && hasStrict && !cd('callgraph')) return { type: 'callgraph', symbol: symbols.symbols[0] };
|
|
143
|
+
if (filePaths.length > 0 && !cd('overview')) return { type: 'overview', path: filePaths[0].replace(/\/[^/]+$/, '/') };
|
|
144
|
+
if ((intents.search || intents.implement || hasQualified) && symbols.symbols.length > 0 && !cd('search')) return { type: 'search', symbol: symbols.symbols[0] };
|
|
145
|
+
if ((intents.understand || !hasAny) && symbols.symbols.length > 0 && !cd('search')) return { type: 'search', symbol: symbols.symbols[0] };
|
|
146
|
+
|
|
147
|
+
return null;
|
|
162
148
|
}
|
|
163
149
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
process.
|
|
150
|
+
// --- Main execution (only when run directly) ---
|
|
151
|
+
if (require.main === module) {
|
|
152
|
+
if (shouldSkip(message)) process.exit(0);
|
|
153
|
+
|
|
154
|
+
const filePaths = extractFilePaths(message);
|
|
155
|
+
const symbols = extractSymbols(message);
|
|
156
|
+
const intents = detectIntents(message);
|
|
157
|
+
const query = determineQueryType(intents, symbols, filePaths, isCoolingDown);
|
|
158
|
+
|
|
159
|
+
if (!query) process.exit(0);
|
|
160
|
+
|
|
161
|
+
const PREFIXES = {
|
|
162
|
+
impact: '[code-graph:impact] Blast radius — review before editing:',
|
|
163
|
+
overview: '[code-graph:structure] Module structure:',
|
|
164
|
+
callgraph: '[code-graph:callgraph] Call relationships:',
|
|
165
|
+
search: '[code-graph:search] Relevant code:',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
let result = '';
|
|
170
|
+
if (query.type === 'impact') result = run('code-graph-mcp', ['impact', query.symbol]);
|
|
171
|
+
else if (query.type === 'callgraph') result = run('code-graph-mcp', ['callgraph', query.symbol, '--depth', '2']);
|
|
172
|
+
else if (query.type === 'overview') result = run('code-graph-mcp', ['overview', query.path]);
|
|
173
|
+
else if (query.type === 'search') result = run('code-graph-mcp', ['search', query.symbol, '--limit', '8']);
|
|
174
|
+
|
|
175
|
+
if (result && result.trim()) {
|
|
176
|
+
markCooldown(query.type);
|
|
177
|
+
process.stdout.write(`${PREFIXES[query.type]}\n${result.trim()}\n`);
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
process.exit(0);
|
|
181
|
+
}
|
|
167
182
|
}
|
|
168
183
|
|
|
184
|
+
module.exports = { shouldSkip, extractFilePaths, extractSymbols, detectIntents, determineQueryType, STOP_WORDS, PLAIN_WORD_EXCLUDE };
|
|
185
|
+
|
|
169
186
|
// --- Helpers ---
|
|
170
187
|
|
|
171
188
|
function run(cmd, args) {
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
shouldSkip,
|
|
9
|
+
extractFilePaths,
|
|
10
|
+
extractSymbols,
|
|
11
|
+
detectIntents,
|
|
12
|
+
determineQueryType,
|
|
13
|
+
} = require('./user-prompt-context');
|
|
14
|
+
|
|
15
|
+
// ── shouldSkip ──────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
test('shouldSkip: simple confirmations (EN)', () => {
|
|
18
|
+
for (const msg of ['yes', 'no', 'ok', 'done', 'y', 'n', 'commit', 'push', 'thanks']) {
|
|
19
|
+
assert.ok(shouldSkip(msg), `should skip "${msg}"`);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('shouldSkip: simple confirmations (ZH)', () => {
|
|
24
|
+
for (const msg of ['继续', '确认', '好的', '好', '是的', '不', '可以', '行', '对', '提交', '推送', '没问题', '谢谢', '发布', '更新', '清理']) {
|
|
25
|
+
assert.ok(shouldSkip(msg), `should skip "${msg}"`);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('shouldSkip: with trailing punctuation', () => {
|
|
30
|
+
assert.ok(shouldSkip('好的。'));
|
|
31
|
+
assert.ok(shouldSkip('ok!'));
|
|
32
|
+
assert.ok(shouldSkip('确认?'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('shouldSkip: action-only without code entities', () => {
|
|
36
|
+
assert.equal(shouldSkip('修复这些问题'), 'action-only');
|
|
37
|
+
assert.equal(shouldSkip('按优先级实施'), 'action-only');
|
|
38
|
+
assert.equal(shouldSkip('执行这个方案'), 'action-only');
|
|
39
|
+
assert.equal(shouldSkip('开始吧'), 'action-only');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('shouldSkip: action with 3+ Latin chars passes through', () => {
|
|
43
|
+
assert.equal(shouldSkip('修复 parse_code 里的bug'), false);
|
|
44
|
+
assert.equal(shouldSkip('修复这段逻辑的bug'), false); // "bug" = 3 chars
|
|
45
|
+
assert.equal(shouldSkip('修复 API 的问题'), false); // "API" = 3 chars
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('shouldSkip: NOT skip legitimate code tasks', () => {
|
|
49
|
+
assert.equal(shouldSkip('帮我写一个工具函数'), false);
|
|
50
|
+
assert.equal(shouldSkip('帮我优化一下这个查询'), false);
|
|
51
|
+
assert.equal(shouldSkip('优化 parse_code 的性能'), false);
|
|
52
|
+
assert.equal(shouldSkip('看看 src/mcp/ 模块的代码结构'), false);
|
|
53
|
+
assert.equal(shouldSkip('重构一下这个模块'), false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('shouldSkip: messages below length threshold exit early in main', () => {
|
|
57
|
+
// The 8-char minimum is checked in the main block, not in shouldSkip
|
|
58
|
+
// shouldSkip itself doesn't enforce length
|
|
59
|
+
assert.equal(shouldSkip('短消息很短'), false); // passes shouldSkip but would exit in main
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ── extractFilePaths ────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
test('extractFilePaths: extracts src/ paths', () => {
|
|
65
|
+
assert.deepEqual(extractFilePaths('看看 src/mcp/server.rs'), ['src/mcp/server.rs']);
|
|
66
|
+
assert.deepEqual(extractFilePaths('修改 src/parser/relations.rs 和 src/storage/db.rs'), ['src/parser/relations.rs', 'src/storage/db.rs']);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('extractFilePaths: extracts lib/test/pkg paths', () => {
|
|
70
|
+
assert.deepEqual(extractFilePaths('check lib/utils/helpers.js'), ['lib/utils/helpers.js']);
|
|
71
|
+
assert.deepEqual(extractFilePaths('test/integration.rs is failing'), ['test/integration.rs']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('extractFilePaths: limits to 2 paths', () => {
|
|
75
|
+
const result = extractFilePaths('src/a.rs src/b.rs src/c.rs');
|
|
76
|
+
assert.equal(result.length, 2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('extractFilePaths: no match for non-code paths', () => {
|
|
80
|
+
assert.deepEqual(extractFilePaths('这个函数有问题'), []);
|
|
81
|
+
assert.deepEqual(extractFilePaths('update the readme'), []);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ── extractSymbols ──────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
test('extractSymbols: snake_case', () => {
|
|
87
|
+
const r = extractSymbols('修改 parse_code 函数');
|
|
88
|
+
assert.deepEqual(r.symbols, ['parse_code']);
|
|
89
|
+
assert.equal(r.lowConfidence, false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('extractSymbols: camelCase', () => {
|
|
93
|
+
const r = extractSymbols('fix the handleMessage function');
|
|
94
|
+
assert.ok(r.symbols.includes('handleMessage'));
|
|
95
|
+
assert.equal(r.lowConfidence, false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('extractSymbols: PascalCase compound', () => {
|
|
99
|
+
const r = extractSymbols('implement McpServer class');
|
|
100
|
+
assert.ok(r.symbols.includes('McpServer'));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('extractSymbols: qualified names (Foo::bar)', () => {
|
|
104
|
+
const r = extractSymbols('check Foo::bar::baz');
|
|
105
|
+
assert.ok(r.symbols.some(s => s.includes('::')));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('extractSymbols: backtick-quoted fallback', () => {
|
|
109
|
+
const r = extractSymbols('修改 `parse` 函数');
|
|
110
|
+
assert.ok(r.symbols.includes('parse'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('extractSymbols: backtick with longer name', () => {
|
|
114
|
+
const r = extractSymbols('看看 `fts5_search` 怎么实现的');
|
|
115
|
+
assert.ok(r.symbols.includes('fts5_search'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('extractSymbols: plain word fallback (low confidence)', () => {
|
|
119
|
+
const r = extractSymbols('write tests for the embedding module');
|
|
120
|
+
assert.ok(r.symbols.includes('embedding'));
|
|
121
|
+
assert.equal(r.lowConfidence, true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('extractSymbols: plain words excluded (common English verbs)', () => {
|
|
125
|
+
const r = extractSymbols('help me understand the refactor approach');
|
|
126
|
+
// "understand" and "refactor" are excluded, "approach" is excluded
|
|
127
|
+
assert.equal(r.symbols.length, 0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('extractSymbols: stop words filtered', () => {
|
|
131
|
+
const r = extractSymbols('fix the default function');
|
|
132
|
+
// "default" and "function" are stop words
|
|
133
|
+
assert.equal(r.symbols.length, 0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('extractSymbols: limits to 3 symbols', () => {
|
|
137
|
+
const r = extractSymbols('modify parse_code and run_full_index and extract_relations and hash_file');
|
|
138
|
+
assert.ok(r.symbols.length <= 3);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ── detectIntents ───────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
// --- Impact intent ---
|
|
144
|
+
test('detectIntents: impact (EN)', () => {
|
|
145
|
+
assert.ok(detectIntents('what is the impact of this change').impact);
|
|
146
|
+
assert.ok(detectIntents('check the risk of modifying this').impact);
|
|
147
|
+
assert.ok(detectIntents('this bug is critical').impact);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('detectIntents: impact (ZH)', () => {
|
|
151
|
+
assert.ok(detectIntents('这个改动有什么影响').impact);
|
|
152
|
+
assert.ok(detectIntents('改动范围有多大').impact);
|
|
153
|
+
assert.ok(detectIntents('会不会跟其他模块冲突').impact);
|
|
154
|
+
assert.ok(detectIntents('修改前先看看').impact);
|
|
155
|
+
assert.ok(detectIntents('有什么风险').impact);
|
|
156
|
+
assert.ok(detectIntents('这个bug怎么回事').impact);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// --- Modify intent ---
|
|
160
|
+
test('detectIntents: modify (EN)', () => {
|
|
161
|
+
assert.ok(detectIntents('refactor this module').modify);
|
|
162
|
+
assert.ok(detectIntents('rename the function').modify);
|
|
163
|
+
assert.ok(detectIntents('fix the broken test').modify);
|
|
164
|
+
assert.ok(detectIntents('update the config').modify);
|
|
165
|
+
assert.ok(detectIntents('remove deprecated code').modify);
|
|
166
|
+
assert.ok(detectIntents('replace with new impl').modify);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('detectIntents: modify (ZH)', () => {
|
|
170
|
+
const words = ['修改', '修复', '重构', '优化', '简化', '精简', '适配', '统一', '修正', '调整', '去掉', '整理', '清理', '解耦', '更新', '升级', '迁移', '拆分', '合并', '提取'];
|
|
171
|
+
for (const w of words) {
|
|
172
|
+
assert.ok(detectIntents(`${w}这个模块`).modify, `"${w}" should trigger modify`);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('detectIntents: modify (ZH compound)', () => {
|
|
177
|
+
assert.ok(detectIntents('把这个函数改成异步的').modify);
|
|
178
|
+
assert.ok(detectIntents('把返回值类型换成 Result').modify);
|
|
179
|
+
assert.ok(detectIntents('把同步改成异步').modify);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- Implement intent ---
|
|
183
|
+
test('detectIntents: implement (EN)', () => {
|
|
184
|
+
assert.ok(detectIntents('add a new tool').implement);
|
|
185
|
+
assert.ok(detectIntents('implement error handling').implement);
|
|
186
|
+
assert.ok(detectIntents('create a helper function').implement);
|
|
187
|
+
assert.ok(detectIntents('build the CI pipeline').implement);
|
|
188
|
+
assert.ok(detectIntents('write unit tests').implement);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('detectIntents: implement (ZH)', () => {
|
|
192
|
+
const words = ['新增', '添加', '实现', '创建', '编写', '开发', '增加', '加上', '加个', '搭建', '补充', '引入', '支持', '封装', '接入', '对接', '配置'];
|
|
193
|
+
for (const w of words) {
|
|
194
|
+
assert.ok(detectIntents(`${w}一个功能`).implement, `"${w}" should trigger implement`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('detectIntents: implement - "写" variants', () => {
|
|
199
|
+
assert.ok(detectIntents('写个测试').implement);
|
|
200
|
+
assert.ok(detectIntents('写一个工具函数').implement);
|
|
201
|
+
assert.ok(detectIntents('帮我写一个函数').implement);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// --- Understand intent ---
|
|
205
|
+
test('detectIntents: understand (EN)', () => {
|
|
206
|
+
assert.ok(detectIntents('how does this module work').understand);
|
|
207
|
+
assert.ok(detectIntents('explain the architecture').understand);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('detectIntents: understand (ZH)', () => {
|
|
211
|
+
const words = ['看看', '看一下', '理解', '了解', '分析', '评估', '检查', '审核', '审查', '验证', '诊断', '深入思考'];
|
|
212
|
+
for (const w of words) {
|
|
213
|
+
assert.ok(detectIntents(`${w}这段代码`).understand, `"${w}" should trigger understand`);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('detectIntents: understand (ZH question patterns)', () => {
|
|
218
|
+
assert.ok(detectIntents('这个模块是干什么的').understand);
|
|
219
|
+
assert.ok(detectIntents('工作原理是什么').understand);
|
|
220
|
+
assert.ok(detectIntents('整个流程是怎么走的').understand);
|
|
221
|
+
assert.ok(detectIntents('这个功能怎么实现的').understand);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// --- Callgraph intent ---
|
|
225
|
+
test('detectIntents: callgraph (EN)', () => {
|
|
226
|
+
assert.ok(detectIntents('who calls this function').callgraph);
|
|
227
|
+
assert.ok(detectIntents('what calls parse_code').callgraph);
|
|
228
|
+
assert.ok(detectIntents('trace the request flow').callgraph);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('detectIntents: callgraph (ZH)', () => {
|
|
232
|
+
assert.ok(detectIntents('这个函数被谁调了').callgraph);
|
|
233
|
+
assert.ok(detectIntents('看看调用链路').callgraph);
|
|
234
|
+
assert.ok(detectIntents('追踪一下请求路径').callgraph);
|
|
235
|
+
assert.ok(detectIntents('上下游依赖关系是什么').callgraph);
|
|
236
|
+
assert.ok(detectIntents('这个事件怎么触发的').callgraph);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// --- Search intent ---
|
|
240
|
+
test('detectIntents: search (EN)', () => {
|
|
241
|
+
assert.ok(detectIntents('where is the config defined').search);
|
|
242
|
+
assert.ok(detectIntents('find the error handling code').search);
|
|
243
|
+
assert.ok(detectIntents('search for all usages').search);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('detectIntents: search (ZH)', () => {
|
|
247
|
+
assert.ok(detectIntents('这个函数定义在哪').search);
|
|
248
|
+
assert.ok(detectIntents('找一下处理错误的代码').search);
|
|
249
|
+
assert.ok(detectIntents('搜索所有用到这个类型的地方').search);
|
|
250
|
+
assert.ok(detectIntents('在哪里用了这个常量').search);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
// --- No false positives ---
|
|
254
|
+
test('detectIntents: simple confirmations have no code intent', () => {
|
|
255
|
+
const r = detectIntents('好的');
|
|
256
|
+
// "什么" would match in some words, but "好的" shouldn't trigger understand
|
|
257
|
+
assert.equal(r.modify, false);
|
|
258
|
+
assert.equal(r.implement, false);
|
|
259
|
+
assert.equal(r.callgraph, false);
|
|
260
|
+
assert.equal(r.search, false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ── determineQueryType (priority logic) ─────────────────
|
|
264
|
+
|
|
265
|
+
test('priority: impact/modify + strict symbol → impact', () => {
|
|
266
|
+
const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
|
|
267
|
+
const symbols = { symbols: ['parse_code'], lowConfidence: false };
|
|
268
|
+
const result = determineQueryType(intents, symbols, []);
|
|
269
|
+
assert.equal(result.type, 'impact');
|
|
270
|
+
assert.equal(result.symbol, 'parse_code');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('priority: modify + strict symbol → impact', () => {
|
|
274
|
+
const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
|
|
275
|
+
const symbols = { symbols: ['handleMessage'], lowConfidence: false };
|
|
276
|
+
const result = determineQueryType(intents, symbols, []);
|
|
277
|
+
assert.equal(result.type, 'impact');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('priority: modify + low-confidence symbol → NOT impact (falls to overview/search)', () => {
|
|
281
|
+
const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
|
|
282
|
+
const symbols = { symbols: ['embedding'], lowConfidence: true };
|
|
283
|
+
const result = determineQueryType(intents, symbols, ['src/embed/']);
|
|
284
|
+
// Should fall through to overview (file paths exist)
|
|
285
|
+
assert.equal(result.type, 'overview');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('priority: callgraph + strict symbol → callgraph', () => {
|
|
289
|
+
const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: true, search: false };
|
|
290
|
+
const symbols = { symbols: ['parse_code'], lowConfidence: false };
|
|
291
|
+
const result = determineQueryType(intents, symbols, []);
|
|
292
|
+
assert.equal(result.type, 'callgraph');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('priority: file paths → overview (regardless of intent)', () => {
|
|
296
|
+
const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
|
|
297
|
+
const symbols = { symbols: [], lowConfidence: false };
|
|
298
|
+
const result = determineQueryType(intents, symbols, ['src/storage/queries.rs']);
|
|
299
|
+
assert.equal(result.type, 'overview');
|
|
300
|
+
assert.equal(result.path, 'src/storage/');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('priority: search intent + symbol → search', () => {
|
|
304
|
+
const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: true };
|
|
305
|
+
const symbols = { symbols: ['parse_code'], lowConfidence: false };
|
|
306
|
+
const result = determineQueryType(intents, symbols, []);
|
|
307
|
+
assert.equal(result.type, 'search');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('priority: implement intent + symbol → search', () => {
|
|
311
|
+
const intents = { impact: false, modify: false, implement: true, understand: false, callgraph: false, search: false };
|
|
312
|
+
const symbols = { symbols: ['embedding'], lowConfidence: true };
|
|
313
|
+
const result = determineQueryType(intents, symbols, []);
|
|
314
|
+
assert.equal(result.type, 'search');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('priority: understand + symbol → search', () => {
|
|
318
|
+
const intents = { impact: false, modify: false, implement: false, understand: true, callgraph: false, search: false };
|
|
319
|
+
const symbols = { symbols: ['pipeline'], lowConfidence: true };
|
|
320
|
+
const result = determineQueryType(intents, symbols, []);
|
|
321
|
+
assert.equal(result.type, 'search');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test('priority: no intent, no symbol, no path → null', () => {
|
|
325
|
+
const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
|
|
326
|
+
const symbols = { symbols: [], lowConfidence: false };
|
|
327
|
+
const result = determineQueryType(intents, symbols, []);
|
|
328
|
+
assert.equal(result, null);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test('priority: cooldown blocks query', () => {
|
|
332
|
+
const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
|
|
333
|
+
const symbols = { symbols: ['parse_code'], lowConfidence: false };
|
|
334
|
+
const result = determineQueryType(intents, symbols, [], (type) => type === 'impact');
|
|
335
|
+
// Impact blocked by cooldown, falls through; no file path, no search intent → try search via understand fallback
|
|
336
|
+
// Actually: no understand intent and hasAny=true, so the last condition (!hasAny) is false → null
|
|
337
|
+
// But symbol exists and we have filePaths=[] → falls to search via implement/qualified check → no
|
|
338
|
+
// Actually it should return null since all fallbacks require conditions not met
|
|
339
|
+
assert.equal(result, null);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test('priority: cooldown on impact → falls to overview when file paths exist', () => {
|
|
343
|
+
const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
|
|
344
|
+
const symbols = { symbols: ['parse_code'], lowConfidence: false };
|
|
345
|
+
const result = determineQueryType(intents, symbols, ['src/parser/mod.rs'], (type) => type === 'impact');
|
|
346
|
+
assert.equal(result.type, 'overview');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// ── Full integration: message → query type ──────────────
|
|
350
|
+
|
|
351
|
+
function analyze(msg) {
|
|
352
|
+
if (shouldSkip(msg)) return { skipped: true };
|
|
353
|
+
const fp = extractFilePaths(msg);
|
|
354
|
+
const sym = extractSymbols(msg);
|
|
355
|
+
const intents = detectIntents(msg);
|
|
356
|
+
const query = determineQueryType(intents, sym, fp);
|
|
357
|
+
return { query, intents, symbols: sym, filePaths: fp };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
test('integration: 修改 parse_code 函数增加错误处理 → impact', () => {
|
|
361
|
+
const r = analyze('修改 parse_code 函数增加错误处理');
|
|
362
|
+
assert.equal(r.query.type, 'impact');
|
|
363
|
+
assert.equal(r.query.symbol, 'parse_code');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test('integration: 看看 src/mcp/ 模块的代码结构 → overview', () => {
|
|
367
|
+
const r = analyze('看看 src/mcp/ 模块的代码结构');
|
|
368
|
+
assert.equal(r.query.type, 'overview');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test('integration: refactor src/storage/queries.rs → overview (not impact on "refactor")', () => {
|
|
372
|
+
const r = analyze('refactor src/storage/queries.rs to use parameterized queries');
|
|
373
|
+
assert.equal(r.query.type, 'overview');
|
|
374
|
+
assert.ok(r.query.path.includes('src/storage/'));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('integration: help me understand the indexer pipeline → search', () => {
|
|
378
|
+
const r = analyze('help me understand the indexer pipeline');
|
|
379
|
+
assert.equal(r.query.type, 'search');
|
|
380
|
+
assert.equal(r.query.symbol, 'pipeline');
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test('integration: write tests for the embedding module → search', () => {
|
|
384
|
+
const r = analyze('write tests for the embedding module');
|
|
385
|
+
assert.equal(r.query.type, 'search');
|
|
386
|
+
assert.equal(r.query.symbol, 'embedding');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test('integration: 修复这段逻辑的bug → not skipped (bug=3 chars)', () => {
|
|
390
|
+
const r = analyze('修复这段逻辑的bug');
|
|
391
|
+
assert.ok(!r.skipped);
|
|
392
|
+
assert.ok(r.intents.impact); // "bug"
|
|
393
|
+
assert.ok(r.intents.modify); // "修复"
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('integration: 按优先级修复这些问题 → skipped (no code entity)', () => {
|
|
397
|
+
const r = analyze('按优先级修复这些问题');
|
|
398
|
+
assert.ok(r.skipped);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test('integration: 帮我写一个工具函数 → implement intent', () => {
|
|
402
|
+
const r = analyze('帮我写一个工具函数');
|
|
403
|
+
assert.ok(!r.skipped);
|
|
404
|
+
assert.ok(r.intents.implement);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('integration: 对整个项目进行一次完整的代码审核 → understand', () => {
|
|
408
|
+
const r = analyze('对整个项目进行一次完整的代码审核');
|
|
409
|
+
assert.ok(r.intents.understand);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test('integration: 更新一下readme.md → modify intent', () => {
|
|
413
|
+
const r = analyze('更新一下readme.md这个文件');
|
|
414
|
+
assert.ok(r.intents.modify);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('integration: 配置 pre-commit hook → implement intent', () => {
|
|
418
|
+
const r = analyze('配置提交代码时的git pre-commit hook检查');
|
|
419
|
+
assert.ok(r.intents.implement);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
test('integration: 检查下我们插件上下文token占用情况 → understand', () => {
|
|
423
|
+
const r = analyze('检查下我们插件上下文token占用情况');
|
|
424
|
+
assert.ok(r.intents.understand);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test('integration: 诊断一下性能问题 → understand', () => {
|
|
428
|
+
const r = analyze('诊断一下性能问题');
|
|
429
|
+
assert.ok(r.intents.understand);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('integration: simple confirmation → skipped', () => {
|
|
433
|
+
assert.ok(analyze('好的').skipped);
|
|
434
|
+
assert.ok(analyze('继续').skipped);
|
|
435
|
+
assert.ok(analyze('ok').skipped);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ── Skill files validation ──────────────────────────────
|
|
439
|
+
|
|
440
|
+
test('skills: explore.md has correct frontmatter', () => {
|
|
441
|
+
const content = fs.readFileSync(path.join(__dirname, '../skills/explore.md'), 'utf8');
|
|
442
|
+
assert.match(content, /^---\nname: explore/);
|
|
443
|
+
assert.match(content, /description:/);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('skills: index.md has correct frontmatter', () => {
|
|
447
|
+
const content = fs.readFileSync(path.join(__dirname, '../skills/index.md'), 'utf8');
|
|
448
|
+
assert.match(content, /^---\nname: index/);
|
|
449
|
+
assert.match(content, /description:/);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('skills: commands directory is empty (all converted to skills)', () => {
|
|
453
|
+
const commandsDir = path.join(__dirname, '../commands');
|
|
454
|
+
const exists = fs.existsSync(commandsDir);
|
|
455
|
+
if (exists) {
|
|
456
|
+
const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
|
|
457
|
+
assert.equal(files.length, 0, 'commands/ should have no .md files');
|
|
458
|
+
}
|
|
459
|
+
// Directory not existing is also valid
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('skills: only expected skills exist', () => {
|
|
463
|
+
const skillsDir = path.join(__dirname, '../skills');
|
|
464
|
+
const files = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md')).sort();
|
|
465
|
+
assert.deepEqual(files, ['explore.md', 'index.md']);
|
|
466
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explore
|
|
3
|
+
description: |
|
|
4
|
+
Understand code structure efficiently using the AST index. Use BEFORE reading
|
|
5
|
+
files one by one — when starting work in unfamiliar code, exploring a module
|
|
6
|
+
before changes, or finding the right file to edit. One overview call replaces
|
|
7
|
+
5+ Read calls and saves significant context.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Explore Code (indexed project)
|
|
11
|
+
|
|
12
|
+
Use these BEFORE reading individual files:
|
|
13
|
+
|
|
14
|
+
| Need | Command | Replaces |
|
|
15
|
+
|------|---------|----------|
|
|
16
|
+
| Module structure | `code-graph-mcp overview <dir>` | 5+ Read calls |
|
|
17
|
+
| Project architecture | `code-graph-mcp map --compact` | ls + README |
|
|
18
|
+
| Who calls / what calls | `code-graph-mcp callgraph <symbol>` | Grep + manual trace |
|
|
19
|
+
| Find by concept | `code-graph-mcp search "concept"` | 3+ Grep attempts |
|
|
20
|
+
| Impact before edit | `code-graph-mcp impact <symbol>` | Grep for callers |
|
|
21
|
+
|
|
22
|
+
**Workflow**: overview first → Read only the file you will edit.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: index
|
|
3
|
+
description: |
|
|
4
|
+
Diagnose and fix code-graph index issues. Use when: search returns unexpected/empty
|
|
5
|
+
results, or after major codebase restructuring. These management commands are NOT
|
|
6
|
+
exposed via MCP tools — this skill is the only way to access them.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Index Maintenance
|
|
10
|
+
|
|
11
|
+
## Check health
|
|
12
|
+
```bash
|
|
13
|
+
code-graph-mcp health-check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Rebuild (incremental — only changed files)
|
|
17
|
+
```bash
|
|
18
|
+
code-graph-mcp incremental-index
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Full rebuild (when incremental isn't enough)
|
|
22
|
+
```bash
|
|
23
|
+
rm -rf .code-graph/ && code-graph-mcp incremental-index
|
|
24
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"node": ">=16"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@sdsrs/code-graph-linux-x64": "0.7.
|
|
38
|
-
"@sdsrs/code-graph-linux-arm64": "0.7.
|
|
39
|
-
"@sdsrs/code-graph-darwin-x64": "0.7.
|
|
40
|
-
"@sdsrs/code-graph-darwin-arm64": "0.7.
|
|
41
|
-
"@sdsrs/code-graph-win32-x64": "0.7.
|
|
37
|
+
"@sdsrs/code-graph-linux-x64": "0.7.15",
|
|
38
|
+
"@sdsrs/code-graph-linux-arm64": "0.7.15",
|
|
39
|
+
"@sdsrs/code-graph-darwin-x64": "0.7.15",
|
|
40
|
+
"@sdsrs/code-graph-darwin-arm64": "0.7.15",
|
|
41
|
+
"@sdsrs/code-graph-win32-x64": "0.7.15"
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Analyze impact scope before modifying a symbol
|
|
3
|
-
argument-hint: <symbol_name>
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Impact Analysis
|
|
7
|
-
!`code-graph-mcp impact $ARGUMENTS 2>/dev/null || echo "Symbol not found or no index. Run: code-graph-mcp incremental-index"`
|
|
8
|
-
|
|
9
|
-
Present the risk assessment and recommend whether it's safe to proceed.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Trace call flow from a handler or route
|
|
3
|
-
argument-hint: <handler_or_route>
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Call Graph (callees)
|
|
7
|
-
!`code-graph-mcp callgraph $ARGUMENTS --direction callees --depth 4 2>/dev/null || echo "Symbol not found or no index."`
|
|
8
|
-
|
|
9
|
-
## Call Graph (callers)
|
|
10
|
-
!`code-graph-mcp callgraph $ARGUMENTS --direction callers --depth 2 2>/dev/null`
|
|
11
|
-
|
|
12
|
-
Map the flow and highlight error handling, auth checks, and data access points.
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Deep dive into a module or file's architecture
|
|
3
|
-
argument-hint: <file_or_dir_path>
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## Module Overview
|
|
7
|
-
!`code-graph-mcp overview $ARGUMENTS 2>/dev/null || echo "No index or no symbols found. Run: code-graph-mcp incremental-index"`
|
|
8
|
-
|
|
9
|
-
## Call Graph (top symbols)
|
|
10
|
-
!`code-graph-mcp search "$ARGUMENTS" --limit 5 2>/dev/null`
|
|
11
|
-
|
|
12
|
-
Analyze the above and summarize: purpose, public API, key internal helpers, and hot paths.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: code-navigation
|
|
3
|
-
description: Code search and understanding via CLI. Use when exploring code structure, searching by concept, or checking impact before edits.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Code Graph CLI
|
|
7
|
-
|
|
8
|
-
Indexed project. Use Bash — one command replaces multi-file Grep/Read:
|
|
9
|
-
|
|
10
|
-
| Task | Command | Replaces |
|
|
11
|
-
|------|---------|----------|
|
|
12
|
-
| grep + AST context | `code-graph-mcp grep "pattern" [path]` | Grep |
|
|
13
|
-
| search by concept | `code-graph-mcp search "query"` | Grep (no exact name needed) |
|
|
14
|
-
| structural search | `code-graph-mcp ast-search "q" --type fn --returns Result` | — |
|
|
15
|
-
| project map | `code-graph-mcp map` | Read multiple files |
|
|
16
|
-
| module overview | `code-graph-mcp overview src/path/` | Read directory files |
|
|
17
|
-
| call graph | `code-graph-mcp callgraph symbol` | Grep + Read tracing |
|
|
18
|
-
| impact analysis | `code-graph-mcp impact symbol` | — |
|
|
19
|
-
|
|
20
|
-
Still use Grep for exact strings/constants/regex. Still use Read for files you'll edit.
|