@sdsrs/code-graph 0.75.0 → 0.75.2

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.75.0",
7
+ "version": "0.75.2",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -1359,6 +1359,63 @@ test('resolveProjectRoot: no index up to $HOME → null (home itself still check
1359
1359
  } finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
1360
1360
  });
1361
1361
 
1362
+ test('resolveProjectRoot: skips a STRAY nested subdir index, prefers the .git root', () => {
1363
+ // monorepo (daagu shape): root has .git + index; a subdir carries a stray
1364
+ // index relic but no .git. Resolving from the subdir must climb to the root,
1365
+ // not pin the stray nested index (the statusline "oscillation" root cause).
1366
+ const base = fsE2e.mkdtempSync(pathE2e.join(osE2e.tmpdir(), 'cg-root-'));
1367
+ try {
1368
+ const proj = pathE2e.join(base, 'proj');
1369
+ fsE2e.mkdirSync(pathE2e.join(proj, '.git'), { recursive: true });
1370
+ fsE2e.mkdirSync(pathE2e.join(proj, '.code-graph'), { recursive: true });
1371
+ fsE2e.writeFileSync(pathE2e.join(proj, '.code-graph', 'index.db'), '');
1372
+ const sub = pathE2e.join(proj, 'backend');
1373
+ fsE2e.mkdirSync(pathE2e.join(sub, '.code-graph'), { recursive: true });
1374
+ fsE2e.writeFileSync(pathE2e.join(sub, '.code-graph', 'index.db'), '');
1375
+ assert.equal(resolveProjectRoot(sub, { home: base }), proj);
1376
+ } finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
1377
+ });
1378
+
1379
+ test('resolveProjectRoot: a nested index with its OWN .git (submodule) still wins', () => {
1380
+ const base = fsE2e.mkdtempSync(pathE2e.join(osE2e.tmpdir(), 'cg-root-'));
1381
+ try {
1382
+ const proj = pathE2e.join(base, 'proj');
1383
+ fsE2e.mkdirSync(pathE2e.join(proj, '.git'), { recursive: true });
1384
+ fsE2e.mkdirSync(pathE2e.join(proj, '.code-graph'), { recursive: true });
1385
+ fsE2e.writeFileSync(pathE2e.join(proj, '.code-graph', 'index.db'), '');
1386
+ const sub = pathE2e.join(proj, 'vendored');
1387
+ fsE2e.mkdirSync(pathE2e.join(sub, '.git'), { recursive: true });
1388
+ fsE2e.mkdirSync(pathE2e.join(sub, '.code-graph'), { recursive: true });
1389
+ fsE2e.writeFileSync(pathE2e.join(sub, '.code-graph', 'index.db'), '');
1390
+ assert.equal(resolveProjectRoot(sub, { home: base }), sub);
1391
+ } finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
1392
+ });
1393
+
1394
+ test('resolveProjectRoot: start with its OWN .git but no index → null (boundary, no escape)', () => {
1395
+ const base = fsE2e.mkdtempSync(pathE2e.join(osE2e.tmpdir(), 'cg-root-'));
1396
+ try {
1397
+ const proj = pathE2e.join(base, 'proj'); // indexed parent
1398
+ fsE2e.mkdirSync(pathE2e.join(proj, '.code-graph'), { recursive: true });
1399
+ fsE2e.writeFileSync(pathE2e.join(proj, '.code-graph', 'index.db'), '');
1400
+ const sub = pathE2e.join(proj, 'sub'); // own .git, no index
1401
+ fsE2e.mkdirSync(pathE2e.join(sub, '.git'), { recursive: true });
1402
+ assert.equal(resolveProjectRoot(sub, { home: base }), null);
1403
+ } finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
1404
+ });
1405
+
1406
+ test('resolveProjectRoot: non-git monorepo — stray subdir index resolves to indexed ancestor', () => {
1407
+ const base = fsE2e.mkdtempSync(pathE2e.join(osE2e.tmpdir(), 'cg-root-'));
1408
+ try {
1409
+ const root = pathE2e.join(base, 'mono'); // indexed, NO .git
1410
+ fsE2e.mkdirSync(pathE2e.join(root, '.code-graph'), { recursive: true });
1411
+ fsE2e.writeFileSync(pathE2e.join(root, '.code-graph', 'index.db'), '');
1412
+ const sub = pathE2e.join(root, 'backend'); // stray index, no .git
1413
+ fsE2e.mkdirSync(pathE2e.join(sub, '.code-graph'), { recursive: true });
1414
+ fsE2e.writeFileSync(pathE2e.join(sub, '.code-graph', 'index.db'), '');
1415
+ assert.equal(resolveProjectRoot(sub, { home: base }), root);
1416
+ } finally { fsE2e.rmSync(base, { recursive: true, force: true }); }
1417
+ });
1418
+
1362
1419
  test('rebaseRelativePaths: daagu shape — bare `app` from backend/ cwd', () => {
1363
1420
  const exists = (p) => p.endsWith(pathE2e.join('backend', 'app'));
1364
1421
  const cmd = 'grep -rn "rr_source\\|max_retries" app --include=*.py';
@@ -14,16 +14,61 @@ const fs = require('fs');
14
14
  const os = require('os');
15
15
  const path = require('path');
16
16
 
17
+ // Resolves to the project's CANONICAL index dir, skipping STRAY nested indexes.
18
+ // A monorepo subdir (`daagu/backend`, `daagu/frontend`) can carry its own
19
+ // `.code-graph/index.db` — a relic an older binary created — nested under the
20
+ // real root's index. Returning the nearest such index made every consumer
21
+ // (statusline gate, hooks) read a different DB per cwd (statusline "oscillation",
22
+ // `✗ 0 nodes` in an empty subdir index). Mirror the Rust resolver: the start's
23
+ // own index wins only if it is NOT a stray nested index (no indexed ancestor) OR
24
+ // start is itself a project boundary (`.git`, i.e. a real submodule). Otherwise
25
+ // prefer the project root: the nearest indexed `.git` root, else the outermost
26
+ // indexed dir on the chain. `null` when nothing on start→…→home is indexed.
17
27
  function resolveProjectRoot(startDir, opts = {}) {
18
28
  const home = opts.home !== undefined ? opts.home : os.homedir();
19
29
  const exists = opts.exists || fs.existsSync;
20
- let dir = path.resolve(startDir || '.');
30
+ const hasIndex = (d) => exists(path.join(d, '.code-graph', 'index.db'));
31
+ const hasGit = (d) => exists(path.join(d, '.git'));
32
+ const start = path.resolve(startDir || '.');
33
+
34
+ // start's own `.git` is a hard project boundary (a real submodule / distinct
35
+ // repo): use its index if present, else `null` — never escape to an ancestor's
36
+ // index. Mirrors the Rust resolver's rule 1 (which returns cwd even without an
37
+ // index because it CREATES one; the JS reader has nothing to read → null).
38
+ if (hasGit(start)) return hasIndex(start) ? start : null;
39
+
40
+ // Detect whether `start` is a STRAY nested index: walk STRICT ancestors up to
41
+ // the nearest `.git` root (project boundary), bounded at home. An indexed
42
+ // ancestor within that boundary means start's own index is a monorepo-subdir
43
+ // relic. Stop AT the git root — an index above it (e.g. `~/.code-graph`) is an
44
+ // unrelated outer project and must not poison this one.
45
+ let gitRootIndexed = null;
46
+ let ancestorIndexed = false;
47
+ let dir = start;
21
48
  for (;;) {
22
- if (exists(path.join(dir, '.code-graph', 'index.db'))) return dir;
23
- if (dir === home) return null;
24
49
  const parent = path.dirname(dir);
25
- if (parent === dir) return null;
50
+ // Stop BEFORE home (and fs root): an index at/above home (e.g. ~/.code-graph
51
+ // from indexing a home dir) is an unrelated outer project, never a parent
52
+ // that makes `start` stray.
53
+ if (parent === dir || parent === home) break;
26
54
  dir = parent;
55
+ if (hasIndex(dir)) ancestorIndexed = true;
56
+ if (hasGit(dir)) { if (hasIndex(dir)) gitRootIndexed = dir; break; }
57
+ }
58
+
59
+ // start's own index wins unless it is stray (an indexed ancestor within the
60
+ // git boundary). start's own `.git` was already handled above.
61
+ if (hasIndex(start) && !ancestorIndexed) return start;
62
+ if (gitRootIndexed) return gitRootIndexed;
63
+ // Otherwise the nearest indexed ancestor (skipping a stray start), bounded at
64
+ // home; null if nothing on the chain is indexed. Mirrors the original walk.
65
+ let d = hasIndex(start) ? path.dirname(start) : start;
66
+ for (;;) {
67
+ if (hasIndex(d)) return d;
68
+ if (d === home) return null;
69
+ const parent = path.dirname(d);
70
+ if (parent === d) return null;
71
+ d = parent;
27
72
  }
28
73
  }
29
74
 
@@ -5,6 +5,7 @@ const fs = require('fs');
5
5
  const os = require('os');
6
6
  const path = require('path');
7
7
  const { findBinary } = require('./find-binary');
8
+ const { resolveProjectRoot } = require('./project-root');
8
9
  const lifecycle = require('./lifecycle');
9
10
  const cleanupDisabledStatusline = lifecycle.cleanupDisabledStatusline || (() => ({ cleaned: false }));
10
11
 
@@ -24,14 +25,19 @@ function updatePending() {
24
25
  const disabledCleanup = cleanupDisabledStatusline();
25
26
  if (disabledCleanup.cleaned) process.exit(0);
26
27
 
27
- // Only show status in projects that have a code-graph directory.
28
- // The statusLine config is global, so we must exit silently for
29
- // directories that aren't code-graph projects.
30
- const cwd = process.cwd();
31
- const codeGraphDir = path.join(cwd, '.code-graph');
32
- if (!fs.existsSync(codeGraphDir)) {
28
+ // Only show status in projects that have a code-graph directory. The statusLine
29
+ // config is global, so we must exit silently for non-code-graph directories.
30
+ // Walk UP to the canonical project root (resolveProjectRoot) rather than keying
31
+ // on the bare process.cwd(): when the shell sits in a subdir, the bare-cwd gate
32
+ // either showed a STRAY nested subdir index (monorepo relic — the statusline
33
+ // "oscillating" between root/backend/frontend node counts) or, in a clean subdir
34
+ // with no local index, showed nothing at all. The resolver skips stray nested
35
+ // indexes, so the statusline tracks one DB — the project root — from any subdir.
36
+ const root = resolveProjectRoot(process.cwd());
37
+ if (!root) {
33
38
  process.exit(0);
34
39
  }
40
+ const codeGraphDir = path.join(root, '.code-graph');
35
41
 
36
42
  // Check for background indexing progress file first
37
43
  const progressFile = path.join(codeGraphDir, 'indexing-status.json');
@@ -108,7 +114,11 @@ let errText = '';
108
114
  try {
109
115
  report = parseReport(execFileSync(bin, ['health-check', '--format', 'json'], {
110
116
  timeout: 3000,
111
- stdio: ['pipe', 'pipe', 'pipe']
117
+ stdio: ['pipe', 'pipe', 'pipe'],
118
+ // Run the binary FROM the resolved root so its own project-root resolution
119
+ // lands on the same DB the gate above picked (a subdir cwd would otherwise
120
+ // re-resolve to a stray nested index inside the binary).
121
+ cwd: root
112
122
  }).toString());
113
123
  } catch (e) {
114
124
  // health-check exits NON-ZERO on an unhealthy/empty index but still writes the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.75.0",
3
+ "version": "0.75.2",
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.75.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.75.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.75.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.75.0",
42
- "@sdsrs/code-graph-win32-x64": "0.75.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.75.2",
39
+ "@sdsrs/code-graph-linux-arm64": "0.75.2",
40
+ "@sdsrs/code-graph-darwin-x64": "0.75.2",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.75.2",
42
+ "@sdsrs/code-graph-win32-x64": "0.75.2"
43
43
  }
44
44
  }