@sdsrs/code-graph 0.95.1 → 0.96.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.
|
@@ -66,8 +66,11 @@ const VERB_STRIP = new RegExp(`^\\s*(?:env\\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=\\S*\\
|
|
|
66
66
|
// entities/migrations/tasks/jobs/workers/features/modules/api/web. Generic
|
|
67
67
|
// terms like `core`/`utils`/`shared`/`common`/`types` deliberately omitted —
|
|
68
68
|
// they appear in too many non-code contexts to be precise enough.
|
|
69
|
+
// v0.96 — added `skills` (Claude Code plugin / agent monorepos keep source
|
|
70
|
+
// under `skills/<name>/…`, e.g. `skills/moa/scripts/moa.py`); a grep there was
|
|
71
|
+
// invisible to the hook so it could never scope the answer to the real target.
|
|
69
72
|
const SRC_PREFIXES =
|
|
70
|
-
'src|tests|lib|libs|scripts|claude-plugin|tools|pkg|cmd|internal|app|apps|components?|server|client|crates|packages|backend|frontend|services|models|domain|controllers|views|handlers|middleware|routes|repositories|entities|migrations|tasks|jobs|workers|features|modules|api|web';
|
|
73
|
+
'src|tests|lib|libs|scripts|skills|claude-plugin|tools|pkg|cmd|internal|app|apps|components?|server|client|crates|packages|backend|frontend|services|models|domain|controllers|views|handlers|middleware|routes|repositories|entities|migrations|tasks|jobs|workers|features|modules|api|web';
|
|
71
74
|
const SRC_PATH = new RegExp(`(?:^|\\s|["'])(${SRC_PREFIXES})/`);
|
|
72
75
|
// Anchored variant for whole-token matching in extractSearchPath.
|
|
73
76
|
const SRC_PATH_TOKEN = new RegExp(`^(?:\\./)?(${SRC_PREFIXES})/`);
|
|
@@ -88,6 +91,41 @@ const CONFIG_TARGET_ONLY = new RegExp(`(?:^|\\s)[^\\s|<>]*\\.(?:${NON_SOURCE_EXT
|
|
|
88
91
|
// data-file tokens both match; global so every one is peeled before the SRC_PATH re-check.
|
|
89
92
|
const CONFIG_TARGET_STRIP = new RegExp(`(?:^|\\s)[^\\s|<>]*\\.(?:${NON_SOURCE_EXTS})(?=\\s|$)`, 'gi');
|
|
90
93
|
|
|
94
|
+
// v0.96 — the grep's OWN args end at the first top-level shell separator
|
|
95
|
+
// (`;` `|` `&` `>` `<` newline). Everything after is a DIFFERENT command whose
|
|
96
|
+
// paths/flags/patterns must NOT be attributed to the grep. This closes a sibling
|
|
97
|
+
// hole: countNamedPaths stopped at the separator in v0.70, but the SRC_PATH gate
|
|
98
|
+
// in shouldHint, extractSearchPath, extractPatterns, and classifyBlock's flag
|
|
99
|
+
// checks all still scanned the WHOLE compound command. Real 2026-07-13 miss:
|
|
100
|
+
// `grep -n "VERSION" skills/moa/scripts/moa.py | head; …; python3 … scripts/bump-version.sh`
|
|
101
|
+
// — `skills/` was not an allowed prefix, so the gate/searchPath skipped the real
|
|
102
|
+
// target and latched onto `scripts/bump-version.sh` in the tail, then presented a
|
|
103
|
+
// confidently WRONG "already ran for you" answer for a file the user never grepped.
|
|
104
|
+
// Quote-aware (POSIX): a separator inside quotes is literal; inside DOUBLE quotes a
|
|
105
|
+
// backslash escapes the next char (so `\"` does not close) — mirrors
|
|
106
|
+
// splitTopLevelSegments so both share one notion of "quote-terminating vs escaped".
|
|
107
|
+
function firstShellClause(cmd) {
|
|
108
|
+
if (!cmd || typeof cmd !== 'string') return cmd;
|
|
109
|
+
let quote = null;
|
|
110
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
111
|
+
const c = cmd[i];
|
|
112
|
+
if (quote) {
|
|
113
|
+
if (quote === '"' && c === '\\' && i + 1 < cmd.length) { i++; continue; }
|
|
114
|
+
if (c === quote) quote = null;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (c === '"' || c === "'") { quote = c; continue; }
|
|
118
|
+
// Control operators END the grep's argument list → truncate. NOT redirects
|
|
119
|
+
// (`>` `<`): `2>&1`, `>out`, and process substitution `-f <(cat pats) src/`
|
|
120
|
+
// all keep grep path args AFTER them, so a redirect is not a boundary. NOT a
|
|
121
|
+
// single background `&` either (it collides with `2>&1`/`&>` and a
|
|
122
|
+
// backgrounded grep's args still precede it). `&&`/`||` DO terminate.
|
|
123
|
+
if (c === ';' || c === '|' || c === '\n') return cmd.slice(0, i);
|
|
124
|
+
if (c === '&' && cmd[i + 1] === '&') return cmd.slice(0, i);
|
|
125
|
+
}
|
|
126
|
+
return cmd;
|
|
127
|
+
}
|
|
128
|
+
|
|
91
129
|
// v0.71 — `git grep --cached`/`--staged` searches the STAGED index, and a treeish
|
|
92
130
|
// ref (`git grep "X" HEAD~3 -- src/`, `git grep "X" main -- src/`) searches another
|
|
93
131
|
// commit/branch — a scope the working-tree inline answer (`code-graph-mcp grep`)
|
|
@@ -123,10 +161,13 @@ function shouldHint(cmd) {
|
|
|
123
161
|
if (PIPE_INTO_GREP.test(cmd)) return false; // `cargo test | grep FAILED` is output filter
|
|
124
162
|
if (!GREP_HEAD.test(cmd)) return false; // not a search command
|
|
125
163
|
if (isRevisionScopedGitGrep(cmd)) return false; // v0.71 — git grep --cached/treeish: scope cg can't honor
|
|
126
|
-
|
|
164
|
+
// v0.96 — the source-path gate must see ONLY the grep's own args, not a path in
|
|
165
|
+
// a non-grep tail (`grep X skills/a.py; wc scripts/b` must not fire on scripts/b).
|
|
166
|
+
const clause = firstShellClause(cmd);
|
|
167
|
+
if (!SRC_PATH.test(clause)) return false; // not against indexed source tree
|
|
127
168
|
// If a config file appears AND no source path remains after stripping it, skip.
|
|
128
|
-
if (CONFIG_TARGET_ONLY.test(
|
|
129
|
-
const stripped =
|
|
169
|
+
if (CONFIG_TARGET_ONLY.test(clause)) {
|
|
170
|
+
const stripped = clause.replace(CONFIG_TARGET_STRIP, ' ');
|
|
130
171
|
if (!SRC_PATH.test(stripped)) return false;
|
|
131
172
|
}
|
|
132
173
|
return true;
|
|
@@ -164,8 +205,10 @@ const MARKER_ONLY =
|
|
|
164
205
|
// symbol-shaped target".
|
|
165
206
|
function extractPatterns(cmd) {
|
|
166
207
|
if (!cmd || typeof cmd !== 'string') return [];
|
|
208
|
+
// v0.96 — only the grep's own clause; a quoted string in a compound tail
|
|
209
|
+
// (`; echo "SomeWord"`) is not a grep pattern and must not be screened.
|
|
167
210
|
// Strip leading verb + env/assignment prefix (kept in sync with GREP_HEAD)
|
|
168
|
-
const stripped = cmd.replace(VERB_STRIP, '');
|
|
211
|
+
const stripped = firstShellClause(cmd).replace(VERB_STRIP, '');
|
|
169
212
|
// Collect every quoted argument — first one is the pattern in standard grep
|
|
170
213
|
// usage; subsequent ones (e.g. `-e "second"`) are also patterns or filter
|
|
171
214
|
// expressions and worth screening too.
|
|
@@ -193,12 +236,16 @@ function extractDeclSymbols(patterns) {
|
|
|
193
236
|
/// null — hint tier (marker scans, unquoted, unanswerable flags)
|
|
194
237
|
function classifyBlock(cmd) {
|
|
195
238
|
if (!shouldHint(cmd)) return null; // narrower than hint
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
239
|
+
// v0.96 — every flag/pattern check below must see the grep's OWN clause, not a
|
|
240
|
+
// tail command's flags (`grep X src/a.py; grep -v Y src/b.py` — the tail's -v
|
|
241
|
+
// must not disqualify the answerable head grep).
|
|
242
|
+
const clause = firstShellClause(cmd);
|
|
243
|
+
if (UNANSWERABLE_FLAGS.test(clause)) return null; // intent the answer can't honor
|
|
244
|
+
if (MARKER_ONLY.test(clause)) return null; // bare TODO/FIXME — no cg equivalent
|
|
245
|
+
const patterns = extractPatterns(clause);
|
|
199
246
|
if (patterns.length === 0) return null; // unquoted pattern — conservative, hint
|
|
200
247
|
if (!patterns.some(p => IDENTIFIER_LIKE.test(p))) return null;
|
|
201
|
-
if (CONTEXT_FLAG.test(
|
|
248
|
+
if (CONTEXT_FLAG.test(clause)) {
|
|
202
249
|
const symbols = extractDeclSymbols(patterns);
|
|
203
250
|
if (symbols.length === 0) return null; // context read without named decls
|
|
204
251
|
return { mode: 'show', symbols: symbols.slice(0, 3) };
|
|
@@ -208,7 +255,7 @@ function classifyBlock(cmd) {
|
|
|
208
255
|
// first-path-only answer (the rest silently dropped) — an incomplete substitute that
|
|
209
256
|
// rationally teaches CODE_GRAPH_NO_BLOCK_GREP bypass. Downgrade to hint: the model's complete
|
|
210
257
|
// grep runs and the hint still nudges. (show mode above is symbol-scoped, not path → unaffected.)
|
|
211
|
-
if (countNamedPaths(
|
|
258
|
+
if (countNamedPaths(clause, patterns) >= 2) return null;
|
|
212
259
|
return { mode: 'grep' };
|
|
213
260
|
}
|
|
214
261
|
|
|
@@ -308,6 +355,11 @@ function extractUnansweredTail(cmd) {
|
|
|
308
355
|
for (let i = 0; i < cmd.length; i++) {
|
|
309
356
|
const c = cmd[i];
|
|
310
357
|
if (quote) {
|
|
358
|
+
// v0.96 — same POSIX escape rule the rest of the quote-parser family uses
|
|
359
|
+
// (firstShellClause / splitTopLevelSegments): inside DOUBLE quotes `\"` does
|
|
360
|
+
// not close, so a `;`/`&&` inside a `grep "a\";b" …` pattern stays literal
|
|
361
|
+
// and the re-issue NOTE isn't garbled by splitting mid-pattern.
|
|
362
|
+
if (quote === '"' && c === '\\' && i + 1 < cmd.length) { i++; continue; }
|
|
311
363
|
if (c === quote) quote = null;
|
|
312
364
|
continue;
|
|
313
365
|
}
|
|
@@ -337,7 +389,10 @@ function appendUnansweredTailNote(lines, tail) {
|
|
|
337
389
|
// the inline answer can scope its search the same way the raw grep would have.
|
|
338
390
|
function extractSearchPath(cmd) {
|
|
339
391
|
if (!cmd || typeof cmd !== 'string') return undefined;
|
|
340
|
-
|
|
392
|
+
// v0.96 — scope to the grep's own clause so the answer is never scoped to a
|
|
393
|
+
// path in a non-grep tail (the file the user actually grepped is the only one
|
|
394
|
+
// the "already ran for you" answer may claim to have searched).
|
|
395
|
+
for (const raw of firstShellClause(cmd).split(/\s+/)) {
|
|
341
396
|
const token = raw.replace(/^["']|["']$/g, '');
|
|
342
397
|
if (!token || token.startsWith('-')) continue;
|
|
343
398
|
if (token.includes('..')) return undefined; // traversal — don't scope, don't guess
|
|
@@ -356,17 +411,12 @@ function extractSearchPath(cmd) {
|
|
|
356
411
|
function countNamedPaths(cmd, patterns) {
|
|
357
412
|
if (!cmd || typeof cmd !== 'string') return 0;
|
|
358
413
|
const pats = new Set(patterns || []);
|
|
359
|
-
// Only the grep's OWN path args count.
|
|
360
|
-
// path in a compound tail (`grep X src/a.py | sed …
|
|
361
|
-
//
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const c = seg[i];
|
|
366
|
-
if (quote) { if (c === quote) quote = null; continue; }
|
|
367
|
-
if (c === '"' || c === "'") { quote = c; continue; }
|
|
368
|
-
if (c === ';' || c === '|' || c === '&' || c === '>' || c === '<' || c === '\n') { seg = seg.slice(0, i); break; }
|
|
369
|
-
}
|
|
414
|
+
// Only the grep's OWN path args count. firstShellClause stops at the first
|
|
415
|
+
// top-level separator so a path in a compound tail (`grep X src/a.py | sed …
|
|
416
|
+
// src/b.py`) is NOT mistaken for a second grep target — that would wrongly
|
|
417
|
+
// downgrade a complete single-file grep to a hint. (v0.96 — was an inline scan;
|
|
418
|
+
// now shares the ONE clause definition with shouldHint/extractSearchPath.)
|
|
419
|
+
const seg = firstShellClause(cmd).replace(VERB_STRIP, '');
|
|
370
420
|
let n = 0;
|
|
371
421
|
for (const raw of seg.split(/\s+/)) {
|
|
372
422
|
const tok = raw.replace(/^["']|["']$/g, '');
|
|
@@ -776,6 +826,7 @@ module.exports = {
|
|
|
776
826
|
shouldBlock,
|
|
777
827
|
classifyBlock, // v0.49 — intent-aware block tiers
|
|
778
828
|
splitTopLevelSegments, // compound-grep — quote-aware top-level segment splitter (PostToolUse reuse)
|
|
829
|
+
firstShellClause, // v0.96 — grep's own clause (up to first top-level separator)
|
|
779
830
|
extractDeclSymbols, // v0.49 — show-mode symbol extraction
|
|
780
831
|
translateBreToRg, // v0.49 — BRE→rust-regex dialect bridge
|
|
781
832
|
buildShowDenyReason, // v0.49 — show-mode deny copy
|
|
@@ -119,10 +119,10 @@ code-graph-mcp ast-search "q" --type fn # 结构化筛选
|
|
|
119
119
|
code-graph-mcp map # 项目架构
|
|
120
120
|
code-graph-mcp overview src/mcp/ # 模块总览
|
|
121
121
|
code-graph-mcp callgraph SYMBOL # 调用图
|
|
122
|
-
code-graph-mcp impact SYMBOL #
|
|
122
|
+
code-graph-mcp impact SYMBOL # 影响面(--change-type ∈ signature|behavior|remove,默认 behavior)
|
|
123
123
|
code-graph-mcp show SYMBOL # 节点详情
|
|
124
|
-
code-graph-mcp refs SYMBOL --relation calls #
|
|
125
|
-
code-graph-mcp refs SYMBOL --min-confidence extracted #
|
|
124
|
+
code-graph-mcp refs SYMBOL --relation calls # --relation ∈ calls|imports|inherits|implements|references|all
|
|
125
|
+
code-graph-mcp refs SYMBOL --min-confidence extracted # ∈ extracted|inferred|ambiguous;extracted=只看精确边(callgraph/impact/trace 同款)
|
|
126
126
|
code-graph-mcp centrality # 架构咽喉(betweenness 桥节点;补 map 的 caller_count)
|
|
127
127
|
code-graph-mcp cycles # 循环导入依赖(文件级 import 环 / SCC)
|
|
128
128
|
code-graph-mcp surprising # 可疑跨模块耦合(低置信 + 跨模块 + sole-bridge 打分)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.0",
|
|
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": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"node": ">=16"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
40
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
42
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
43
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
39
|
+
"@sdsrs/code-graph-linux-x64": "0.96.0",
|
|
40
|
+
"@sdsrs/code-graph-linux-arm64": "0.96.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-x64": "0.96.0",
|
|
42
|
+
"@sdsrs/code-graph-darwin-arm64": "0.96.0",
|
|
43
|
+
"@sdsrs/code-graph-win32-x64": "0.96.0"
|
|
44
44
|
}
|
|
45
45
|
}
|