@sdsrs/code-graph 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/adopt.js +237 -3
- package/claude-plugin/scripts/adopt.test.js +232 -0
- package/claude-plugin/scripts/incremental-index.js +38 -9
- package/claude-plugin/scripts/incremental-index.test.js +55 -0
- package/claude-plugin/scripts/user-prompt-context.js +270 -60
- package/claude-plugin/scripts/user-prompt-context.test.js +91 -0
- package/package.json +6 -6
|
@@ -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
|
+
});
|
|
@@ -3,12 +3,41 @@
|
|
|
3
3
|
const { execFileSync } = require('child_process');
|
|
4
4
|
const { findBinary } = require('./find-binary');
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
// v0.21 — gated default-off. v0.18.0 added query-time freshness
|
|
7
|
+
// (ensure_file_indexed) inside MCP tools that take a file_path arg, so a
|
|
8
|
+
// PostToolUse hook spawning a fresh process on every Edit/Write was redundant
|
|
9
|
+
// for the MCP-driven workflow and just burnt ~80ms cold-start per edit.
|
|
10
|
+
//
|
|
11
|
+
// CLI-only workflows (running `code-graph-mcp search` after Bash-side edits
|
|
12
|
+
// without going through MCP) need the hook to keep the DB fresh, so the knob
|
|
13
|
+
// lets users opt back in.
|
|
14
|
+
//
|
|
15
|
+
// Priority (high → low):
|
|
16
|
+
// 1. CODE_GRAPH_HOOK_INDEX=on → run the hook (opt-in)
|
|
17
|
+
// 2. CODE_GRAPH_HOOK_INDEX=off → skip
|
|
18
|
+
// 3. default → skip (v0.21 flip)
|
|
19
|
+
function shouldRun(env = process.env) {
|
|
20
|
+
const v = (env.CODE_GRAPH_HOOK_INDEX || '').toLowerCase();
|
|
21
|
+
if (v === 'on' || v === '1' || v === 'true') return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runMain() {
|
|
26
|
+
if (!shouldRun()) return;
|
|
27
|
+
|
|
28
|
+
const bin = findBinary();
|
|
29
|
+
if (!bin) return; // silent — binary not installed yet
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
execFileSync(bin, ['incremental-index', '--quiet'], {
|
|
33
|
+
timeout: 8000,
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
35
|
+
});
|
|
36
|
+
} catch { /* timeout or error — silent for hook */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (require.main === module) {
|
|
40
|
+
runMain();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = { shouldRun };
|
|
@@ -7,6 +7,61 @@ const os = require('os');
|
|
|
7
7
|
const { spawnSync } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const { findBinary } = require('./find-binary');
|
|
10
|
+
const { shouldRun } = require('./incremental-index');
|
|
11
|
+
|
|
12
|
+
// ── shouldRun gate (v0.21 opt-in flip) ──────────────────
|
|
13
|
+
|
|
14
|
+
test('shouldRun: default (no env) is OFF', () => {
|
|
15
|
+
// v0.21: hook-driven incremental-index is opt-in. Rely on
|
|
16
|
+
// ensure_file_indexed query-time freshness for MCP workflows.
|
|
17
|
+
assert.equal(shouldRun({}), false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=on enables hook (opt-in)', () => {
|
|
21
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'on' }), true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=1 enables hook (truthy alias)', () => {
|
|
25
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: '1' }), true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=true enables hook (truthy alias)', () => {
|
|
29
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'true' }), true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX=off keeps hook off (explicit)', () => {
|
|
33
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'off' }), false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX with empty string is OFF', () => {
|
|
37
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: '' }), false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('shouldRun: CODE_GRAPH_HOOK_INDEX is case-insensitive', () => {
|
|
41
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'ON' }), true);
|
|
42
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'On' }), true);
|
|
43
|
+
assert.equal(shouldRun({ CODE_GRAPH_HOOK_INDEX: 'TRUE' }), true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('hook script default-env spawn does not invoke the binary (default OFF)', () => {
|
|
47
|
+
// End-to-end check: with the v0.21 opt-in flip, a default-env spawn of
|
|
48
|
+
// incremental-index.js exits 0 immediately without touching the binary.
|
|
49
|
+
const script = path.join(__dirname, 'incremental-index.js');
|
|
50
|
+
const cleanEnv = { ...process.env };
|
|
51
|
+
delete cleanEnv.CODE_GRAPH_HOOK_INDEX;
|
|
52
|
+
const t0 = Date.now();
|
|
53
|
+
const proc = spawnSync(process.execPath, [script], {
|
|
54
|
+
env: cleanEnv,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
timeout: 2000,
|
|
57
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
58
|
+
});
|
|
59
|
+
// Should be much faster than the 80ms+ cold-start of running the binary.
|
|
60
|
+
// 500ms is generous — actual is ~30-50ms node startup.
|
|
61
|
+
assert.equal(proc.status, 0, `expected exit 0, got ${proc.status}; stderr: ${proc.stderr}`);
|
|
62
|
+
assert.equal(proc.stdout, '', 'stdout must be empty when default-OFF');
|
|
63
|
+
assert.ok(Date.now() - t0 < 500, 'default-OFF must short-circuit fast (< 500ms)');
|
|
64
|
+
});
|
|
10
65
|
|
|
11
66
|
test('incremental-index bails silently when cwd is not a git repo', (t) => {
|
|
12
67
|
const bin = findBinary();
|
|
@@ -8,28 +8,8 @@ const fs = require('fs');
|
|
|
8
8
|
const path = require('path');
|
|
9
9
|
const os = require('os');
|
|
10
10
|
|
|
11
|
-
//
|
|
12
|
-
// If hooks are running but lifecycle install() hasn't executed yet (no manifest),
|
|
13
|
-
// the plugin was installed mid-session and the MCP server isn't connected.
|
|
14
|
-
// Claude Code only starts MCP servers at session startup; /mcp reconnect cannot
|
|
15
|
-
// start servers that were never initialized.
|
|
11
|
+
// Mid-session install detection: hook fires but no manifest yet.
|
|
16
12
|
const MANIFEST_PATH = path.join(os.homedir(), '.cache', 'code-graph', 'install-manifest.json');
|
|
17
|
-
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
18
|
-
const noticeFile = path.join(os.tmpdir(), '.code-graph-mcp-restart-notice');
|
|
19
|
-
try {
|
|
20
|
-
// Show once per hour to avoid spam
|
|
21
|
-
if (Date.now() - fs.statSync(noticeFile).mtimeMs < 3600000) process.exit(0);
|
|
22
|
-
} catch { /* first notice */ }
|
|
23
|
-
try { fs.writeFileSync(noticeFile, ''); } catch { /* ok */ }
|
|
24
|
-
process.stdout.write(
|
|
25
|
-
'[code-graph] Plugin installed — MCP server requires a session restart to connect.\n' +
|
|
26
|
-
'MCP servers are only initialized at session startup. To activate:\n' +
|
|
27
|
-
' 1. Press Ctrl+C to exit the current session\n' +
|
|
28
|
-
' 2. Re-run `claude` to start a new session\n' +
|
|
29
|
-
'Meanwhile, CLI tools work directly: code-graph-mcp search <query>, code-graph-mcp map, etc.\n'
|
|
30
|
-
);
|
|
31
|
-
process.exit(0);
|
|
32
|
-
}
|
|
33
13
|
|
|
34
14
|
// --- Per-type rate limiting (replaces single global cooldown) ---
|
|
35
15
|
const COOLDOWNS = {
|
|
@@ -53,25 +33,24 @@ function markCooldown(type) {
|
|
|
53
33
|
} catch { /* ok */ }
|
|
54
34
|
}
|
|
55
35
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
36
|
+
// v0.21 — flipped to opt-in default. Routing-bench backend P@1=100% (v0.20.0)
|
|
37
|
+
// proves Sonnet 4.5 picks tools correctly without push injection; per-prompt
|
|
38
|
+
// CLI exec was costing 200-500 tokens/turn across N turns to repeat what the
|
|
39
|
+
// agent would have called itself. Mirrors session-init.js computeQuietHooks
|
|
40
|
+
// priority chain so a single env knob covers both hooks.
|
|
41
|
+
//
|
|
42
|
+
// Priority (high → low):
|
|
43
|
+
// 1. CODE_GRAPH_QUIET_HOOKS=0 → forced noisy (legacy back-compat)
|
|
44
|
+
// 2. CODE_GRAPH_QUIET_HOOKS=1 → forced quiet (legacy back-compat)
|
|
45
|
+
// 3. CODE_GRAPH_VERBOSE_HOOKS=1 → opt-in noisy (new, recommended)
|
|
46
|
+
// 4. default → quiet
|
|
47
|
+
function computeQuietHooks(env = process.env) {
|
|
48
|
+
const envQuiet = env.CODE_GRAPH_QUIET_HOOKS;
|
|
49
|
+
if (envQuiet === '0') return false;
|
|
50
|
+
if (envQuiet === '1') return true;
|
|
51
|
+
if (env.CODE_GRAPH_VERBOSE_HOOKS === '1') return false;
|
|
52
|
+
return true;
|
|
67
53
|
}
|
|
68
|
-
// Chinese chars are ~3 bytes but 1 char; "看看 fts5_search" is only 16 chars
|
|
69
|
-
if (!message || message.length < 8) process.exit(0);
|
|
70
|
-
|
|
71
|
-
// --- Check index ---
|
|
72
|
-
const cwd = process.cwd();
|
|
73
|
-
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
74
|
-
if (!fs.existsSync(dbPath)) process.exit(0);
|
|
75
54
|
|
|
76
55
|
// --- Pure logic (exported for testing) ---
|
|
77
56
|
|
|
@@ -121,14 +100,203 @@ function extractSymbols(msg) {
|
|
|
121
100
|
return { symbols: candidates, lowConfidence };
|
|
122
101
|
}
|
|
123
102
|
|
|
103
|
+
// v0.21 — replaced 6 mixed-language regex piles with per-keyword weighted
|
|
104
|
+
// patterns. Each row is testable in isolation, weights ready for tuning when
|
|
105
|
+
// false-positive data accumulates. Threshold 0.5 + uniform weight 1.0
|
|
106
|
+
// preserves the original OR-of-alternatives behavior 1:1; future tuning can
|
|
107
|
+
// downweight noisy short keywords like "bug" or "什么" that currently fire
|
|
108
|
+
// too eagerly. Maintenance cost: ~150 lines of table vs 6 × 200-char regex —
|
|
109
|
+
// regression history (#5754, #7713) shows the regex form was the higher
|
|
110
|
+
// silent-bug surface.
|
|
111
|
+
const INTENT_PATTERNS = {
|
|
112
|
+
impact: [
|
|
113
|
+
[/impact/i, 1.0],
|
|
114
|
+
[/影响/, 1.0],
|
|
115
|
+
[/修改前/, 1.0],
|
|
116
|
+
[/改之前/, 1.0],
|
|
117
|
+
[/blast radius/i, 1.0],
|
|
118
|
+
[/before (?:edit|chang|modif)/i, 1.0],
|
|
119
|
+
[/risk/i, 1.0],
|
|
120
|
+
[/风险/, 1.0],
|
|
121
|
+
[/改动范围/, 1.0],
|
|
122
|
+
[/波及/, 1.0],
|
|
123
|
+
[/问题在/, 1.0],
|
|
124
|
+
[/bug/i, 1.0],
|
|
125
|
+
[/干扰/, 1.0],
|
|
126
|
+
[/冲突/, 1.0],
|
|
127
|
+
[/卡/, 1.0],
|
|
128
|
+
],
|
|
129
|
+
modify: [
|
|
130
|
+
[/改(?!变)/, 1.0],
|
|
131
|
+
[/修改/, 1.0],
|
|
132
|
+
[/修复/, 1.0],
|
|
133
|
+
[/重构/, 1.0],
|
|
134
|
+
[/优化/, 1.0],
|
|
135
|
+
[/简化/, 1.0],
|
|
136
|
+
[/精简/, 1.0],
|
|
137
|
+
[/适配/, 1.0],
|
|
138
|
+
[/统一/, 1.0],
|
|
139
|
+
[/修正/, 1.0],
|
|
140
|
+
[/调整/, 1.0],
|
|
141
|
+
[/去掉/, 1.0],
|
|
142
|
+
[/整理/, 1.0],
|
|
143
|
+
[/清理/, 1.0],
|
|
144
|
+
[/解耦/, 1.0],
|
|
145
|
+
[/更新/, 1.0],
|
|
146
|
+
[/\brefactor\b/i, 1.0],
|
|
147
|
+
[/\bchange\b/i, 1.0],
|
|
148
|
+
[/\brename\b/i, 1.0],
|
|
149
|
+
[/\bfix\b/i, 1.0],
|
|
150
|
+
[/移动/, 1.0],
|
|
151
|
+
[/\bmove\b/i, 1.0],
|
|
152
|
+
[/删(?!除文件)/, 1.0],
|
|
153
|
+
[/\bremove\b/i, 1.0],
|
|
154
|
+
[/替换/, 1.0],
|
|
155
|
+
[/\breplace\b/i, 1.0],
|
|
156
|
+
[/\bupdate\b/i, 1.0],
|
|
157
|
+
[/升级/, 1.0],
|
|
158
|
+
[/\bmigrate\b/i, 1.0],
|
|
159
|
+
[/迁移/, 1.0],
|
|
160
|
+
[/拆分/, 1.0],
|
|
161
|
+
[/\bsplit\b/i, 1.0],
|
|
162
|
+
[/合并/, 1.0],
|
|
163
|
+
[/\bmerge\b/i, 1.0],
|
|
164
|
+
[/提取/, 1.0],
|
|
165
|
+
[/\bextract\b/i, 1.0],
|
|
166
|
+
[/改成/, 1.0],
|
|
167
|
+
[/改为/, 1.0],
|
|
168
|
+
[/换成/, 1.0],
|
|
169
|
+
[/转为/, 1.0],
|
|
170
|
+
[/异步/, 1.0],
|
|
171
|
+
[/同步/, 1.0],
|
|
172
|
+
],
|
|
173
|
+
implement: [
|
|
174
|
+
[/\badd\b/i, 1.0],
|
|
175
|
+
[/\bimplement\b/i, 1.0],
|
|
176
|
+
[/\bcreate\b/i, 1.0],
|
|
177
|
+
[/\bbuild\b/i, 1.0],
|
|
178
|
+
[/\bwrite\b/i, 1.0],
|
|
179
|
+
[/新增/, 1.0],
|
|
180
|
+
[/添加/, 1.0],
|
|
181
|
+
[/实现/, 1.0],
|
|
182
|
+
[/创建/, 1.0],
|
|
183
|
+
[/编写/, 1.0],
|
|
184
|
+
[/开发/, 1.0],
|
|
185
|
+
[/增加/, 1.0],
|
|
186
|
+
[/加上/, 1.0],
|
|
187
|
+
[/加个/, 1.0],
|
|
188
|
+
[/写/, 1.0],
|
|
189
|
+
[/做个/, 1.0],
|
|
190
|
+
[/搭建/, 1.0],
|
|
191
|
+
[/补充/, 1.0],
|
|
192
|
+
[/引入/, 1.0],
|
|
193
|
+
[/支持/, 1.0],
|
|
194
|
+
[/封装/, 1.0],
|
|
195
|
+
[/接入/, 1.0],
|
|
196
|
+
[/对接/, 1.0],
|
|
197
|
+
[/配置/, 1.0],
|
|
198
|
+
],
|
|
199
|
+
understand: [
|
|
200
|
+
[/how does/i, 1.0],
|
|
201
|
+
[/怎么工作/, 1.0],
|
|
202
|
+
[/怎么实现/, 1.0],
|
|
203
|
+
[/怎么做/, 1.0],
|
|
204
|
+
[/什么/, 1.0],
|
|
205
|
+
[/理解/, 1.0],
|
|
206
|
+
[/看看/, 1.0],
|
|
207
|
+
[/看一下/, 1.0],
|
|
208
|
+
[/了解/, 1.0],
|
|
209
|
+
[/分析/, 1.0],
|
|
210
|
+
[/explain/i, 1.0],
|
|
211
|
+
[/understand/i, 1.0],
|
|
212
|
+
[/架构/, 1.0],
|
|
213
|
+
[/architecture/i, 1.0],
|
|
214
|
+
[/structure/i, 1.0],
|
|
215
|
+
[/overview/i, 1.0],
|
|
216
|
+
[/模块/, 1.0],
|
|
217
|
+
[/概览/, 1.0],
|
|
218
|
+
[/干什么/, 1.0],
|
|
219
|
+
[/做什么/, 1.0],
|
|
220
|
+
[/工作原理/, 1.0],
|
|
221
|
+
[/逻辑/, 1.0],
|
|
222
|
+
[/机制/, 1.0],
|
|
223
|
+
[/流程/, 1.0],
|
|
224
|
+
[/功能/, 1.0],
|
|
225
|
+
[/结合度/, 1.0],
|
|
226
|
+
[/效率/, 1.0],
|
|
227
|
+
[/评估/, 1.0],
|
|
228
|
+
[/调研/, 1.0],
|
|
229
|
+
[/是什么/, 1.0],
|
|
230
|
+
[/有什么/, 1.0],
|
|
231
|
+
[/能用不/, 1.0],
|
|
232
|
+
[/高效不/, 1.0],
|
|
233
|
+
[/达标/, 1.0],
|
|
234
|
+
[/起作用/, 1.0],
|
|
235
|
+
[/科学/, 1.0],
|
|
236
|
+
[/深入思考/, 1.0],
|
|
237
|
+
[/源码/, 1.0],
|
|
238
|
+
[/检查/, 1.0],
|
|
239
|
+
[/审核/, 1.0],
|
|
240
|
+
[/审查/, 1.0],
|
|
241
|
+
[/验证/, 1.0],
|
|
242
|
+
[/诊断/, 1.0],
|
|
243
|
+
],
|
|
244
|
+
callgraph: [
|
|
245
|
+
[/who calls/i, 1.0],
|
|
246
|
+
[/what calls/i, 1.0],
|
|
247
|
+
[/调用/, 1.0],
|
|
248
|
+
[/call(?:graph|er|ee)/i, 1.0],
|
|
249
|
+
[/trace/i, 1.0],
|
|
250
|
+
[/链路/, 1.0],
|
|
251
|
+
[/追踪/, 1.0],
|
|
252
|
+
[/谁调/, 1.0],
|
|
253
|
+
[/被谁调/, 1.0],
|
|
254
|
+
[/调了谁/, 1.0],
|
|
255
|
+
[/上下游/, 1.0],
|
|
256
|
+
[/依赖关系/, 1.0],
|
|
257
|
+
[/触发/, 1.0],
|
|
258
|
+
[/路径/, 1.0],
|
|
259
|
+
[/覆盖/, 1.0],
|
|
260
|
+
[/介入/, 1.0],
|
|
261
|
+
],
|
|
262
|
+
search: [
|
|
263
|
+
[/where is/i, 1.0],
|
|
264
|
+
[/在哪/, 1.0],
|
|
265
|
+
[/find/i, 1.0],
|
|
266
|
+
[/search/i, 1.0],
|
|
267
|
+
[/搜索/, 1.0],
|
|
268
|
+
[/找/, 1.0],
|
|
269
|
+
[/locate/i, 1.0],
|
|
270
|
+
[/哪里用/, 1.0],
|
|
271
|
+
[/哪里定义/, 1.0],
|
|
272
|
+
[/定义在/, 1.0],
|
|
273
|
+
[/实现在/, 1.0],
|
|
274
|
+
[/处理没/, 1.0],
|
|
275
|
+
[/在源码/, 1.0],
|
|
276
|
+
[/加不加/, 1.0],
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const INTENT_THRESHOLD = 0.5;
|
|
281
|
+
|
|
282
|
+
function scoreIntent(msg, intent) {
|
|
283
|
+
const patterns = INTENT_PATTERNS[intent];
|
|
284
|
+
if (!patterns) return 0;
|
|
285
|
+
let max = 0;
|
|
286
|
+
for (const [pattern, weight] of patterns) {
|
|
287
|
+
if (pattern.test(msg) && weight > max) max = weight;
|
|
288
|
+
}
|
|
289
|
+
return max;
|
|
290
|
+
}
|
|
291
|
+
|
|
124
292
|
function detectIntents(msg) {
|
|
125
293
|
return {
|
|
126
|
-
impact:
|
|
127
|
-
modify:
|
|
128
|
-
implement:
|
|
129
|
-
understand:
|
|
130
|
-
callgraph:
|
|
131
|
-
search:
|
|
294
|
+
impact: scoreIntent(msg, 'impact') >= INTENT_THRESHOLD,
|
|
295
|
+
modify: scoreIntent(msg, 'modify') >= INTENT_THRESHOLD,
|
|
296
|
+
implement: scoreIntent(msg, 'implement') >= INTENT_THRESHOLD,
|
|
297
|
+
understand: scoreIntent(msg, 'understand') >= INTENT_THRESHOLD,
|
|
298
|
+
callgraph: scoreIntent(msg, 'callgraph') >= INTENT_THRESHOLD,
|
|
299
|
+
search: scoreIntent(msg, 'search') >= INTENT_THRESHOLD,
|
|
132
300
|
};
|
|
133
301
|
}
|
|
134
302
|
|
|
@@ -152,15 +320,55 @@ function determineQueryType(intents, symbols, filePaths, isCoolingDownFn) {
|
|
|
152
320
|
}
|
|
153
321
|
|
|
154
322
|
// --- Main execution (only when run directly) ---
|
|
155
|
-
|
|
156
|
-
|
|
323
|
+
// All exit-on-condition checks (manifest, computeQuietHooks, message length,
|
|
324
|
+
// db presence) live INSIDE this guard so `require()` from tests doesn't
|
|
325
|
+
// terminate the test process on module load.
|
|
326
|
+
function runMain() {
|
|
327
|
+
// Mid-session install: lifecycle.js install() hasn't run yet (no manifest).
|
|
328
|
+
// MCP server only starts at session startup — tell the user to restart.
|
|
329
|
+
if (!fs.existsSync(MANIFEST_PATH)) {
|
|
330
|
+
const noticeFile = path.join(os.tmpdir(), '.code-graph-mcp-restart-notice');
|
|
331
|
+
try {
|
|
332
|
+
// Show once per hour to avoid spam
|
|
333
|
+
if (Date.now() - fs.statSync(noticeFile).mtimeMs < 3600000) return;
|
|
334
|
+
} catch { /* first notice */ }
|
|
335
|
+
try { fs.writeFileSync(noticeFile, ''); } catch { /* ok */ }
|
|
336
|
+
process.stdout.write(
|
|
337
|
+
'[code-graph] Plugin installed — MCP server requires a session restart to connect.\n' +
|
|
338
|
+
'MCP servers are only initialized at session startup. To activate:\n' +
|
|
339
|
+
' 1. Press Ctrl+C to exit the current session\n' +
|
|
340
|
+
' 2. Re-run `claude` to start a new session\n' +
|
|
341
|
+
'Meanwhile, CLI tools work directly: code-graph-mcp search <query>, code-graph-mcp map, etc.\n'
|
|
342
|
+
);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (computeQuietHooks()) return;
|
|
347
|
+
|
|
348
|
+
// --- Read user message ---
|
|
349
|
+
let message;
|
|
350
|
+
try {
|
|
351
|
+
const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
|
|
352
|
+
message = (input && input.message) || '';
|
|
353
|
+
} catch {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
// Chinese chars are ~3 bytes but 1 char; "看看 fts5_search" is only 16 chars
|
|
357
|
+
if (!message || message.length < 8) return;
|
|
358
|
+
|
|
359
|
+
// --- Check index ---
|
|
360
|
+
const cwd = process.cwd();
|
|
361
|
+
const dbPath = path.join(cwd, '.code-graph', 'index.db');
|
|
362
|
+
if (!fs.existsSync(dbPath)) return;
|
|
363
|
+
|
|
364
|
+
if (shouldSkip(message)) return;
|
|
157
365
|
|
|
158
366
|
const filePaths = extractFilePaths(message);
|
|
159
367
|
const symbols = extractSymbols(message);
|
|
160
368
|
const intents = detectIntents(message);
|
|
161
369
|
const query = determineQueryType(intents, symbols, filePaths, isCoolingDown);
|
|
162
370
|
|
|
163
|
-
if (!query)
|
|
371
|
+
if (!query) return;
|
|
164
372
|
|
|
165
373
|
const PREFIXES = {
|
|
166
374
|
impact: '[code-graph:impact] Blast radius — review before editing:',
|
|
@@ -169,6 +377,15 @@ if (require.main === module) {
|
|
|
169
377
|
search: '[code-graph:search] Relevant code:',
|
|
170
378
|
};
|
|
171
379
|
|
|
380
|
+
function run(cmd, args) {
|
|
381
|
+
return execFileSync(cmd, args, {
|
|
382
|
+
cwd,
|
|
383
|
+
timeout: 3000,
|
|
384
|
+
encoding: 'utf8',
|
|
385
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
172
389
|
try {
|
|
173
390
|
let result = '';
|
|
174
391
|
if (query.type === 'impact') result = run('code-graph-mcp', ['impact', query.symbol]);
|
|
@@ -181,19 +398,12 @@ if (require.main === module) {
|
|
|
181
398
|
process.stdout.write(`${PREFIXES[query.type]}\n${result.trim()}\n`);
|
|
182
399
|
}
|
|
183
400
|
} catch {
|
|
184
|
-
|
|
401
|
+
/* return silently */
|
|
185
402
|
}
|
|
186
403
|
}
|
|
187
404
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
// --- Helpers ---
|
|
191
|
-
|
|
192
|
-
function run(cmd, args) {
|
|
193
|
-
return execFileSync(cmd, args, {
|
|
194
|
-
cwd,
|
|
195
|
-
timeout: 3000,
|
|
196
|
-
encoding: 'utf8',
|
|
197
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
198
|
-
});
|
|
405
|
+
if (require.main === module) {
|
|
406
|
+
runMain();
|
|
199
407
|
}
|
|
408
|
+
|
|
409
|
+
module.exports = { shouldSkip, extractFilePaths, extractSymbols, detectIntents, scoreIntent, INTENT_PATTERNS, INTENT_THRESHOLD, determineQueryType, computeQuietHooks, STOP_WORDS, PLAIN_WORD_EXCLUDE };
|
|
@@ -9,7 +9,11 @@ const {
|
|
|
9
9
|
extractFilePaths,
|
|
10
10
|
extractSymbols,
|
|
11
11
|
detectIntents,
|
|
12
|
+
scoreIntent,
|
|
13
|
+
INTENT_PATTERNS,
|
|
14
|
+
INTENT_THRESHOLD,
|
|
12
15
|
determineQueryType,
|
|
16
|
+
computeQuietHooks,
|
|
13
17
|
} = require('./user-prompt-context');
|
|
14
18
|
|
|
15
19
|
// ── shouldSkip ──────────────────────────────────────────
|
|
@@ -250,6 +254,48 @@ test('detectIntents: search (ZH)', () => {
|
|
|
250
254
|
assert.ok(detectIntents('在哪里用了这个常量').search);
|
|
251
255
|
});
|
|
252
256
|
|
|
257
|
+
// --- Per-keyword scoring (v0.21 weighted-scorer refactor) ---
|
|
258
|
+
test('scoreIntent: matched keyword returns its weight, unmatched returns 0', () => {
|
|
259
|
+
// Each pattern in INTENT_PATTERNS is testable in isolation now.
|
|
260
|
+
assert.equal(scoreIntent('this bug is critical', 'impact'), 1.0);
|
|
261
|
+
assert.equal(scoreIntent('hello world', 'impact'), 0);
|
|
262
|
+
assert.equal(scoreIntent('refactor this module', 'modify'), 1.0);
|
|
263
|
+
assert.equal(scoreIntent('refactor this module', 'implement'), 0);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('scoreIntent: max weight wins when multiple patterns match', () => {
|
|
267
|
+
// "this bug needs a fix and impact analysis" matches `impact`, `bug`,
|
|
268
|
+
// `risk`-no, all three impact rows are weight 1.0 currently — score is 1.0.
|
|
269
|
+
// Spec: scoreIntent returns max(weight) of matching patterns, never sum.
|
|
270
|
+
const score = scoreIntent('this bug needs impact analysis', 'impact');
|
|
271
|
+
assert.equal(score, 1.0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('scoreIntent: unknown intent returns 0 (no throw)', () => {
|
|
275
|
+
assert.equal(scoreIntent('anything', 'nonexistent_intent'), 0);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('INTENT_PATTERNS: every intent has at least 5 patterns and uniform weights', () => {
|
|
279
|
+
// v0.21 starts with uniform weights; future tuning can vary them per-pattern.
|
|
280
|
+
// This test guards against regression to the giant single-regex form.
|
|
281
|
+
const intents = ['impact', 'modify', 'implement', 'understand', 'callgraph', 'search'];
|
|
282
|
+
for (const intent of intents) {
|
|
283
|
+
const patterns = INTENT_PATTERNS[intent];
|
|
284
|
+
assert.ok(Array.isArray(patterns), `${intent} must have patterns array`);
|
|
285
|
+
assert.ok(patterns.length >= 5, `${intent} must have >=5 patterns, got ${patterns.length}`);
|
|
286
|
+
for (const [pattern, weight] of patterns) {
|
|
287
|
+
assert.ok(pattern instanceof RegExp, `${intent} pattern must be RegExp`);
|
|
288
|
+
assert.ok(typeof weight === 'number' && weight > 0 && weight <= 1, `${intent} weight must be (0, 1]`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test('INTENT_THRESHOLD is 0.5 — single weight-1.0 match fires the intent', () => {
|
|
294
|
+
// Threshold contract: any pattern @ weight >= 0.5 → intent fires.
|
|
295
|
+
// If we lower a pattern to weight 0.4, it must NOT fire alone.
|
|
296
|
+
assert.equal(INTENT_THRESHOLD, 0.5);
|
|
297
|
+
});
|
|
298
|
+
|
|
253
299
|
// --- No false positives ---
|
|
254
300
|
test('detectIntents: simple confirmations have no code intent', () => {
|
|
255
301
|
const r = detectIntents('好的');
|
|
@@ -479,3 +525,48 @@ test('CODE_GRAPH_QUIET_HOOKS=1 short-circuits silently on stdout, stderr, exit 0
|
|
|
479
525
|
assert.equal(proc.stderr, '', 'stderr must be empty');
|
|
480
526
|
assert.equal(proc.status, 0, 'must exit 0');
|
|
481
527
|
});
|
|
528
|
+
|
|
529
|
+
// ── computeQuietHooks priority chain (v0.21 opt-in flip) ────────
|
|
530
|
+
|
|
531
|
+
test('computeQuietHooks: default (no env) is QUIET', () => {
|
|
532
|
+
// v0.21: flipped from opt-out to opt-in. Routing-bench P@1=100% earned
|
|
533
|
+
// the right to stop pushing context the agent would have requested.
|
|
534
|
+
assert.equal(computeQuietHooks({}), true);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test('computeQuietHooks: CODE_GRAPH_VERBOSE_HOOKS=1 enables push (opt-in)', () => {
|
|
538
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_VERBOSE_HOOKS: '1' }), false);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('computeQuietHooks: legacy CODE_GRAPH_QUIET_HOOKS=0 forces noisy (back-compat)', () => {
|
|
542
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '0' }), false);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test('computeQuietHooks: legacy CODE_GRAPH_QUIET_HOOKS=1 forces quiet (back-compat)', () => {
|
|
546
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '1' }), true);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('computeQuietHooks: legacy QUIET_HOOKS=0 wins over VERBOSE_HOOKS=1 (priority chain)', () => {
|
|
550
|
+
// Priority order: CODE_GRAPH_QUIET_HOOKS=0/1 > CODE_GRAPH_VERBOSE_HOOKS > default.
|
|
551
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '0', CODE_GRAPH_VERBOSE_HOOKS: '0' }), false);
|
|
552
|
+
assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '1', CODE_GRAPH_VERBOSE_HOOKS: '1' }), true);
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test('default env (no flags) short-circuits silently — opt-in flip', () => {
|
|
556
|
+
// End-to-end check: with the opt-in flip, a default-env spawn produces
|
|
557
|
+
// no stdout/stderr even on a message that previously would have injected.
|
|
558
|
+
const { spawnSync } = require('node:child_process');
|
|
559
|
+
const script = path.join(__dirname, 'user-prompt-context.js');
|
|
560
|
+
const cleanEnv = { ...process.env };
|
|
561
|
+
delete cleanEnv.CODE_GRAPH_QUIET_HOOKS;
|
|
562
|
+
delete cleanEnv.CODE_GRAPH_VERBOSE_HOOKS;
|
|
563
|
+
const proc = spawnSync(process.execPath, [script], {
|
|
564
|
+
input: JSON.stringify({ message: 'impact of refactoring parse_code function' }),
|
|
565
|
+
env: cleanEnv,
|
|
566
|
+
encoding: 'utf8',
|
|
567
|
+
timeout: 2000,
|
|
568
|
+
});
|
|
569
|
+
assert.equal(proc.stdout, '', 'default must be silent on stdout');
|
|
570
|
+
assert.equal(proc.stderr, '', 'default must be silent on stderr');
|
|
571
|
+
assert.equal(proc.status, 0, 'default must exit 0');
|
|
572
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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.21.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.21.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.21.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.21.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.21.0"
|
|
43
43
|
}
|
|
44
44
|
}
|