grepmax 0.17.15 → 0.17.16

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.
@@ -535,6 +535,20 @@ const TOOLS = [
535
535
  required: [],
536
536
  },
537
537
  },
538
+ {
539
+ name: "review_risk",
540
+ description: "Deterministic risk ranking of the symbols a commit's diff touches, ordered by blast radius (inbound callers) × test presence × file churn. No LLM required — fast, graph + git only. Use to triage what a change endangers before a deep review.",
541
+ inputSchema: {
542
+ type: "object",
543
+ properties: {
544
+ commit: {
545
+ type: "string",
546
+ description: "Git ref to analyze (default: HEAD)",
547
+ },
548
+ },
549
+ required: [],
550
+ },
551
+ },
538
552
  ];
539
553
  // ---------------------------------------------------------------------------
540
554
  // Helpers
@@ -2442,6 +2456,28 @@ exports.mcp = new commander_1.Command("mcp")
2442
2456
  }
2443
2457
  break;
2444
2458
  }
2459
+ case "review_risk": {
2460
+ ensureWatcher();
2461
+ const commitRef = String(toolArgs.commit || "HEAD");
2462
+ try {
2463
+ const db = getVectorDb();
2464
+ const builder = new graph_builder_1.GraphBuilder(db, projectRoot);
2465
+ const { gatherRiskInputs, computeRiskTable, formatRiskTable } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
2466
+ const inputs = yield gatherRiskInputs(commitRef, projectRoot, {
2467
+ vectorDb: db,
2468
+ graphBuilder: builder,
2469
+ });
2470
+ const rows = computeRiskTable(inputs);
2471
+ result =
2472
+ rows.length === 0
2473
+ ? ok("(no changed symbols in this diff)")
2474
+ : ok(formatRiskTable(rows, { agent: true }));
2475
+ }
2476
+ catch (e) {
2477
+ result = err(`Risk ranking failed: ${e instanceof Error ? e.message : String(e)}`);
2478
+ }
2479
+ break;
2480
+ }
2445
2481
  case "review_report": {
2446
2482
  try {
2447
2483
  const { readReport, formatReportText } = yield Promise.resolve().then(() => __importStar(require("../lib/llm/report")));
@@ -55,11 +55,15 @@ exports.review = new commander_1.Command("review")
55
55
  .option("--commit <ref>", "Commit to review", "HEAD")
56
56
  .option("--root <dir>", "Project root directory")
57
57
  .option("--background", "Run review asynchronously via daemon", false)
58
+ .option("--risk", "Deterministic risk ranking of changed symbols (no LLM)", false)
59
+ .option("--agent", "Ultra-compact output for AI agents", false)
58
60
  .option("-v, --verbose", "Print progress to stderr", false)
59
61
  .addHelpText("after", `
60
62
  Examples:
61
63
  gmax review Review HEAD
62
64
  gmax review --commit abc1234 Review specific commit
65
+ gmax review --risk Rank changed symbols by risk (no LLM)
66
+ gmax review --risk --agent Risk ranking, compact for agents
63
67
  gmax review --background Run async via daemon
64
68
 
65
69
  Subcommands:
@@ -75,6 +79,31 @@ Subcommands:
75
79
  return;
76
80
  const projectRoot = (_a = (0, project_root_1.findProjectRoot)(root)) !== null && _a !== void 0 ? _a : root;
77
81
  const commitRef = opts.commit;
82
+ // Deterministic risk ranking — no LLM, no daemon LLM-start. Pure graph +
83
+ // git over the diff (Phase 8).
84
+ if (opts.risk) {
85
+ const { VectorDB } = yield Promise.resolve().then(() => __importStar(require("../lib/store/vector-db")));
86
+ const { GraphBuilder } = yield Promise.resolve().then(() => __importStar(require("../lib/graph/graph-builder")));
87
+ const { ensureProjectPaths } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/project-root")));
88
+ const { gatherRiskInputs, computeRiskTable, formatRiskTable, } = yield Promise.resolve().then(() => __importStar(require("../lib/review/risk")));
89
+ const paths = ensureProjectPaths(projectRoot);
90
+ const vectorDb = new VectorDB(paths.lancedbDir);
91
+ try {
92
+ const graphBuilder = new GraphBuilder(vectorDb, projectRoot);
93
+ const inputs = yield gatherRiskInputs(commitRef, projectRoot, {
94
+ vectorDb,
95
+ graphBuilder,
96
+ });
97
+ const rows = computeRiskTable(inputs);
98
+ console.log(formatRiskTable(rows, { agent: !!opts.agent }));
99
+ if (rows.length === 0)
100
+ process.exitCode = 1;
101
+ }
102
+ finally {
103
+ yield vectorDb.close();
104
+ }
105
+ return;
106
+ }
78
107
  if (opts.background) {
79
108
  // Fire-and-forget via daemon
80
109
  const { ensureDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
@@ -4,6 +4,7 @@ exports.SYMBOL_MAX = exports.DIFF_MAX_LINES = void 0;
4
4
  exports.extractDiff = extractDiff;
5
5
  exports.readCommitInfo = readCommitInfo;
6
6
  exports.extractChangedFiles = extractChangedFiles;
7
+ exports.fileChurn = fileChurn;
7
8
  exports.extractSymbols = extractSymbols;
8
9
  exports.detectLanguages = detectLanguages;
9
10
  const node_child_process_1 = require("node:child_process");
@@ -84,6 +85,27 @@ function extractChangedFiles(ref, root) {
84
85
  return [];
85
86
  }
86
87
  }
88
+ /**
89
+ * Churn: number of commits that have touched a file across history. A simple,
90
+ * deterministic instability proxy for the risk preamble (Phase 8) — frequently
91
+ * rewritten files are historically more bug-prone. `file` may be absolute or
92
+ * root-relative; git resolves it against `root`. Returns 0 on any error.
93
+ */
94
+ function fileChurn(file, root) {
95
+ if (!file)
96
+ return 0;
97
+ try {
98
+ const rel = file.startsWith(root)
99
+ ? file.slice(root.endsWith("/") ? root.length : root.length + 1)
100
+ : file;
101
+ const raw = git(["rev-list", "--count", "HEAD", "--", rel], root).trim();
102
+ const n = Number.parseInt(raw, 10);
103
+ return Number.isFinite(n) ? n : 0;
104
+ }
105
+ catch (_a) {
106
+ return 0;
107
+ }
108
+ }
87
109
  /**
88
110
  * Extract symbol names from a unified diff.
89
111
  * Pass 1: hunk headers (git auto-detects enclosing function/class).
@@ -56,6 +56,7 @@ const config_1 = require("./config");
56
56
  const diff_1 = require("./diff");
57
57
  const report_1 = require("./report");
58
58
  const tools_1 = require("./tools");
59
+ const risk_1 = require("../review/risk");
59
60
  function reviewCommit(opts) {
60
61
  return __awaiter(this, void 0, void 0, function* () {
61
62
  var _a, _b, _c, _d;
@@ -81,13 +82,25 @@ function reviewCommit(opts) {
81
82
  }
82
83
  // 4. Gather context via gmax internal APIs
83
84
  let contextStr = "";
85
+ let riskStr = "";
84
86
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
85
87
  const vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
86
88
  try {
87
89
  const searcher = new searcher_1.Searcher(vectorDb);
88
90
  const graphBuilder = new graph_builder_1.GraphBuilder(vectorDb, projectRoot);
89
91
  const ctx = { vectorDb, searcher, graphBuilder, projectRoot };
90
- contextStr = yield gatherContext(symbols, changedFiles, ctx, verbose);
92
+ // Deterministic risk ranking (Phase 8) gives the LLM an explicit
93
+ // blast-radius × tests × churn ordering to anchor its judgement, rather
94
+ // than inferring importance from prose alone.
95
+ const [context, riskInputs] = yield Promise.all([
96
+ gatherContext(symbols, changedFiles, ctx, verbose),
97
+ (0, risk_1.gatherRiskInputs)(commitRef, projectRoot, { vectorDb, graphBuilder }).catch(() => []),
98
+ ]);
99
+ contextStr = context;
100
+ const riskRows = (0, risk_1.computeRiskTable)(riskInputs);
101
+ if (riskRows.length > 0) {
102
+ riskStr = (0, risk_1.formatRiskTable)(riskRows, { agent: false });
103
+ }
91
104
  }
92
105
  catch (err) {
93
106
  if (verbose) {
@@ -99,7 +112,7 @@ function reviewCommit(opts) {
99
112
  }
100
113
  // 5. Build prompts
101
114
  const systemPrompt = buildSystemPrompt(languages);
102
- const userPrompt = buildUserPrompt(info, diff, symbols, contextStr);
115
+ const userPrompt = buildUserPrompt(info, diff, symbols, contextStr, riskStr);
103
116
  // 6. Call LLM (single shot)
104
117
  const config = (0, config_1.getLlmConfig)();
105
118
  const modelName = path.basename(config.model, path.extname(config.model));
@@ -318,7 +331,7 @@ Severity guide:
318
331
  Be concise. One sentence per message. Evidence from the codebase context, not speculation.`;
319
332
  return prompt;
320
333
  }
321
- function buildUserPrompt(info, diff, symbols, context) {
334
+ function buildUserPrompt(info, diff, symbols, context, risk) {
322
335
  let prompt = `## Commit
323
336
  ${info.short} — ${info.message}
324
337
 
@@ -330,6 +343,9 @@ ${diff}
330
343
  if (symbols.length > 0) {
331
344
  prompt += `### Changed Symbols\n${symbols.join("\n")}\n\n`;
332
345
  }
346
+ if (risk) {
347
+ prompt += `### ${risk}\n\n`;
348
+ }
333
349
  if (context) {
334
350
  prompt += context;
335
351
  }
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.computeRiskTable = computeRiskTable;
13
+ exports.formatRiskTable = formatRiskTable;
14
+ exports.gatherRiskInputs = gatherRiskInputs;
15
+ const impact_1 = require("../graph/impact");
16
+ const diff_1 = require("../llm/diff");
17
+ // No test safety net → treat the change as twice as risky. A single, named
18
+ // constant keeps the score explainable rather than a tuned black box.
19
+ const UNTESTED_MULTIPLIER = 2;
20
+ /**
21
+ * Score and rank changed symbols, riskiest first. Pure — no I/O — so the
22
+ * ranking logic is unit-tested without a graph or git. Score is
23
+ * `(callers + 1) × testFactor × churnFactor`: blast radius dominates, the
24
+ * untested penalty doubles it, and churn contributes on a log scale so a
25
+ * very churny file nudges rather than swamps the ranking.
26
+ */
27
+ function computeRiskTable(inputs) {
28
+ const rows = inputs.map((r) => {
29
+ const blast = r.callerCount + 1; // +1 so a zero-caller symbol still scores
30
+ const testFactor = r.hasTests ? 1 : UNTESTED_MULTIPLIER;
31
+ const churnFactor = 1 + Math.log2(r.churn + 1);
32
+ const score = Math.round(blast * testFactor * churnFactor * 100) / 100;
33
+ return Object.assign(Object.assign({}, r), { score });
34
+ });
35
+ rows.sort((a, b) => b.score - a.score ||
36
+ b.callerCount - a.callerCount ||
37
+ a.symbol.localeCompare(b.symbol));
38
+ return rows;
39
+ }
40
+ /** Render the ranking — TSV-ish for agents, an aligned table for humans. */
41
+ function formatRiskTable(rows, opts) {
42
+ if (rows.length === 0) {
43
+ return opts.agent
44
+ ? "(no changed symbols)"
45
+ : "No changed symbols to rank.";
46
+ }
47
+ if (opts.agent) {
48
+ return rows
49
+ .map((r) => `risk\t${r.score}\t${r.symbol}\t${r.file}:${r.line}\tcallers=${r.callerCount}\ttests=${r.hasTests ? "y" : "n"}\tchurn=${r.churn}`)
50
+ .join("\n");
51
+ }
52
+ const lines = rows.map((r) => {
53
+ const flag = !r.hasTests && r.callerCount > 0 ? " ⚠ untested" : "";
54
+ return ` ${String(r.score).padStart(7)} ${r.symbol} (${r.callerCount} callers, ${r.hasTests ? "tested" : "no tests"}, churn ${r.churn}) ${r.file}:${r.line}${flag}`;
55
+ });
56
+ return `Risk ranking — blast radius × tests × churn (riskiest first):\n${lines.join("\n")}`;
57
+ }
58
+ /**
59
+ * Gather risk inputs for the symbols a ref's diff touches. Impure: reads git
60
+ * (diff + churn) and the graph (callers + defining location + tests). Each
61
+ * symbol is independent, so failures degrade that row rather than the whole
62
+ * table.
63
+ */
64
+ function gatherRiskInputs(ref, projectRoot, deps) {
65
+ return __awaiter(this, void 0, void 0, function* () {
66
+ const diff = (0, diff_1.extractDiff)(ref, projectRoot);
67
+ if (!diff)
68
+ return [];
69
+ const symbols = (0, diff_1.extractSymbols)(diff);
70
+ if (symbols.length === 0)
71
+ return [];
72
+ const root = projectRoot.endsWith("/") ? projectRoot : `${projectRoot}/`;
73
+ const relativize = (f) => (f.startsWith(root) ? f.slice(root.length) : f);
74
+ const inputs = yield Promise.all(symbols.map((symbol) => __awaiter(this, void 0, void 0, function* () {
75
+ var _a, _b;
76
+ const [callers, loc, tests] = yield Promise.all([
77
+ deps.graphBuilder.callersOf(symbol).catch(() => []),
78
+ deps.graphBuilder.resolveLocation(symbol).catch(() => null),
79
+ (0, impact_1.findTests)([symbol], deps.vectorDb, projectRoot).catch(() => []),
80
+ ]);
81
+ const file = (_a = loc === null || loc === void 0 ? void 0 : loc.file) !== null && _a !== void 0 ? _a : "";
82
+ return {
83
+ symbol,
84
+ file: file ? relativize(file) : "(unindexed)",
85
+ line: (_b = loc === null || loc === void 0 ? void 0 : loc.line) !== null && _b !== void 0 ? _b : 0,
86
+ callerCount: callers.length,
87
+ hasTests: tests.length > 0,
88
+ churn: (0, diff_1.fileChurn)(file, projectRoot),
89
+ };
90
+ })));
91
+ return inputs;
92
+ });
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.15",
3
+ "version": "0.17.16",
4
4
  "author": "Robert Owens <78518764+reowens@users.noreply.github.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.17.15",
3
+ "version": "0.17.16",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",