@sdsrs/code-graph 0.38.0 → 0.39.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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/pre-grep-guide.js +3 -0
- package/claude-plugin/scripts/pre-read-guide.js +5 -1
- package/claude-plugin/scripts/recommendation-log.js +41 -0
- package/claude-plugin/scripts/recommendation-log.test.js +44 -0
- package/claude-plugin/scripts/session-init.js +16 -2
- package/claude-plugin/scripts/session-init.test.js +15 -1
- package/package.json +6 -6
|
@@ -29,6 +29,7 @@ const fs = require('fs');
|
|
|
29
29
|
const path = require('path');
|
|
30
30
|
const crypto = require('crypto');
|
|
31
31
|
const { cgTmpDir } = require('./tmp-dir');
|
|
32
|
+
const { recordRecommendation } = require('./recommendation-log');
|
|
32
33
|
|
|
33
34
|
// --- Pure logic (testable) ---
|
|
34
35
|
|
|
@@ -194,6 +195,7 @@ function runMain() {
|
|
|
194
195
|
// ignored by Claude Code — the grep ran anyway. The hookSpecificOutput form
|
|
195
196
|
// is the documented modern path. Exit 0 — this is a routing decision, not
|
|
196
197
|
// a hook failure (exit 2 would mark the tool call as "hook errored").
|
|
198
|
+
recordRecommendation(cwd, { hook: 'grep', action: 'deny' });
|
|
197
199
|
process.stdout.write(JSON.stringify({
|
|
198
200
|
hookSpecificOutput: {
|
|
199
201
|
hookEventName: 'PreToolUse',
|
|
@@ -204,6 +206,7 @@ function runMain() {
|
|
|
204
206
|
return;
|
|
205
207
|
}
|
|
206
208
|
|
|
209
|
+
recordRecommendation(cwd, { hook: 'grep', action: 'hint' });
|
|
207
210
|
process.stdout.write(buildHint() + '\n');
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -26,6 +26,7 @@ const fs = require('fs');
|
|
|
26
26
|
const path = require('path');
|
|
27
27
|
const crypto = require('crypto');
|
|
28
28
|
const { cgTmpDir } = require('./tmp-dir');
|
|
29
|
+
const { recordRecommendation } = require('./recommendation-log');
|
|
29
30
|
|
|
30
31
|
// --- Configuration ---
|
|
31
32
|
|
|
@@ -158,7 +159,10 @@ function runMain() {
|
|
|
158
159
|
fired = true;
|
|
159
160
|
}
|
|
160
161
|
saveState(cwd, state);
|
|
161
|
-
if (fired)
|
|
162
|
+
if (fired) {
|
|
163
|
+
recordRecommendation(cwd, { hook: 'read', action: 'hint' });
|
|
164
|
+
process.stdout.write(buildHint(dir) + '\n');
|
|
165
|
+
}
|
|
162
166
|
}
|
|
163
167
|
|
|
164
168
|
if (require.main === module) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
// Real-session conversion metric (JS counterpart to the Rust MCP usage.jsonl).
|
|
4
|
+
//
|
|
5
|
+
// The PreToolUse hooks (pre-grep-guide / pre-read-guide) RECOMMEND a code-graph
|
|
6
|
+
// tool when Claude reaches for raw grep / fans out Reads. This module records
|
|
7
|
+
// each emitted recommendation so `code-graph-mcp stats` can compute the *field*
|
|
8
|
+
// conversion rate (recommend → actual cg tool call) — the metric the synthetic
|
|
9
|
+
// routing_bench oracle can't see (memory: self-dogfood-blindspot / feedback_routing_bench).
|
|
10
|
+
//
|
|
11
|
+
// Bounded + best-effort by construction:
|
|
12
|
+
// - appends to <cwd>/.code-graph/recommendations.jsonl and NEVER creates the
|
|
13
|
+
// `.code-graph` dir — so a non-project / tmp cwd (no index) leaves zero
|
|
14
|
+
// footprint, mirroring each hook's existing `.code-graph/index.db` guard.
|
|
15
|
+
// - swallows every error: telemetry must never break or delay a tool call.
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const REC_FILE = 'recommendations.jsonl';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Append one recommendation event to <cwd>/.code-graph/recommendations.jsonl.
|
|
23
|
+
* @param {string} cwd project root (the hook's process.cwd())
|
|
24
|
+
* @param {object} event e.g. { hook: 'grep', action: 'deny' }
|
|
25
|
+
* @returns {boolean} true if a line was written
|
|
26
|
+
*/
|
|
27
|
+
function recordRecommendation(cwd, event = {}) {
|
|
28
|
+
try {
|
|
29
|
+
const dir = path.join(cwd, '.code-graph');
|
|
30
|
+
// Append-only: do NOT create .code-graph. Its absence means "not an indexed
|
|
31
|
+
// project" — recording there would pollute non-project cwds.
|
|
32
|
+
if (!fs.existsSync(dir)) return false;
|
|
33
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
|
|
34
|
+
fs.appendFileSync(path.join(dir, REC_FILE), line);
|
|
35
|
+
return true;
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { recordRecommendation, REC_FILE };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { test } = require('node:test');
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { recordRecommendation, REC_FILE } = require('./recommendation-log');
|
|
8
|
+
|
|
9
|
+
function tmpProject(t, withCodeGraph) {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-rec-'));
|
|
11
|
+
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
12
|
+
if (withCodeGraph) fs.mkdirSync(path.join(dir, '.code-graph'));
|
|
13
|
+
return dir;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test('recordRecommendation appends a JSON line with ts + fields', (t) => {
|
|
17
|
+
const cwd = tmpProject(t, true);
|
|
18
|
+
assert.equal(recordRecommendation(cwd, { hook: 'grep', action: 'deny' }), true);
|
|
19
|
+
const content = fs.readFileSync(path.join(cwd, '.code-graph', REC_FILE), 'utf8');
|
|
20
|
+
const lines = content.trim().split('\n');
|
|
21
|
+
assert.equal(lines.length, 1);
|
|
22
|
+
const rec = JSON.parse(lines[0]);
|
|
23
|
+
assert.equal(rec.hook, 'grep');
|
|
24
|
+
assert.equal(rec.action, 'deny');
|
|
25
|
+
assert.ok(typeof rec.ts === 'string' && rec.ts.length > 0, 'ts should be a timestamp');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('recordRecommendation is a no-op (no dir created) when .code-graph absent', (t) => {
|
|
29
|
+
const cwd = tmpProject(t, false);
|
|
30
|
+
assert.equal(recordRecommendation(cwd, { hook: 'grep', action: 'hint' }), false);
|
|
31
|
+
// Must NOT create the dir or file — zero footprint in non-project cwd.
|
|
32
|
+
assert.equal(fs.existsSync(path.join(cwd, '.code-graph')), false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('recordRecommendation appends across calls (one line each)', (t) => {
|
|
36
|
+
const cwd = tmpProject(t, true);
|
|
37
|
+
recordRecommendation(cwd, { hook: 'grep', action: 'hint' });
|
|
38
|
+
recordRecommendation(cwd, { hook: 'read', action: 'hint' });
|
|
39
|
+
recordRecommendation(cwd, { hook: 'grep', action: 'deny' });
|
|
40
|
+
const lines = fs.readFileSync(path.join(cwd, '.code-graph', REC_FILE), 'utf8').trim().split('\n');
|
|
41
|
+
assert.equal(lines.length, 3);
|
|
42
|
+
const hooks = lines.map((l) => JSON.parse(l).hook);
|
|
43
|
+
assert.deepEqual(hooks, ['grep', 'read', 'grep']);
|
|
44
|
+
});
|
|
@@ -30,6 +30,16 @@ function computeQuietHooks({ env = {} } = {}) {
|
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// SessionStart project-map injection gate. Beyond the (already default-quiet)
|
|
34
|
+
// verbose opt-in, the map is now also ADOPTED-ONLY: cross-project measurement
|
|
35
|
+
// (memory cross-project-interference) found the ≈2 KB dump is zero-referenced
|
|
36
|
+
// in projects the user hasn't adopted into their MEMORY.md workflow, so it only
|
|
37
|
+
// earns its standing-context cost for adopted projects. Unadopted projects get
|
|
38
|
+
// no map even under CODE_GRAPH_VERBOSE_HOOKS / legacy QUIET_HOOKS=0.
|
|
39
|
+
function shouldInjectMap({ available, quietHooks, adopted } = {}) {
|
|
40
|
+
return !!(available && !quietHooks && adopted);
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
function launchBackgroundAutoUpdate(spawnFn = spawn, env = process.env) {
|
|
34
44
|
try {
|
|
35
45
|
const child = spawnFn(process.execPath, [path.join(__dirname, 'auto-update.js'), 'check', '--silent'], {
|
|
@@ -316,11 +326,14 @@ function runSessionInit() {
|
|
|
316
326
|
|
|
317
327
|
// quietHooks: default quiet (project_map injection duplicates MEMORY.md +
|
|
318
328
|
// on-demand tool). CODE_GRAPH_VERBOSE_HOOKS=1 to opt in to the dump;
|
|
319
|
-
// legacy CODE_GRAPH_QUIET_HOOKS=0/1 still force the old behavior.
|
|
329
|
+
// legacy CODE_GRAPH_QUIET_HOOKS=0/1 still force the old behavior. The opt-in
|
|
330
|
+
// dump is further gated to adopted projects (shouldInjectMap).
|
|
320
331
|
const adopted = isAdopted();
|
|
321
332
|
const quietHooks = computeQuietHooks({ env: process.env });
|
|
322
333
|
|
|
323
|
-
const mapInjected = binaryCheck.available
|
|
334
|
+
const mapInjected = shouldInjectMap({ available: binaryCheck.available, quietHooks, adopted })
|
|
335
|
+
? injectProjectMap()
|
|
336
|
+
: false;
|
|
324
337
|
const consistencyIssues = binaryCheck.available
|
|
325
338
|
? consistencyCheck(binaryCheck.binary)
|
|
326
339
|
: [];
|
|
@@ -369,6 +382,7 @@ module.exports = {
|
|
|
369
382
|
consistencyCheck,
|
|
370
383
|
runSessionInit,
|
|
371
384
|
computeQuietHooks,
|
|
385
|
+
shouldInjectMap,
|
|
372
386
|
};
|
|
373
387
|
|
|
374
388
|
if (require.main === module) {
|
|
@@ -4,7 +4,7 @@ const assert = require('node:assert/strict');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
|
|
7
|
-
const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary, computeQuietHooks } = require('./session-init');
|
|
7
|
+
const { launchBackgroundAutoUpdate, syncLifecycleConfig, ensureIndexFresh, verifyBinary, computeQuietHooks, shouldInjectMap } = require('./session-init');
|
|
8
8
|
|
|
9
9
|
test('syncLifecycleConfig is exported as a callable helper', () => {
|
|
10
10
|
assert.equal(typeof syncLifecycleConfig, 'function');
|
|
@@ -147,6 +147,20 @@ test('computeQuietHooks: legacy `adopted` param is ignored under new default', (
|
|
|
147
147
|
assert.equal(computeQuietHooks({ adopted: false, env: {} }), true);
|
|
148
148
|
});
|
|
149
149
|
|
|
150
|
+
test('shouldInjectMap: only injects when available + not-quiet + adopted', () => {
|
|
151
|
+
// The single positive case: opted into verbose AND adopted.
|
|
152
|
+
assert.equal(shouldInjectMap({ available: true, quietHooks: false, adopted: true }), true);
|
|
153
|
+
// Adopted-only gate: verbose but unadopted → no injection (the zero-referenced
|
|
154
|
+
// case cross-project-interference flagged).
|
|
155
|
+
assert.equal(shouldInjectMap({ available: true, quietHooks: false, adopted: false }), false);
|
|
156
|
+
// Quiet default suppresses regardless of adoption.
|
|
157
|
+
assert.equal(shouldInjectMap({ available: true, quietHooks: true, adopted: true }), false);
|
|
158
|
+
// No binary → nothing to inject.
|
|
159
|
+
assert.equal(shouldInjectMap({ available: false, quietHooks: false, adopted: true }), false);
|
|
160
|
+
// Missing args default to falsey → no injection.
|
|
161
|
+
assert.equal(shouldInjectMap(), false);
|
|
162
|
+
});
|
|
163
|
+
|
|
150
164
|
test('consistencyCheck returns version-mismatch when versions differ', (t) => {
|
|
151
165
|
const os = require('os');
|
|
152
166
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cc-'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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.
|
|
39
|
-
"@sdsrs/code-graph-linux-arm64": "0.
|
|
40
|
-
"@sdsrs/code-graph-darwin-x64": "0.
|
|
41
|
-
"@sdsrs/code-graph-darwin-arm64": "0.
|
|
42
|
-
"@sdsrs/code-graph-win32-x64": "0.
|
|
38
|
+
"@sdsrs/code-graph-linux-x64": "0.39.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.39.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.39.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.39.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.39.0"
|
|
43
43
|
}
|
|
44
44
|
}
|