@sdsrs/code-graph 0.10.0 → 0.11.1

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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.10.0",
7
+ "version": "0.11.1",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -10,9 +10,9 @@ const os = require('os');
10
10
  const SENTINEL_BEGIN = '<!-- code-graph-mcp:begin v1 -->';
11
11
  const SENTINEL_END = '<!-- code-graph-mcp:end -->';
12
12
  const INDEX_LINE = [
13
- '- [code-graph-mcp](plugin_code_graph_mcp.md) — 代码理解 12 工具(替代 Grep/Read 多步):',
14
- ' - 问"谁调/改动/模块/概念/HTTP/相似" → `get_call_graph`/`impact_analysis`/`module_overview`/`semantic_code_search`/`trace_http_chain`/`find_similar_code`',
15
- ' - 问"返回型/引用/死码/依赖/架构/看签名" → `ast_search`/`find_references`/`find_dead_code`/`dependency_graph`/`project_map`/`get_ast_node`',
13
+ '- [code-graph-mcp](plugin_code_graph_mcp.md) — v0.10.0 tools/list 默认 7 核心 + 5 隐藏可调(省启动 token)',
14
+ ' - 核心 7(默认暴露):`get_call_graph`/`module_overview`/`semantic_code_search`/`ast_search`/`find_references`/`get_ast_node`/`project_map`',
15
+ ' - 进阶 5(隐藏按名可调):`impact_analysis`/`trace_http_chain`/`dependency_graph`/`find_similar_code`/`find_dead_code`',
16
16
  ].join('\n');
17
17
  const TEMPLATE_PATH = path.resolve(__dirname, '..', 'templates', 'plugin_code_graph_mcp.md');
18
18
  const TARGET_NAME = 'plugin_code_graph_mcp.md';
@@ -102,6 +102,26 @@ function isAdopted({ cwd, home } = {}) {
102
102
  return index.includes(SENTINEL_BEGIN) && index.includes(SENTINEL_END);
103
103
  }
104
104
 
105
+ // v0.11.0 — shipped template / INDEX_LINE 与已落地版本出现漂移时返回 true。
106
+ // 让已 adopt 的项目在下次 SessionStart 自动对齐到插件最新决策表,避免"老用户
107
+ // 永远停留在首次 adopt 时的 snapshot"。手动编辑会被覆盖——锁定方式:
108
+ // CODE_GRAPH_NO_TEMPLATE_REFRESH=1。
109
+ function needsRefresh({ cwd, home, templatePath } = {}) {
110
+ const dir = memoryDir(cwd, home);
111
+ const target = path.join(dir, TARGET_NAME);
112
+ const indexPath = path.join(dir, 'MEMORY.md');
113
+ const tpl = templatePath || TEMPLATE_PATH;
114
+ if (!fs.existsSync(target) || !fs.existsSync(tpl) || !fs.existsSync(indexPath)) {
115
+ return false;
116
+ }
117
+ const shipped = fs.readFileSync(tpl);
118
+ const current = fs.readFileSync(target);
119
+ if (!shipped.equals(current)) return true;
120
+ const index = fs.readFileSync(indexPath, 'utf8');
121
+ const desiredBlock = `${SENTINEL_BEGIN}\n${INDEX_LINE}\n${SENTINEL_END}`;
122
+ return !index.includes(desiredBlock);
123
+ }
124
+
105
125
  // 检测脚本是否从 Claude Code 插件 cache 运行。
106
126
  // 走 __dirname 而非 CLAUDE_PLUGIN_ROOT — 后者在多插件共存时会互相污染
107
127
  // (见 feedback_plugin_env_isolation.md)。
@@ -122,6 +142,12 @@ function maybeAutoAdopt({ cwd, home, env, scriptPath } = {}) {
122
142
  return { attempted: false, reason: 'not-plugin-mode' };
123
143
  }
