@in-the-loop-labs/pair-review 2.6.3 → 2.7.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.
- 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 +194 -0
- package/public/index.html +168 -3
- package/public/js/components/AIPanel.js +16 -2
- package/public/js/components/ChatPanel.js +41 -6
- package/public/js/components/ConfirmDialog.js +21 -2
- package/public/js/components/CouncilProgressModal.js +13 -0
- package/public/js/components/DiffOptionsDropdown.js +410 -23
- package/public/js/components/SuggestionNavigator.js +12 -5
- package/public/js/components/TabTitle.js +96 -0
- package/public/js/components/Toast.js +6 -0
- package/public/js/index.js +648 -43
- package/public/js/local.js +569 -76
- package/public/js/modules/analysis-history.js +3 -2
- package/public/js/modules/comment-manager.js +5 -0
- package/public/js/modules/comment-minimizer.js +304 -0
- package/public/js/pr.js +82 -6
- package/public/local.html +14 -0
- package/public/pr.html +3 -0
- package/src/ai/analyzer.js +17 -11
- package/src/config.js +2 -0
- package/src/database.js +590 -39
- package/src/git/base-branch.js +173 -0
- package/src/git/sha-abbrev.js +35 -0
- package/src/github/client.js +32 -1
- package/src/hooks/hook-runner.js +100 -0
- package/src/hooks/payloads.js +212 -0
- package/src/local-review.js +468 -129
- package/src/local-scope.js +58 -0
- package/src/main.js +55 -4
- package/src/routes/analyses.js +73 -10
- package/src/routes/chat.js +33 -0
- package/src/routes/config.js +1 -0
- package/src/routes/local.js +734 -68
- package/src/routes/mcp.js +20 -10
- package/src/routes/pr.js +90 -12
- package/src/routes/setup.js +1 -0
- package/src/routes/worktrees.js +212 -148
- package/src/server.js +30 -0
- package/src/setup/local-setup.js +46 -5
- package/src/setup/pr-setup.js +28 -5
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 = 33;
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Database schema SQL statements
|
|
@@ -42,7 +42,12 @@ const SCHEMA_SQL = {
|
|
|
42
42
|
local_path TEXT,
|
|
43
43
|
local_head_sha TEXT,
|
|
44
44
|
summary TEXT,
|
|
45
|
-
name TEXT
|
|
45
|
+
name TEXT,
|
|
46
|
+
local_mode TEXT DEFAULT 'uncommitted',
|
|
47
|
+
local_base_branch TEXT,
|
|
48
|
+
local_head_branch TEXT,
|
|
49
|
+
local_scope_start TEXT DEFAULT 'unstaged',
|
|
50
|
+
local_scope_end TEXT DEFAULT 'untracked'
|
|
46
51
|
)
|
|
47
52
|
`,
|
|
48
53
|
|
|
@@ -94,6 +99,7 @@ const SCHEMA_SQL = {
|
|
|
94
99
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
95
100
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
96
101
|
|
|
102
|
+
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE,
|
|
97
103
|
FOREIGN KEY (adopted_as_id) REFERENCES comments(id),
|
|
98
104
|
FOREIGN KEY (parent_id) REFERENCES comments(id)
|
|
99
105
|
)
|
|
@@ -113,6 +119,7 @@ const SCHEMA_SQL = {
|
|
|
113
119
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
114
120
|
pr_data TEXT,
|
|
115
121
|
last_ai_run_id TEXT,
|
|
122
|
+
last_accessed_at TEXT,
|
|
116
123
|
UNIQUE(pr_number, repository)
|
|
117
124
|
)
|
|
118
125
|
`,
|
|
@@ -141,6 +148,7 @@ const SCHEMA_SQL = {
|
|
|
141
148
|
default_tab TEXT,
|
|
142
149
|
default_chat_instructions TEXT,
|
|
143
150
|
local_path TEXT,
|
|
151
|
+
auto_branch_review INTEGER DEFAULT 0,
|
|
144
152
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
145
153
|
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
146
154
|
)
|
|
@@ -167,6 +175,8 @@ const SCHEMA_SQL = {
|
|
|
167
175
|
parent_run_id TEXT,
|
|
168
176
|
config_type TEXT DEFAULT 'single',
|
|
169
177
|
levels_config TEXT,
|
|
178
|
+
scope_start TEXT,
|
|
179
|
+
scope_end TEXT,
|
|
170
180
|
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
|
|
171
181
|
)
|
|
172
182
|
`,
|
|
@@ -220,8 +230,8 @@ const SCHEMA_SQL = {
|
|
|
220
230
|
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'closed', 'error')),
|
|
221
231
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
222
232
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
223
|
-
FOREIGN KEY (review_id) REFERENCES reviews(id),
|
|
224
|
-
FOREIGN KEY (context_comment_id) REFERENCES comments(id)
|
|
233
|
+
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE,
|
|
234
|
+
FOREIGN KEY (context_comment_id) REFERENCES comments(id) ON DELETE SET NULL
|
|
225
235
|
)
|
|
226
236
|
`,
|
|
227
237
|
|
|
@@ -277,10 +287,11 @@ const INDEX_SQL = [
|
|
|
277
287
|
'CREATE INDEX IF NOT EXISTS idx_comments_status ON comments(status)',
|
|
278
288
|
'CREATE INDEX IF NOT EXISTS idx_comments_file_level ON comments(review_id, file, is_file_level)',
|
|
279
289
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_pr_metadata_unique ON pr_metadata(pr_number, repository)',
|
|
290
|
+
'CREATE INDEX IF NOT EXISTS idx_pr_metadata_last_accessed ON pr_metadata(last_accessed_at)',
|
|
280
291
|
'CREATE INDEX IF NOT EXISTS idx_worktrees_last_accessed ON worktrees(last_accessed_at)',
|
|
281
292
|
'CREATE INDEX IF NOT EXISTS idx_worktrees_repo ON worktrees(repository)',
|
|
282
293
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_repo_settings_repository ON repo_settings(repository)',
|
|
283
|
-
'CREATE UNIQUE INDEX IF NOT EXISTS idx_reviews_local ON reviews(local_path, local_head_sha) WHERE review_type = \'local\'',
|
|
294
|
+
'CREATE UNIQUE INDEX IF NOT EXISTS idx_reviews_local ON reviews(local_path, local_head_sha, local_head_branch) WHERE review_type = \'local\'',
|
|
284
295
|
// Partial unique index for PR reviews only (NULL pr_number values for local reviews should not conflict)
|
|
285
296
|
'CREATE UNIQUE INDEX IF NOT EXISTS idx_reviews_pr_unique ON reviews(pr_number, repository) WHERE review_type = \'pr\'',
|
|
286
297
|
// Analysis runs indexes
|
|
@@ -1230,6 +1241,329 @@ const MIGRATIONS = {
|
|
|
1230
1241
|
}
|
|
1231
1242
|
|
|
1232
1243
|
console.log('Migration to schema version 27 complete');
|
|
1244
|
+
},
|
|
1245
|
+
|
|
1246
|
+
28: (db) => {
|
|
1247
|
+
console.log('Migrating to schema version 28: Add branch review columns');
|
|
1248
|
+
|
|
1249
|
+
// Add local_mode to reviews
|
|
1250
|
+
if (!columnExists(db, 'reviews', 'local_mode')) {
|
|
1251
|
+
try {
|
|
1252
|
+
db.prepare("ALTER TABLE reviews ADD COLUMN local_mode TEXT DEFAULT 'uncommitted'").run();
|
|
1253
|
+
console.log(' Added local_mode column to reviews');
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1256
|
+
console.log(' Column local_mode already exists (race condition)');
|
|
1257
|
+
}
|
|
1258
|
+
} else {
|
|
1259
|
+
console.log(' Column local_mode already exists');
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Add local_base_branch to reviews
|
|
1263
|
+
if (!columnExists(db, 'reviews', 'local_base_branch')) {
|
|
1264
|
+
try {
|
|
1265
|
+
db.prepare('ALTER TABLE reviews ADD COLUMN local_base_branch TEXT').run();
|
|
1266
|
+
console.log(' Added local_base_branch column to reviews');
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1269
|
+
console.log(' Column local_base_branch already exists (race condition)');
|
|
1270
|
+
}
|
|
1271
|
+
} else {
|
|
1272
|
+
console.log(' Column local_base_branch already exists');
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Add auto_branch_review to repo_settings
|
|
1276
|
+
if (!columnExists(db, 'repo_settings', 'auto_branch_review')) {
|
|
1277
|
+
try {
|
|
1278
|
+
db.prepare('ALTER TABLE repo_settings ADD COLUMN auto_branch_review INTEGER DEFAULT 0').run();
|
|
1279
|
+
console.log(' Added auto_branch_review column to repo_settings');
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1282
|
+
console.log(' Column auto_branch_review already exists (race condition)');
|
|
1283
|
+
}
|
|
1284
|
+
} else {
|
|
1285
|
+
console.log(' Column auto_branch_review already exists');
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
console.log('Migration to schema version 28 complete');
|
|
1289
|
+
},
|
|
1290
|
+
|
|
1291
|
+
// Migration to version 29: adds scope columns for flexible diff range selection
|
|
1292
|
+
29: (db) => {
|
|
1293
|
+
console.log('Migrating to schema version 29: Add scope columns to reviews and analysis_runs');
|
|
1294
|
+
|
|
1295
|
+
// Add local_scope_start to reviews
|
|
1296
|
+
if (!columnExists(db, 'reviews', 'local_scope_start')) {
|
|
1297
|
+
try {
|
|
1298
|
+
db.prepare("ALTER TABLE reviews ADD COLUMN local_scope_start TEXT DEFAULT 'unstaged'").run();
|
|
1299
|
+
console.log(' Added local_scope_start column to reviews');
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1302
|
+
console.log(' Column local_scope_start already exists (race condition)');
|
|
1303
|
+
}
|
|
1304
|
+
} else {
|
|
1305
|
+
console.log(' Column local_scope_start already exists');
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Add local_scope_end to reviews
|
|
1309
|
+
if (!columnExists(db, 'reviews', 'local_scope_end')) {
|
|
1310
|
+
try {
|
|
1311
|
+
db.prepare("ALTER TABLE reviews ADD COLUMN local_scope_end TEXT DEFAULT 'untracked'").run();
|
|
1312
|
+
console.log(' Added local_scope_end column to reviews');
|
|
1313
|
+
} catch (error) {
|
|
1314
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1315
|
+
console.log(' Column local_scope_end already exists (race condition)');
|
|
1316
|
+
}
|
|
1317
|
+
} else {
|
|
1318
|
+
console.log(' Column local_scope_end already exists');
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// Migrate existing data from local_mode to new scope columns
|
|
1322
|
+
// uncommitted → start='unstaged', end='untracked'
|
|
1323
|
+
db.prepare(`
|
|
1324
|
+
UPDATE reviews SET local_scope_start = 'unstaged', local_scope_end = 'untracked'
|
|
1325
|
+
WHERE local_mode = 'uncommitted' OR local_mode IS NULL
|
|
1326
|
+
`).run();
|
|
1327
|
+
console.log(' Migrated uncommitted reviews to scope columns');
|
|
1328
|
+
|
|
1329
|
+
// branch → start='branch', end='branch'
|
|
1330
|
+
db.prepare(`
|
|
1331
|
+
UPDATE reviews SET local_scope_start = 'branch', local_scope_end = 'branch'
|
|
1332
|
+
WHERE local_mode = 'branch'
|
|
1333
|
+
`).run();
|
|
1334
|
+
console.log(' Migrated branch reviews to scope columns');
|
|
1335
|
+
|
|
1336
|
+
// Add scope_start to analysis_runs
|
|
1337
|
+
if (!columnExists(db, 'analysis_runs', 'scope_start')) {
|
|
1338
|
+
try {
|
|
1339
|
+
db.prepare('ALTER TABLE analysis_runs ADD COLUMN scope_start TEXT').run();
|
|
1340
|
+
console.log(' Added scope_start column to analysis_runs');
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1343
|
+
console.log(' Column scope_start already exists (race condition)');
|
|
1344
|
+
}
|
|
1345
|
+
} else {
|
|
1346
|
+
console.log(' Column scope_start already exists');
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Add scope_end to analysis_runs
|
|
1350
|
+
if (!columnExists(db, 'analysis_runs', 'scope_end')) {
|
|
1351
|
+
try {
|
|
1352
|
+
db.prepare('ALTER TABLE analysis_runs ADD COLUMN scope_end TEXT').run();
|
|
1353
|
+
console.log(' Added scope_end column to analysis_runs');
|
|
1354
|
+
} catch (error) {
|
|
1355
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1356
|
+
console.log(' Column scope_end already exists (race condition)');
|
|
1357
|
+
}
|
|
1358
|
+
} else {
|
|
1359
|
+
console.log(' Column scope_end already exists');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
console.log('Migration to schema version 29 complete');
|
|
1363
|
+
},
|
|
1364
|
+
|
|
1365
|
+
// Migration to version 30: adds head branch tracking for branch-aware session identity
|
|
1366
|
+
30: (db) => {
|
|
1367
|
+
console.log('Migrating to schema version 30: Add local_head_branch to reviews');
|
|
1368
|
+
|
|
1369
|
+
if (!columnExists(db, 'reviews', 'local_head_branch')) {
|
|
1370
|
+
try {
|
|
1371
|
+
db.prepare('ALTER TABLE reviews ADD COLUMN local_head_branch TEXT').run();
|
|
1372
|
+
console.log(' Added local_head_branch column to reviews');
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (!error.message.includes('duplicate column name')) throw error;
|
|
1375
|
+
console.log(' Column local_head_branch already exists (race condition)');
|
|
1376
|
+
}
|
|
1377
|
+
} else {
|
|
1378
|
+
console.log(' Column local_head_branch already exists');
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Recreate unique index to include local_head_branch in session identity
|
|
1382
|
+
db.prepare('DROP INDEX IF EXISTS idx_reviews_local').run();
|
|
1383
|
+
db.prepare("CREATE UNIQUE INDEX IF NOT EXISTS idx_reviews_local ON reviews(local_path, local_head_sha, local_head_branch) WHERE review_type = 'local'").run();
|
|
1384
|
+
console.log(' Recreated idx_reviews_local with local_head_branch');
|
|
1385
|
+
|
|
1386
|
+
console.log('Migration to schema version 30 complete');
|
|
1387
|
+
},
|
|
1388
|
+
|
|
1389
|
+
// Migration to version 31: Add last_accessed_at to pr_metadata and backfill from worktrees
|
|
1390
|
+
31: (db) => {
|
|
1391
|
+
console.log('Migrating to schema version 31: Add last_accessed_at to pr_metadata');
|
|
1392
|
+
|
|
1393
|
+
const hasCol = columnExists(db, 'pr_metadata', 'last_accessed_at');
|
|
1394
|
+
if (!hasCol) {
|
|
1395
|
+
try {
|
|
1396
|
+
db.prepare('ALTER TABLE pr_metadata ADD COLUMN last_accessed_at TEXT').run();
|
|
1397
|
+
console.log(' Added last_accessed_at column to pr_metadata');
|
|
1398
|
+
} catch (error) {
|
|
1399
|
+
if (!error.message.includes('duplicate column name')) {
|
|
1400
|
+
throw error;
|
|
1401
|
+
}
|
|
1402
|
+
console.log(' Column last_accessed_at already exists (race condition)');
|
|
1403
|
+
}
|
|
1404
|
+
} else {
|
|
1405
|
+
console.log(' Column last_accessed_at already exists');
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Backfill from worktrees table (best available access timestamp)
|
|
1409
|
+
const backfilled = db.prepare(`
|
|
1410
|
+
UPDATE pr_metadata
|
|
1411
|
+
SET last_accessed_at = (
|
|
1412
|
+
SELECT MAX(w.last_accessed_at) FROM worktrees w
|
|
1413
|
+
WHERE w.pr_number = pr_metadata.pr_number
|
|
1414
|
+
AND w.repository = pr_metadata.repository COLLATE NOCASE
|
|
1415
|
+
)
|
|
1416
|
+
WHERE last_accessed_at IS NULL
|
|
1417
|
+
`).run();
|
|
1418
|
+
console.log(` Backfilled ${backfilled.changes} pr_metadata rows from worktrees`);
|
|
1419
|
+
|
|
1420
|
+
// For any remaining rows without a worktree, use updated_at as fallback
|
|
1421
|
+
const fallback = db.prepare(`
|
|
1422
|
+
UPDATE pr_metadata
|
|
1423
|
+
SET last_accessed_at = updated_at
|
|
1424
|
+
WHERE last_accessed_at IS NULL
|
|
1425
|
+
`).run();
|
|
1426
|
+
if (fallback.changes > 0) {
|
|
1427
|
+
console.log(` Used updated_at fallback for ${fallback.changes} pr_metadata rows`);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
console.log('Migration to schema version 31 complete');
|
|
1431
|
+
},
|
|
1432
|
+
|
|
1433
|
+
// Migration to version 32: Add ON DELETE CASCADE/SET NULL to chat_sessions FKs
|
|
1434
|
+
// SQLite doesn't support ALTER CONSTRAINT, so we recreate the table.
|
|
1435
|
+
32: (db) => {
|
|
1436
|
+
console.log('Migrating to schema version 32: Add cascade deletes to chat_sessions FKs');
|
|
1437
|
+
|
|
1438
|
+
if (!tableExists(db, 'chat_sessions')) {
|
|
1439
|
+
console.log(' chat_sessions table does not exist, skipping');
|
|
1440
|
+
console.log('Migration to schema version 32 complete');
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS chat_sessions_new (
|
|
1445
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1446
|
+
review_id INTEGER NOT NULL,
|
|
1447
|
+
context_comment_id INTEGER,
|
|
1448
|
+
agent_session_id TEXT,
|
|
1449
|
+
provider TEXT NOT NULL,
|
|
1450
|
+
model TEXT,
|
|
1451
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'closed', 'error')),
|
|
1452
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1453
|
+
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
1454
|
+
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE,
|
|
1455
|
+
FOREIGN KEY (context_comment_id) REFERENCES comments(id) ON DELETE SET NULL
|
|
1456
|
+
)`).run();
|
|
1457
|
+
|
|
1458
|
+
db.prepare(`INSERT INTO chat_sessions_new
|
|
1459
|
+
SELECT * FROM chat_sessions`).run();
|
|
1460
|
+
|
|
1461
|
+
db.prepare('DROP TABLE chat_sessions').run();
|
|
1462
|
+
db.prepare('ALTER TABLE chat_sessions_new RENAME TO chat_sessions').run();
|
|
1463
|
+
|
|
1464
|
+
console.log(' Recreated chat_sessions with ON DELETE CASCADE/SET NULL');
|
|
1465
|
+
console.log('Migration to schema version 32 complete');
|
|
1466
|
+
},
|
|
1467
|
+
|
|
1468
|
+
// Migration to version 33: Add ON DELETE CASCADE to comments FK for review_id
|
|
1469
|
+
// SQLite doesn't support ALTER CONSTRAINT, so we recreate the table.
|
|
1470
|
+
33: (db) => {
|
|
1471
|
+
console.log('Migrating to schema version 33: Add cascade delete to comments.review_id FK');
|
|
1472
|
+
|
|
1473
|
+
if (!tableExists(db, 'comments')) {
|
|
1474
|
+
console.log(' comments table does not exist, skipping');
|
|
1475
|
+
console.log('Migration to schema version 33 complete');
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// Disable FK checks for the rebuild — old databases may have orphaned
|
|
1480
|
+
// comments (review_id pointing to deleted reviews) because FK enforcement
|
|
1481
|
+
// wasn't always active or CASCADE wasn't defined.
|
|
1482
|
+
db.pragma('foreign_keys = OFF');
|
|
1483
|
+
|
|
1484
|
+
// Clean up orphaned comments before rebuild
|
|
1485
|
+
if (tableExists(db, 'reviews')) {
|
|
1486
|
+
const orphaned = db.prepare(
|
|
1487
|
+
'DELETE FROM comments WHERE review_id IS NOT NULL AND review_id NOT IN (SELECT id FROM reviews)'
|
|
1488
|
+
).run();
|
|
1489
|
+
if (orphaned.changes > 0) {
|
|
1490
|
+
console.log(` Cleaned up ${orphaned.changes} orphaned comments`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
db.prepare(`CREATE TABLE IF NOT EXISTS comments_rebuild (
|
|
1495
|
+
id INTEGER PRIMARY KEY,
|
|
1496
|
+
review_id INTEGER,
|
|
1497
|
+
source TEXT,
|
|
1498
|
+
author TEXT,
|
|
1499
|
+
|
|
1500
|
+
ai_run_id TEXT,
|
|
1501
|
+
ai_level INTEGER,
|
|
1502
|
+
ai_confidence REAL,
|
|
1503
|
+
|
|
1504
|
+
file TEXT,
|
|
1505
|
+
line_start INTEGER,
|
|
1506
|
+
line_end INTEGER,
|
|
1507
|
+
diff_position INTEGER,
|
|
1508
|
+
side TEXT DEFAULT 'RIGHT' CHECK(side IN ('LEFT', 'RIGHT')),
|
|
1509
|
+
commit_sha TEXT,
|
|
1510
|
+
type TEXT,
|
|
1511
|
+
title TEXT,
|
|
1512
|
+
body TEXT,
|
|
1513
|
+
suggestion_text TEXT,
|
|
1514
|
+
reasoning TEXT,
|
|
1515
|
+
|
|
1516
|
+
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'dismissed', 'adopted', 'submitted', 'draft', 'inactive')),
|
|
1517
|
+
adopted_as_id INTEGER,
|
|
1518
|
+
parent_id INTEGER,
|
|
1519
|
+
is_file_level INTEGER DEFAULT 0,
|
|
1520
|
+
|
|
1521
|
+
voice_id TEXT,
|
|
1522
|
+
is_raw INTEGER DEFAULT 0,
|
|
1523
|
+
|
|
1524
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1525
|
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
1526
|
+
|
|
1527
|
+
FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE,
|
|
1528
|
+
FOREIGN KEY (adopted_as_id) REFERENCES comments(id),
|
|
1529
|
+
FOREIGN KEY (parent_id) REFERENCES comments(id)
|
|
1530
|
+
)`).run();
|
|
1531
|
+
|
|
1532
|
+
// Use explicit column names — SELECT * would break if column order differs
|
|
1533
|
+
// (e.g., columns added via ALTER TABLE ADD COLUMN are appended to the end)
|
|
1534
|
+
const cols = [
|
|
1535
|
+
'id', 'review_id', 'source', 'author',
|
|
1536
|
+
'ai_run_id', 'ai_level', 'ai_confidence',
|
|
1537
|
+
'file', 'line_start', 'line_end', 'diff_position',
|
|
1538
|
+
'side', 'commit_sha', 'type', 'title', 'body',
|
|
1539
|
+
'suggestion_text', 'reasoning',
|
|
1540
|
+
'status', 'adopted_as_id', 'parent_id', 'is_file_level',
|
|
1541
|
+
'voice_id', 'is_raw',
|
|
1542
|
+
'created_at', 'updated_at'
|
|
1543
|
+
].join(', ');
|
|
1544
|
+
// Wrap in transaction so a crash between DROP and RENAME can't strand
|
|
1545
|
+
// data in comments_rebuild. PRAGMA foreign_keys = OFF must stay outside
|
|
1546
|
+
// the transaction (SQLite requirement).
|
|
1547
|
+
const rebuild = db.transaction(() => {
|
|
1548
|
+
db.prepare(`INSERT INTO comments_rebuild (${cols}) SELECT ${cols} FROM comments`).run();
|
|
1549
|
+
db.prepare('DROP TABLE comments').run();
|
|
1550
|
+
db.prepare('ALTER TABLE comments_rebuild RENAME TO comments').run();
|
|
1551
|
+
});
|
|
1552
|
+
rebuild();
|
|
1553
|
+
|
|
1554
|
+
// Re-enable FK checks
|
|
1555
|
+
db.pragma('foreign_keys = ON');
|
|
1556
|
+
|
|
1557
|
+
// Recreate all indexes on the new table
|
|
1558
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_comments_review_file ON comments(review_id, file, line_start)').run();
|
|
1559
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_comments_ai_run ON comments(ai_run_id)').run();
|
|
1560
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_comments_status ON comments(status)').run();
|
|
1561
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_comments_file_level ON comments(review_id, file, is_file_level)').run();
|
|
1562
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_comments_voice ON comments(voice_id)').run();
|
|
1563
|
+
db.prepare('CREATE INDEX IF NOT EXISTS idx_comments_is_raw ON comments(is_raw)').run();
|
|
1564
|
+
|
|
1565
|
+
console.log(' Recreated comments with ON DELETE CASCADE for review_id');
|
|
1566
|
+
console.log('Migration to schema version 33 complete');
|
|
1233
1567
|
}
|
|
1234
1568
|
};
|
|
1235
1569
|
|
|
@@ -1729,7 +2063,7 @@ class RepoSettingsRepository {
|
|
|
1729
2063
|
*/
|
|
1730
2064
|
async getRepoSettings(repository) {
|
|
1731
2065
|
const row = await queryOne(this.db, `
|
|
1732
|
-
SELECT id, repository, default_instructions, default_provider, default_model, default_council_id, default_tab, default_chat_instructions, local_path, created_at, updated_at
|
|
2066
|
+
SELECT id, repository, default_instructions, default_provider, default_model, default_council_id, default_tab, default_chat_instructions, local_path, auto_branch_review, created_at, updated_at
|
|
1733
2067
|
FROM repo_settings
|
|
1734
2068
|
WHERE repository = ? COLLATE NOCASE
|
|
1735
2069
|
`, [repository]);
|
|
@@ -2420,6 +2754,11 @@ class ReviewRepository {
|
|
|
2420
2754
|
params.push(updates.name);
|
|
2421
2755
|
}
|
|
2422
2756
|
|
|
2757
|
+
if (updates.local_head_branch !== undefined) {
|
|
2758
|
+
setClauses.push('local_head_branch = ?');
|
|
2759
|
+
params.push(updates.local_head_branch);
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2423
2762
|
if (updates.submittedAt !== undefined) {
|
|
2424
2763
|
setClauses.push('submitted_at = ?');
|
|
2425
2764
|
const submittedAt = updates.submittedAt instanceof Date
|
|
@@ -2500,16 +2839,17 @@ class ReviewRepository {
|
|
|
2500
2839
|
* @param {string} reviewInfo.repository - Repository in owner/repo format
|
|
2501
2840
|
* @param {Object} [reviewInfo.reviewData] - Additional review data
|
|
2502
2841
|
* @param {string} [reviewInfo.customInstructions] - Custom instructions
|
|
2503
|
-
* @returns {Promise<Object>}
|
|
2842
|
+
* @returns {Promise<{review: Object, created: boolean}>} Tuple with review record and creation flag
|
|
2504
2843
|
*/
|
|
2505
2844
|
async getOrCreate({ prNumber, repository, reviewData = null, customInstructions = null }) {
|
|
2506
2845
|
const existing = await this.getReviewByPR(prNumber, repository);
|
|
2507
2846
|
|
|
2508
2847
|
if (existing) {
|
|
2509
|
-
return existing;
|
|
2848
|
+
return { review: existing, created: false };
|
|
2510
2849
|
}
|
|
2511
2850
|
|
|
2512
|
-
|
|
2851
|
+
const review = await this.createReview({ prNumber, repository, reviewData, customInstructions });
|
|
2852
|
+
return { review, created: true };
|
|
2513
2853
|
}
|
|
2514
2854
|
|
|
2515
2855
|
/**
|
|
@@ -2572,19 +2912,6 @@ class ReviewRepository {
|
|
|
2572
2912
|
}
|
|
2573
2913
|
}
|
|
2574
2914
|
|
|
2575
|
-
/**
|
|
2576
|
-
* Delete a review record by ID
|
|
2577
|
-
* @param {number} id - Review ID
|
|
2578
|
-
* @returns {Promise<boolean>} True if record was deleted
|
|
2579
|
-
*/
|
|
2580
|
-
async deleteReview(id) {
|
|
2581
|
-
const result = await run(this.db, `
|
|
2582
|
-
DELETE FROM reviews WHERE id = ?
|
|
2583
|
-
`, [id]);
|
|
2584
|
-
|
|
2585
|
-
return result.changes > 0;
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
2915
|
/**
|
|
2589
2916
|
* List reviews for a repository
|
|
2590
2917
|
* @param {string} repository - Repository in owner/repo format
|
|
@@ -2617,42 +2944,106 @@ class ReviewRepository {
|
|
|
2617
2944
|
* @param {string} context.repository - Repository identifier (can be derived from path)
|
|
2618
2945
|
* @returns {Promise<number>} The review ID
|
|
2619
2946
|
*/
|
|
2620
|
-
async upsertLocalReview({ localPath, localHeadSha, repository }) {
|
|
2621
|
-
// Try to find existing local review by path and
|
|
2622
|
-
const existing = await this.getLocalReview(localPath, localHeadSha);
|
|
2947
|
+
async upsertLocalReview({ localPath, localHeadSha, repository, scopeStart, scopeEnd, localMode, localBaseBranch, localHeadBranch }) {
|
|
2948
|
+
// Try to find existing local review by path, SHA, and branch
|
|
2949
|
+
const existing = await this.getLocalReview(localPath, localHeadSha, localHeadBranch);
|
|
2623
2950
|
|
|
2624
2951
|
if (existing) {
|
|
2625
|
-
// Update the updated_at timestamp
|
|
2952
|
+
// Update the updated_at timestamp (and scope/base if provided)
|
|
2953
|
+
const updates = ['updated_at = CURRENT_TIMESTAMP'];
|
|
2954
|
+
const params = [];
|
|
2955
|
+
if (scopeStart !== undefined) {
|
|
2956
|
+
updates.push('local_scope_start = ?');
|
|
2957
|
+
params.push(scopeStart);
|
|
2958
|
+
// Also write local_mode for backward compat during transition
|
|
2959
|
+
updates.push('local_mode = ?');
|
|
2960
|
+
params.push(scopeStart === 'branch' ? 'branch' : 'uncommitted');
|
|
2961
|
+
} else if (localMode !== undefined) {
|
|
2962
|
+
updates.push('local_mode = ?');
|
|
2963
|
+
params.push(localMode);
|
|
2964
|
+
}
|
|
2965
|
+
if (scopeEnd !== undefined) {
|
|
2966
|
+
updates.push('local_scope_end = ?');
|
|
2967
|
+
params.push(scopeEnd);
|
|
2968
|
+
}
|
|
2969
|
+
if (localBaseBranch !== undefined) {
|
|
2970
|
+
updates.push('local_base_branch = ?');
|
|
2971
|
+
params.push(localBaseBranch);
|
|
2972
|
+
}
|
|
2973
|
+
if (localHeadBranch !== undefined) {
|
|
2974
|
+
updates.push('local_head_branch = ?');
|
|
2975
|
+
params.push(localHeadBranch);
|
|
2976
|
+
}
|
|
2977
|
+
params.push(existing.id);
|
|
2626
2978
|
await run(this.db, `
|
|
2627
2979
|
UPDATE reviews
|
|
2628
|
-
SET
|
|
2980
|
+
SET ${updates.join(', ')}
|
|
2629
2981
|
WHERE id = ?
|
|
2630
|
-
`,
|
|
2982
|
+
`, params);
|
|
2631
2983
|
return existing.id;
|
|
2632
2984
|
}
|
|
2633
2985
|
|
|
2986
|
+
// Derive local_mode from scopeStart for backward compat
|
|
2987
|
+
const effectiveMode = scopeStart === 'branch' ? 'branch' : (localMode || 'uncommitted');
|
|
2988
|
+
const effectiveScopeStart = scopeStart || 'unstaged';
|
|
2989
|
+
const effectiveScopeEnd = scopeEnd || 'untracked';
|
|
2990
|
+
|
|
2634
2991
|
// Create new local review
|
|
2635
2992
|
const result = await run(this.db, `
|
|
2636
|
-
INSERT INTO reviews (pr_number, repository, status, review_type, local_path, local_head_sha)
|
|
2637
|
-
VALUES (NULL, ?, 'draft', 'local', ?, ?)
|
|
2638
|
-
`, [repository, localPath, localHeadSha]);
|
|
2993
|
+
INSERT INTO reviews (pr_number, repository, status, review_type, local_path, local_head_sha, local_mode, local_base_branch, local_head_branch, local_scope_start, local_scope_end)
|
|
2994
|
+
VALUES (NULL, ?, 'draft', 'local', ?, ?, ?, ?, ?, ?, ?)
|
|
2995
|
+
`, [repository, localPath, localHeadSha, effectiveMode, localBaseBranch || null, localHeadBranch || null, effectiveScopeStart, effectiveScopeEnd]);
|
|
2639
2996
|
|
|
2640
2997
|
return result.lastID;
|
|
2641
2998
|
}
|
|
2642
2999
|
|
|
2643
3000
|
/**
|
|
2644
|
-
* Get a local review by path
|
|
3001
|
+
* Get a local review by path, HEAD SHA, and branch.
|
|
2645
3002
|
* @param {string} localPath - Absolute path to the local repository
|
|
2646
3003
|
* @param {string} localHeadSha - Current HEAD SHA of the repository
|
|
3004
|
+
* @param {string} [headBranch] - Branch name; when falsy, matches only NULL-branch sessions
|
|
2647
3005
|
* @returns {Promise<Object|null>} Review record or null if not found
|
|
2648
3006
|
*/
|
|
2649
|
-
async getLocalReview(localPath, localHeadSha) {
|
|
3007
|
+
async getLocalReview(localPath, localHeadSha, headBranch) {
|
|
3008
|
+
const branchClause = headBranch
|
|
3009
|
+
? 'AND local_head_branch = ?'
|
|
3010
|
+
: 'AND local_head_branch IS NULL';
|
|
3011
|
+
const params = [localPath, localHeadSha];
|
|
3012
|
+
if (headBranch) params.push(headBranch);
|
|
2650
3013
|
const row = await queryOne(this.db, `
|
|
2651
3014
|
SELECT id, pr_number, repository, status, review_id,
|
|
2652
3015
|
created_at, updated_at, submitted_at, review_data, custom_instructions,
|
|
2653
|
-
review_type, local_path, local_head_sha, summary, name
|
|
3016
|
+
review_type, local_path, local_head_sha, summary, name,
|
|
3017
|
+
local_mode, local_base_branch, local_head_branch, local_scope_start, local_scope_end
|
|
3018
|
+
FROM reviews
|
|
3019
|
+
WHERE review_type = 'local' AND local_path = ? AND local_head_sha = ? ${branchClause}
|
|
3020
|
+
`, params);
|
|
3021
|
+
|
|
3022
|
+
if (!row) return null;
|
|
3023
|
+
|
|
3024
|
+
return {
|
|
3025
|
+
...row,
|
|
3026
|
+
review_data: row.review_data ? JSON.parse(row.review_data) : null
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
|
|
3030
|
+
/**
|
|
3031
|
+
* Get a local review by path and HEAD SHA only (ignoring branch).
|
|
3032
|
+
* Used by external callers (MCP, analysis results) that may not have branch context.
|
|
3033
|
+
* @param {string} localPath - Absolute path to the local repository
|
|
3034
|
+
* @param {string} localHeadSha - Current HEAD SHA of the repository
|
|
3035
|
+
* @returns {Promise<Object|null>} Review record or null if not found
|
|
3036
|
+
*/
|
|
3037
|
+
async getLocalReviewByPathAndSha(localPath, localHeadSha) {
|
|
3038
|
+
const row = await queryOne(this.db, `
|
|
3039
|
+
SELECT id, pr_number, repository, status, review_id,
|
|
3040
|
+
created_at, updated_at, submitted_at, review_data, custom_instructions,
|
|
3041
|
+
review_type, local_path, local_head_sha, summary, name,
|
|
3042
|
+
local_mode, local_base_branch, local_head_branch, local_scope_start, local_scope_end
|
|
2654
3043
|
FROM reviews
|
|
2655
3044
|
WHERE review_type = 'local' AND local_path = ? AND local_head_sha = ?
|
|
3045
|
+
ORDER BY updated_at DESC
|
|
3046
|
+
LIMIT 1
|
|
2656
3047
|
`, [localPath, localHeadSha]);
|
|
2657
3048
|
|
|
2658
3049
|
if (!row) return null;
|
|
@@ -2663,6 +3054,102 @@ class ReviewRepository {
|
|
|
2663
3054
|
};
|
|
2664
3055
|
}
|
|
2665
3056
|
|
|
3057
|
+
/**
|
|
3058
|
+
* Find a local review by path and SHA, trying branch-exact match first,
|
|
3059
|
+
* then falling back to branch-agnostic lookup.
|
|
3060
|
+
* @param {string} localPath - Absolute path to the local repository
|
|
3061
|
+
* @param {string} localHeadSha - Current HEAD SHA of the repository
|
|
3062
|
+
* @param {string} [headBranch] - Branch name for exact match
|
|
3063
|
+
* @returns {Promise<Object|null>} Review record or null if not found
|
|
3064
|
+
*/
|
|
3065
|
+
async findLocalReview(localPath, localHeadSha, headBranch) {
|
|
3066
|
+
const review = await this.getLocalReview(localPath, localHeadSha, headBranch);
|
|
3067
|
+
if (review) return review;
|
|
3068
|
+
// Only adopt sessions that predate branch tracking (NULL branch)
|
|
3069
|
+
const fallback = await this.getLocalReviewByPathAndSha(localPath, localHeadSha);
|
|
3070
|
+
if (fallback && fallback.local_head_branch === null) return fallback;
|
|
3071
|
+
return null;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
/**
|
|
3075
|
+
* Get an existing branch-mode local review by path (ignoring HEAD SHA).
|
|
3076
|
+
* Branch-mode sessions persist across HEAD changes — only the path matters.
|
|
3077
|
+
* @param {string} localPath - Absolute path to the local repository
|
|
3078
|
+
* @returns {Promise<Object|null>} Most recent branch-mode review or null
|
|
3079
|
+
*/
|
|
3080
|
+
async getLocalBranchReview(localPath, headBranch) {
|
|
3081
|
+
return this.getLocalBranchScopeReview(localPath, headBranch);
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
/**
|
|
3085
|
+
* Get an existing branch-scope local review by path and head branch.
|
|
3086
|
+
* Branch-scope sessions persist across HEAD changes but are scoped to a specific branch.
|
|
3087
|
+
* @param {string} localPath - Absolute path to the local repository
|
|
3088
|
+
* @param {string} headBranch - Current branch name
|
|
3089
|
+
* @returns {Promise<Object|null>} Most recent branch-scope review or null
|
|
3090
|
+
*/
|
|
3091
|
+
async getLocalBranchScopeReview(localPath, headBranch) {
|
|
3092
|
+
const row = await queryOne(this.db, `
|
|
3093
|
+
SELECT id, pr_number, repository, status, review_id,
|
|
3094
|
+
created_at, updated_at, submitted_at, review_data, custom_instructions,
|
|
3095
|
+
review_type, local_path, local_head_sha, summary, name,
|
|
3096
|
+
local_mode, local_base_branch, local_head_branch, local_scope_start, local_scope_end
|
|
3097
|
+
FROM reviews
|
|
3098
|
+
WHERE review_type = 'local' AND local_path = ? AND local_scope_start = 'branch' AND local_head_branch = ?
|
|
3099
|
+
ORDER BY updated_at DESC
|
|
3100
|
+
LIMIT 1
|
|
3101
|
+
`, [localPath, headBranch]);
|
|
3102
|
+
if (!row) return null;
|
|
3103
|
+
return { ...row, review_data: row.review_data ? JSON.parse(row.review_data) : null };
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
/**
|
|
3107
|
+
* Update the HEAD SHA for a local review.
|
|
3108
|
+
* Used when branch-mode sessions persist across commits — the unique index
|
|
3109
|
+
* on (local_path, local_head_sha) requires careful handling.
|
|
3110
|
+
* @param {number} id - Review ID
|
|
3111
|
+
* @param {string} newHeadSha - New HEAD SHA to set
|
|
3112
|
+
* @returns {Promise<boolean>} True if record was updated
|
|
3113
|
+
*/
|
|
3114
|
+
async updateLocalHeadSha(id, newHeadSha) {
|
|
3115
|
+
const result = await run(this.db, `
|
|
3116
|
+
UPDATE reviews
|
|
3117
|
+
SET local_head_sha = ?, updated_at = CURRENT_TIMESTAMP
|
|
3118
|
+
WHERE id = ?
|
|
3119
|
+
`, [newHeadSha, id]);
|
|
3120
|
+
return result.changes > 0;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
/**
|
|
3124
|
+
* Update the scope range and optionally the base branch for a local review.
|
|
3125
|
+
* @param {number} id - Review ID
|
|
3126
|
+
* @param {string} scopeStart - Scope start value (e.g. 'unstaged', 'staged', 'branch')
|
|
3127
|
+
* @param {string} scopeEnd - Scope end value (e.g. 'staged', 'untracked', 'branch')
|
|
3128
|
+
* @param {string} [baseBranch] - Base branch name (only relevant for branch scope)
|
|
3129
|
+
* @returns {Promise<boolean>} True if record was updated
|
|
3130
|
+
*/
|
|
3131
|
+
async updateLocalScope(id, scopeStart, scopeEnd, baseBranch, headBranch) {
|
|
3132
|
+
const updates = ['local_scope_start = ?', 'local_scope_end = ?', 'updated_at = CURRENT_TIMESTAMP'];
|
|
3133
|
+
const params = [scopeStart, scopeEnd];
|
|
3134
|
+
// Also write local_mode for backward compat
|
|
3135
|
+
updates.push('local_mode = ?');
|
|
3136
|
+
params.push(scopeStart === 'branch' ? 'branch' : 'uncommitted');
|
|
3137
|
+
if (baseBranch !== undefined) {
|
|
3138
|
+
updates.push('local_base_branch = ?');
|
|
3139
|
+
params.push(baseBranch);
|
|
3140
|
+
}
|
|
3141
|
+
// Store head branch when entering branch scope, clear when leaving
|
|
3142
|
+
updates.push('local_head_branch = ?');
|
|
3143
|
+
params.push(scopeStart === 'branch' ? (headBranch || null) : null);
|
|
3144
|
+
params.push(id);
|
|
3145
|
+
const result = await run(this.db, `
|
|
3146
|
+
UPDATE reviews
|
|
3147
|
+
SET ${updates.join(', ')}
|
|
3148
|
+
WHERE id = ?
|
|
3149
|
+
`, params);
|
|
3150
|
+
return result.changes > 0;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
2666
3153
|
/**
|
|
2667
3154
|
* Get a local review by its database ID
|
|
2668
3155
|
* @param {number} id - Review ID
|
|
@@ -2672,7 +3159,8 @@ class ReviewRepository {
|
|
|
2672
3159
|
const row = await queryOne(this.db, `
|
|
2673
3160
|
SELECT id, pr_number, repository, status, review_id,
|
|
2674
3161
|
created_at, updated_at, submitted_at, review_data, custom_instructions,
|
|
2675
|
-
review_type, local_path, local_head_sha, summary, name
|
|
3162
|
+
review_type, local_path, local_head_sha, summary, name,
|
|
3163
|
+
local_mode, local_base_branch, local_head_branch, local_scope_start, local_scope_end
|
|
2676
3164
|
FROM reviews
|
|
2677
3165
|
WHERE id = ? AND review_type = 'local'
|
|
2678
3166
|
`, [id]);
|
|
@@ -2798,6 +3286,69 @@ class ReviewRepository {
|
|
|
2798
3286
|
`, [reviewId]);
|
|
2799
3287
|
return result.changes > 0;
|
|
2800
3288
|
}
|
|
3289
|
+
|
|
3290
|
+
/**
|
|
3291
|
+
* Find reviews older than the given cutoff date (based on updated_at).
|
|
3292
|
+
* @param {string} cutoffDate - ISO 8601 date string
|
|
3293
|
+
* @returns {Promise<Array<{id: number, pr_number: number|null, repository: string, review_type: string}>>}
|
|
3294
|
+
*/
|
|
3295
|
+
async findStale(cutoffDate) {
|
|
3296
|
+
return query(this.db, `
|
|
3297
|
+
SELECT id, pr_number, repository, review_type
|
|
3298
|
+
FROM reviews
|
|
3299
|
+
WHERE updated_at < ?
|
|
3300
|
+
`, [cutoffDate]);
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
/**
|
|
3304
|
+
* Delete a review and all associated data in a single transaction.
|
|
3305
|
+
*
|
|
3306
|
+
* Cascade-deleted by FK constraints: analysis_runs, local_diffs, github_reviews,
|
|
3307
|
+
* chat_sessions (→ chat_messages), context_files, comments.
|
|
3308
|
+
*
|
|
3309
|
+
* Orphan-cleaned explicitly: pr_metadata, github_pr_cache (only when no other
|
|
3310
|
+
* reviews reference the same PR).
|
|
3311
|
+
*
|
|
3312
|
+
* @param {number} reviewId - Review ID to delete
|
|
3313
|
+
* @param {Object} [opts] - Options
|
|
3314
|
+
* @param {number|null} [opts.prNumber] - PR number (skips orphan cleanup if null)
|
|
3315
|
+
* @param {string|null} [opts.repository] - Repository in owner/repo format
|
|
3316
|
+
* @returns {Promise<boolean>} True if review was deleted
|
|
3317
|
+
*/
|
|
3318
|
+
async deleteWithRelatedData(reviewId, { prNumber = null, repository = null } = {}) {
|
|
3319
|
+
return withTransaction(this.db, async () => {
|
|
3320
|
+
// Delete the review row — FK cascades handle related tables
|
|
3321
|
+
const result = await run(this.db, 'DELETE FROM reviews WHERE id = ?', [reviewId]);
|
|
3322
|
+
|
|
3323
|
+
if (result.changes === 0) return false;
|
|
3324
|
+
|
|
3325
|
+
// Clean up orphaned pr_metadata and github_pr_cache if this was a PR review
|
|
3326
|
+
if (prNumber != null && repository) {
|
|
3327
|
+
const remaining = await queryOne(this.db, `
|
|
3328
|
+
SELECT COUNT(*) as cnt FROM reviews
|
|
3329
|
+
WHERE pr_number = ? AND repository = ? COLLATE NOCASE
|
|
3330
|
+
`, [prNumber, repository]);
|
|
3331
|
+
|
|
3332
|
+
if (remaining.cnt === 0) {
|
|
3333
|
+
await run(this.db, `
|
|
3334
|
+
DELETE FROM pr_metadata
|
|
3335
|
+
WHERE pr_number = ? AND repository = ? COLLATE NOCASE
|
|
3336
|
+
`, [prNumber, repository]);
|
|
3337
|
+
|
|
3338
|
+
// Parse owner/repo for github_pr_cache
|
|
3339
|
+
const parts = repository.split('/');
|
|
3340
|
+
if (parts.length === 2) {
|
|
3341
|
+
await run(this.db, `
|
|
3342
|
+
DELETE FROM github_pr_cache
|
|
3343
|
+
WHERE owner = ? AND repo = ? AND number = ?
|
|
3344
|
+
`, [parts[0], parts[1], prNumber]);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
return true;
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
2801
3352
|
}
|
|
2802
3353
|
|
|
2803
3354
|
/**
|
|
@@ -2927,14 +3478,14 @@ class AnalysisRunRepository {
|
|
|
2927
3478
|
* @param {string} [runInfo.status='running'] - Initial status (default 'running'; pass 'completed' for externally-produced results)
|
|
2928
3479
|
* @returns {Promise<Object>} Created analysis run record
|
|
2929
3480
|
*/
|
|
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 }) {
|
|
3481
|
+
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, scopeStart = null, scopeEnd = null }) {
|
|
2931
3482
|
const isTerminal = ['completed', 'failed', 'cancelled'].includes(status);
|
|
2932
3483
|
const completedAt = isTerminal ? 'CURRENT_TIMESTAMP' : 'NULL';
|
|
2933
3484
|
const levelsConfigJson = levelsConfig ? JSON.stringify(levelsConfig) : null;
|
|
2934
3485
|
await run(this.db, `
|
|
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]);
|
|
3486
|
+
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, scope_start, scope_end)
|
|
3487
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?, ?, ?)
|
|
3488
|
+
`, [id, reviewId, provider, model, tier, customInstructions, repoInstructions, requestInstructions, headSha, diff, status, parentRunId, configType, levelsConfigJson, scopeStart, scopeEnd]);
|
|
2938
3489
|
|
|
2939
3490
|
// Query back the inserted row to return actual database values (including timestamps)
|
|
2940
3491
|
return await this.getById(id);
|