@sdsrs/code-graph 0.58.0 → 0.60.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.58.0",
7
+ "version": "0.60.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -24,6 +24,23 @@ function readAdoptedBy(filePath) {
24
24
  return m ? m[1] : null;
25
25
  } catch { return null; }
26
26
  }
27
+ // Atomic write (tmp in same dir → rename) so a crash mid-write can't leave a
28
+ // half-written MEMORY.md / detail file — the dir is shared with claude-mem-lite,
29
+ // which reads MEMORY.md on every keyword match. Mirrors lifecycle.js
30
+ // writeJsonAtomic / auto-update.js binary promote; accepts a string or Buffer.
31
+ function writeFileAtomic(filePath, data) {
32
+ const tmp = filePath + '.tmp.' + process.pid;
33
+ fs.writeFileSync(tmp, data);
34
+ try {
35
+ fs.renameSync(tmp, filePath);
36
+ } catch (e) {
37
+ // rename can fail (ENOSPC / EACCES / EROFS on the dir). Don't orphan the
38
+ // temp in the shared memory dir — mirror auto-update.js's binary promote,
39
+ // which cleans its tmp on failure. Best-effort unlink, then rethrow original.
40
+ try { fs.unlinkSync(tmp); } catch { /* already gone */ }
41
+ throw e;
42
+ }
43
+ }
27
44
  // One-liner per MEMORY.md spec ("each entry should be one line"). All routing
28
45
  // triggers from prior multi-line block preserved verbatim — collapsing to single
29
46
  // line is a structural fix, not a signal change. Decision table lives in the
@@ -354,7 +371,7 @@ function adopt({ cwd, home, templatePath } = {}) {
354
371
  // ADOPTED_BY_RE strip below).
355
372
  const tplBody = fs.readFileSync(tpl);
356
373
  const marker = Buffer.from(`<!-- adopted-by: ${effectiveCwd} -->\n`);
357
- fs.writeFileSync(target, Buffer.concat([marker, tplBody]));
374
+ writeFileAtomic(target, Buffer.concat([marker, tplBody]));
358
375
 
359
376
  const indexPath = path.join(dir, 'MEMORY.md');
360
377
  const index = fs.existsSync(indexPath) ? fs.readFileSync(indexPath, 'utf8') : '# Memory Index\n';
@@ -372,7 +389,7 @@ function adopt({ cwd, home, templatePath } = {}) {
372
389
  const cleaned = stripSentinelBlock(index);
373
390
  const healed = cleaned !== index;
374
391
  const base = cleaned.endsWith('\n') ? cleaned : cleaned + '\n';
375
- fs.writeFileSync(indexPath, base + desiredBlock + '\n');
392
+ writeFileAtomic(indexPath, base + desiredBlock + '\n');
376
393
  return { ok: true, target, indexPath, indexed: true, healed, collisionWith };
377
394
  }
378
395
 
@@ -479,7 +496,7 @@ function unadopt({ cwd, home } = {}) {
479
496
  const before = fs.readFileSync(indexPath, 'utf8');
480
497
  const after = stripSentinelBlock(before);
481
498
  if (after !== before) {
482
- fs.writeFileSync(indexPath, after);
499
+ writeFileAtomic(indexPath, after);
483
500
  indexPruned = true;
484
501
  }
485
502
  }
@@ -90,6 +90,39 @@ test('adopt preserves existing MEMORY.md content and appends', () => {
90
90
  } finally { sb.cleanup(); }
91
91
  });
92
92
 
93
+ test('adopt + unadopt write atomically — no .tmp residue in the memory dir', () => {
94
+ // The memory dir is shared with claude-mem-lite, which reads MEMORY.md on
95
+ // every keyword match; a non-atomic write crashing mid-flight would corrupt
96
+ // it. adopt/unadopt now go through writeFileAtomic (tmp + rename). Proof the
97
+ // rename completed cleanly across all three write sites (detail file + adopt
98
+ // index + unadopt prune): no leftover `*.tmp.<pid>` entries.
99
+ const sb = makeSandbox();
100
+ try {
101
+ adopt({ cwd: sb.cwd, home: sb.home });
102
+ unadopt({ cwd: sb.cwd, home: sb.home });
103
+ const residue = fs.readdirSync(sb.dir).filter((f) => f.includes('.tmp.'));
104
+ assert.deepStrictEqual(residue, [], `no atomic-write tmp residue; found: ${residue}`);
105
+ } finally { sb.cleanup(); }
106
+ });
107
+
108
+ test('writeFileAtomic cleans its temp file when rename fails (no orphaned .tmp)', () => {
109
+ // The success path leaves no residue (above). This pins the FAILURE path: if
110
+ // renameSync throws (ENOSPC/EACCES/EROFS on the shared memory dir) the temp must
111
+ // be unlinked, not orphaned. Force every rename to fail and assert no `.tmp.<pid>`
112
+ // survives the (failed) adopt.
113
+ const sb = makeSandbox();
114
+ const realRename = fs.renameSync;
115
+ try {
116
+ fs.renameSync = () => { const e = new Error('EROFS: simulated read-only fs'); e.code = 'EROFS'; throw e; };
117
+ try { adopt({ cwd: sb.cwd, home: sb.home }); } catch { /* expected — rename failed */ }
118
+ const residue = fs.readdirSync(sb.dir).filter((f) => f.includes('.tmp.'));
119
+ assert.deepStrictEqual(residue, [], `failed rename must not orphan a temp; found: ${residue}`);
120
+ } finally {
121
+ fs.renameSync = realRename;
122
+ sb.cleanup();
123
+ }
124
+ });
125
+
93
126
  test('adopt refuses a non-project cwd even when the memory dir already exists (regression: /tmp adoption)', () => {
94
127
  // Bug: the isProjectRoot guard was nested inside `if (!fs.existsSync(dir))`,
95
128
  // so when Claude Code had already created ~/.claude/projects/<slug>/memory
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.58.0",
3
+ "version": "0.60.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.58.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.58.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.58.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.58.0",
42
- "@sdsrs/code-graph-win32-x64": "0.58.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.60.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.60.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.60.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.60.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.60.0"
43
43
  }
44
44
  }