grepmax 0.17.14 → 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 +36 -0
- package/dist/commands/review.js +29 -0
- package/dist/commands/search.js +25 -1
- package/dist/lib/daemon/daemon.js +60 -10
- package/dist/lib/index/batch-processor.js +4 -0
- package/dist/lib/llm/diff.js +22 -0
- package/dist/lib/llm/review.js +19 -3
- package/dist/lib/output/index-state-footer.js +29 -0
- package/dist/lib/review/risk.js +93 -0
- package/package.json +1 -1
- package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
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/commands/search.js
CHANGED
|
@@ -50,6 +50,7 @@ const grammar_loader_1 = require("../lib/index/grammar-loader");
|
|
|
50
50
|
const sync_helpers_1 = require("../lib/index/sync-helpers");
|
|
51
51
|
const syncer_1 = require("../lib/index/syncer");
|
|
52
52
|
const agent_search_formatter_1 = require("../lib/output/agent-search-formatter");
|
|
53
|
+
const index_state_footer_1 = require("../lib/output/index-state-footer");
|
|
53
54
|
const searcher_1 = require("../lib/search/searcher");
|
|
54
55
|
const setup_helpers_1 = require("../lib/setup/setup-helpers");
|
|
55
56
|
const skeleton_1 = require("../lib/skeleton");
|
|
@@ -575,6 +576,7 @@ Examples:
|
|
|
575
576
|
let searchResult = null;
|
|
576
577
|
let precomputedSkeletons;
|
|
577
578
|
let precomputedGraph;
|
|
579
|
+
let indexState;
|
|
578
580
|
if (!options.sync && !options.dryRun) {
|
|
579
581
|
try {
|
|
580
582
|
const { isDaemonRunning, sendDaemonCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
|
|
@@ -600,6 +602,7 @@ Examples:
|
|
|
600
602
|
};
|
|
601
603
|
precomputedSkeletons = resp.skeletons;
|
|
602
604
|
precomputedGraph = resp.graph;
|
|
605
|
+
indexState = resp.indexState;
|
|
603
606
|
}
|
|
604
607
|
else if (process.env.GMAX_DEBUG === "1") {
|
|
605
608
|
console.error(`[search] daemon path unavailable: ${(_d = resp.error) !== null && _d !== void 0 ? _d : "unknown"}`);
|
|
@@ -617,9 +620,16 @@ Examples:
|
|
|
617
620
|
if (!searchResult) {
|
|
618
621
|
vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
|
|
619
622
|
// Check for active indexing lock and warn if present
|
|
620
|
-
|
|
623
|
+
const locked = (0, lock_1.isLocked)(paths.dataDir);
|
|
624
|
+
if (!options.agent && locked) {
|
|
621
625
|
console.warn("⚠️ Warning: Indexing in progress... search results may be incomplete.");
|
|
622
626
|
}
|
|
627
|
+
// No daemon here, so no precise pending count — surface the coarse
|
|
628
|
+
// signal (active lock or initial index not yet complete) so agent mode
|
|
629
|
+
// still gets a partial-index footer.
|
|
630
|
+
if (!indexState && (locked || project.status === "pending")) {
|
|
631
|
+
indexState = { indexing: true, pendingFiles: 0 };
|
|
632
|
+
}
|
|
623
633
|
const hasRows = yield vectorDb.hasAnyRows();
|
|
624
634
|
const needsSync = options.sync || !hasRows;
|
|
625
635
|
if (needsSync) {
|
|
@@ -694,6 +704,14 @@ Examples:
|
|
|
694
704
|
console.warn(`Warning: ${w}`);
|
|
695
705
|
}
|
|
696
706
|
}
|
|
707
|
+
// Partial-index signal (Phase 6): when the index is mid-catchup, results
|
|
708
|
+
// may be incomplete. Non-agent renders it now as a warning; agent mode
|
|
709
|
+
// appends a machine-readable footer after the results below.
|
|
710
|
+
if (!options.agent) {
|
|
711
|
+
const footer = (0, index_state_footer_1.formatIndexStateFooter)(indexState, { agent: false });
|
|
712
|
+
if (footer)
|
|
713
|
+
console.warn(footer);
|
|
714
|
+
}
|
|
697
715
|
let filteredData = searchResult.data.filter((r) => typeof r.score !== "number" || r.score >= minScore);
|
|
698
716
|
// Post-filter by symbol name regex
|
|
699
717
|
if (options.name) {
|
|
@@ -766,6 +784,12 @@ Examples:
|
|
|
766
784
|
}
|
|
767
785
|
catch (_1) { }
|
|
768
786
|
}
|
|
787
|
+
// Partial-index footer last, so it's the final line the agent reads —
|
|
788
|
+
// and emitted even on "(none)", where an empty result may just mean the
|
|
789
|
+
// relevant files aren't indexed yet.
|
|
790
|
+
const footer = (0, index_state_footer_1.formatIndexStateFooter)(indexState, { agent: true });
|
|
791
|
+
if (footer)
|
|
792
|
+
console.log(footer);
|
|
769
793
|
return;
|
|
770
794
|
}
|
|
771
795
|
if (options.skeleton) {
|
|
@@ -142,6 +142,10 @@ class Daemon {
|
|
|
142
142
|
this.lastOverflowMs = new Map();
|
|
143
143
|
this.lastCatchupEndMs = new Map();
|
|
144
144
|
this.projectLocks = new Map();
|
|
145
|
+
// Full-index progress per root while initialSync runs (--reset / initial
|
|
146
|
+
// index). Presence = a full index is in flight; value drives the partial-
|
|
147
|
+
// result pending count (Phase 6). Cleared in the indexProject finally.
|
|
148
|
+
this.indexProgress = new Map();
|
|
145
149
|
this.shutdownAbortControllers = new Set();
|
|
146
150
|
this.llmServer = null;
|
|
147
151
|
this.mlxChild = null;
|
|
@@ -809,19 +813,51 @@ class Daemon {
|
|
|
809
813
|
* caller is responsible for binding `signal` to socket close so we abort if
|
|
810
814
|
* the client disconnects mid-search.
|
|
811
815
|
*/
|
|
816
|
+
/**
|
|
817
|
+
* Live (re)index progress for a project: whether indexing is underway and how
|
|
818
|
+
* many files are still queued. Derived from the batch processor's pending map
|
|
819
|
+
* plus the registry's initial-index status. Cheap (in-memory) — safe to call
|
|
820
|
+
* on every search to annotate partial-result responses (Phase 6).
|
|
821
|
+
*/
|
|
822
|
+
indexState(root) {
|
|
823
|
+
var _a, _b, _c;
|
|
824
|
+
const processor = this.processors.get(root);
|
|
825
|
+
const batchPending = (_a = processor === null || processor === void 0 ? void 0 : processor.progress.pendingFiles) !== null && _a !== void 0 ? _a : 0;
|
|
826
|
+
const processing = (_b = processor === null || processor === void 0 ? void 0 : processor.progress.processing) !== null && _b !== void 0 ? _b : false;
|
|
827
|
+
// status === "pending" means the initial full index hasn't completed.
|
|
828
|
+
const initialPending = ((_c = (0, project_registry_1.getProject)(root)) === null || _c === void 0 ? void 0 : _c.status) === "pending";
|
|
829
|
+
// A full index (--reset / initial) bypasses the batch processor; its
|
|
830
|
+
// onProgress feeds indexProgress, giving a real remaining count.
|
|
831
|
+
const fullIdx = this.indexProgress.get(root);
|
|
832
|
+
let pendingFiles = batchPending;
|
|
833
|
+
if (fullIdx && fullIdx.total > 0) {
|
|
834
|
+
pendingFiles = Math.max(pendingFiles, fullIdx.total - fullIdx.processed);
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
indexing: !!fullIdx || processing || batchPending > 0 || initialPending,
|
|
838
|
+
pendingFiles,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
812
841
|
search(payload, signal) {
|
|
813
842
|
return __awaiter(this, void 0, void 0, function* () {
|
|
814
|
-
var _a, _b, _c;
|
|
843
|
+
var _a, _b, _c, _d;
|
|
815
844
|
if (!this.vectorDb) {
|
|
816
845
|
return { ok: false, error: "daemon not ready" };
|
|
817
846
|
}
|
|
818
847
|
const root = payload.projectRoot;
|
|
819
848
|
if (!this.processors.has(root)) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
849
|
+
// A full index (--reset) or the initial index removes/defers the
|
|
850
|
+
// processor while (re)building. The partial index is still queryable, so
|
|
851
|
+
// answer the search and flag it partial (below) rather than erroring —
|
|
852
|
+
// only truly-unwatched, not-indexing projects get "not watched".
|
|
853
|
+
const indexingNow = this.indexProgress.has(root) || ((_a = (0, project_registry_1.getProject)(root)) === null || _a === void 0 ? void 0 : _a.status) === "pending";
|
|
854
|
+
if (!indexingNow) {
|
|
855
|
+
return {
|
|
856
|
+
ok: false,
|
|
857
|
+
error: "project not watched",
|
|
858
|
+
hint: `run: gmax add ${root}`,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
825
861
|
}
|
|
826
862
|
let searcher = this.searchers.get(root);
|
|
827
863
|
if (!searcher) {
|
|
@@ -841,8 +877,14 @@ class Daemon {
|
|
|
841
877
|
return { ok: false, error: "search_failed", hint: msg };
|
|
842
878
|
}
|
|
843
879
|
const response = { ok: true, data: result.data };
|
|
844
|
-
if ((
|
|
880
|
+
if ((_b = result.warnings) === null || _b === void 0 ? void 0 : _b.length)
|
|
845
881
|
response.warnings = result.warnings;
|
|
882
|
+
// Annotate partial results when the index is still catching up, so an
|
|
883
|
+
// agent can caveat or retry. Only attached when actually indexing (the
|
|
884
|
+
// formatter suppresses the settled case anyway).
|
|
885
|
+
const idx = this.indexState(root);
|
|
886
|
+
if (idx.indexing)
|
|
887
|
+
response.indexState = idx;
|
|
846
888
|
// --skeleton support: fetch per-file skeletons inline so the CLI doesn't
|
|
847
889
|
// have to open its own VectorDB. getStoredSkeleton is a single LIMIT-1
|
|
848
890
|
// lookup; cheap enough to call for the top N distinct paths.
|
|
@@ -851,7 +893,7 @@ class Daemon {
|
|
|
851
893
|
const seen = new Set();
|
|
852
894
|
const skeletons = {};
|
|
853
895
|
for (const chunk of result.data) {
|
|
854
|
-
const p = (
|
|
896
|
+
const p = (_c = chunk.path) !== null && _c !== void 0 ? _c : (_d = chunk.metadata) === null || _d === void 0 ? void 0 : _d.path;
|
|
855
897
|
if (!p || seen.has(p))
|
|
856
898
|
continue;
|
|
857
899
|
seen.add(p);
|
|
@@ -862,7 +904,7 @@ class Daemon {
|
|
|
862
904
|
if (sk)
|
|
863
905
|
skeletons[p] = sk;
|
|
864
906
|
}
|
|
865
|
-
catch (
|
|
907
|
+
catch (_e) {
|
|
866
908
|
// best-effort — drop the entry, keep the search result
|
|
867
909
|
}
|
|
868
910
|
}
|
|
@@ -877,7 +919,7 @@ class Daemon {
|
|
|
877
919
|
const builder = new GraphBuilder(this.vectorDb, root);
|
|
878
920
|
response.graph = yield builder.buildGraphMultiHop(payload.query, 1);
|
|
879
921
|
}
|
|
880
|
-
catch (
|
|
922
|
+
catch (_f) {
|
|
881
923
|
// best-effort — drop graph, keep results
|
|
882
924
|
}
|
|
883
925
|
}
|
|
@@ -1018,6 +1060,9 @@ class Daemon {
|
|
|
1018
1060
|
this.vectorDb.pauseMaintenanceLoop();
|
|
1019
1061
|
const stopHeartbeat = (0, ipc_handler_1.startHeartbeat)(conn);
|
|
1020
1062
|
let lastProgressTime = 0;
|
|
1063
|
+
// Mark this root as full-indexing so concurrent searches get a
|
|
1064
|
+
// partial-result footer (Phase 6); seeded at 0/0 until the first tick.
|
|
1065
|
+
this.indexProgress.set(root, { processed: 0, total: 0 });
|
|
1021
1066
|
try {
|
|
1022
1067
|
const result = yield (0, syncer_1.initialSync)({
|
|
1023
1068
|
projectRoot: root,
|
|
@@ -1028,6 +1073,10 @@ class Daemon {
|
|
|
1028
1073
|
signal: ac.signal,
|
|
1029
1074
|
onProgress: (info) => {
|
|
1030
1075
|
this.resetActivity();
|
|
1076
|
+
this.indexProgress.set(root, {
|
|
1077
|
+
processed: info.processed,
|
|
1078
|
+
total: info.total,
|
|
1079
|
+
});
|
|
1031
1080
|
const now = Date.now();
|
|
1032
1081
|
if (now - lastProgressTime < 100)
|
|
1033
1082
|
return;
|
|
@@ -1057,6 +1106,7 @@ class Daemon {
|
|
|
1057
1106
|
}
|
|
1058
1107
|
finally {
|
|
1059
1108
|
stopHeartbeat();
|
|
1109
|
+
this.indexProgress.delete(root);
|
|
1060
1110
|
this.shutdownAbortControllers.delete(ac);
|
|
1061
1111
|
(_a = this.vectorDb) === null || _a === void 0 ? void 0 : _a.resumeMaintenanceLoop();
|
|
1062
1112
|
// Re-enable watcher (skip if shutting down)
|
|
@@ -96,6 +96,10 @@ class ProjectBatchProcessor {
|
|
|
96
96
|
(_a = this.onActivity) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
97
97
|
this.scheduleBatch();
|
|
98
98
|
}
|
|
99
|
+
/** Live (re)index progress: files queued + whether a batch is running. */
|
|
100
|
+
get progress() {
|
|
101
|
+
return { pendingFiles: this.pending.size, processing: this.processing };
|
|
102
|
+
}
|
|
99
103
|
close() {
|
|
100
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
101
105
|
var _a;
|
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,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Phase 6 — partial-index signal for agent-mode search.
|
|
3
|
+
//
|
|
4
|
+
// During the catchup window the index is incomplete but search still returns
|
|
5
|
+
// (partial) results. Non-agent output already warns about this; agent output
|
|
6
|
+
// historically did not. This formats a single machine-readable footer so an
|
|
7
|
+
// agent can decide to caveat its answer or retry once indexing settles.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.formatIndexStateFooter = formatIndexStateFooter;
|
|
10
|
+
/**
|
|
11
|
+
* One-line footer describing an in-progress index, or null when there's
|
|
12
|
+
* nothing to say (no state, or the index is settled). Suppressing the
|
|
13
|
+
* settled case keeps steady-state search silent — the footer only appears
|
|
14
|
+
* while results may actually be incomplete.
|
|
15
|
+
*/
|
|
16
|
+
function formatIndexStateFooter(state, opts) {
|
|
17
|
+
if (!state || !state.indexing)
|
|
18
|
+
return null;
|
|
19
|
+
const count = state.pendingFiles > 0 ? `~${state.pendingFiles} files pending` : null;
|
|
20
|
+
if (opts.agent) {
|
|
21
|
+
const parts = ["index: syncing"];
|
|
22
|
+
if (count)
|
|
23
|
+
parts.push(count);
|
|
24
|
+
parts.push("results may be incomplete — retry for full coverage");
|
|
25
|
+
return `[${parts.join(" · ")}]`;
|
|
26
|
+
}
|
|
27
|
+
const detail = count ? ` (${count})` : "";
|
|
28
|
+
return `⚠️ Index still syncing${detail} — results may be incomplete.`;
|
|
29
|
+
}
|
|
@@ -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