@sdsrs/code-graph 0.85.2 → 0.85.4

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.85.2",
7
+ "version": "0.85.4",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -496,10 +496,21 @@ function runRepairs(results) {
496
496
  case 'hooks-invalid': {
497
497
  console.log('\n Repairing hooks...');
498
498
  if (relicRepairGuard()) break;
499
- const { install } = require('./lifecycle');
499
+ const { install, scanForBrokenPaths } = require('./lifecycle');
500
500
  install();
501
- console.log(' \u2705 Hooks repaired \u2014 restart Claude Code to apply');
502
- fixed++;
501
+ // Diagnosis already ran install()+re-scan and the paths were STILL
502
+ // broken (that `repaired:false` is what raised hooks-invalid). Verify
503
+ // this second attempt actually cleared them before counting it fixed \u2014
504
+ // a blind fixed++ here would let doctor exit 0 ("healthy") while the
505
+ // hook paths stay broken.
506
+ const remaining = scanForBrokenPaths();
507
+ if (remaining.length === 0) {
508
+ console.log(' \u2705 Hooks repaired \u2014 restart Claude Code to apply');
509
+ fixed++;
510
+ } else {
511
+ console.log(` \u274c ${remaining.length} hook path(s) still invalid \u2014 plugin scripts may be missing.`);
512
+ console.log(' Reinstall: npm install -g @sdsrs/code-graph (or re-run the plugin installer)');
513
+ }
503
514
  break;
504
515
  }
505
516
 
@@ -532,25 +543,43 @@ function runRepairs(results) {
532
543
 
533
544
  // ── Main ──────────────────────────────────────────────────
534
545
 
546
+ // Exit status for a doctor run reflects what remains BROKEN, not what was found:
547
+ // --check-only → every found issue is unresolved (report cleanliness, no repair).
548
+ // repair mode → issueCount minus what runRepairs resolved. A run that fixes
549
+ // everything ("N/N addressed") reports 0 so `doctor && …` and
550
+ // self-heal automation don't read a successful repair as a
551
+ // failure. runRepairs counts an issue fixed only when its repair
552
+ // reports success — and the hooks arm re-scans after install() to
553
+ // confirm, so a still-broken re-scan is NOT counted (stays
554
+ // unresolved → exit 1). An issue with no working repair
555
+ // (schema-mismatch is advisory only) likewise keeps this > 0.
556
+ function unresolvedCount({ checkOnly, issueCount, fixed }) {
557
+ return checkOnly ? issueCount : issueCount - fixed;
558
+ }
559
+
535
560
  function runDoctor(opts = {}) {
536
561
  const results = runDiagnostics();
537
562
  console.log(formatReport(results, { checkOnly: opts.checkOnly }));
538
563
 
539
564
  const issues = results.filter(r => r.status === 'warn' || r.status === 'error');
540
565
 
566
+ let fixed = 0;
541
567
  if (issues.length > 0 && !opts.checkOnly) {
542
- const fixed = runRepairs(results);
568
+ fixed = runRepairs(results);
543
569
  console.log(`\n ${fixed}/${issues.length} issue(s) addressed.`);
544
570
  }
545
571
 
546
- return { results, issueCount: issues.length };
572
+ const unresolved = unresolvedCount({
573
+ checkOnly: opts.checkOnly, issueCount: issues.length, fixed,
574
+ });
575
+ return { results, issueCount: issues.length, unresolved };
547
576
  }
548
577
 
549
- module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, surveyHookCoverage, relicRepairGuard, classifyEmbeddings, detectEmbedModel, devBuildCommand };
578
+ module.exports = { runDiagnostics, formatReport, runRepairs, runDoctor, unresolvedCount, surveyHookCoverage, relicRepairGuard, classifyEmbeddings, detectEmbedModel, devBuildCommand };
550
579
 
551
580
  if (require.main === module) {
552
581
  const args = process.argv.slice(2);
553
582
  const checkOnly = args.includes('--check-only');
554
- const { issueCount } = runDoctor({ checkOnly });
555
- process.exit(issueCount > 0 ? 1 : 0);
583
+ const { unresolved } = runDoctor({ checkOnly });
584
+ process.exit(unresolved > 0 ? 1 : 0);
556
585
  }
@@ -165,3 +165,51 @@ test('detectEmbedModel reads model_available from `health-check --json`; probe f
165
165
  // no binary → null
166
166
  assert.equal(detectEmbedModel(null), null);
167
167
  });
