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.
package/dist/commands/mcp.js
CHANGED
|
@@ -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")));
|
package/dist/commands/review.js
CHANGED
|
@@ -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")));
|
package/dist/lib/llm/diff.js
CHANGED
|
@@ -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).
|
package/dist/lib/llm/review.js
CHANGED
|
@@ -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
|
-
|
|
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