124
144
  if (isAdopted({ cwd, home })) {
145
+ // v0.11.0: shipped template / INDEX_LINE 漂移时重跑 adopt 对齐。
146
+ // opt-out: CODE_GRAPH_NO_TEMPLATE_REFRESH=1(锁定手动编辑)。
147
+ if (env.CODE_GRAPH_NO_TEMPLATE_REFRESH !== '1' && needsRefresh({ cwd, home })) {
148
+ const result = adopt({ cwd, home });
149
+ return { attempted: true, reason: 'refreshed', result };
150
+ }
125
151
  return { attempted: false, reason: 'already-adopted' };
126
152
  }
127
153
  const result = adopt({ cwd, home });
@@ -197,6 +223,6 @@ if (require.main === module) {
197
223
 
198
224
  module.exports = {
199
225
  adopt, unadopt, memoryDir, formatResult, stripSentinelBlock,
200
- isAdopted, isPluginModeInstall, maybeAutoAdopt,
226
+ isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh,
201
227
  SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
202
228
  };
@@ -6,8 +6,8 @@ const path = require('path');
6
6
  const os = require('os');
7
7
  const {
8
8
  adopt, unadopt, memoryDir, stripSentinelBlock,
9
- isAdopted, isPluginModeInstall, maybeAutoAdopt,
10
- SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH,
9
+ isAdopted, isPluginModeInstall, maybeAutoAdopt, needsRefresh,
10
+ SENTINEL_BEGIN, SENTINEL_END, INDEX_LINE, TEMPLATE_PATH, TARGET_NAME,
11
11
  } = require('./adopt');
12
12
 
13
13
  function makeSandbox() {
@@ -348,6 +348,123 @@ test('maybeAutoAdopt returns no-memory-dir when project memory missing', () => {
348
348
  }
349
349
  });
350
350
 
351
+ // v0.11.0 — template-refresh on drift
352
+
353
+ test('needsRefresh returns false when target matches shipped template + INDEX_LINE', () => {
354
+ const sb = makeSandbox();
355
+ try {
356
+ adopt({ cwd: sb.cwd, home: sb.home });
357
+ assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), false);
358
+ } finally { sb.cleanup(); }
359
+ });
360
+
361
+ test('needsRefresh returns true when target content drifted from shipped template', () => {
362
+ const sb = makeSandbox();
363
+ try {
364
+ adopt({ cwd: sb.cwd, home: sb.home });
365
+ const target = path.join(sb.dir, TARGET_NAME);
366
+ fs.writeFileSync(target, '# stale content from earlier plugin version\n');
367
+ assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), true);
368
+ } finally { sb.cleanup(); }
369
+ });
370
+
371
+ test('needsRefresh returns true when MEMORY.md INDEX_LINE drifted', () => {
372
+ const sb = makeSandbox();
373
+ try {
374
+ adopt({ cwd: sb.cwd, home: sb.home });
375
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
376
+ const stale = `# Memory Index\n\n${SENTINEL_BEGIN}\n- old 12-tool index line\n${SENTINEL_END}\n`;
377
+ fs.writeFileSync(indexPath, stale);
378
+ assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), true);
379
+ } finally { sb.cleanup(); }
380
+ });
381
+
382
+ test('needsRefresh returns false when not adopted (nothing to refresh)', () => {
383
+ const sb = makeSandbox();
384
+ try {
385
+ assert.strictEqual(needsRefresh({ cwd: sb.cwd, home: sb.home }), false);
386
+ } finally { sb.cleanup(); }
387
+ });
388
+
389
+ test('maybeAutoAdopt refreshes drifted target on re-run (reason=refreshed)', () => {
390
+ const sb = makeSandbox();
391
+ try {
392
+ adopt({ cwd: sb.cwd, home: sb.home });
393
+ const target = path.join(sb.dir, TARGET_NAME);
394
+ fs.writeFileSync(target, '# stale\n');
395
+ const res = maybeAutoAdopt({
396
+ cwd: sb.cwd, home: sb.home,
397
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
398
+ env: {},
399
+ });
400
+ assert.strictEqual(res.attempted, true);
401
+ assert.strictEqual(res.reason, 'refreshed');
402
+ assert.strictEqual(res.result.ok, true);
403
+ // Target now matches shipped template
404
+ const shipped = fs.readFileSync(TEMPLATE_PATH);
405
+ const current = fs.readFileSync(target);
406
+ assert.ok(shipped.equals(current), 'target re-synced to shipped template');
407
+ // Sentinel preserved in MEMORY.md
408
+ assert.strictEqual(isAdopted({ cwd: sb.cwd, home: sb.home }), true);
409
+ } finally { sb.cleanup(); }
410
+ });
411
+
412
+ test('maybeAutoAdopt refreshes drifted INDEX_LINE in MEMORY.md', () => {
413
+ const sb = makeSandbox();
414
+ try {
415
+ adopt({ cwd: sb.cwd, home: sb.home });
416
+ const indexPath = path.join(sb.dir, 'MEMORY.md');
417
+ const stale = `# Memory Index\n\n${SENTINEL_BEGIN}\n- old 12-tool index line\n${SENTINEL_END}\n`;
418
+ fs.writeFileSync(indexPath, stale);
419
+ const res = maybeAutoAdopt({
420
+ cwd: sb.cwd, home: sb.home,
421
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
422
+ env: {},
423
+ });
424
+ assert.strictEqual(res.attempted, true);
425
+ assert.strictEqual(res.reason, 'refreshed');
426
+ const index = fs.readFileSync(indexPath, 'utf8');
427
+ assert.ok(index.includes(INDEX_LINE), 'INDEX_LINE restored from current constant');
428
+ assert.ok(!index.includes('old 12-tool index line'), 'stale line removed');
429
+ } finally { sb.cleanup(); }
430
+ });
431
+
432
+ test('maybeAutoAdopt skips refresh when CODE_GRAPH_NO_TEMPLATE_REFRESH=1 (locks manual edits)', () => {
433
+ const sb = makeSandbox();
434
+ try {
435
+ adopt({ cwd: sb.cwd, home: sb.home });
436
+ const target = path.join(sb.dir, TARGET_NAME);
437
+ const userEdit = '# my hand-edited decision table\n';
438
+ fs.writeFileSync(target, userEdit);
439
+ const res = maybeAutoAdopt({
440
+ cwd: sb.cwd, home: sb.home,
441
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
442
+ env: { CODE_GRAPH_NO_TEMPLATE_REFRESH: '1' },
443
+ });
444
+ assert.strictEqual(res.attempted, false);
445
+ assert.strictEqual(res.reason, 'already-adopted');
446
+ assert.strictEqual(fs.readFileSync(target, 'utf8'), userEdit, 'user edit preserved');
447
+ } finally { sb.cleanup(); }
448
+ });
449
+
450
+ test('maybeAutoAdopt stays already-adopted when in sync (no gratuitous refresh)', () => {
451
+ const sb = makeSandbox();
452
+ try {
453
+ adopt({ cwd: sb.cwd, home: sb.home });
454
+ const target = path.join(sb.dir, TARGET_NAME);
455
+ const mtimeBefore = fs.statSync(target).mtimeMs;
456
+ const res = maybeAutoAdopt({
457
+ cwd: sb.cwd, home: sb.home,
458
+ scriptPath: '/home/u/.claude/plugins/cache/code-graph-mcp/scripts',
459
+ env: {},
460
+ });
461
+ assert.strictEqual(res.attempted, false);
462
+ assert.strictEqual(res.reason, 'already-adopted');
463
+ const mtimeAfter = fs.statSync(target).mtimeMs;
464
+ assert.strictEqual(mtimeAfter, mtimeBefore, 'target file not touched when in sync');
465
+ } finally { sb.cleanup(); }
466
+ });
467
+
351
468
  test('Windows platform is rejected with clear reason', { skip: process.platform === 'win32' }, () => {
352
469
  const orig = process.platform;
353
470
  Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
@@ -259,14 +259,22 @@ function runSessionInit() {
259
259
  const indexFreshness = binaryCheck.available ? ensureIndexFresh() : 'skipped';
260
260
 
261
261
  // v0.9.0 C' 上下文感知默认:插件模式下首次 SessionStart 自动 adopt。
262
- // 仅一次 stderr 提示(采纳成功时),让用户知道发生了什么 + 如何回退。
262
+ // v0.11.0: adopt 的项目如果 shipped template 漂移也会触发一次刷新。
263
+ // 两种情况都发一次 stderr 提示,让用户知道发生了什么 + 如何回退。
263
264
  const autoAdopt = maybeAutoAdopt({ scriptPath: __dirname });
264
265
  if (autoAdopt.attempted && autoAdopt.result && autoAdopt.result.ok) {
265
- process.stderr.write(
266
- '[code-graph] Auto-adopted into project MEMORY.md (plugin install → knowing consent).\n' +
267
- ' Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env\n' +
268
- ' Reverse: code-graph-mcp unadopt\n'
269
- );
266
+ if (autoAdopt.reason === 'refreshed') {
267
+ process.stderr.write(
268
+ '[code-graph] Refreshed decision table to latest shipped version.\n' +
269
+ ' Lock file: CODE_GRAPH_NO_TEMPLATE_REFRESH=1 in ~/.claude/settings.json env\n'
270
+ );
271
+ } else {
272
+ process.stderr.write(
273
+ '[code-graph] Auto-adopted into project MEMORY.md (plugin install → knowing consent).\n' +
274
+ ' Opt out: CODE_GRAPH_NO_AUTO_ADOPT=1 in ~/.claude/settings.json env\n' +
275
+ ' Reverse: code-graph-mcp unadopt\n'
276
+ );
277
+ }
270
278
  }
271
279
 
272
280
  // quietHooks: adopted → quiet by default (rely on MEMORY.md pointer + on-demand
@@ -11,23 +11,38 @@ type: reference
11
11
  > 本文件自动写入,自动切换 quietHooks(跳过每次 project_map 注入)。
12
12
  > 退出:`CODE_GRAPH_NO_AUTO_ADOPT=1` 阻止,`code-graph-mcp unadopt` 回退。
13
13
  > 手动强控:`CODE_GRAPH_QUIET_HOOKS=0` 强制注入 / `=1` 强制静默(覆盖 adoption 推导)。
14
+ >
15
+ > **v0.11.0 起**:已 adopt 的项目在下次 SessionStart 会自动对齐到插件 shipped
16
+ > 的最新决策表(本文件 SHA 与 template 差异时覆盖)。手动编辑会被覆盖——
17
+ > 要锁定自己的版本,设 `CODE_GRAPH_NO_TEMPLATE_REFRESH=1`(不影响首次 adopt)。
14
18
 
15
19
  ## 何时调用 MCP/CLI(替代多步 Grep/Read)
16
20
 
21
+ > v0.10.0 起:tools/list 默认只暴露 7 个核心工具;下表中 `⚙` 标记的 5 个工具
22
+ > 已从 tools/list 隐藏以节省 session 启动 tokens,但**仍可通过名称直接调用**
23
+ > (向后兼容别名)或用 CLI 子命令。高频场景可优先用核心 7 个。
24
+
25
+ ### 核心 7(tools/list 默认暴露)
26
+
17
27
  | 意图 | 工具 | 关键参数 / 例子 |
18
28
  |------|------|----------------|
19
29
  | "谁调用 X?" / "X 调了啥?" | `get_call_graph` / `callgraph X` | 替代 `grep "X("` |
20
- | "改 X 会炸啥?" | `impact_analysis` / `impact X` | 修改函数签名前必跑 |
21
30
  | "Y 模块长啥样?" | `module_overview` / `overview Y/` | 替代逐文件 Read |
22
31
  | "找做 Z 的代码"(概念) | `semantic_code_search` / `search "Z"` | 不知道精确名 |
23
32
  | "返回 T 类型的函数" | `ast_search --returns T` | 结构化筛选 |
24
33
  | "X 在哪被引用?" | `find_references` / `refs X` | 含 callers/importers |
25
- | "未使用的代码" | `find_dead_code` / `dead-code [path]` | 清理 exports |
26
- | "相似/重复函数" | `find_similar_code` / `similar X` | 需 embedding |
27
- | "X 文件依赖谁?" | `dependency_graph` / `deps X` | file 级别 |
28
- | "看 X 的源码 / 签名" | `get_ast_node` / `show X` | `--include-impact` 含影响面 |
34
+ | "看 X 的源码 / 签名" | `get_ast_node` / `show X` | `include_impact=true` 含影响面(替代 impact_analysis) |
29
35
  | "项目结构总览" | `project_map` / `map` | 起手势用 `--compact` |
30
- | HTTP 路由 → handler 链路 | `trace_http_chain` / `trace ROUTE` | API 调试 |
36
+
37
+ ### 进阶 5(隐藏但可调)
38
+
39
+ | 意图 | 工具 | 关键参数 / 例子 |
40
+ |------|------|----------------|
41
+ | ⚙ "改 X 会炸啥?" | `impact_analysis` / `impact X` | 修改签名前用;或 `get_ast_node` + `include_impact=true` |
42
+ | ⚙ HTTP 路由 → handler 链路 | `trace_http_chain` / `trace ROUTE` | API 调试 |
43
+ | ⚙ "X 文件依赖谁?" | `dependency_graph` / `deps X` | file 级别 |
44
+ | ⚙ "相似/重复函数" | `find_similar_code` / `similar X` | 需 embedding |
45
+ | ⚙ "未使用的代码" | `find_dead_code` / `dead-code [path]` | 清理 exports |
31
46
 
32
47
  ## 不要替代
33
48
 
@@ -40,7 +55,8 @@ type: reference
40
55
  1. 起手 `project_map --compact` 看架构
41
56
  2. `semantic_code_search` 默认带 `compact=true`,省 token
42
57
  3. 展开节点:`get_ast_node node_id=N compact=true` 看签名 / 不带 compact 看全文
43
- 4. 改前必跑 `impact_analysis`
58
+ 4. 改前评估影响:`get_ast_node symbol_name=X include_impact=true`(核心 7 内)
59
+ 或直接 `impact_analysis symbol_name=X`(隐藏但可调,输出更细:风险等级 + 路由 + 文件计数)
44
60
  5. 搜不到结果 → `code-graph-mcp health-check` 检查索引与 embedding 覆盖率
45
61
 
46
62
  可用 prompts:`impact-analysis`、`understand-module`、`trace-request`
@@ -73,4 +89,5 @@ code-graph-mcp health-check # 索引健康
73
89
 
74
90
  - `code-graph-mcp unadopt` — 精确移除 sentinel 段 + 本文件,quietHooks 自动回到 false(下次 SessionStart 恢复 project_map 注入)。
75
91
  - `CODE_GRAPH_NO_AUTO_ADOPT=1`(`~/.claude/settings.json` env) — 阻止未来自动 adopt,不影响已 adopted 状态。
92
+ - `CODE_GRAPH_NO_TEMPLATE_REFRESH=1`(v0.11.0+) — 锁定本文件不随插件升级刷新;允许手动编辑长久保留。
76
93
  - `CODE_GRAPH_QUIET_HOOKS=0` — 强制恢复 project_map 注入(即使已 adopted)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "MCP server that indexes codebases into an AST knowledge graph with semantic search, call graph traversal, and HTTP route tracing",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,10 +34,10 @@
34
34
  "node": ">=16"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@sdsrs/code-graph-linux-x64": "0.10.0",
38
- "@sdsrs/code-graph-linux-arm64": "0.10.0",
39
- "@sdsrs/code-graph-darwin-x64": "0.10.0",
40
- "@sdsrs/code-graph-darwin-arm64": "0.10.0",
41
- "@sdsrs/code-graph-win32-x64": "0.10.0"
37
+ "@sdsrs/code-graph-linux-x64": "0.11.1",
38
+ "@sdsrs/code-graph-linux-arm64": "0.11.1",
39
+ "@sdsrs/code-graph-darwin-x64": "0.11.1",
40
+ "@sdsrs/code-graph-darwin-arm64": "0.11.1",
41
+ "@sdsrs/code-graph-win32-x64": "0.11.1"
42
42
  }
43
43
  }