168
+
169
+ test('unresolvedCount: repair mode exits 0 iff every found issue was fixed', () => {
170
+ const { unresolvedCount } = require('./doctor');
171
+ // Clean run — nothing found.
172
+ assert.equal(unresolvedCount({ checkOnly: false, issueCount: 0, fixed: 0 }), 0);
173
+ // Repair fixed everything ("N/N addressed") → 0, so `doctor && …` and
174
+ // self-heal automation don't read a successful repair as failure. This is
175
+ // the regression this contract guards: previously exited 1 on any issue found.
176
+ assert.equal(unresolvedCount({ checkOnly: false, issueCount: 3, fixed: 3 }), 0);
177
+ // Partial repair → the remainder is unresolved (nonzero → exit 1).
178
+ assert.equal(unresolvedCount({ checkOnly: false, issueCount: 3, fixed: 1 }), 2);
179
+ // Advisory-only issue with no working auto-repair (fixed stays 0) → unresolved.
180
+ assert.equal(unresolvedCount({ checkOnly: false, issueCount: 1, fixed: 0 }), 1);
181
+ });
182
+
183
+ test('unresolvedCount: --check-only reports every found issue (never repairs)', () => {
184
+ const { unresolvedCount } = require('./doctor');
185
+ // check-only performs no repair, so fixed is 0; a found issue must still
186
+ // surface as unresolved (exit 1) — check mode reports cleanliness.
187
+ assert.equal(unresolvedCount({ checkOnly: true, issueCount: 2, fixed: 0 }), 2);
188
+ assert.equal(unresolvedCount({ checkOnly: true, issueCount: 0, fixed: 0 }), 0);
189
+ });
190
+
191
+ test('runRepairs: hooks-invalid counts fixed only when the post-install re-scan is clean', () => {
192
+ // hooks-invalid is raised only after diagnosis already ran install()+re-scan
193
+ // and paths were STILL broken. The repair arm must re-verify, else it reports
194
+ // a false exit 0 ("healthy") while the hooks stay broken. Stub the lifecycle
195
+ // deps runRepairs pulls via require('./lifecycle') on the shared cached export
196
+ // object; restore in finally so no other test sees the stubs.
197
+ const { runRepairs } = require('./doctor');
198
+ const lc = require('./lifecycle');
199
+ const orig = { install: lc.install, scan: lc.scanForBrokenPaths, relic: lc.isStaleRelicContext };
200
+ const hooksInvalid = [{ name: 'Hooks', status: 'warn', fixId: 'hooks-invalid' }];
201
+ try {
202
+ lc.isStaleRelicContext = () => false; // not a relic → repair proceeds
203
+ lc.install = () => {}; // install() that cannot restore the paths
204
+ // Re-scan still broken → must NOT count as fixed (old code did fixed++ blindly).
205
+ lc.scanForBrokenPaths = () => [{ type: 'hook', event: 'PreToolUse:Edit', path: '/gone.js' }];
206
+ assert.equal(runRepairs(hooksInvalid), 0, 'still-broken after install must not count as fixed');
207
+ // Re-scan clean → the repair took effect → counts as fixed.
208
+ lc.scanForBrokenPaths = () => [];
209
+ assert.equal(runRepairs(hooksInvalid), 1, 'verified-clean after install counts as fixed');
210
+ } finally {
211
+ lc.install = orig.install;
212
+ lc.scanForBrokenPaths = orig.scan;
213
+ lc.isStaleRelicContext = orig.relic;
214
+ }
215
+ });
@@ -983,8 +983,10 @@ if (require.main === module) {
983
983
  } else if (cmd === 'doctor') {
984
984
  const { runDoctor } = require('./doctor');
985
985
  const checkOnly = process.argv.includes('--check-only');
986
- const { issueCount } = runDoctor({ checkOnly });
987
- process.exit(issueCount > 0 ? 1 : 0);
986
+ // Exit on issues that remain UNRESOLVED after repair, not issues found —
987
+ // a run that fixes everything exits 0. See unresolvedCount in doctor.js.
988
+ const { unresolved } = runDoctor({ checkOnly });
989
+ process.exit(unresolved > 0 ? 1 : 0);
988
990
  } else if (cmd === 'verify-hooks-fire') {
989
991
  // v0.67.0 — Layer-A firing self-test. Spawned detached by session-init
990
992
  // (off the SessionStart budget); writes a small state file the next
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.85.2",
3
+ "version": "0.85.4",
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.85.2",
39
- "@sdsrs/code-graph-linux-arm64": "0.85.2",
40
- "@sdsrs/code-graph-darwin-x64": "0.85.2",
41
- "@sdsrs/code-graph-darwin-arm64": "0.85.2",
42
- "@sdsrs/code-graph-win32-x64": "0.85.2"
38
+ "@sdsrs/code-graph-linux-x64": "0.85.4",
39
+ "@sdsrs/code-graph-linux-arm64": "0.85.4",
40
+ "@sdsrs/code-graph-darwin-x64": "0.85.4",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.85.4",
42
+ "@sdsrs/code-graph-win32-x64": "0.85.4"
43
43
  }
44
44
  }