@sdsrs/code-graph 0.91.0 → 0.93.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.91.0",
7
+ "version": "0.93.0",
8
8
  "keywords": [
9
9
  "code-graph",
10
10
  "ast",
@@ -570,6 +570,16 @@ function buildNoHitsFyi(pattern) {
570
570
  return `[code-graph] FYI: \`code-graph-mcp grep "${pattern}"\` found no matches — raw grep proceeding. (Regex-metachar patterns: \`code-graph-mcp grep -F\` searches literally.)`;
571
571
  }
572
572
 
573
+ // v0.92 — cg was allowed to answer but the binary ran-and-failed ('unavailable')
574
+ // or could not be found ('no-binary'). Like buildNoHitsFyi this is a breadcrumb
575
+ // only (PreToolUse exit-0 stdout → debug log, never the model); the operative
576
+ // effect at the call site is the ALLOW (no deny emitted) so the raw grep runs
577
+ // intact instead of a static deny that would hand the model nothing.
578
+ function buildUnavailableFyi(pattern, status) {
579
+ const why = status === 'no-binary' ? 'binary not found' : 'ran but failed';
580
+ return `[code-graph] FYI: \`code-graph-mcp grep "${pattern}"\` unavailable (${why}) — raw grep proceeding.`;
581
+ }
582
+
573
583
  // --- Main execution (only when run directly) ---
574
584
 
575
585
  // Kill switch: matches user-prompt-context.js convention. =1 forces silence
@@ -687,9 +697,24 @@ function runMain() {
687
697
  }
688
698
  }
689
699
 
690
- if (answer.status === 'no-hits') {
691
- recordRecommendation(root, { hook: 'grep', action: 'hint', fallthrough: 'no-hits' });
692
- process.stdout.write(buildNoHitsFyi(pattern) + '\n');
700
+ // v0.92 cg was allowed to answer but couldn't deliver hits: 'no-hits'
701
+ // (regex-dialect miss proof of absence), 'unavailable' (binary ran but
702
+ // failed/timed out), or 'no-binary' (binary missing). In every case a static
703
+ // deny hands the model NOTHING — pure friction that teaches the
704
+ // CODE_GRAPH_NO_BLOCK_GREP bypass (ubuntu-sec 2026-07 dogfood: the only 2
705
+ // non-converting denies were `unavailable`, one a `def render` compound cmd
706
+ // that then half-ran — grep blocked, `; python3 …` tail dropped, no result).
707
+ // ALLOW the raw grep so the command runs intact; record the fallthrough
708
+ // reason so the funnel still tells no-hits / unavailable / no-binary apart.
709
+ // Exception: CODE_GRAPH_NO_ANSWER_IN_DENY=1 means the user opted into the
710
+ // static deny (the answer never ran → status stays the default 'unavailable')
711
+ // — that path falls through to the v0.46 static deny below.
712
+ if (answer.status !== 'hits' && !isAnswerDisabled()) {
713
+ recordRecommendation(root, { hook: 'grep', action: 'hint', fallthrough: answer.status });
714
+ process.stdout.write(
715
+ (answer.status === 'no-hits'
716
+ ? buildNoHitsFyi(pattern)
717
+ : buildUnavailableFyi(pattern, answer.status)) + '\n');
693
718
  return;
694
719
  }
695
720
 
@@ -769,6 +794,7 @@ module.exports = {
769
794
  buildBlockReason,
770
795
  buildBlockReasonWithAnswer,
771
796
  buildNoHitsFyi,
797
+ buildUnavailableFyi, // v0.92 — allow-on-unavailable breadcrumb
772
798
  commandHash,
773
799
  isOnCooldown,
774
800
  markCooldown,
@@ -1127,24 +1127,25 @@ test('e2e: stub reports no matches → grep allowed with FYI + records fallthrou
1127
1127
  }
1128
1128
  });
1129
1129
 
