@sdsrs/code-graph 0.7.14 → 0.7.16

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.
@@ -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
- // --- Constants ---
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,100 +79,109 @@ const STOP_WORDS = new Set([
79
79
  'being', 'through', 'default', 'function', 'method', 'class',
80
80
  ]);
81
81
 
82
- // --- Detect intent + entities ---
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
- // Skip non-code prompts (commit, push, simple confirmations, chat, instructions, etc.)
85
- const trimmed = message.trim();
86
- if (/^(yes|no|ok|commit|push|y|n|done|thanks|thank you|继续|确认|好的|好|是的|不|可以|行|对|提交|推送|没问题|谢谢|发布|更新|编译|安装|卸载|重启|重连|清理)\s*[.!?。!?]?\s*$/i.test(trimmed)) {
87
- process.exit(0);
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
- // Skip action-only prompts without code entities (修复这些问题, 按优先级实施, etc.)
90
- if (/^(修复|优化|实施|执行|开始|按|实测|帮我|进入|用|重新)/.test(trimmed) && !/[a-zA-Z_]{4,}/.test(trimmed)) {
91
- process.exit(0);
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
- // Extract file paths from message
95
- const filePaths = (message.match(/(?:src|lib|test|pkg|cmd|internal|app|components?)\/[\w/.-]+/g) || [])
96
- .slice(0, 2);
97
-
98
- // Extract potential symbol names (camelCase, snake_case, PascalCase, qualified like Foo::bar, Foo.bar, Foo::bar::baz)
99
- const symbolCandidates = (message.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) || [])
100
- .filter(s => s.length > 4)
101
- .filter(s => !STOP_WORDS.has(s.toLowerCase()))
102
- .slice(0, 3);
103
-
104
- // Fallback: extract backtick-quoted symbols (common in mixed Chinese+code: "修改 `parse_code` 函数")
105
- if (symbolCandidates.length === 0) {
106
- const backtickSymbols = (message.match(/`([a-zA-Z_]\w{2,})`/g) || [])
107
- .map(s => s.replace(/`/g, ''))
108
- .filter(s => s.length >= 3 && !STOP_WORDS.has(s.toLowerCase()));
109
- symbolCandidates.push(...backtickSymbols.slice(0, 3));
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 };
110
118
  }
111
119
 
112
- // Fallback: plain lowercase words (8+ chars) likely to be function/type names.
113
- // Only when strict patterns found nothing — avoids false positives from English prose.
114
- // Minimum 8 chars filters most common English words while keeping technical terms
115
- // (authenticate, serialize, initialize, dispatch, resolver, etc.)
116
- if (symbolCandidates.length === 0) {
117
- const plain = (message.match(/\b[a-z][a-z]{7,}\b/g) || [])
118
- .filter(s => !STOP_WORDS.has(s))
119
- .filter(s => !/^(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)$/.test(s));
120
- symbolCandidates.push(...plain.slice(0, 2));
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
+ };
121
129
  }
122
130
 
123
- // Detect intent keywords (EN + ZH, derived from user's actual prompt history)
124
- const intentImpact = /(?:impact|影响|修改前|改之前|blast radius|before (?:edit|chang|modif)|risk|风险|改动范围|波及|问题在|bug|干扰|冲突|卡)/i.test(message);
125
- const intentModify = /(?:改(?!变)|修改|重构|\brefactor\b|\bchange\b|\brename\b|移动|\bmove\b|删(?!除文件)|\bremove\b|替换|\breplace\b|\bupdate\b|升级|\bmigrate\b|迁移|拆分|\bsplit\b|合并|\bmerge\b|提取|\bextract\b|改成|改为|换成|转为|异步|同步)/i.test(message);
126
- const intentUnderstand = /(?:how does|怎么工作|怎么实现|怎么做|什么|理解|看看|看一下|了解|分析|explain|understand|架构|architecture|structure|overview|模块|概览|干什么|做什么|工作原理|逻辑|机制|流程|功能|结合度|效率|评估|调研|是什么|有什么|能用不|高效不|达标|起作用|科学|深入思考|源码)/i.test(message);
127
- const intentCallgraph = /(?:who calls|what calls|调用|call(?:graph|er|ee)|trace|链路|追踪|谁调|被谁调|调了谁|上下游|依赖关系|触发|路径|覆盖|介入)/i.test(message);
128
- const intentSearch = /(?:where is|在哪|find|search|搜索|找|locate|哪里用|哪里定义|定义在|实现在|处理没|在源码|加不加)/i.test(message);
129
-
130
- // Need entities AND intent, or strong entity signal (qualified names like Foo::bar)
131
- const hasQualifiedSymbol = symbolCandidates.some(s => s.includes('::'));
132
- const hasIntent = intentImpact || intentModify || intentUnderstand || intentCallgraph || intentSearch;
133
- if (!hasIntent && !hasQualifiedSymbol && filePaths.length === 0) {
134
- process.exit(0);
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;
135
+
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;
135
148
  }
136
149
 
137
- // --- Semantic output prefixes ---
138
- const PREFIXES = {
139
- impact: '[code-graph:impact] Blast radius — review before editing:',
140
- overview: '[code-graph:structure] Module structure:',
141
- callgraph: '[code-graph:callgraph] Call relationships:',
142
- search: '[code-graph:search] Relevant code:',
143
- };
150
+ // --- Main execution (only when run directly) ---
151
+ if (require.main === module) {
152
+ if (shouldSkip(message)) process.exit(0);
144
153
 
145
- // --- Run ONE targeted CLI query (per-type cooldown allows different types to fire) ---
146
- let queryType = null;
147
- let result = '';
148
- try {
149
- // Priority: impact/modify > callgraph > understand/overview > search
150
- // intentModify + symbol → inject impact so Claude knows blast radius before editing
151
- if ((intentImpact || intentModify) && symbolCandidates.length > 0 && !isCoolingDown('impact')) {
152
- queryType = 'impact';
153
- result = run('code-graph-mcp', ['impact', symbolCandidates[0]]);
154
- } else if (intentCallgraph && symbolCandidates.length > 0 && !isCoolingDown('callgraph')) {
155
- queryType = 'callgraph';
156
- result = run('code-graph-mcp', ['callgraph', symbolCandidates[0], '--depth', '2']);
157
- } else if (filePaths.length > 0 && (intentUnderstand || !hasIntent) && !isCoolingDown('overview')) {
158
- queryType = 'overview';
159
- const dir = filePaths[0].replace(/\/[^/]+$/, '/');
160
- result = run('code-graph-mcp', ['overview', dir]);
161
- } else if ((intentSearch || hasQualifiedSymbol) && symbolCandidates.length > 0 && !isCoolingDown('search')) {
162
- queryType = 'search';
163
- result = run('code-graph-mcp', ['search', symbolCandidates[0], '--limit', '8']);
164
- } else if (intentUnderstand && symbolCandidates.length > 0 && !isCoolingDown('search')) {
165
- queryType = 'search';
166
- result = run('code-graph-mcp', ['search', symbolCandidates[0], '--limit', '8']);
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);
167
181
  }
168
- } catch {
169
- process.exit(0);
170
182
  }
171
183
 
172
- if (result && result.trim() && queryType) {
173
- markCooldown(queryType);
174
- process.stdout.write(`${PREFIXES[queryType]}\n${result.trim()}\n`);
175
- }
184
+ module.exports = { shouldSkip, extractFilePaths, extractSymbols, detectIntents, determineQueryType, STOP_WORDS, PLAIN_WORD_EXCLUDE };
176
185
 
177
186
  // --- Helpers ---
178
187