@in-the-loop-labs/pair-review 2.4.1 → 2.4.3
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/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +5 -2
- package/public/js/pr.js +20 -14
- package/src/ai/analyzer.js +51 -2
- package/src/chat/claude-code-bridge.js +8 -0
- package/src/database.js +88 -13
- package/src/routes/pr.js +13 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
package/public/css/pr.css
CHANGED
|
@@ -12277,9 +12277,12 @@ body.resizing * {
|
|
|
12277
12277
|
font-size: 14px;
|
|
12278
12278
|
}
|
|
12279
12279
|
|
|
12280
|
-
/*
|
|
12280
|
+
/* Don't grow (keeps badge next to text), shrink for long paths, clip from the left so the filename stays visible */
|
|
12281
12281
|
.context-file-header .d2h-file-name {
|
|
12282
|
-
flex:
|
|
12282
|
+
flex: 0 1 auto;
|
|
12283
|
+
direction: rtl;
|
|
12284
|
+
unicode-bidi: plaintext;
|
|
12285
|
+
text-overflow: ellipsis;
|
|
12283
12286
|
}
|
|
12284
12287
|
|
|
12285
12288
|
/* Push viewed checkbox (and everything after it) to the right */
|
package/public/js/pr.js
CHANGED
|
@@ -4480,7 +4480,16 @@ class PRManager {
|
|
|
4480
4480
|
const tbody = document.createElement('tbody');
|
|
4481
4481
|
tbody.className = 'd2h-diff-tbody context-chunk';
|
|
4482
4482
|
tbody.dataset.contextId = contextFile.id;
|
|
4483
|
-
|
|
4483
|
+
|
|
4484
|
+
// Compute effective display range, shifting for end-of-file
|
|
4485
|
+
const clampedEnd = Math.min(contextFile.line_end, data.lines.length);
|
|
4486
|
+
const intendedSize = contextFile.line_end - contextFile.line_start + 1;
|
|
4487
|
+
let effectiveStart = contextFile.line_start;
|
|
4488
|
+
const actualSize = clampedEnd - effectiveStart + 1;
|
|
4489
|
+
if (actualSize < intendedSize && effectiveStart > 1) {
|
|
4490
|
+
effectiveStart = Math.max(1, effectiveStart - (intendedSize - actualSize));
|
|
4491
|
+
}
|
|
4492
|
+
tbody.dataset.lineStart = effectiveStart;
|
|
4484
4493
|
|
|
4485
4494
|
// Chunk header row with range label and per-chunk dismiss button
|
|
4486
4495
|
const headerRow = document.createElement('tr');
|
|
@@ -4493,8 +4502,7 @@ class PRManager {
|
|
|
4493
4502
|
contentTd.colSpan = 3;
|
|
4494
4503
|
const rangeLabel = document.createElement('span');
|
|
4495
4504
|
rangeLabel.className = 'context-range-label';
|
|
4496
|
-
|
|
4497
|
-
rangeLabel.textContent = `Lines ${contextFile.line_start}\u2013${lineEnd}`;
|
|
4505
|
+
rangeLabel.textContent = `Lines ${effectiveStart}\u2013${clampedEnd}`;
|
|
4498
4506
|
contentTd.appendChild(rangeLabel);
|
|
4499
4507
|
const chunkDismiss = document.createElement('button');
|
|
4500
4508
|
chunkDismiss.className = 'context-chunk-dismiss';
|
|
@@ -4508,25 +4516,22 @@ class PRManager {
|
|
|
4508
4516
|
headerRow.appendChild(contentTd);
|
|
4509
4517
|
tbody.appendChild(headerRow);
|
|
4510
4518
|
|
|
4511
|
-
const lineStart = contextFile.line_start;
|
|
4512
|
-
const clampedEnd = Math.min(contextFile.line_end, data.lines.length);
|
|
4513
|
-
|
|
4514
4519
|
// Add expand-up gap row if there are lines above the context range
|
|
4515
|
-
if (
|
|
4516
|
-
const gapAboveSize =
|
|
4520
|
+
if (effectiveStart > 1) {
|
|
4521
|
+
const gapAboveSize = effectiveStart - 1;
|
|
4517
4522
|
const gapAbove = window.HunkParser.createGapRowElement(
|
|
4518
4523
|
contextFile.file,
|
|
4519
|
-
1,
|
|
4520
|
-
|
|
4524
|
+
1, // startLine (old coords)
|
|
4525
|
+
effectiveStart - 1, // endLine (old coords)
|
|
4521
4526
|
gapAboveSize,
|
|
4522
4527
|
'above',
|
|
4523
4528
|
this.expandGapContext.bind(this),
|
|
4524
|
-
1
|
|
4529
|
+
1 // startLineNew (same as old for context files — no diff offset)
|
|
4525
4530
|
);
|
|
4526
4531
|
tbody.appendChild(gapAbove);
|
|
4527
4532
|
}
|
|
4528
4533
|
|
|
4529
|
-
for (let i =
|
|
4534
|
+
for (let i = effectiveStart; i <= clampedEnd; i++) {
|
|
4530
4535
|
const lineData = {
|
|
4531
4536
|
type: 'context',
|
|
4532
4537
|
oldNumber: i,
|
|
@@ -4874,8 +4879,9 @@ class PRManager {
|
|
|
4874
4879
|
lineStartVal = 1;
|
|
4875
4880
|
lineEndVal = 100;
|
|
4876
4881
|
} else if (lineEnd == null) {
|
|
4877
|
-
|
|
4878
|
-
|
|
4882
|
+
// Center a ~21-line window around the target line (±10 lines)
|
|
4883
|
+
lineStartVal = Math.max(1, lineStart - 10);
|
|
4884
|
+
lineEndVal = lineStartVal + 20;
|
|
4879
4885
|
} else {
|
|
4880
4886
|
lineStartVal = lineStart;
|
|
4881
4887
|
lineEndVal = Math.min(lineEnd, lineStart + 499);
|
package/src/ai/analyzer.js
CHANGED
|
@@ -38,6 +38,29 @@ function buildReviewerLabel(idx, voice) {
|
|
|
38
38
|
return `Reviewer ${idx + 1} (${voice.provider}/${voice.model})`;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Capture a unified diff snapshot for an analysis run.
|
|
43
|
+
* This ensures the diff is preserved even if the branch is force-pushed later.
|
|
44
|
+
*
|
|
45
|
+
* @param {Analyzer} analyzer - Analyzer instance (provides buildGitDiffCommand)
|
|
46
|
+
* @param {string} worktreePath - Path to the git worktree
|
|
47
|
+
* @param {Object} prMetadata - PR metadata with base/head branch info
|
|
48
|
+
* @param {string} logPrefix - Prefix for log messages
|
|
49
|
+
* @returns {Promise<string|null>} The diff snapshot or null if capture failed
|
|
50
|
+
*/
|
|
51
|
+
async function captureDiffSnapshot(analyzer, worktreePath, prMetadata, logPrefix = '') {
|
|
52
|
+
try {
|
|
53
|
+
const diffCmd = analyzer.buildGitDiffCommand(prMetadata);
|
|
54
|
+
const { stdout } = await execPromise(diffCmd, { cwd: worktreePath, maxBuffer: 50 * 1024 * 1024 });
|
|
55
|
+
logger.info(`${logPrefix}Captured diff snapshot (${stdout.length} bytes)`);
|
|
56
|
+
return stdout;
|
|
57
|
+
} catch (diffError) {
|
|
58
|
+
logger.warn(`${logPrefix}Failed to capture diff snapshot: ${diffError.message}`);
|
|
59
|
+
// Continue without diff snapshot - share endpoint will fall back to current PR diff
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
/**
|
|
42
65
|
* Build shared context for a council voice/reviewer.
|
|
43
66
|
* Used by both single-voice and multi-voice paths in runReviewerCentricCouncil.
|
|
@@ -167,7 +190,11 @@ class Analyzer {
|
|
|
167
190
|
logger.info(`${logPrefix}HEAD SHA: ${headSha}`);
|
|
168
191
|
}
|
|
169
192
|
|
|
170
|
-
//
|
|
193
|
+
// Capture unified diff snapshot for this analysis run
|
|
194
|
+
// This ensures the diff is preserved even if the branch is force-pushed later
|
|
195
|
+
const diffSnapshot = await captureDiffSnapshot(this, worktreePath, prMetadata, logPrefix);
|
|
196
|
+
|
|
197
|
+
// Create or update analysis run record in database
|
|
171
198
|
const analysisRunRepo = new AnalysisRunRepository(this.db);
|
|
172
199
|
if (!skipRunCreation) {
|
|
173
200
|
try {
|
|
@@ -180,13 +207,22 @@ class Analyzer {
|
|
|
180
207
|
customInstructions: mergedInstructions, // Keep for backward compat
|
|
181
208
|
repoInstructions,
|
|
182
209
|
requestInstructions,
|
|
183
|
-
headSha
|
|
210
|
+
headSha,
|
|
211
|
+
diff: diffSnapshot
|
|
184
212
|
});
|
|
185
213
|
logger.info(`${logPrefix}Created analysis_run record: ${runId}`);
|
|
186
214
|
} catch (createError) {
|
|
187
215
|
logger.warn(`${logPrefix}Failed to create analysis_run record: ${createError.message}`);
|
|
188
216
|
// Continue with analysis even if record creation fails
|
|
189
217
|
}
|
|
218
|
+
} else if (diffSnapshot) {
|
|
219
|
+
// Run was created by caller, but we still need to store the diff snapshot
|
|
220
|
+
try {
|
|
221
|
+
await analysisRunRepo.update(runId, { diff: diffSnapshot });
|
|
222
|
+
logger.info(`${logPrefix}Updated analysis_run with diff snapshot`);
|
|
223
|
+
} catch (updateError) {
|
|
224
|
+
logger.warn(`${logPrefix}Failed to update analysis_run with diff: ${updateError.message}`);
|
|
225
|
+
}
|
|
190
226
|
}
|
|
191
227
|
|
|
192
228
|
// Load generated file patterns to skip during analysis
|
|
@@ -2657,6 +2693,9 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2657
2693
|
const headSha = prMetadata?.head_sha || null;
|
|
2658
2694
|
const analysisRunRepo = new AnalysisRunRepository(this.db);
|
|
2659
2695
|
|
|
2696
|
+
// Capture unified diff snapshot for this analysis run
|
|
2697
|
+
const diffSnapshot = await captureDiffSnapshot(this, worktreePath, prMetadata, '[ReviewerCouncil] ');
|
|
2698
|
+
|
|
2660
2699
|
// Create parent analysis run only if caller didn't already create it
|
|
2661
2700
|
// (when runId is passed via options, the route handler has already inserted the record)
|
|
2662
2701
|
if (!options.runId) {
|
|
@@ -2671,6 +2710,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2671
2710
|
repoInstructions: instructions?.repoInstructions || null,
|
|
2672
2711
|
requestInstructions: instructions?.requestInstructions || null,
|
|
2673
2712
|
headSha,
|
|
2713
|
+
diff: diffSnapshot,
|
|
2674
2714
|
configType: 'council',
|
|
2675
2715
|
levelsConfig: enabledLevels
|
|
2676
2716
|
});
|
|
@@ -2678,6 +2718,14 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2678
2718
|
} catch (err) {
|
|
2679
2719
|
logger.warn(`[ReviewerCouncil] Failed to create parent run record: ${err.message}`);
|
|
2680
2720
|
}
|
|
2721
|
+
} else if (diffSnapshot) {
|
|
2722
|
+
// Run was created by caller, but we still need to store the diff snapshot
|
|
2723
|
+
try {
|
|
2724
|
+
await analysisRunRepo.update(parentRunId, { diff: diffSnapshot });
|
|
2725
|
+
logger.info(`[ReviewerCouncil] Updated analysis_run with diff snapshot`);
|
|
2726
|
+
} catch (updateError) {
|
|
2727
|
+
logger.warn(`[ReviewerCouncil] Failed to update analysis_run with diff: ${updateError.message}`);
|
|
2728
|
+
}
|
|
2681
2729
|
}
|
|
2682
2730
|
|
|
2683
2731
|
const voices = councilConfig.voices || [];
|
|
@@ -2768,6 +2816,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2768
2816
|
repoInstructions: instructions?.repoInstructions || null,
|
|
2769
2817
|
requestInstructions: instructions?.requestInstructions || null,
|
|
2770
2818
|
headSha,
|
|
2819
|
+
diff: diffSnapshot,
|
|
2771
2820
|
parentRunId,
|
|
2772
2821
|
configType: 'council',
|
|
2773
2822
|
levelsConfig: enabledLevels
|
|
@@ -465,6 +465,14 @@ class ClaudeCodeBridge extends EventEmitter {
|
|
|
465
465
|
toolName: name,
|
|
466
466
|
status: 'start',
|
|
467
467
|
});
|
|
468
|
+
} else if (event.content_block && event.content_block.type === 'text') {
|
|
469
|
+
// When a new text block starts and we already have accumulated text
|
|
470
|
+
// from a previous block, inject paragraph separation so the markdown
|
|
471
|
+
// renderer doesn't smash the blocks together (e.g., "diff.The").
|
|
472
|
+
if (this._accumulatedText) {
|
|
473
|
+
this._accumulatedText += '\n\n';
|
|
474
|
+
this.emit('delta', { text: '\n\n' });
|
|
475
|
+
}
|
|
468
476
|
}
|
|
469
477
|
break;
|
|
470
478
|
|
package/src/database.js
CHANGED
|
@@ -20,7 +20,7 @@ function getDbPath() {
|
|
|
20
20
|
/**
|
|
21
21
|
* Current schema version - increment this when adding new migrations
|
|
22
22
|
*/
|
|
23
|
-
const CURRENT_SCHEMA_VERSION =
|
|
23
|
+
const CURRENT_SCHEMA_VERSION = 27;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Database schema SQL statements
|
|
@@ -157,6 +157,7 @@ const SCHEMA_SQL = {
|
|
|
157
157
|
repo_instructions TEXT,
|
|
158
158
|
request_instructions TEXT,
|
|
159
159
|
head_sha TEXT,
|
|
160
|
+
diff TEXT,
|
|
160
161
|
summary TEXT,
|
|
161
162
|
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
|
162
163
|
total_suggestions INTEGER DEFAULT 0,
|
|
@@ -1206,6 +1207,29 @@ const MIGRATIONS = {
|
|
|
1206
1207
|
}
|
|
1207
1208
|
|
|
1208
1209
|
console.log('Migration to schema version 26 complete');
|
|
1210
|
+
},
|
|
1211
|
+
|
|
1212
|
+
// Migration to version 27: adds diff column to analysis_runs for snapshot preservation
|
|
1213
|
+
27: (db) => {
|
|
1214
|
+
console.log('Migrating to schema version 27: Add diff column to analysis_runs');
|
|
1215
|
+
|
|
1216
|
+
const hasDiff = columnExists(db, 'analysis_runs', 'diff');
|
|
1217
|
+
if (!hasDiff) {
|
|
1218
|
+
try {
|
|
1219
|
+
db.prepare('ALTER TABLE analysis_runs ADD COLUMN diff TEXT').run();
|
|
1220
|
+
console.log(' Added diff column to analysis_runs');
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
// Ignore duplicate column errors (race condition protection)
|
|
1223
|
+
if (!error.message.includes('duplicate column name')) {
|
|
1224
|
+
throw error;
|
|
1225
|
+
}
|
|
1226
|
+
console.log(' Column diff already exists (race condition)');
|
|
1227
|
+
}
|
|
1228
|
+
} else {
|
|
1229
|
+
console.log(' Column diff already exists');
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
console.log('Migration to schema version 27 complete');
|
|
1209
1233
|
}
|
|
1210
1234
|
};
|
|
1211
1235
|
|
|
@@ -2899,17 +2923,18 @@ class AnalysisRunRepository {
|
|
|
2899
2923
|
* @param {string} [runInfo.repoInstructions] - Repository-level instructions from repo_settings
|
|
2900
2924
|
* @param {string} [runInfo.requestInstructions] - Request-level instructions from the analyze request
|
|
2901
2925
|
* @param {string} [runInfo.headSha] - Git HEAD SHA at the time of analysis (PR head commit or local HEAD)
|
|
2926
|
+
* @param {string} [runInfo.diff] - Unified diff snapshot at the time of analysis
|
|
2902
2927
|
* @param {string} [runInfo.status='running'] - Initial status (default 'running'; pass 'completed' for externally-produced results)
|
|
2903
2928
|
* @returns {Promise<Object>} Created analysis run record
|
|
2904
2929
|
*/
|
|
2905
|
-
async create({ id, reviewId, provider = null, model = null, tier = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null }) {
|
|
2930
|
+
async create({ id, reviewId, provider = null, model = null, tier = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, diff = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null }) {
|
|
2906
2931
|
const isTerminal = ['completed', 'failed', 'cancelled'].includes(status);
|
|
2907
2932
|
const completedAt = isTerminal ? 'CURRENT_TIMESTAMP' : 'NULL';
|
|
2908
2933
|
const levelsConfigJson = levelsConfig ? JSON.stringify(levelsConfig) : null;
|
|
2909
2934
|
await run(this.db, `
|
|
2910
|
-
INSERT INTO analysis_runs (id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions, head_sha, status, completed_at, parent_run_id, config_type, levels_config)
|
|
2911
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?)
|
|
2912
|
-
`, [id, reviewId, provider, model, tier, customInstructions, repoInstructions, requestInstructions, headSha, status, parentRunId, configType, levelsConfigJson]);
|
|
2935
|
+
INSERT INTO analysis_runs (id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions, head_sha, diff, status, completed_at, parent_run_id, config_type, levels_config)
|
|
2936
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?)
|
|
2937
|
+
`, [id, reviewId, provider, model, tier, customInstructions, repoInstructions, requestInstructions, headSha, diff, status, parentRunId, configType, levelsConfigJson]);
|
|
2913
2938
|
|
|
2914
2939
|
// Query back the inserted row to return actual database values (including timestamps)
|
|
2915
2940
|
return await this.getById(id);
|
|
@@ -2923,6 +2948,7 @@ class AnalysisRunRepository {
|
|
|
2923
2948
|
* @param {string} [updates.summary] - Analysis summary
|
|
2924
2949
|
* @param {number} [updates.totalSuggestions] - Total suggestions count
|
|
2925
2950
|
* @param {number} [updates.filesAnalyzed] - Files analyzed count
|
|
2951
|
+
* @param {string} [updates.diff] - Unified diff snapshot to store
|
|
2926
2952
|
* @param {Object} [options] - Update options
|
|
2927
2953
|
* @param {string} [options.skipIfStatus] - Skip the update if the record already has this status (prevents redundant writes)
|
|
2928
2954
|
* @returns {Promise<boolean>} True if record was updated
|
|
@@ -2956,6 +2982,11 @@ class AnalysisRunRepository {
|
|
|
2956
2982
|
params.push(updates.filesAnalyzed);
|
|
2957
2983
|
}
|
|
2958
2984
|
|
|
2985
|
+
if (updates.diff !== undefined) {
|
|
2986
|
+
setClauses.push('diff = ?');
|
|
2987
|
+
params.push(updates.diff);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2959
2990
|
if (setClauses.length === 0) {
|
|
2960
2991
|
return false;
|
|
2961
2992
|
}
|
|
@@ -2980,13 +3011,21 @@ class AnalysisRunRepository {
|
|
|
2980
3011
|
/**
|
|
2981
3012
|
* Get an analysis run by ID
|
|
2982
3013
|
* @param {string} id - Analysis run ID
|
|
3014
|
+
* @param {Object} [options] - Optional query options
|
|
3015
|
+
* @param {boolean} [options.includeDiff=false] - Include the diff column (can be large)
|
|
2983
3016
|
* @returns {Promise<Object|null>} Analysis run record or null
|
|
2984
3017
|
*/
|
|
2985
|
-
async getById(id) {
|
|
3018
|
+
async getById(id, { includeDiff = false } = {}) {
|
|
3019
|
+
const columns = [
|
|
3020
|
+
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'repo_instructions', 'request_instructions',
|
|
3021
|
+
'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
|
|
3022
|
+
'parent_run_id', 'config_type', 'levels_config'
|
|
3023
|
+
];
|
|
3024
|
+
if (includeDiff) {
|
|
3025
|
+
columns.splice(9, 0, 'diff'); // Insert diff after head_sha
|
|
3026
|
+
}
|
|
2986
3027
|
const row = await queryOne(this.db, `
|
|
2987
|
-
SELECT
|
|
2988
|
-
head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
|
|
2989
|
-
parent_run_id, config_type, levels_config
|
|
3028
|
+
SELECT ${columns.join(', ')}
|
|
2990
3029
|
FROM analysis_runs
|
|
2991
3030
|
WHERE id = ?
|
|
2992
3031
|
`, [id]);
|
|
@@ -3000,14 +3039,21 @@ class AnalysisRunRepository {
|
|
|
3000
3039
|
* @param {number} reviewId - Review ID (works for both PR and local modes)
|
|
3001
3040
|
* @param {Object} [options] - Optional query options
|
|
3002
3041
|
* @param {number} [options.limit] - Maximum number of runs to return
|
|
3042
|
+
* @param {boolean} [options.includeDiff=false] - Include the diff column (can be large)
|
|
3003
3043
|
* @returns {Promise<Array<Object>>} Array of analysis run records
|
|
3004
3044
|
*/
|
|
3005
|
-
async getByReviewId(reviewId, { limit } = {}) {
|
|
3045
|
+
async getByReviewId(reviewId, { limit, includeDiff = false } = {}) {
|
|
3006
3046
|
const params = [reviewId];
|
|
3047
|
+
const columns = [
|
|
3048
|
+
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'repo_instructions', 'request_instructions',
|
|
3049
|
+
'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
|
|
3050
|
+
'parent_run_id', 'config_type', 'levels_config'
|
|
3051
|
+
];
|
|
3052
|
+
if (includeDiff) {
|
|
3053
|
+
columns.splice(9, 0, 'diff'); // Insert diff after head_sha
|
|
3054
|
+
}
|
|
3007
3055
|
let sql = `
|
|
3008
|
-
SELECT
|
|
3009
|
-
head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
|
|
3010
|
-
parent_run_id, config_type, levels_config
|
|
3056
|
+
SELECT ${columns.join(', ')}
|
|
3011
3057
|
FROM analysis_runs
|
|
3012
3058
|
WHERE review_id = ?
|
|
3013
3059
|
ORDER BY COALESCE(completed_at, started_at) DESC, CASE WHEN parent_run_id IS NULL THEN 0 ELSE 1 END, started_at DESC, id DESC`;
|
|
@@ -3028,12 +3074,41 @@ class AnalysisRunRepository {
|
|
|
3028
3074
|
return rows.length > 0 ? rows[0] : null;
|
|
3029
3075
|
}
|
|
3030
3076
|
|
|
3077
|
+
/**
|
|
3078
|
+
* Get the most recently completed analysis run for a review
|
|
3079
|
+
* @param {number} reviewId - Review ID (works for both PR and local modes)
|
|
3080
|
+
* @param {Object} [options] - Optional query options
|
|
3081
|
+
* @param {boolean} [options.includeDiff=false] - Include the diff column (can be large)
|
|
3082
|
+
* @returns {Promise<Object|null>} Most recently completed analysis run or null
|
|
3083
|
+
*/
|
|
3084
|
+
async getLatestCompletedByReviewId(reviewId, { includeDiff = false } = {}) {
|
|
3085
|
+
const columns = [
|
|
3086
|
+
'id', 'review_id', 'provider', 'model', 'tier', 'custom_instructions', 'repo_instructions', 'request_instructions',
|
|
3087
|
+
'head_sha', 'summary', 'status', 'total_suggestions', 'files_analyzed', 'started_at', 'completed_at',
|
|
3088
|
+
'parent_run_id', 'config_type', 'levels_config'
|
|
3089
|
+
];
|
|
3090
|
+
if (includeDiff) {
|
|
3091
|
+
columns.splice(9, 0, 'diff'); // Insert diff after head_sha
|
|
3092
|
+
}
|
|
3093
|
+
const row = await queryOne(this.db, `
|
|
3094
|
+
SELECT ${columns.join(', ')}
|
|
3095
|
+
FROM analysis_runs
|
|
3096
|
+
WHERE review_id = ? AND status = 'completed'
|
|
3097
|
+
ORDER BY completed_at DESC
|
|
3098
|
+
LIMIT 1
|
|
3099
|
+
`, [reviewId]);
|
|
3100
|
+
|
|
3101
|
+
return row || null;
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3031
3104
|
/**
|
|
3032
3105
|
* Get child runs for a parent council run, ordered by start time ascending
|
|
3033
3106
|
* @param {string} parentRunId - Parent analysis run ID
|
|
3034
3107
|
* @returns {Promise<Array<Object>>} Array of child analysis run records
|
|
3035
3108
|
*/
|
|
3036
3109
|
async getChildRuns(parentRunId) {
|
|
3110
|
+
// Note: diff column is intentionally omitted - child runs share the same diff as parent
|
|
3111
|
+
// to avoid data duplication. Use the parent run's diff when needed.
|
|
3037
3112
|
return query(this.db, `
|
|
3038
3113
|
SELECT id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions,
|
|
3039
3114
|
head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
|
package/src/routes/pr.js
CHANGED
|
@@ -1900,7 +1900,8 @@ router.get('/api/pr/:owner/:repo/:number/share', async (req, res) => {
|
|
|
1900
1900
|
|
|
1901
1901
|
if (requestedRunId) {
|
|
1902
1902
|
// Specific run requested - fetch it directly
|
|
1903
|
-
|
|
1903
|
+
// Include diff since the share endpoint needs it for the snapshot
|
|
1904
|
+
targetRun = await analysisRunRepo.getById(requestedRunId, { includeDiff: true });
|
|
1904
1905
|
// Verify it belongs to this review and is completed
|
|
1905
1906
|
if (!targetRun || targetRun.review_id !== review.id || targetRun.status !== 'completed') {
|
|
1906
1907
|
targetRun = null;
|
|
@@ -1909,8 +1910,8 @@ router.get('/api/pr/:owner/:repo/:number/share', async (req, res) => {
|
|
|
1909
1910
|
|
|
1910
1911
|
// If no specific run requested or it wasn't found/valid, fall back to the most recently completed run
|
|
1911
1912
|
if (!targetRun) {
|
|
1912
|
-
|
|
1913
|
-
targetRun =
|
|
1913
|
+
// Include diff since the share endpoint needs it for the snapshot
|
|
1914
|
+
targetRun = await analysisRunRepo.getLatestCompletedByReviewId(review.id, { includeDiff: true });
|
|
1914
1915
|
}
|
|
1915
1916
|
|
|
1916
1917
|
if (targetRun) {
|
|
@@ -1927,6 +1928,15 @@ router.get('/api/pr/:owner/:repo/:number/share', async (req, res) => {
|
|
|
1927
1928
|
customInstructions: targetRun.custom_instructions || targetRun.request_instructions || null
|
|
1928
1929
|
};
|
|
1929
1930
|
|
|
1931
|
+
// Use the run's snapshot of headSha and diff if available (for consistency with suggestions)
|
|
1932
|
+
// Falls back to current PR data for old runs that predate snapshot capture
|
|
1933
|
+
if (targetRun.head_sha) {
|
|
1934
|
+
payload.headSha = targetRun.head_sha;
|
|
1935
|
+
}
|
|
1936
|
+
if (targetRun.diff) {
|
|
1937
|
+
payload.diff = targetRun.diff;
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1930
1940
|
// Get suggestions for this run
|
|
1931
1941
|
const rows = await query(db, `
|
|
1932
1942
|
SELECT
|