@sdsrs/code-graph 0.19.0 → 0.20.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.
|
@@ -28,12 +28,234 @@ function readAdoptedBy(filePath) {
|
|
|
28
28
|
// line is a structural fix, not a signal change. Decision table lives in the
|
|
29
29
|
// linked plugin_code_graph_mcp.md; this line is the router. Tag syntax
|
|
30
30
|
// `[tag1, tag2]` per spec for explicit keyword matching.
|
|
31
|
+
//
|
|
32
|
+
// Generic default — used when no project-type markers detected (e.g. /tmp,
|
|
33
|
+
// scratch dirs, mixed repos). Per-type variants live in `buildIndexLine` and
|
|
34
|
+
// are computed per-cwd at adopt + needsRefresh time. Adopted-project receives
|
|
35
|
+
// the typed variant; everyone else falls back to this canonical line.
|
|
31
36
|
const INDEX_LINE =
|
|
32
37
|
'- [code-graph-mcp](plugin_code_graph_mcp.md) ' +
|
|
33
38
|
'[impact, callgraph, refs, overview, semantic, ast-search, dead-code, similar, deps, trace] — ' +
|
|
34
39
|
'改 X 影响面/谁调用 X/X 被谁用/看 X 源码/Y 模块长啥样/概念查询 优先于 Grep;字面匹配走 Grep。' +
|
|
35
40
|
'核心 7(get_call_graph/module_overview/semantic_code_search/ast_search/find_references/get_ast_node/project_map)' +
|
|
36
41
|
'+ 进阶 5(impact_analysis/trace_http_chain/dependency_graph/find_similar_code/find_dead_code),决策表见全文';
|
|
42
|
+
|
|
43
|
+
// memdir L1 升格 (per sdscc 重构方案 §5.0): the INDEX_LINE that lands in
|
|
44
|
+
// MEMORY.md is what Claude sees first on every keyword match. Tailoring it
|
|
45
|
+
// per project type primes the right tools and demotes the irrelevant ones —
|
|
46
|
+
// e.g. a Rust CLI never benefits from `trace_http_chain` priming, and a React
|
|
47
|
+
// frontend cares more about `find_references` for rename audits than `impact`.
|
|
48
|
+
//
|
|
49
|
+
// Detection is cheap substring-on-marker (no AST, no graph): the cost is one
|
|
50
|
+
// fs.readFileSync per cwd. Failure mode is silent fall-back to 'generic' —
|
|
51
|
+
// false-negatives are strictly safer than false-positives that promote the
|
|
52
|
+
// wrong tool.
|
|
53
|
+
function readFileQuiet(p) {
|
|
54
|
+
try { return fs.readFileSync(p, 'utf8'); } catch { return ''; }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Valid project type buckets — also serves as the allow-list for
|
|
58
|
+
// `CODE_GRAPH_PROJECT_TYPE` env override. Anything not in this set falls back
|
|
59
|
+
// to file-based detection (so a typo'd env var does not silently break).
|
|
60
|
+
const PROJECT_TYPES = new Set([
|
|
61
|
+
'rust', 'web-rs', 'web-node', 'web-py', 'web-go',
|
|
62
|
+
'frontend', 'python', 'go', 'node', 'generic',
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
// Cargo.toml: strip `# ...` comment lines (line-leading or trailing). Then
|
|
66
|
+
// extract the contents of the [dependencies] section only — dev/build/target
|
|
67
|
+
// deps don't characterize the project's runtime web-vs-cli posture.
|
|
68
|
+
//
|
|
69
|
+
// Why a state machine and not a TOML parser: we have zero deps and don't
|
|
70
|
+
// want to add one for a coarse classification. This handles the >95% case
|
|
71
|
+
// (well-formed [dependencies] block); pathological TOML (e.g. inline-table
|
|
72
|
+
// dependencies) falls through to false-negative `rust` which is safer than
|
|
73
|
+
// false-positive `web-rs`.
|
|
74
|
+
function extractCargoRuntimeDeps(cargo) {
|
|
75
|
+
const lines = cargo.split(/\r?\n/);
|
|
76
|
+
const out = [];
|
|
77
|
+
let inDeps = false;
|
|
78
|
+
for (const raw of lines) {
|
|
79
|
+
// Strip `# comment` (line-leading or trailing). Inside string literals
|
|
80
|
+
// `#` is rare in Cargo dep specs; accept the false-strip risk.
|
|
81
|
+
const line = raw.replace(/(^|\s)#.*$/, '$1').trim();
|
|
82
|
+
if (line.startsWith('[')) {
|
|
83
|
+
// New section heading — only `[dependencies]` (canonical, exact match)
|
|
84
|
+
// gates web-framework detection. `[dev-dependencies]`,
|
|
85
|
+
// `[build-dependencies]`, `[target.'...'.dependencies]` deliberately
|
|
86
|
+
// skipped: a project that pulls in axum only for tests is not a web
|
|
87
|
+
// project for routing purposes.
|
|
88
|
+
inDeps = (line === '[dependencies]');
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (inDeps && line) out.push(line);
|
|
92
|
+
}
|
|
93
|
+
return out.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// pyproject.toml: same pattern as Cargo.toml — strip comments, scan only
|
|
97
|
+
// [tool.poetry.dependencies] (Poetry) or [project.dependencies] (PEP 621).
|
|
98
|
+
function extractPyRuntimeDeps(pyproj) {
|
|
99
|
+
const lines = pyproj.split(/\r?\n/);
|
|
100
|
+
const out = [];
|
|
101
|
+
let inDeps = false;
|
|
102
|
+
for (const raw of lines) {
|
|
103
|
+
const line = raw.replace(/(^|\s)#.*$/, '$1').trim();
|
|
104
|
+
if (line.startsWith('[')) {
|
|
105
|
+
inDeps = (
|
|
106
|
+
line === '[tool.poetry.dependencies]' ||
|
|
107
|
+
line === '[project.dependencies]' ||
|
|
108
|
+
line === '[project]' // PEP 621 inline `dependencies = [...]` lives here
|
|
109
|
+
);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (inDeps && line) out.push(line);
|
|
113
|
+
}
|
|
114
|
+
return out.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// go.mod: skip `// indirect` lines (transitive deps) and `// comment` lines.
|
|
118
|
+
// Direct require blocks are what matter for "is this a web service?" — a
|
|
119
|
+
// project that transitively pulls gin via a CLI dep is still a CLI.
|
|
120
|
+
function extractGoDirectRequires(gomod) {
|
|
121
|
+
const lines = gomod.split(/\r?\n/);
|
|
122
|
+
const out = [];
|
|
123
|
+
let inRequire = false;
|
|
124
|
+
for (const raw of lines) {
|
|
125
|
+
const trimmed = raw.trim();
|
|
126
|
+
if (trimmed.startsWith('//')) continue; // pure comment line
|
|
127
|
+
if (/\/\/\s*indirect\b/.test(raw)) continue; // indirect dep marker
|
|
128
|
+
if (trimmed === 'require (') { inRequire = true; continue; }
|
|
129
|
+
if (inRequire && trimmed === ')') { inRequire = false; continue; }
|
|
130
|
+
if (trimmed.startsWith('require ')) out.push(trimmed.slice(8).trim());
|
|
131
|
+
else if (inRequire && trimmed) out.push(trimmed);
|
|
132
|
+
}
|
|
133
|
+
return out.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function detectProjectType(cwd = process.cwd(), env = process.env) {
|
|
137
|
+
// 2D: env override beats file-based detection. Honors only valid bucket
|
|
138
|
+
// names; invalid override silently falls through to detection (avoids a
|
|
139
|
+
// typo'd env var silently classifying everything as 'generic'). Power
|
|
140
|
+
// users / CI can pin without touching the heuristics.
|
|
141
|
+
const override = env && env.CODE_GRAPH_PROJECT_TYPE;
|
|
142
|
+
if (override && PROJECT_TYPES.has(override)) {
|
|
143
|
+
return override;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cargo = readFileQuiet(path.join(cwd, 'Cargo.toml'));
|
|
147
|
+
if (cargo) {
|
|
148
|
+
const deps = extractCargoRuntimeDeps(cargo);
|
|
149
|
+
// Web-framework detection: match on the dep name token (start-of-line
|
|
150
|
+
// or after whitespace/quote) to avoid hits inside path strings or
|
|
151
|
+
// unrelated metadata. `hyper` deliberately omitted from web-rs — it is
|
|
152
|
+
// also commonly used as a plain HTTP client in CLI tools (false-positive
|
|
153
|
+
// risk too high). axum/actix-web/etc. are unambiguous server stacks.
|
|
154
|
+
if (/^(actix-web|axum|rocket|warp|poem|tide|salvo)\s*=/m.test(deps)) {
|
|
155
|
+
return 'web-rs';
|
|
156
|
+
}
|
|
157
|
+
return 'rust';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const pkgRaw = readFileQuiet(path.join(cwd, 'package.json'));
|
|
161
|
+
if (pkgRaw) {
|
|
162
|
+
let pkg = null;
|
|
163
|
+
try { pkg = JSON.parse(pkgRaw); } catch { /* malformed → fall through */ }
|
|
164
|
+
if (pkg && typeof pkg === 'object') {
|
|
165
|
+
// Only `dependencies` matters — devDependencies are build/test only,
|
|
166
|
+
// and a project with `react` only in devDependencies is likely a
|
|
167
|
+
// component library, not a frontend app.
|
|
168
|
+
const deps = pkg.dependencies && typeof pkg.dependencies === 'object'
|
|
169
|
+
? Object.keys(pkg.dependencies)
|
|
170
|
+
: [];
|
|
171
|
+
const has = (name) => deps.includes(name);
|
|
172
|
+
if (has('next') || has('react') || has('vue') || has('svelte') ||
|
|
173
|
+
has('@angular/core') || has('nuxt') || has('astro') ||
|
|
174
|
+
has('remix') || has('solid-js')) {
|
|
175
|
+
return 'frontend';
|
|
176
|
+
}
|
|
177
|
+
if (has('express') || has('fastify') || has('koa') || has('hono') ||
|
|
178
|
+
has('@nestjs/core') || has('@hapi/hapi')) {
|
|
179
|
+
return 'web-node';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return 'node';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const pyproj = readFileQuiet(path.join(cwd, 'pyproject.toml'));
|
|
186
|
+
if (pyproj) {
|
|
187
|
+
const deps = extractPyRuntimeDeps(pyproj);
|
|
188
|
+
if (/\b(django|flask|fastapi|starlette|sanic|tornado|quart)\b/i.test(deps)) {
|
|
189
|
+
return 'web-py';
|
|
190
|
+
}
|
|
191
|
+
return 'python';
|
|
192
|
+
}
|
|
193
|
+
// requirements.txt fallback: line-format `pkg==ver` / `pkg>=ver`. Strip
|
|
194
|
+
// `#` comment lines; scan remaining as a flat list (no section headers
|
|
195
|
+
// in this format).
|
|
196
|
+
const reqs = readFileQuiet(path.join(cwd, 'requirements.txt'));
|
|
197
|
+
if (reqs) {
|
|
198
|
+
const cleaned = reqs.split(/\r?\n/)
|
|
199
|
+
.map(l => l.replace(/(^|\s)#.*$/, '$1').trim())
|
|
200
|
+
.filter(Boolean)
|
|
201
|
+
.join('\n');
|
|
202
|
+
if (/^(django|flask|fastapi|starlette|sanic|tornado|quart)\b/im.test(cleaned)) {
|
|
203
|
+
return 'web-py';
|
|
204
|
+
}
|
|
205
|
+
return 'python';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const gomod = readFileQuiet(path.join(cwd, 'go.mod'));
|
|
209
|
+
if (gomod) {
|
|
210
|
+
const direct = extractGoDirectRequires(gomod);
|
|
211
|
+
if (/\b(gin-gonic|labstack\/echo|gofiber|go-chi|gorilla\/mux)\b/.test(direct)) {
|
|
212
|
+
return 'web-go';
|
|
213
|
+
}
|
|
214
|
+
return 'go';
|
|
215
|
+
}
|
|
216
|
+
return 'generic';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Build the MEMORY.md index line for a project type. The 'generic' bucket
|
|
220
|
+
// returns the canonical INDEX_LINE so untyped projects (and the existing
|
|
221
|
+
// adopt.test.js fixtures, which use empty tmp dirs) stay byte-identical.
|
|
222
|
+
//
|
|
223
|
+
// For typed projects, the difference from generic is the tag set + the lead
|
|
224
|
+
// sentence — body of plugin_code_graph_mcp.md is unchanged. Decision table
|
|
225
|
+
// stays one source of truth; the index line just primes which subset matters
|
|
226
|
+
// most for THIS project.
|
|
227
|
+
function buildIndexLine(projectType = 'generic') {
|
|
228
|
+
const prefix = '- [code-graph-mcp](plugin_code_graph_mcp.md) ';
|
|
229
|
+
const coreSuffix =
|
|
230
|
+
'核心 7(get_call_graph/module_overview/semantic_code_search/ast_search/find_references/get_ast_node/project_map)' +
|
|
231
|
+
'+ 进阶 5(impact_analysis/trace_http_chain/dependency_graph/find_similar_code/find_dead_code),决策表见全文';
|
|
232
|
+
switch (projectType) {
|
|
233
|
+
case 'web-rs':
|
|
234
|
+
case 'web-node':
|
|
235
|
+
case 'web-py':
|
|
236
|
+
case 'web-go':
|
|
237
|
+
return prefix +
|
|
238
|
+
'[trace, route, callgraph, impact, refs, overview, semantic, deps] — ' +
|
|
239
|
+
'HTTP 路由→handler 链路用 trace_http_chain(或 get_call_graph route_path=);改 handler 影响面用 impact;' +
|
|
240
|
+
'其他结构化查询同上 优先于 Grep。' + coreSuffix;
|
|
241
|
+
case 'frontend':
|
|
242
|
+
return prefix +
|
|
243
|
+
'[refs, overview, semantic, callgraph, impact, ast-search] — ' +
|
|
244
|
+
'组件重命名/重构用 find_references(含 imports/inherits);模块层级用 module_overview;' +
|
|
245
|
+
'改 props/接口前用 impact 看下游;HTTP route 通常不适用。' + coreSuffix;
|
|
246
|
+
case 'rust':
|
|
247
|
+
case 'go':
|
|
248
|
+
case 'python':
|
|
249
|
+
case 'node':
|
|
250
|
+
return prefix +
|
|
251
|
+
'[callgraph, impact, refs, overview, semantic, ast-search, dead-code, deps] — ' +
|
|
252
|
+
'改 X 影响面/谁调用 X/Y 模块 优先于 Grep;HTTP route 追踪通常不适用(无 web 框架);' +
|
|
253
|
+
'字面匹配走 Grep。' + coreSuffix;
|
|
254
|
+
case 'generic':
|
|
255
|
+
default:
|
|
256
|
+
return INDEX_LINE;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
37
259
|
const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
|
|
38
260
|
const TARGET_NAME = 'plugin_code_graph_mcp.md';
|
|
39
261
|
|
|
@@ -126,7 +348,11 @@ function adopt({ cwd, home, templatePath } = {}) {
|
|
|
126
348
|
|
|
127
349
|
const indexPath = path.join(dir, 'MEMORY.md');
|
|
128
350
|
const index = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n';
|
|
129
|
-
|
|
351
|
+
// Per-project index line: tagged tools + lead sentence tailored to the
|
|
352
|
+
// detected project type. Falls back to the canonical INDEX_LINE for
|
|
353
|
+
// generic / untyped cwds (preserves byte-identity with prior versions).
|
|
354
|
+
const indexLine = buildIndexLine(detectProjectType(effectiveCwd));
|
|
355
|
+
const desiredBlock = `${SENTINEL_BEGIN}\n${indexLine}\n${SENTINEL_END}`;
|
|
130
356
|
|
|
131
357
|
// Already-adopted-and-well-formed: skip the write entirely.
|
|
132
358
|
if (index.includes(desiredBlock)) {
|
|
@@ -175,7 +401,13 @@ function needsRefresh({ cwd, home, templatePath } = {}) {
|
|
|
175
401
|
}
|
|
176
402
|
if (!shipped.equals(body)) return true;
|
|
177
403
|
const index = fs.readFileSync(indexPath, 'utf8');
|
|
178
|
-
|
|
404
|
+
// Compare against the typed INDEX_LINE for this project. Detection is
|
|
405
|
+
// deterministic (file-existence + substring scan) so adopt and needsRefresh
|
|
406
|
+
// always agree on the variant. Drift triggers refresh — including when a
|
|
407
|
+
// project gains a web framework dep and switches type bucket.
|
|
408
|
+
const effectiveCwd = cwd || process.cwd();
|
|
409
|
+
const indexLine = buildIndexLine(detectProjectType(effectiveCwd));
|
|
410
|
+
const desiredBlock = `${SENTINEL_BEGIN}\n${indexLine}\n${SENTINEL_END}`;
|
|
179
411
|
return !index.includes(desiredBlock);
|
|
180
412
|
}
|
|
181
413
|
|
|
@@ -294,6 +526,8 @@ if (require.main === module) {
|
|
|
294
526
|
module.exports = {
|
|
295
527
|
adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
|
|
296
528
|
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
|
|
529
|
+
detectProjectType, buildIndexLine,
|
|
530
|
+
extractCargoRuntimeDeps, extractPyRuntimeDeps, extractGoDirectRequires,
|
|
297
531
|
SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
|
|
298
|
-
PROJECT_MARKERS,
|
|
532
|
+
PROJECT_MARKERS, PROJECT_TYPES,
|
|
299
533
|
};
|
|
@@ -7,6 +7,7 @@ const os = require('os');
|
|
|
7
7
|
const {
|
|
8
8
|
adopt, unadopt, memoryDir, stripSentinelBlock,
|
|
9
9
|
isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh, isProjectRoot,
|
|
10
|
+
detectProjectType, buildIndexLine,
|
|
10
11
|
SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
|
|
11
12
|
PROJECT_MARKERS,
|
|
12
13
|
} = require('./adopt');
|
|
@@ -585,3 +586,234 @@ test('needsRefresh ignores the adopted-by marker when bytewise comparing', () =>
|
|
|
585
586
|
'needsRefresh should be false right after adopt — marker must not trigger drift');
|
|
586
587
|
} finally { sb.cleanup(); }
|
|
587
588
|
});
|
|
589
|
+
|
|
590
|
+
// memdir L1 升格 — project-typed INDEX_LINE coverage.
|
|
591
|
+
|
|
592
|
+
test('detectProjectType returns generic for an empty cwd', () => {
|
|
593
|
+
const sb = makeSandbox();
|
|
594
|
+
try {
|
|
595
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'generic');
|
|
596
|
+
} finally { sb.cleanup(); }
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test('detectProjectType returns rust for a Cargo.toml without web framework', () => {
|
|
600
|
+
const sb = makeSandbox();
|
|
601
|
+
try {
|
|
602
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[package]\nname="x"\n[dependencies]\nserde="1"\n');
|
|
603
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'rust');
|
|
604
|
+
} finally { sb.cleanup(); }
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test('detectProjectType returns web-rs when Cargo.toml has axum/actix/etc', () => {
|
|
608
|
+
const sb = makeSandbox();
|
|
609
|
+
try {
|
|
610
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[dependencies]\naxum = "0.7"\n');
|
|
611
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'web-rs');
|
|
612
|
+
} finally { sb.cleanup(); }
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test('detectProjectType returns frontend for package.json with React/Next/Vue', () => {
|
|
616
|
+
const sb = makeSandbox();
|
|
617
|
+
try {
|
|
618
|
+
fs.writeFileSync(path.join(sb.cwd, 'package.json'),
|
|
619
|
+
'{"dependencies":{"next":"^14","react":"^18"}}');
|
|
620
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'frontend');
|
|
621
|
+
} finally { sb.cleanup(); }
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
test('detectProjectType returns web-node for package.json with express/fastify', () => {
|
|
625
|
+
const sb = makeSandbox();
|
|
626
|
+
try {
|
|
627
|
+
fs.writeFileSync(path.join(sb.cwd, 'package.json'),
|
|
628
|
+
'{"dependencies":{"express":"^4"}}');
|
|
629
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'web-node');
|
|
630
|
+
} finally { sb.cleanup(); }
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('detectProjectType returns web-py for FastAPI in pyproject.toml', () => {
|
|
634
|
+
const sb = makeSandbox();
|
|
635
|
+
try {
|
|
636
|
+
fs.writeFileSync(path.join(sb.cwd, 'pyproject.toml'),
|
|
637
|
+
'[tool.poetry.dependencies]\nfastapi = "^0.115"\n');
|
|
638
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'web-py');
|
|
639
|
+
} finally { sb.cleanup(); }
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test('buildIndexLine generic returns the canonical INDEX_LINE byte-for-byte', () => {
|
|
643
|
+
// Critical: keeps backward compatibility with adopted projects that have no
|
|
644
|
+
// markers. Any drift here invalidates needsRefresh's idempotency assumption.
|
|
645
|
+
assert.strictEqual(buildIndexLine('generic'), INDEX_LINE);
|
|
646
|
+
assert.strictEqual(buildIndexLine(undefined), INDEX_LINE);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
test('buildIndexLine web-rs prepends route/trace tags + handler-focused lead', () => {
|
|
650
|
+
const line = buildIndexLine('web-rs');
|
|
651
|
+
assert.match(line, /\[trace, route,/, 'web-rs index line should lead with trace/route tags');
|
|
652
|
+
assert.match(line, /HTTP 路由/, 'lead sentence should mention HTTP routes');
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test('buildIndexLine frontend emphasizes refs/overview, drops HTTP route priming', () => {
|
|
656
|
+
const line = buildIndexLine('frontend');
|
|
657
|
+
assert.match(line, /组件重命名|find_references/, 'frontend should mention rename audit / refs');
|
|
658
|
+
assert.match(line, /HTTP route 通常不适用/, 'frontend should explicitly demote HTTP route tracing');
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
test('adopt + needsRefresh agree on typed INDEX_LINE — no spurious refresh in a Rust project', () => {
|
|
662
|
+
// The detection function is deterministic + adopt and needsRefresh both call
|
|
663
|
+
// it; together they must produce a consistent indexLine, otherwise every
|
|
664
|
+
// SessionStart triggers a rewrite.
|
|
665
|
+
const sb = makeSandbox();
|
|
666
|
+
try {
|
|
667
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[package]\nname="x"\n');
|
|
668
|
+
adopt({ cwd: sb.cwd, home: sb.home });
|
|
669
|
+
assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), false,
|
|
670
|
+
'needsRefresh must be false right after adopt for a Rust project');
|
|
671
|
+
const indexPath = path.join(sb.dir, 'MEMORY.md');
|
|
672
|
+
const index = fs.readFileSync(indexPath, 'utf8');
|
|
673
|
+
assert.ok(index.includes('优先于 Grep'),
|
|
674
|
+
'MEMORY.md should contain the rust-typed index line');
|
|
675
|
+
} finally { sb.cleanup(); }
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// 2A — false-positive hardening: comment-strip + section-aware scan.
|
|
679
|
+
|
|
680
|
+
test('detectProjectType ignores commented-out web-framework deps in Cargo.toml', () => {
|
|
681
|
+
// Pre-fix: `# axum = "0.7"` substring-matched and falsely promoted to web-rs.
|
|
682
|
+
// Post-fix: comment stripping happens before section scan.
|
|
683
|
+
const sb = makeSandbox();
|
|
684
|
+
try {
|
|
685
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'),
|
|
686
|
+
'[package]\nname="x"\n[dependencies]\n# axum = "0.7" # disabled, was for prototype\nserde = "1"\n');
|
|
687
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'rust',
|
|
688
|
+
'commented dep must not promote to web-rs');
|
|
689
|
+
} finally { sb.cleanup(); }
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test('detectProjectType ignores axum in [dev-dependencies] only', () => {
|
|
693
|
+
// axum used solely for tests does not make this a web project.
|
|
694
|
+
const sb = makeSandbox();
|
|
695
|
+
try {
|
|
696
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'),
|
|
697
|
+
'[package]\nname="x"\n[dependencies]\nserde = "1"\n[dev-dependencies]\naxum = "0.7"\n');
|
|
698
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'rust',
|
|
699
|
+
'axum in dev-dependencies must not promote to web-rs');
|
|
700
|
+
} finally { sb.cleanup(); }
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
test('detectProjectType ignores axum in [build-dependencies] only', () => {
|
|
704
|
+
const sb = makeSandbox();
|
|
705
|
+
try {
|
|
706
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'),
|
|
707
|
+
'[package]\nname="x"\n[dependencies]\nserde = "1"\n[build-dependencies]\naxum = "0.7"\n');
|
|
708
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'rust',
|
|
709
|
+
'axum in build-dependencies must not promote to web-rs');
|
|
710
|
+
} finally { sb.cleanup(); }
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
test('detectProjectType ignores react in devDependencies of package.json', () => {
|
|
714
|
+
// A library that lists react in devDependencies for testing should not
|
|
715
|
+
// be classified as a frontend app.
|
|
716
|
+
const sb = makeSandbox();
|
|
717
|
+
try {
|
|
718
|
+
fs.writeFileSync(path.join(sb.cwd, 'package.json'),
|
|
719
|
+
JSON.stringify({
|
|
720
|
+
dependencies: { lodash: '^4' },
|
|
721
|
+
devDependencies: { react: '^18', 'react-dom': '^18' },
|
|
722
|
+
}));
|
|
723
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'node',
|
|
724
|
+
'react in devDependencies must not promote to frontend');
|
|
725
|
+
} finally { sb.cleanup(); }
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test('detectProjectType ignores // indirect deps in go.mod', () => {
|
|
729
|
+
const sb = makeSandbox();
|
|
730
|
+
try {
|
|
731
|
+
fs.writeFileSync(path.join(sb.cwd, 'go.mod'),
|
|
732
|
+
'module example.com/x\n\nrequire (\n\tgithub.com/some/cli v1.0.0\n\tgithub.com/gin-gonic/gin v1.9.0 // indirect\n)\n');
|
|
733
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'go',
|
|
734
|
+
'indirect gin must not promote to web-go');
|
|
735
|
+
} finally { sb.cleanup(); }
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test('detectProjectType handles malformed package.json without throwing', () => {
|
|
739
|
+
// JSON.parse failure must not crash detection; falls back to 'node' since
|
|
740
|
+
// package.json exists but is unreadable.
|
|
741
|
+
const sb = makeSandbox();
|
|
742
|
+
try {
|
|
743
|
+
fs.writeFileSync(path.join(sb.cwd, 'package.json'), '{not valid json');
|
|
744
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'node',
|
|
745
|
+
'malformed package.json should fall back to node bucket');
|
|
746
|
+
} finally { sb.cleanup(); }
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test('detectProjectType detects PEP 621 [project] dependencies block', () => {
|
|
750
|
+
// PEP 621 puts `dependencies = [...]` inside [project], not a separate
|
|
751
|
+
// [project.dependencies] section — our state machine accepts both.
|
|
752
|
+
const sb = makeSandbox();
|
|
753
|
+
try {
|
|
754
|
+
fs.writeFileSync(path.join(sb.cwd, 'pyproject.toml'),
|
|
755
|
+
'[project]\nname = "x"\ndependencies = ["fastapi>=0.115", "uvicorn"]\n');
|
|
756
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'web-py');
|
|
757
|
+
} finally { sb.cleanup(); }
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test('detectProjectType reads requirements.txt as fallback', () => {
|
|
761
|
+
const sb = makeSandbox();
|
|
762
|
+
try {
|
|
763
|
+
fs.writeFileSync(path.join(sb.cwd, 'requirements.txt'),
|
|
764
|
+
'# web stack\nflask>=3.0\ngunicorn\n');
|
|
765
|
+
assert.strictEqual(detectProjectType(sb.cwd), 'web-py');
|
|
766
|
+
} finally { sb.cleanup(); }
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
// 2D — env override.
|
|
770
|
+
|
|
771
|
+
test('CODE_GRAPH_PROJECT_TYPE env override beats file-based detection', () => {
|
|
772
|
+
const sb = makeSandbox();
|
|
773
|
+
try {
|
|
774
|
+
// Cargo.toml says rust — env says web-rs. Env wins.
|
|
775
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[package]\nname="x"\n');
|
|
776
|
+
assert.strictEqual(
|
|
777
|
+
detectProjectType(sb.cwd, { CODE_GRAPH_PROJECT_TYPE: 'web-rs' }),
|
|
778
|
+
'web-rs',
|
|
779
|
+
);
|
|
780
|
+
} finally { sb.cleanup(); }
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test('CODE_GRAPH_PROJECT_TYPE env override falls through on invalid value', () => {
|
|
784
|
+
// Typo'd / unknown bucket name should not silently classify everything as
|
|
785
|
+
// 'generic' — fall through to file-based detection so the project still
|
|
786
|
+
// gets a meaningful index line.
|
|
787
|
+
const sb = makeSandbox();
|
|
788
|
+
try {
|
|
789
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[package]\nname="x"\n');
|
|
790
|
+
assert.strictEqual(
|
|
791
|
+
detectProjectType(sb.cwd, { CODE_GRAPH_PROJECT_TYPE: 'web-rust' /* typo */ }),
|
|
792
|
+
'rust',
|
|
793
|
+
'invalid override must fall through to file detection',
|
|
794
|
+
);
|
|
795
|
+
} finally { sb.cleanup(); }
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
test('CODE_GRAPH_PROJECT_TYPE env override unset uses file detection', () => {
|
|
799
|
+
// Empty env reaches file-based detection unchanged.
|
|
800
|
+
const sb = makeSandbox();
|
|
801
|
+
try {
|
|
802
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'), '[package]\nname="x"\n');
|
|
803
|
+
assert.strictEqual(detectProjectType(sb.cwd, {}), 'rust');
|
|
804
|
+
} finally { sb.cleanup(); }
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
test('CODE_GRAPH_PROJECT_TYPE env override forces generic in a Rust project', () => {
|
|
808
|
+
// Power-user case: explicit opt-out of typed routing for a project that
|
|
809
|
+
// would otherwise be auto-classified.
|
|
810
|
+
const sb = makeSandbox();
|
|
811
|
+
try {
|
|
812
|
+
fs.writeFileSync(path.join(sb.cwd, 'Cargo.toml'),
|
|
813
|
+
'[package]\nname="x"\n[dependencies]\naxum = "0.7"\n');
|
|
814
|
+
assert.strictEqual(
|
|
815
|
+
detectProjectType(sb.cwd, { CODE_GRAPH_PROJECT_TYPE: 'generic' }),
|
|
816
|
+
'generic',
|
|
817
|
+
);
|
|
818
|
+
} finally { sb.cleanup(); }
|
|
819
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"node": ">=16"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@sdsrs/code-graph-linux-x64": "0.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.20.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.20.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.20.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.20.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.20.0"
|
|
43
43
|
}
|
|
44
44
|
}
|