@sdsrs/code-graph 0.63.0 → 0.66.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 +12 -2
- package/claude-plugin/scripts/pre-grep-guide.test.js +39 -0
- package/claude-plugin/scripts/recommendation-log.js +8 -0
- package/claude-plugin/scripts/recommendation-log.test.js +13 -0
- package/package.json +6 -6
|
@@ -484,6 +484,13 @@ function runMain() {
|
|
|
484
484
|
if (relPrefix) cmd = rebaseRelativePaths(cmd, relPrefix, root);
|
|
485
485
|
if (!shouldHint(cmd)) return;
|
|
486
486
|
|
|
487
|
+
// v0.64 — fingerprint the grep's pattern once, shared by the emit points below.
|
|
488
|
+
// The funnel (aggregate_recommendations_jsonl) uses it to tell a verbatim re-grep
|
|
489
|
+
// of an answered deny (inline answer ignored → fall-through) from a deeper
|
|
490
|
+
// drill-down. undefined when there's no identifier-like pattern (unquoted / prose
|
|
491
|
+
// grep) → omitted from the event, so the funnel stays back-compatible.
|
|
492
|
+
const grepPattern = translateBreToRg(cmd, pickBlockPattern(cmd));
|
|
493
|
+
|
|
487
494
|
// v0.48 — deliberate escape: record it (funnel visibility) and stay silent.
|
|
488
495
|
// Before GREP_HEAD accepted bare KEY=VALUE prefixes these were invisible.
|
|
489
496
|
if (commandHasBypass(rawCmd)) {
|
|
@@ -495,7 +502,7 @@ function runMain() {
|
|
|
495
502
|
// Outcome proxy: a source grep re-issued within the cooldown window runs
|
|
496
503
|
// silently (no deny/hint). Record it so `stats` sees the model's grep
|
|
497
504
|
// fan-out — especially a re-grep right after cg answered the same query.
|
|
498
|
-
recordRecommendation(root, { hook: 'grep', action: 'observe' });
|
|
505
|
+
recordRecommendation(root, { hook: 'grep', action: 'observe', ...(grepPattern ? { pattern: grepPattern } : {}) });
|
|
499
506
|
return;
|
|
500
507
|
}
|
|
501
508
|
|
|
@@ -510,7 +517,7 @@ function runMain() {
|
|
|
510
517
|
// v0.49 — intent-aware: declaration+context greps get `show` bodies,
|
|
511
518
|
// falling back to the grep answer, then the static deny.
|
|
512
519
|
let answer = { status: 'unavailable' };
|
|
513
|
-
const pattern =
|
|
520
|
+
const pattern = grepPattern; // computed once above; reused for the answer + deny fingerprint
|
|
514
521
|
// v0.48 — glob-truncated once, shared by the run and the deny message
|
|
515
522
|
// (a literal `dir/*.py` argv made rg exit 1 → answered:false static deny).
|
|
516
523
|
const searchPath = sanitizeSearchPath(extractSearchPath(cmd));
|
|
@@ -544,6 +551,9 @@ function runMain() {
|
|
|
544
551
|
const unansweredTail = extractUnansweredTail(rawCmd);
|
|
545
552
|
recordRecommendation(root, {
|
|
546
553
|
hook: 'grep', action: 'deny', answered,
|
|
554
|
+
// pattern fingerprints the denied search so the funnel can score a verbatim
|
|
555
|
+
// re-grep of it (the inline answer was ignored) as fall-through, not a win.
|
|
556
|
+
...(pattern ? { pattern } : {}),
|
|
547
557
|
// mode segments which answer type converts (show=bodies, grep=hits).
|
|
548
558
|
...(answered ? { mode: answeredMode } : {}),
|
|
549
559
|
// reason segments WHY an unanswered deny fell back to the static copy:
|
|
@@ -909,6 +909,45 @@ test('e2e: denied grep with stub hits → deny JSON embeds the answer + records
|
|
|
909
909
|
}
|
|
910
910
|
});
|
|
911
911
|
|
|
912
|
+
test('e2e: denied grep records the denied pattern (fingerprint for verbatim re-grep detection)', () => {
|
|
913
|
+
// The Rust funnel (aggregate_recommendations_jsonl) scores a follow-up search
|
|
914
|
+
// carrying the SAME pattern as the armed answered deny as fall-through, not a
|
|
915
|
+
// sustained drill-down. That needs the pattern on the deny event.
|
|
916
|
+
const uniq = `StubPat${Date.now()}`;
|
|
917
|
+
const fixture = e2eFixture(`process.stdout.write('src/foo.rs:7 hit\\n');`);
|
|
918
|
+
const cmd = `grep -rn "${uniq}" src/`;
|
|
919
|
+
try {
|
|
920
|
+
const res = runHook(cmd, fixture);
|
|
921
|
+
assert.equal(res.status, 0);
|
|
922
|
+
const rec = JSON.parse(fsE2e.readFileSync(
|
|
923
|
+
pathE2e.join(fixture.dir, '.code-graph', 'recommendations.jsonl'), 'utf8').trim().split('\n').pop());
|
|
924
|
+
assert.equal(rec.action, 'deny');
|
|
925
|
+
assert.equal(rec.pattern, uniq, 'deny event carries the denied pattern as a fingerprint');
|
|
926
|
+
} finally {
|
|
927
|
+
cleanupFixture(fixture, cmd);
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
test('e2e: re-grep within cooldown → observe carries the same pattern (answer-ignored fingerprint)', () => {
|
|
932
|
+
// First grep denies + marks the cooldown; the verbatim re-grep within the
|
|
933
|
+
// window runs silently as an observe. It must carry the same pattern so the
|
|
934
|
+
// funnel can tell "ignored the inline answer" from "drilled into something new".
|
|
935
|
+
const uniq = `StubCool${Date.now()}`;
|
|
936
|
+
const fixture = e2eFixture(`process.stdout.write('src/foo.rs:7 hit\\n');`);
|
|
937
|
+
const cmd = `grep -rn "${uniq}" src/`;
|
|
938
|
+
try {
|
|
939
|
+
runHook(cmd, fixture); // 1st → deny + markCooldown
|
|
940
|
+
const res2 = runHook(cmd, fixture); // 2nd within window → observe
|
|
941
|
+
assert.equal(res2.status, 0);
|
|
942
|
+
const last = JSON.parse(fsE2e.readFileSync(
|
|
943
|
+
pathE2e.join(fixture.dir, '.code-graph', 'recommendations.jsonl'), 'utf8').trim().split('\n').pop());
|
|
944
|
+
assert.equal(last.action, 'observe');
|
|
945
|
+
assert.equal(last.pattern, uniq, 'cooldown observe carries the re-grepped pattern');
|
|
946
|
+
} finally {
|
|
947
|
+
cleanupFixture(fixture, cmd);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
|
|
912
951
|
test('e2e: stub reports no matches → grep allowed with FYI + records fallthrough', () => {
|
|
913
952
|
const uniq = `StubMiss${Date.now()}`;
|
|
914
953
|
const fixture = e2eFixture(
|
|
@@ -17,6 +17,9 @@ const fs = require('fs');
|
|
|
17
17
|
const path = require('path');
|
|
18
18
|
|
|
19
19
|
const REC_FILE = 'recommendations.jsonl';
|
|
20
|
+
// Opt-in per-project metrics-silence sentinel (under .code-graph/). Mirror of the
|
|
21
|
+
// Rust `domain::NO_METRICS_SENTINEL` — keep the literal in sync.
|
|
22
|
+
const NO_METRICS_FILE = '.no-metrics';
|
|
20
23
|
|
|
21
24
|
// Bounded growth: recommendations.jsonl is append-only and written per-event
|
|
22
25
|
// from BOTH here and the Rust CLI (cli::record_cli_use). Keep these constants in
|
|
@@ -57,6 +60,11 @@ function recordRecommendation(cwd, event = {}) {
|
|
|
57
60
|
// Append-only: do NOT create .code-graph. Its absence means "not an indexed
|
|
58
61
|
// project" — recording there would pollute non-project cwds.
|
|
59
62
|
if (!fs.existsSync(dir)) return false;
|
|
63
|
+
// Opt-in metrics silence for dev/dogfood checkouts: when the project marks
|
|
64
|
+
// itself with `.code-graph/.no-metrics`, the tool's own hook/CLI runs (sims,
|
|
65
|
+
// functionality testing) must not self-pollute its adoption metrics. Mirrors
|
|
66
|
+
// the Rust cli::record_cli_use guard. Reversible: delete the file to re-enable.
|
|
67
|
+
if (fs.existsSync(path.join(dir, NO_METRICS_FILE))) return false;
|
|
60
68
|
const file = path.join(dir, REC_FILE);
|
|
61
69
|
rotateIfNeeded(file); // rotate-before-append so the file never exceeds ~max + one line
|
|
62
70
|
const line = JSON.stringify({ ts: new Date().toISOString(), ...event }) + '\n';
|
|
@@ -32,6 +32,19 @@ test('recordRecommendation is a no-op (no dir created) when .code-graph absent',
|
|
|
32
32
|
assert.equal(fs.existsSync(path.join(cwd, '.code-graph')), false);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
test('recordRecommendation is a no-op when .code-graph/.no-metrics sentinel present', (t) => {
|
|
36
|
+
const cwd = tmpProject(t, true);
|
|
37
|
+
// Without the sentinel it records normally...
|
|
38
|
+
assert.equal(recordRecommendation(cwd, { hook: 'grep', action: 'deny' }), true);
|
|
39
|
+
const before = fs.readFileSync(path.join(cwd, '.code-graph', REC_FILE), 'utf8');
|
|
40
|
+
// ...then the project marks itself metrics-silent (a dev/dogfood checkout)...
|
|
41
|
+
fs.writeFileSync(path.join(cwd, '.code-graph', '.no-metrics'), '');
|
|
42
|
+
// ...and subsequent recordings are suppressed, leaving the file byte-unchanged.
|
|
43
|
+
assert.equal(recordRecommendation(cwd, { hook: 'grep', action: 'hint' }), false);
|
|
44
|
+
const after = fs.readFileSync(path.join(cwd, '.code-graph', REC_FILE), 'utf8');
|
|
45
|
+
assert.equal(after, before, 'sentinel must suppress further recordings');
|
|
46
|
+
});
|
|
47
|
+
|
|
35
48
|
test('recordRecommendation appends across calls (one line each)', (t) => {
|
|
36
49
|
const cwd = tmpProject(t, true);
|
|
37
50
|
recordRecommendation(cwd, { hook: 'grep', action: 'hint' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdsrs/code-graph",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.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.66.0",
|
|
39
|
+
"@sdsrs/code-graph-linux-arm64": "0.66.0",
|
|
40
|
+
"@sdsrs/code-graph-darwin-x64": "0.66.0",
|
|
41
|
+
"@sdsrs/code-graph-darwin-arm64": "0.66.0",
|
|
42
|
+
"@sdsrs/code-graph-win32-x64": "0.66.0"
|
|
43
43
|
}
|
|
44
44
|
}
|