@sdsrs/code-graph 0.45.3 → 0.46.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.
@@ -4,7 +4,7 @@
4
4
  "author": {
5
5
  "name": "sdsrs"
6
6
  },
7
- "version": "0.45.3",
7
+ "version": "0.46.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -206,19 +206,28 @@ function runDiagnostics() {
206
206
  try {
207
207
  const settings = readJson(settingsPath()) || {};
208
208
  const cov = surveyHookCoverage(settings);
209
- if (cov.missing.length === 0) {
209
+ if (cov.missing.length === 0 && cov.stale.length === 0) {
210
210
  results.push({
211
211
  name: 'Hook coverage',
212
212
  status: 'ok',
213
213
  detail: `settings.json has all ${cov.expected.length} expected entries`,
214
214
  });
215
- } else {
215
+ } else if (cov.missing.length > 0) {
216
216
  results.push({
217
217
  name: 'Hook coverage',
218
218
  status: 'warn',
219
219
  detail: `missing ${cov.missing.length}/${cov.expected.length} settings.json entries: ${cov.missing.join(', ')}`,
220
220
  fixId: 'missing-hooks-in-settings',
221
221
  });
222
+ } else {
223
+ // Present but stale path(s) — re-register rewrites them to the current
224
+ // version. A stale PreToolUse hook can keep the conversion metric dark.
225
+ results.push({
226
+ name: 'Hook coverage',
227
+ status: 'warn',
228
+ detail: `${cov.stale.length}/${cov.expected.length} settings.json entries point at a stale path (re-register to current version): ${cov.stale.join(', ')}`,
229
+ fixId: 'missing-hooks-in-settings',
230
+ });
222
231
  }
223
232
  } catch { /* probe failed — skip */ }
224
233
 
@@ -230,26 +239,42 @@ function runDiagnostics() {
230
239
  function surveyHookCoverage(settings) {
231
240
  const desired = buildSettingsHookEntries();
232
241
  const expected = [];
242
+ const desiredCmd = {}; // key -> command string we would write now
233
243
  for (const [event, entries] of Object.entries(desired)) {
234
244
  for (const e of entries) {
235
- expected.push(`${event}:${e.matcher || '*'}`);
245
+ const key = `${event}:${e.matcher || '*'}`;
246
+ expected.push(key);
247
+ desiredCmd[key] = e.hooks && e.hooks[0] && e.hooks[0].command;
236
248
  }
237
249
  }
238
250
 
239
251
  const present = new Set();
252
+ const presentCmd = {}; // key -> command currently registered
240
253
  if (settings && settings.hooks) {
241
254
  for (const [event, entries] of Object.entries(settings.hooks)) {
242
255
  if (!Array.isArray(entries)) continue;
243
256
  for (const entry of entries) {
244
257
  if (isOurHookEntry(entry)) {
245
- present.add(`${event}:${entry.matcher || '*'}`);
258
+ const key = `${event}:${entry.matcher || '*'}`;
259
+ present.add(key);
260
+ if (entry.hooks && entry.hooks[0] && entry.hooks[0].command) {
261
+ presentCmd[key] = entry.hooks[0].command;
262
+ }
246
263
  }
247
264
  }
248
265
  }
249
266
  }
250
267
 
251
268
  const missing = expected.filter(k => !present.has(k));
252
- return { expected, present: [...present], missing };
269
+ // Stale = present but the registered command no longer matches what we'd write
270
+ // now (points at an old plugin-cache version dir / moved path). A stale path can
271
+ // run pre-recordRecommendation hook code, so the hook fires but the conversion
272
+ // metric stays dark — invisible to a present/absent check. This is the
273
+ // 0.45.1-registered-while-0.45.4-active case the RCA surfaced.
274
+ const stale = expected.filter(k =>
275
+ present.has(k) && desiredCmd[k] && presentCmd[k] && presentCmd[k] !== desiredCmd[k]
276
+ );
277
+ return { expected, present: [...present], missing, stale };
253
278
  }
254
279
 
255
280
  // ── Report Formatting ─────────────────────────────────────
@@ -449,7 +474,7 @@ function runDoctor(opts = {}) {
449
474
  return { results, issueCount: issues.length };
450
475
  }
451
476
 
452
- module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor };
477
+ module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, surveyHookCoverage };
453
478
 
454
479
  if (require.main === module) {
455
480
  const args = process.argv.slice(2);
@@ -2,7 +2,18 @@
2
2
  const test = require('node:test');
3
3
  const assert = require('node:assert/strict');
4
4
 
5
- const { runDiagnostics, formatReport } = require('./doctor');
5
+ const { runDiagnostics, formatReport, surveyHookCoverage } = require('./doctor');
6
+ const { buildSettingsHookEntries } = require('./lifecycle');
7
+
8
+ // Build a settings.json whose hooks exactly mirror what we'd register now.
9
+ function settingsWithCurrentHooks() {
10
+ const desired = buildSettingsHookEntries();
11
+ const hooks = {};
12
+ for (const [event, entries] of Object.entries(desired)) {
13
+ hooks[event] = entries.map(e => JSON.parse(JSON.stringify(e)));
14
+ }
15
+ return { hooks };
16
+ }
6
17
 
7
18
  test('runDiagnostics returns an array of check results', () => {
8
19
  const results = runDiagnostics();
@@ -45,3 +56,27 @@ test('formatReport shows all-clear when no problems', () => {
45
56
  const output = formatReport(results);
46
57
  assert.ok(output.includes('All checks passed') || output.includes('0 issues'));
47
58
  });
59
+
60
+ test('surveyHookCoverage reports clean when all entries are current', () => {
61
+ const cov = surveyHookCoverage(settingsWithCurrentHooks());
62
+ assert.equal(cov.missing.length, 0, 'no missing entries');
63
+ assert.equal(cov.stale.length, 0, 'no stale entries');
64
+ });
65
+
66
+ test('surveyHookCoverage flags a present-but-stale hook path', () => {
67
+ const settings = settingsWithCurrentHooks();
68
+ // Repoint one PreToolUse entry at an old plugin-cache version dir — present,
69
+ // recognized as ours (description unchanged), but command no longer current.
70
+ const bash = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
71
+ bash.hooks[0].command = bash.hooks[0].command.replace('/scripts/', '/0.0.1-old/scripts/');
72
+ const cov = surveyHookCoverage(settings);
73
+ assert.equal(cov.missing.length, 0, 'entry is present, not missing');
74
+ assert.ok(cov.stale.includes('PreToolUse:Bash'),
75
+ `stale Bash path should be flagged; got stale=${JSON.stringify(cov.stale)}`);
76
+ });
77
+
78
+ test('surveyHookCoverage flags missing entries when settings empty', () => {
79
+ const cov = surveyHookCoverage({});
80
+ assert.ok(cov.missing.length === cov.expected.length, 'all expected entries missing');
81
+ assert.equal(cov.stale.length, 0, 'nothing present to be stale');
82
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.45.3",
3
+ "version": "0.46.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.45.3",
39
- "@sdsrs/code-graph-linux-arm64": "0.45.3",
40
- "@sdsrs/code-graph-darwin-x64": "0.45.3",
41
- "@sdsrs/code-graph-darwin-arm64": "0.45.3",
42
- "@sdsrs/code-graph-win32-x64": "0.45.3"
38
+ "@sdsrs/code-graph-linux-x64": "0.46.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.46.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.46.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.46.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.46.0"
43
43
  }
44
44
  }