@sdsrs/code-graph 0.54.2 → 0.56.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/bin/cli.js CHANGED
@@ -18,17 +18,19 @@ if (sub === "adopt" || sub === "unadopt") {
18
18
  process.exit(result.ok === false ? 1 : 0);
19
19
  }
20
20
 
21
- const { findBinary } = require("../claude-plugin/scripts/find-binary");
21
+ const { findBinary, unsupportedPlatformHint } = require("../claude-plugin/scripts/find-binary");
22
22
 
23
23
  const binary = findBinary();
24
24
 
25
25
  if (!binary) {
26
+ const hint = unsupportedPlatformHint();
26
27
  console.error(
27
28
  "Error: code-graph-mcp binary not found.\n\n" +
29
+ (hint ? hint + "\n\n" : "") +
28
30
  "To install:\n" +
29
31
  " npm install -g @sdsrs/code-graph\n\n" +
30
32
  "To build from source:\n" +
31
- " cargo build --release\n"
33
+ " cargo install code-graph-mcp --features embed-model\n"
32
34
  );
33
35
  process.exit(1);
34
36
  }
@@ -41,6 +43,10 @@ const child = spawn(binary, process.argv.slice(2), {
41
43
 
42
44
  child.on("error", (err) => {
43
45
  console.error(`Failed to start code-graph-mcp: ${err.message}`);
46
+ // A glibc binary installed on musl (older npm ignores the `libc` field) is present
47
+ // but fails to exec — surface the actionable platform hint instead of a bare error.
48
+ const hint = unsupportedPlatformHint();
49
+ if (hint) console.error("\n" + hint);
44
50
  process.exit(1);
45
51
  });
46
52
 
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.54.2",
7
+ "version": "0.56.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -13,6 +13,38 @@ const { findBinary, clearCache: clearBinaryCache } = require('./find-binary');
13
13
 
14
14
  // ── Diagnostics ───────────────────────────────────────────
15
15
 
16
+ /**
17
+ * Classify embedding/vector availability from a `health-check --json` payload.
18
+ * Pure (no I/O) so it is unit-testable. Surfaces a silent FTS5-only degradation
19
+ * that the prior embedding_progress-only check false-greened as "no embeddable
20
+ * nodes": when the binary is embed-capable and the index HAS embeddable nodes but
21
+ * none are embedded, the vector channel is inactive and semantic search runs
22
+ * FTS5-only — that is a 'warn', not 'ok'.
23
+ * @returns {{name:string, status:'ok'|'warn', detail:string}}
24
+ */
25
+ function classifyEmbeddings(hc) {
26
+ const ep = (hc && hc.embedding_progress) || '0/0';
27
+ const [done, total] = ep.split('/').map(Number);
28
+ if (hc && hc.model_available === false) {
29
+ return { name: 'Embeddings', status: 'warn',
30
+ detail: 'binary built without embed-model — semantic search is FTS5-only; reinstall via npm/plugin for the hybrid binary' };
31
+ }
32
+ if (!total) {
33
+ return { name: 'Embeddings', status: 'ok', detail: 'no embeddable nodes' };
34
+ }
35
+ if (!done) {
36
+ const why = (hc && hc.embedding_status === 'pending')
37
+ ? 'model not downloaded/loaded yet — auto-downloads in background on first search; retry shortly or restart the MCP server'
38
+ : `embedding_status=${(hc && hc.embedding_status) || 'unknown'}`;
39
+ return { name: 'Embeddings', status: 'warn',
40
+ detail: `vector INACTIVE — ${total} embeddable nodes, 0 embedded; semantic search is FTS5-only (${why})` };
41
+ }
42
+ if (done < total) {
43
+ return { name: 'Embeddings', status: 'ok', detail: `hybrid — ${Math.round((done / total) * 100)}% embedded (${done}/${total})` };
44
+ }
45
+ return { name: 'Embeddings', status: 'ok', detail: `hybrid — embeddings complete (${done}/${total})` };
46
+ }
47
+
16
48
  /**
17
49
  * Run all diagnostic checks. Returns an array of:
18
50
  * { name: string, status: 'ok'|'warn'|'error'|'skip', detail: string, fixId?: string }
@@ -119,17 +151,9 @@ function runDiagnostics() {
119
151
  });
120
152
  }
121
153
 
122
- // Embeddings
123
- const ep = hc.embedding_progress || '0/0';
124
- const [done, total] = ep.split('/').map(Number);
125
- if (total > 0 && done < total) {
126
- const pct = Math.round((done / total) * 100);
127
- results.push({ name: 'Embeddings', status: 'ok', detail: `${pct}% (${done}/${total})` });
128
- } else if (total === 0) {
129
- results.push({ name: 'Embeddings', status: 'ok', detail: 'no embeddable nodes' });
130
- } else {
131
- results.push({ name: 'Embeddings', status: 'ok', detail: `100% (${done}/${total})` });
132
- }
154
+ // Embeddings / vector availability — pure classifier; warns on FTS5-only
155
+ // degradation (model missing/not loaded) instead of false-greening it.
156
+ results.push(classifyEmbeddings(hc));
133
157
  }
134
158
  } catch (e) {
135
159
  const rawStderr = e.stderr ? e.stderr.toString() : '';
@@ -453,7 +477,7 @@ function runDoctor(opts = {}) {
453
477
  return { results, issueCount: issues.length };
454
478
  }
455
479
 
456
- module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, surveyHookCoverage, relicRepairGuard };
480
+ module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, surveyHookCoverage, relicRepairGuard, classifyEmbeddings };
457
481
 
458
482
  if (require.main === module) {
459
483
  const args = process.argv.slice(2);
@@ -93,3 +93,32 @@ test('relicRepairGuard blocks settings repair from a relic copy and redirects',
93
93
  // Active (or dev/npm) context → repair proceeds.
94
94
  assert.equal(relicRepairGuard({ relic: false, log: () => {} }), false);
95
95
  });
96
+
97
+ // ── classifyEmbeddings (vector-availability — warns on silent FTS5-only) ──
98
+
99
+ test('classifyEmbeddings WARNS when embed-capable but nothing embedded (vector inactive)', () => {
100
+ const { classifyEmbeddings } = require('./doctor');
101
+ // The exact silent-FTS5 gap: model_available compile-flag true, real embeddable
102
+ // nodes exist, but 0 embedded (model never downloaded/loaded).
103
+ const r = classifyEmbeddings({ model_available: true, embedding_progress: '0/2745',
104
+ embedding_status: 'pending', search_mode: 'fts_only' });
105
+ assert.equal(r.status, 'warn', 'must not false-green a vector-inactive index');
106
+ assert.match(r.detail, /FTS5-only|vector INACTIVE/);
107
+ });
108
+
109
+ test('classifyEmbeddings WARNS when binary lacks embed-model feature', () => {
110
+ const { classifyEmbeddings } = require('./doctor');
111
+ const r = classifyEmbeddings({ model_available: false, embedding_progress: '0/0' });
112
+ assert.equal(r.status, 'warn');
113
+ assert.match(r.detail, /without embed-model/);
114
+ });
115
+
116
+ test('classifyEmbeddings OK for hybrid (partial + complete) and no-embeddable', () => {
117
+ const { classifyEmbeddings } = require('./doctor');
118
+ assert.equal(classifyEmbeddings({ model_available: true, embedding_progress: '900/2745' }).status, 'ok');
119
+ assert.equal(classifyEmbeddings({ model_available: true, embedding_progress: '2745/2745' }).status, 'ok');
120
+ // total === 0 is a non-code index, genuinely nothing to embed → ok, not a false warn.
121
+ const none = classifyEmbeddings({ model_available: true, embedding_progress: '0/0' });
122
+ assert.equal(none.status, 'ok');
123
+ assert.match(none.detail, /no embeddable nodes/);
124
+ });
@@ -12,6 +12,45 @@ const CACHE_FILE = path.join(os.homedir(), '.cache', 'code-graph', 'binary-path'
12
12
  const BINARY_NAME = PLATFORM === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
13
13
  const PLATFORM_PKG = `@sdsrs/code-graph-${PLATFORM}-${ARCH}`;
14
14
 
15
+ /**
16
+ * libc flavor of the current Linux runtime: 'glibc' or 'musl'. musl (Alpine) has
17
+ * no `glibcVersionRuntime` in the Node process-report header. Best-effort; returns
18
+ * 'glibc' off Linux or when detection is unavailable.
19
+ */
20
+ function detectLibc() {
21
+ if (PLATFORM !== 'linux') return 'glibc';
22
+ try {
23
+ const header = process.report.getReport().header;
24
+ if (header && header.glibcVersionRuntime) return 'glibc';
25
+ return 'musl';
26
+ } catch {
27
+ try { if (fs.existsSync('/etc/alpine-release')) return 'musl'; } catch { /* ignore */ }
28
+ return 'glibc';
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Actionable install hint for a platform that has NO published prebuilt binary
34
+ * (Alpine/musl, or native Windows-on-ARM), or null when the platform is supported
35
+ * (generic messaging applies). Pure in (platform, arch, libc) so it is unit-testable
36
+ * without running on each OS. Prevents the misleading "npm install
37
+ * @sdsrs/code-graph-<plat>-<arch>" suggestion for a package that does not exist.
38
+ */
39
+ function unsupportedPlatformHint(platform = PLATFORM, arch = ARCH, libc = null) {
40
+ const lc = libc || (platform === 'linux' ? detectLibc() : 'glibc');
41
+ if (platform === 'linux' && lc === 'musl') {
42
+ return 'Detected Alpine/musl libc, which has no prebuilt binary. Install from source:\n'
43
+ + ' cargo install code-graph-mcp --features embed-model\n'
44
+ + 'or use a glibc-based image (e.g. node:20-slim / debian, not node:20-alpine).';
45
+ }
46
+ if (platform === 'win32' && arch === 'arm64') {
47
+ return 'Detected Windows on ARM (arm64), which has no native build. Either use an x64 '
48
+ + 'build of Node.js (the published x64 binary runs under Windows ARM emulation), '
49
+ + 'or install from source:\n cargo install code-graph-mcp --features embed-model';
50
+ }
51
+ return null;
52
+ }
53
+
15
54
  /** Read the npm pkg version from this script's package.json (claude-plugin/../package.json). */
16
55
  function getPackageVersion() {
17
56
  try { return require('../../package.json').version; }
@@ -263,6 +302,7 @@ module.exports = {
263
302
  findBinary, findBinaryUncached, clearCache,
264
303
  globalNodeModulesCandidates, findPlatformBinary,
265
304
  getPackageVersion, compareVersions, isCachedBinaryFresh,
305
+ detectLibc, unsupportedPlatformHint,
266
306
  CACHE_FILE, BINARY_NAME, PLATFORM_PKG,
267
307
  };
268
308
 
@@ -6,7 +6,8 @@ const os = require('os');
6
6
  const path = require('path');
7
7
 
8
8
  const { globalNodeModulesCandidates, findPlatformBinary, BINARY_NAME,
9
- compareVersions, getPackageVersion, isCachedBinaryFresh } = require('./find-binary');
9
+ compareVersions, getPackageVersion, isCachedBinaryFresh,
10
+ unsupportedPlatformHint } = require('./find-binary');
10
11
 
11
12
  function mkDir(t, prefix) {
12
13
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
@@ -219,3 +220,27 @@ test('isCachedBinaryFresh: file basename mismatch → not fresh', (t) => {
219
220
  if (process.platform !== 'win32') fs.chmodSync(wrongName, 0o755);
220
221
  assert.equal(isCachedBinaryFresh(wrongName, '0.25.0'), false);
221
222
  });
223
+
224
+ // ── unsupportedPlatformHint (actionable message for tails with no prebuilt binary) ──
225
+
226
+ test('unsupportedPlatformHint flags Alpine/musl with a source/glibc-image hint', () => {
227
+ const hint = unsupportedPlatformHint('linux', 'x64', 'musl');
228
+ assert.ok(hint, 'musl should produce a hint');
229
+ assert.match(hint, /musl|Alpine/);
230
+ assert.match(hint, /cargo install/);
231
+ });
232
+
233
+ test('unsupportedPlatformHint flags native Windows-on-ARM with emulation/source hint', () => {
234
+ const hint = unsupportedPlatformHint('win32', 'arm64', 'glibc');
235
+ assert.ok(hint, 'win32-arm64 should produce a hint');
236
+ assert.match(hint, /Windows on ARM|arm64/);
237
+ assert.match(hint, /x64|cargo install/);
238
+ });
239
+
240
+ test('unsupportedPlatformHint returns null for supported platforms', () => {
241
+ assert.equal(unsupportedPlatformHint('linux', 'x64', 'glibc'), null);
242
+ assert.equal(unsupportedPlatformHint('linux', 'arm64', 'glibc'), null);
243
+ assert.equal(unsupportedPlatformHint('darwin', 'arm64', 'glibc'), null);
244
+ assert.equal(unsupportedPlatformHint('darwin', 'x64', 'glibc'), null);
245
+ assert.equal(unsupportedPlatformHint('win32', 'x64', 'glibc'), null);
246
+ });
@@ -105,7 +105,7 @@ if (process.env.CODE_GRAPH_FORCE_PLUGIN_MCP !== '1' && isNonProjectCwd(process.c
105
105
  return;
106
106
  }
107
107
 
108
- const { findBinary, clearCache } = require('./find-binary');
108
+ const { findBinary, clearCache, unsupportedPlatformHint } = require('./find-binary');
109
109
 
110
110
  let binary = findBinary();
111
111
 
@@ -169,6 +169,14 @@ if (!binary) {
169
169
  const installedViaMarketplace = fs.existsSync(
170
170
  path.join(__dirname, '..', '.claude-plugin', 'plugin.json')
171
171
  );
172
+ const platformHint = unsupportedPlatformHint();
173
+ if (platformHint) {
174
+ // Unsupported platform (Alpine/musl or native Windows-on-ARM): the per-platform
175
+ // npm package does not exist, so the generic "npm install @sdsrs/code-graph-<plat>-<arch>"
176
+ // suggestion below would point at a nonexistent package. Show the source/emulation hint.
177
+ process.stderr.write('[code-graph] Binary not found.\n' + platformHint + '\n');
178
+ process.exit(1);
179
+ }
172
180
  process.stderr.write('[code-graph] Binary not found. Install manually:\n');
173
181
  if (installedViaMarketplace) {
174
182
  process.stderr.write(
@@ -217,6 +225,10 @@ child.on('error', (err) => {
217
225
  ` xattr -d com.apple.quarantine "${binary}"\n`
218
226
  );
219
227
  }
228
+ // A glibc binary installed on musl (older npm ignores the `libc` field) is present
229
+ // but execs into a loader error — surface the actionable platform hint.
230
+ const platformHint = unsupportedPlatformHint();
231
+ if (platformHint) process.stderr.write(platformHint + '\n');
220
232
  process.exit(1);
221
233
  });
222
234
 
@@ -82,6 +82,9 @@ hidden 5 加载 schema —— 实操中走新 flag。CLI 子命令保持原样
82
82
  | "相似/重复函数"(需 embedding) | `code-graph-mcp similar X` | `get_ast_node symbol_name=X include_similar=true` |
83
83
  | "未使用的代码" | `code-graph-mcp dead-code [path]` | `module_overview path=<path> include_dead=true` |
84
84
  | "架构咽喉/桥节点是谁?" | `code-graph-mcp centrality` | —(CLI-only;betweenness 中心性,补 `map` 的 caller_count 度中心性) |
85
+ | "循环导入依赖(哪些文件互相 import)?" | `code-graph-mcp cycles` | —(CLI-only;文件级 import 环 = SCC;JS/TS/Py/Go 是坏味,Rust 内部环常良性) |
86
+ | "可疑/意外的跨模块耦合?" | `code-graph-mcp surprising` | —(CLI-only;跨文件 calls/refs 按 低置信(ambiguous>inferred)+跨模块+sole-bridge 打分) |
87
+ | "代码健康总览(想要一份报告)?" | `code-graph-mcp report` | —(CLI-only;汇总 summary+置信度 / hot / chokepoints / cycles / surprising / dead-code) |
85
88
 
86
89
  **dead-code 的 `ignore_paths`**:CLI 默认豁免 `["claude-plugin/", "benches/"]`
87
90
  (macro/shell 入口点);`--no-ignore` 关闭。MCP 端也接同名参数。
@@ -117,6 +120,9 @@ code-graph-mcp show SYMBOL # 节点详情
117
120
  code-graph-mcp refs SYMBOL --relation calls # 引用筛选
118
121
  code-graph-mcp refs SYMBOL --min-confidence extracted # 只看精确边(滤跨文件裸名 inferred/ambiguous)
119
122
  code-graph-mcp centrality # 架构咽喉(betweenness 桥节点;补 map 的 caller_count)
123
+ code-graph-mcp cycles # 循环导入依赖(文件级 import 环 / SCC)
124
+ code-graph-mcp surprising # 可疑跨模块耦合(低置信 + 跨模块 + sole-bridge 打分)
125
+ code-graph-mcp report # 代码健康总览(汇总 hot/chokepoints/cycles/surprising/dead-code)
120
126
  code-graph-mcp dead-code [path] # 未使用代码(默认豁免 claude-plugin/)
121
127
  code-graph-mcp dead-code --ignore tmp/ --ignore scripts/bin/ # 自定义豁免前缀
122
128
  code-graph-mcp dead-code --no-ignore # 关掉默认豁免,看完整列表
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.54.2",
3
+ "version": "0.56.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.54.2",
39
- "@sdsrs/code-graph-linux-arm64": "0.54.2",
40
- "@sdsrs/code-graph-darwin-x64": "0.54.2",
41
- "@sdsrs/code-graph-darwin-arm64": "0.54.2",
42
- "@sdsrs/code-graph-win32-x64": "0.54.2"
38
+ "@sdsrs/code-graph-linux-x64": "0.56.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.56.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.56.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.56.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.56.0"
43
43
  }
44
44
  }