1130
- test('e2e: stub failsstatic deny (v0.46 fallback) + records answered:false', () => {
1130
+ test('e2e: stub ran-and-failedgrep ALLOWED (no static deny) + records fallthrough:unavailable', () => {
1131
+ // v0.92 — a binary that ran but failed (exit 3) can't answer, so a static deny
1132
+ // would hand the model nothing (pure friction that teaches the bypass). ALLOW
1133
+ // the raw grep instead; the funnel still distinguishes this from no-hits /
1134
+ // no-binary via the `fallthrough` field on the recorded hint event.
1131
1135
  const uniq = `StubBoom${Date.now()}`;
1132
1136
  const fixture = e2eFixture(`process.exit(3);`);
1133
1137
  const cmd = `grep -rn "${uniq}" src/`;
1134
1138
  try {
1135
1139
  const res = runHook(cmd, fixture);
1136
1140
  assert.equal(res.status, 0);
1137
- const out = JSON.parse(res.stdout);
1138
- assert.equal(out.hookSpecificOutput.permissionDecision, 'deny');
1139
- // static reason, no embedded results
1140
- assert.match(out.hookSpecificOutput.permissionDecisionReason, /denied by code-graph hook/);
1141
+ // No deny JSON — plain FYI text means the grep proceeds.
1142
+ assert.throws(() => JSON.parse(res.stdout));
1143
+ assert.match(res.stdout, /unavailable \(ran but failed\)/);
1141
1144
  const rec = JSON.parse(fsE2e.readFileSync(
1142
1145
  pathE2e.join(fixture.dir, '.code-graph', 'recommendations.jsonl'), 'utf8').trim());
1143
- assert.equal(rec.action, 'deny');
1144
- assert.equal(rec.answered, false);
1145
- // A binary that ran but failed (exit 3) is a runtime 'unavailable' — the
1146
- // funnel must NOT confuse this with a missing-binary ('no-binary') deny.
1147
- assert.equal(rec.reason, 'unavailable');
1146
+ assert.equal(rec.action, 'hint');
1147
+ // Runtime-fail must stay distinguishable from a missing-binary fallthrough.
1148
+ assert.equal(rec.fallthrough, 'unavailable');
1148
1149
  } finally {
1149
1150
  cleanupFixture(fixture, cmd);
1150
1151
  }
@@ -1222,7 +1223,12 @@ test('e2e: compound `grep …; sed` → deny answers grep AND flags the unanswer
1222
1223
  }
1223
1224
  });
1224
1225
 
1225
- test('e2e: compound cmd + answer failure → STATIC deny still flags tail + records tail:true', () => {
1226
+ test('e2e: compound cmd + answer failure → grep ALLOWED, whole command runs intact (no half-run tail drop)', () => {
1227
+ // v0.92 — the marquee fix: when cg can't answer, a static deny used to block
1228
+ // the grep AND drop the `&& cargo test` tail, leaving the model with nothing +
1229
+ // a re-issue chore (ubuntu-sec: the `grep "def render" …; python3 …` case).
1230
+ // Now the whole compound command is ALLOWED to run intact — no deny, no tail
1231
+ // note, no half-run. Recorded as a hint/fallthrough so the funnel still sees it.
1226
1232
  const uniq = `StubTailBoom${Date.now()}`;
1227
1233
  const fixture = e2eFixture(`process.exit(3);`);
1228
1234
  const cmd = `grep -n "${uniq}" src/foo.rs && cargo test -q`;
@@ -1231,16 +1237,14 @@ test('e2e: compound cmd + answer failure → STATIC deny still flags tail + reco
1231
1237
  fsE2e.writeFileSync(pathE2e.join(fixture.dir, 'src', 'foo.rs'), 'fn x() {}\n');
1232
1238
  const res = runHook(cmd, fixture);
1233
1239
  assert.equal(res.status, 0);
1234
- const out = JSON.parse(res.stdout);
1235
- assert.equal(out.hookSpecificOutput.permissionDecision, 'deny');
1236
- const reason = out.hookSpecificOutput.permissionDecisionReason;
1237
- assert.match(reason, /denied by code-graph hook/); // static fallback path
1238
- assert.match(reason, /did NOT run/);
1239
- assert.match(reason, /cargo test -q/);
1240
+ // No deny JSON — the whole compound command proceeds, tail included.
1241
+ assert.throws(() => JSON.parse(res.stdout));
1242
+ assert.match(res.stdout, /unavailable \(ran but failed\)/);
1240
1243
  const rec = JSON.parse(fsE2e.readFileSync(
1241
1244
  pathE2e.join(fixture.dir, '.code-graph', 'recommendations.jsonl'), 'utf8').trim());
1242
- assert.equal(rec.answered, false);
1243
- assert.equal(rec.tail, true);
1245
+ assert.equal(rec.action, 'hint');
1246
+ assert.equal(rec.fallthrough, 'unavailable');
1247
+ assert.equal(rec.tail, undefined); // nothing dropped → no tail flag
1244
1248
  } finally {
1245
1249
  cleanupFixture(fixture, cmd);
1246
1250
  }
@@ -1264,13 +1268,14 @@ test('e2e: simple (non-compound) denied grep → no tail field in the deny recor
1264
1268
  }
1265
1269
  });
1266
1270
 
1267
- test('e2e: missing binary → static deny records reason:no-binary (flagship-dark, distinct from no-hits & unavailable)', () => {
1268
- // The whole point of the `reason` field: a deny that fell back because the
1269
- // binary could not be found ('no-binary') must be distinguishable in the log
1270
- // from one where the binary ran but had nothing useful. We can't make
1271
- // findBinary() return null in-repo (dev target/release is always there), so
1272
- // run the child with a `--require` shim that forces it null and DON'T set
1273
- // _CG_ANSWER_BINARY (it would short-circuit before findBinary()).
1271
+ test('e2e: missing binary → grep ALLOWED (no static deny) records fallthrough:no-binary (distinct from no-hits & unavailable)', () => {
1272
+ // v0.92 when the binary can't be found the hook can't answer AND denying
1273
+ // would block the user's only search tool, so it ALLOWS the raw grep. The
1274
+ // `fallthrough` field still keeps a missing-binary case distinguishable in the
1275
+ // funnel from no-hits / runtime-unavailable. We can't make findBinary() return
1276
+ // null in-repo (dev target/release is always there), so run the child with a
1277
+ // `--require` shim that forces it null and DON'T set _CG_ANSWER_BINARY (it
1278
+ // would short-circuit before findBinary()).
1274
1279
  const uniq = `StubGone${Date.now()}`;
1275
1280
  const fixture = e2eFixture(`process.stdout.write('unused\\n');`);
1276
1281
  const shim = pathE2e.join(fixture.dir, 'no-binary-shim.js');
@@ -1302,16 +1307,14 @@ Module.prototype.require = function (id) {
1302
1307
  },
1303
1308
  });
1304
1309
  assert.equal(res.status, 0);
1305
- const out = JSON.parse(res.stdout);
1306
- // Still a deny, still the static fallback copy (no embedded answer).
1307
- assert.equal(out.hookSpecificOutput.permissionDecision, 'deny');
1308
- assert.match(out.hookSpecificOutput.permissionDecisionReason, /denied by code-graph hook/);
1310
+ // No deny JSON — the raw grep proceeds; FYI names the missing-binary cause.
1311
+ assert.throws(() => JSON.parse(res.stdout));
1312
+ assert.match(res.stdout, /unavailable \(binary not found\)/);
1309
1313
  const rec = JSON.parse(fsE2e.readFileSync(
1310
1314
  pathE2e.join(fixture.dir, '.code-graph', 'recommendations.jsonl'), 'utf8').trim());
1311
- assert.equal(rec.action, 'deny');
1312
- assert.equal(rec.answered, false);
1313
- assert.equal(rec.reason, 'no-binary',
1314
- 'a missing-binary deny must be distinguishable from an unavailable (runtime-fail) deny');
1315
+ assert.equal(rec.action, 'hint');
1316
+ assert.equal(rec.fallthrough, 'no-binary',
1317
+ 'a missing-binary fallthrough must be distinguishable from an unavailable (runtime-fail) one');
1315
1318
  } finally {
1316
1319
  cleanupFixture(fixture, cmd);
1317
1320
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sdsrs/code-graph",
3
- "version": "0.91.0",
3
+ "version": "0.93.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.91.0",
39
- "@sdsrs/code-graph-linux-arm64": "0.91.0",
40
- "@sdsrs/code-graph-darwin-x64": "0.91.0",
41
- "@sdsrs/code-graph-darwin-arm64": "0.91.0",
42
- "@sdsrs/code-graph-win32-x64": "0.91.0"
38
+ "@sdsrs/code-graph-linux-x64": "0.93.0",
39
+ "@sdsrs/code-graph-linux-arm64": "0.93.0",
40
+ "@sdsrs/code-graph-darwin-x64": "0.93.0",
41
+ "@sdsrs/code-graph-darwin-arm64": "0.93.0",
42
+ "@sdsrs/code-graph-win32-x64": "0.93.0"
43
43
  }
44
44
  }