@sdsrs/code-graph 0.71.0 → 0.72.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/covering-tests.js +67 -0
- package/claude-plugin/scripts/covering-tests.test.js +78 -0
- package/claude-plugin/scripts/pre-edit-guide.js +14 -1
- package/claude-plugin/scripts/pre-edit-guide.test.js +21 -0
- package/package.json +6 -6
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Pure formatter for edit-time covering-test targeting.
|
|
3
|
+
//
|
|
4
|
+
// pre-edit-guide.js feeds this the `test_callers` from `impact --json` (the tests
|
|
5
|
+
// that exercise the symbol being edited) plus the edited file's path. It returns a
|
|
6
|
+
// summary fragment that turns the bare "(N tests)" count into an actionable,
|
|
7
|
+
// language-aware run command — so the fix-test-iterate loop runs the TARGETED
|
|
8
|
+
// covering tests, not the whole suite or a guessed test name.
|
|
9
|
+
//
|
|
10
|
+
// Pure + side-effect-free so it's unit-testable by require() — unlike the hook
|
|
11
|
+
// itself, which top-level-exits on require (reads stdin / checks the index).
|
|
12
|
+
|
|
13
|
+
// Display cap: list at most this many "name (file)" entries before collapsing to a
|
|
14
|
+
// bare count. Above the cap a long per-name list is noise in the injected context.
|
|
15
|
+
const LIST_CAP = 6;
|
|
16
|
+
|
|
17
|
+
// Detect the project's test runner from the edited file's extension. v1 emits a
|
|
18
|
+
// REAL targeted command only for Rust (`cargo test` accepts bare substring filters,
|
|
19
|
+
// so the test fn names work directly). Other languages degrade to listing the
|
|
20
|
+
// covering tests with no command — a wrong/guessed command is worse than none
|
|
21
|
+
// (jest vs vitest vs mocha / pytest node ids / `go test -run` all differ).
|
|
22
|
+
function detectRunner(filePath) {
|
|
23
|
+
if (typeof filePath === 'string' && /\.rs$/.test(filePath)) return 'rust';
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function targetedCommand(runner, names) {
|
|
28
|
+
if (runner === 'rust') return `cargo test ${names.join(' ')}`;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function suiteCommand(runner) {
|
|
33
|
+
if (runner === 'rust') return 'cargo test';
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Build the covering-tests summary fragment.
|
|
39
|
+
* @param {Array<{name:string,file:string}>} testCallers from `impact --json` test_callers
|
|
40
|
+
* @param {string} editedFile the file being edited (drives runner detection)
|
|
41
|
+
* @returns {string} a `\n`-terminated fragment, or '' when there's nothing to add
|
|
42
|
+
*/
|
|
43
|
+
function formatCoveringTests(testCallers, editedFile) {
|
|
44
|
+
const tests = Array.isArray(testCallers) ? testCallers.filter((t) => t && t.name) : [];
|
|
45
|
+
const n = tests.length;
|
|
46
|
+
if (n === 0) return '';
|
|
47
|
+
|
|
48
|
+
const runner = detectRunner(editedFile);
|
|
49
|
+
|
|
50
|
+
if (n <= LIST_CAP) {
|
|
51
|
+
const list = tests.map((t) => `${t.name} (${t.file})`).join(', ');
|
|
52
|
+
let out = ` Covering tests (${n}): ${list}\n`;
|
|
53
|
+
const cmd = targetedCommand(runner, tests.map((t) => t.name));
|
|
54
|
+
if (cmd) out += ` → Run after editing: ${cmd}\n`;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// High fan-out: editing a widely-tested symbol. A long targeted command is noise
|
|
59
|
+
// — point at the suite instead (mirrors the blast-size scaling in
|
|
60
|
+
// session-init.js formatRecentImpact).
|
|
61
|
+
let out = ` Covering tests: ${n} — editing a widely-tested symbol\n`;
|
|
62
|
+
const suite = suiteCommand(runner);
|
|
63
|
+
if (suite) out += ` → Run the suite after editing: ${suite}\n`;
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { formatCoveringTests, LIST_CAP };
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test = require('node:test');
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const { formatCoveringTests, LIST_CAP } = require('./covering-tests');
|
|
5
|
+
|
|
6
|
+
// ── empty / robust ──────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
test('covering: empty list → no output', () => {
|
|
9
|
+
assert.equal(formatCoveringTests([], 'src/a.rs'), '');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('covering: missing/undefined → no output (never throws)', () => {
|
|
13
|
+
assert.equal(formatCoveringTests(undefined, 'src/a.rs'), '');
|
|
14
|
+
assert.equal(formatCoveringTests(null, 'src/a.rs'), '');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('covering: entries without a name are dropped', () => {
|
|
18
|
+
const out = formatCoveringTests(
|
|
19
|
+
[{ name: 'test_real', file: 'tests/a.rs' }, { file: 'tests/nameless.rs' }],
|
|
20
|
+
'src/a.rs'
|
|
21
|
+
);
|
|
22
|
+
assert.match(out, /Covering tests \(1\)/); // only the named one counts
|
|
23
|
+
assert.match(out, /test_real/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// ── Rust: a real targeted command ───────────────────────
|
|
27
|
+
|
|
28
|
+
test('covering: Rust ≤cap lists names + a targeted `cargo test` command', () => {
|
|
29
|
+
const out = formatCoveringTests(
|
|
30
|
+
[
|
|
31
|
+
{ name: 'test_alpha', file: 'tests/a.rs' },
|
|
32
|
+
{ name: 'test_beta', file: 'src/b.rs' },
|
|
33
|
+
],
|
|
34
|
+
'src/foo.rs'
|
|
35
|
+
);
|
|
36
|
+
assert.match(out, /Covering tests \(2\)/);
|
|
37
|
+
assert.match(out, /test_alpha \(tests\/a\.rs\)/);
|
|
38
|
+
assert.match(out, /test_beta \(src\/b\.rs\)/);
|
|
39
|
+
// The actionable part: a command that runs exactly the covering tests.
|
|
40
|
+
assert.match(out, /Run after editing: cargo test test_alpha test_beta/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── non-Rust: list only, never a fabricated command ─────
|
|
44
|
+
|
|
45
|
+
test('covering: non-Rust ≤cap lists names but emits NO command (no wrong command)', () => {
|
|
46
|
+
const out = formatCoveringTests(
|
|
47
|
+
[{ name: 'testValidate', file: 'src/auth.test.ts' }],
|
|
48
|
+
'src/auth.ts'
|
|
49
|
+
);
|
|
50
|
+
assert.match(out, /Covering tests \(1\): testValidate \(src\/auth\.test\.ts\)/);
|
|
51
|
+
assert.doesNotMatch(out, /cargo test/);
|
|
52
|
+
assert.doesNotMatch(out, /Run after editing/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── high fan-out: collapse, point at the suite ──────────
|
|
56
|
+
|
|
57
|
+
test('covering: Rust high fan-out (>cap) collapses to a count + suite command, no name list', () => {
|
|
58
|
+
const many = Array.from({ length: LIST_CAP + 1 }, (_, i) => ({
|
|
59
|
+
name: `test_${i}`,
|
|
60
|
+
file: 'tests/wide.rs',
|
|
61
|
+
}));
|
|
62
|
+
const out = formatCoveringTests(many, 'src/hot.rs');
|
|
63
|
+
assert.match(out, new RegExp(`Covering tests: ${LIST_CAP + 1}`));
|
|
64
|
+
assert.match(out, /widely-tested/);
|
|
65
|
+
assert.match(out, /Run the suite after editing: cargo test/);
|
|
66
|
+
// The long per-name list must NOT be inlined when fan-out is high.
|
|
67
|
+
assert.doesNotMatch(out, /test_0 \(/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('covering: non-Rust high fan-out collapses to a count with no command', () => {
|
|
71
|
+
const many = Array.from({ length: LIST_CAP + 3 }, (_, i) => ({
|
|
72
|
+
name: `test_${i}`,
|
|
73
|
+
file: 'a.test.ts',
|
|
74
|
+
}));
|
|
75
|
+
const out = formatCoveringTests(many, 'src/hot.ts');
|
|
76
|
+
assert.match(out, new RegExp(`Covering tests: ${LIST_CAP + 3}`));
|
|
77
|
+
assert.doesNotMatch(out, /cargo test/);
|
|
78
|
+
});
|
|
@@ -13,6 +13,7 @@ const { findBinary } = require('./find-binary');
|
|
|
13
13
|
const { cgTmpDir } = require('./tmp-dir');
|
|
14
14
|
const { resolveProjectRoot } = require('./project-root');
|
|
15
15
|
const { recordRecommendation } = require('./recommendation-log');
|
|
16
|
+
const { formatCoveringTests } = require('./covering-tests');
|
|
16
17
|
|
|
17
18
|
// v0.49 — walk up from the shell cwd (subdir-cwd fix). The per-cwd index.db
|
|
18
19
|
// gate kept this hook dark for entire sessions after `cd backend/` — daagu
|
|
@@ -170,7 +171,13 @@ try { fs.writeFileSync(cooldownFile, ''); } catch { /* ok */ }
|
|
|
170
171
|
// Funnel visibility (v0.49): an injected impact summary is a delivered answer.
|
|
171
172
|
// v0.63 — ack:true marks that this injection carries a salience-forcing directive
|
|
172
173
|
// (the per-caller verdict line below), so a later A/B can segment ack vs non-ack.
|
|
173
|
-
|
|
174
|
+
// test_targets: how many covering tests this injection offered — the forward
|
|
175
|
+
// signal for whether covering-test targeting reduces test-name guessing (read on
|
|
176
|
+
// consumer projects; this dogfood repo's metrics are dark).
|
|
177
|
+
recordRecommendation(cwd, {
|
|
178
|
+
hook: 'edit', action: 'hint', answered: true, ack: true,
|
|
179
|
+
test_targets: (jsonResult.test_callers || []).length,
|
|
180
|
+
});
|
|
174
181
|
|
|
175
182
|
// --- Inject compact impact summary ---
|
|
176
183
|
const routeCount = jsonResult.affected_routes || 0;
|
|
@@ -188,6 +195,12 @@ if (callers.length > 0) {
|
|
|
188
195
|
summary += ' Callers: ' + callers.map(c => `${c.name} (${c.file})`).join(', ') + '\n';
|
|
189
196
|
}
|
|
190
197
|
|
|
198
|
+
// Covering tests — turn the bare "(N tests)" count above into an actionable,
|
|
199
|
+
// targeted run command so the fix-test-iterate loop runs exactly the tests that
|
|
200
|
+
// exercise the edited symbol (not the whole suite or a guessed name). Empty/absent
|
|
201
|
+
// test_callers → appends nothing.
|
|
202
|
+
summary += formatCoveringTests(jsonResult.test_callers, editedFile);
|
|
203
|
+
|
|
191
204
|
// Salience forcing (v0.63) — an injected impact summary that the model merely
|
|
192
205
|
// reads is wasted context. mem's PreToolUse edit hook lifts cite-recall to ~94%
|
|
193
206
|
// by making the model ACT on the injection ("apply each lesson or rule it out")
|
|
@@ -176,3 +176,24 @@ test('pattern-sync: fnPatterns count matches source', () => {
|
|
|
176
176
|
assert.ok(fnPatterns.length === 8, `Expected 8 patterns, got ${fnPatterns.length}`);
|
|
177
177
|
assert.ok(sourcePatternCount >= 7, `Source should have >= 7 language comments, found ${sourcePatternCount}`);
|
|
178
178
|
});
|
|
179
|
+
|
|
180
|
+
// ── Covering-test targeting (edit-time PUSH) ────────────
|
|
181
|
+
// The pure formatter lives in covering-tests.js (unit-tested in covering-tests.test.js);
|
|
182
|
+
// these guard that the hook actually wires it in and records the forward signal.
|
|
183
|
+
// Source-grep, same convention as the salience/pattern-sync guards (hook exits on require).
|
|
184
|
+
|
|
185
|
+
test('covering-tests: hook requires and invokes the covering-tests formatter on test_callers', () => {
|
|
186
|
+
const fs = require('node:fs');
|
|
187
|
+
const path = require('node:path');
|
|
188
|
+
const source = fs.readFileSync(path.join(__dirname, 'pre-edit-guide.js'), 'utf8');
|
|
189
|
+
assert.match(source, /require\(['"]\.\/covering-tests['"]\)/);
|
|
190
|
+
assert.match(source, /formatCoveringTests\(/);
|
|
191
|
+
assert.match(source, /test_callers/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('covering-tests: edit injection records test_targets for the forward funnel', () => {
|
|
195
|
+
const fs = require('node:fs');
|
|
196
|
+
const path = require('node:path');
|
|
197
|
+
const source = fs.readFileSync(path.join(__dirname, 'pre-edit-guide.js'), 'utf8');
|
|
198
|
+
assert.match(source, /test_targets:/);
|
|
199
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.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.72.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.72.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.72.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.72.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.72.0"
|
|
43
43
|
}
|
|
44
44
|
}
|