@in-the-loop-labs/pair-review 3.5.2 → 3.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/README.md +4 -0
- 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/analysis-config.css +1807 -0
- package/public/css/pr.css +1029 -2169
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +39 -23
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/TourBar.js +248 -0
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +64 -8
- package/public/js/modules/cancel-background-job.js +183 -0
- package/public/js/modules/hunk-summary-renderer.js +116 -0
- package/public/js/modules/storage-cleanup.js +16 -0
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +755 -0
- package/public/js/pr.js +1826 -56
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/modal-detection.js +77 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +24 -0
- package/public/pr.html +24 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/background-queue.js +290 -0
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +50 -7
- package/src/ai/codex-provider.js +28 -5
- package/src/ai/copilot-provider.js +22 -3
- package/src/ai/cursor-agent-provider.js +22 -6
- package/src/ai/executable-provider.js +4 -19
- package/src/ai/gemini-provider.js +22 -5
- package/src/ai/hunk-hashing.js +161 -0
- package/src/ai/index.js +2 -0
- package/src/ai/opencode-provider.js +21 -5
- package/src/ai/pi-provider.js +21 -5
- package/src/ai/prompts/hunk-summary.js +199 -0
- package/src/ai/prompts/tour.js +232 -0
- package/src/ai/provider.js +21 -1
- package/src/ai/summary-generator.js +469 -0
- package/src/ai/tour-generator.js +568 -0
- package/src/config.js +778 -10
- package/src/database.js +282 -1
- package/src/external/github-adapter.js +114 -25
- package/src/git/base-branch.js +11 -4
- package/src/github/client.js +482 -588
- package/src/github/errors.js +55 -0
- package/src/github/impl/graphql/pending-review-comments.js +230 -0
- package/src/github/impl/graphql/pending-review.js +153 -0
- package/src/github/impl/graphql/review-lifecycle.js +161 -0
- package/src/github/impl/graphql/stack-walker.js +210 -0
- package/src/github/impl/host/pending-review-comments.js +338 -0
- package/src/github/impl/rest/pending-review.js +251 -0
- package/src/github/impl/rest/review-lifecycle.js +226 -0
- package/src/github/impl/rest/stack-walker.js +309 -0
- package/src/github/operations/pending-review-comments.js +79 -0
- package/src/github/operations/pending-review.js +89 -0
- package/src/github/operations/review-lifecycle.js +126 -0
- package/src/github/operations/stack-walker.js +87 -0
- package/src/github/parser.js +230 -4
- package/src/github/stack-walker.js +14 -189
- package/src/links/repo-links.js +230 -0
- package/src/local-review.js +201 -172
- package/src/main.js +133 -30
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +118 -3
- package/src/routes/context-files.js +2 -29
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +410 -13
- package/src/routes/mcp.js +47 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +556 -71
- package/src/routes/reviews.js +145 -29
- package/src/routes/setup.js +8 -3
- package/src/routes/stack-analysis.js +33 -9
- package/src/routes/worktrees.js +3 -2
- package/src/server.js +2 -0
- package/src/setup/pr-setup.js +37 -11
- package/src/setup/stack-setup.js +13 -3
- package/src/single-port.js +6 -3
- package/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
package/src/routes/reviews.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const express = require('express');
|
|
11
|
-
const { query, queryOne, run, withTransaction, CommentRepository, ReviewRepository, AnalysisRunRepository } = require('../database');
|
|
11
|
+
const { query, queryOne, run, withTransaction, CommentRepository, ReviewRepository, AnalysisRunRepository, HunkSummaryRepository, TourRepository } = require('../database');
|
|
12
12
|
const { calculateStats, getStatsQuery } = require('../utils/stats-calculator');
|
|
13
13
|
const { activeAnalyses, reviewToAnalysisId } = require('./shared');
|
|
14
14
|
const logger = require('../utils/logger');
|
|
15
15
|
const { broadcastReviewEvent } = require('../events/review-events');
|
|
16
|
+
const { backgroundQueue } = require('../ai/background-queue');
|
|
16
17
|
const { ensureContextFileForComment } = require('../utils/auto-context');
|
|
17
18
|
const path = require('path');
|
|
18
19
|
const fs = require('fs').promises;
|
|
@@ -22,37 +23,10 @@ const { normalizeRepository } = require('../utils/paths');
|
|
|
22
23
|
const { resolveFormat, formatAdoptedComment: formatComment } = require('../utils/comment-formatter');
|
|
23
24
|
const { safeParseJson } = require('../utils/safe-parse-json');
|
|
24
25
|
const { resolveOriginalFileContentSpecs } = require('../utils/diff-file-content');
|
|
26
|
+
const validateReviewId = require('./middleware/validate-review-id');
|
|
25
27
|
|
|
26
28
|
const router = express.Router();
|
|
27
29
|
|
|
28
|
-
/**
|
|
29
|
-
* Middleware: validate that :reviewId exists in the reviews table.
|
|
30
|
-
* Attaches the review record to req.review for downstream handlers.
|
|
31
|
-
*/
|
|
32
|
-
async function validateReviewId(req, res, next) {
|
|
33
|
-
try {
|
|
34
|
-
const reviewId = parseInt(req.params.reviewId, 10);
|
|
35
|
-
|
|
36
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
37
|
-
return res.status(400).json({ error: 'Invalid review ID' });
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const db = req.app.get('db');
|
|
41
|
-
const reviewRepo = new ReviewRepository(db);
|
|
42
|
-
const review = await reviewRepo.getReview(reviewId);
|
|
43
|
-
|
|
44
|
-
if (!review) {
|
|
45
|
-
return res.status(404).json({ error: `Review #${reviewId} not found` });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
req.review = review;
|
|
49
|
-
req.reviewId = reviewId;
|
|
50
|
-
next();
|
|
51
|
-
} catch (error) {
|
|
52
|
-
next(error);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
30
|
/**
|
|
57
31
|
* GET /api/reviews/:reviewId/comments
|
|
58
32
|
* Get all comments for a review.
|
|
@@ -1070,4 +1044,146 @@ router.get('/api/reviews/:reviewId/file-content/:fileName(*)', validateReviewId,
|
|
|
1070
1044
|
}
|
|
1071
1045
|
});
|
|
1072
1046
|
|
|
1047
|
+
// ==========================================================================
|
|
1048
|
+
// Hunk Summaries Route
|
|
1049
|
+
// ==========================================================================
|
|
1050
|
+
|
|
1051
|
+
/**
|
|
1052
|
+
* GET /api/reviews/:reviewId/hunk-summaries
|
|
1053
|
+
* Get all hunk summaries for a review (PR or Local).
|
|
1054
|
+
* Returns trivial-marker rows alongside generated summaries; the frontend filters.
|
|
1055
|
+
*/
|
|
1056
|
+
router.get('/api/reviews/:reviewId/hunk-summaries', validateReviewId, async (req, res) => {
|
|
1057
|
+
try {
|
|
1058
|
+
const db = req.app.get('db');
|
|
1059
|
+
const repo = new HunkSummaryRepository(db);
|
|
1060
|
+
const rows = await repo.getByReview(req.reviewId);
|
|
1061
|
+
// `generating` reflects whether the background queue is still working
|
|
1062
|
+
// on this review's summaries; the frontend uses it to show a "generating"
|
|
1063
|
+
// pulse on the toolbar toggle until `review:background_job_finished`
|
|
1064
|
+
// fires for jobType=`summaries:*`.
|
|
1065
|
+
const generating = backgroundQueue.hasActiveForReview(req.reviewId, 'summaries');
|
|
1066
|
+
res.json({
|
|
1067
|
+
summaries: rows.map((row) => ({
|
|
1068
|
+
file_path: row.file_path,
|
|
1069
|
+
content_hash: row.content_hash,
|
|
1070
|
+
summary_text: row.summary_text,
|
|
1071
|
+
trivial_reason: row.trivial_reason
|
|
1072
|
+
})),
|
|
1073
|
+
generating
|
|
1074
|
+
});
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
logger.error('Error fetching hunk summaries:', error);
|
|
1077
|
+
res.status(500).json({ error: 'Failed to fetch hunk summaries' });
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// ==========================================================================
|
|
1082
|
+
// Tour Route
|
|
1083
|
+
// ==========================================================================
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* GET /api/reviews/:reviewId/tour
|
|
1087
|
+
* Get the persisted guided tour for a review (PR or Local).
|
|
1088
|
+
* Returns `{tour: null}` when no tour has been generated yet, otherwise
|
|
1089
|
+
* returns `{tour: {stops, diff_hash, stale, generating, provider, model, created_at}}`.
|
|
1090
|
+
*
|
|
1091
|
+
* `stale` is true when a `tour` job is currently in flight for this review,
|
|
1092
|
+
* meaning the persisted tour may be about to be replaced. `generating` is
|
|
1093
|
+
* true when there is no persisted tour yet but a job is in flight.
|
|
1094
|
+
*/
|
|
1095
|
+
router.get('/api/reviews/:reviewId/tour', validateReviewId, async (req, res) => {
|
|
1096
|
+
try {
|
|
1097
|
+
const db = req.app.get('db');
|
|
1098
|
+
const repo = new TourRepository(db);
|
|
1099
|
+
const row = await repo.get(req.reviewId);
|
|
1100
|
+
const generating = backgroundQueue.hasActiveForReview(req.reviewId, 'tour');
|
|
1101
|
+
|
|
1102
|
+
if (!row) {
|
|
1103
|
+
return res.json({ tour: null, generating });
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
let stops;
|
|
1107
|
+
try {
|
|
1108
|
+
stops = JSON.parse(row.stops);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
logger.warn(`Failed to parse tour.stops for review ${req.reviewId}: ${err.message}`);
|
|
1111
|
+
return res.status(500).json({ error: 'Tour data corrupt' });
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
res.json({
|
|
1115
|
+
tour: {
|
|
1116
|
+
stops,
|
|
1117
|
+
diff_hash: row.diff_hash,
|
|
1118
|
+
stale: generating,
|
|
1119
|
+
provider: row.provider,
|
|
1120
|
+
model: row.model,
|
|
1121
|
+
created_at: row.created_at
|
|
1122
|
+
},
|
|
1123
|
+
generating
|
|
1124
|
+
});
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
logger.error('Error fetching tour:', error);
|
|
1127
|
+
res.status(500).json({ error: 'Failed to fetch tour' });
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
|
|
1131
|
+
// ==========================================================================
|
|
1132
|
+
// Background Job Cancellation
|
|
1133
|
+
// ==========================================================================
|
|
1134
|
+
|
|
1135
|
+
// Only these prefixes are user-cancellable. We deliberately do NOT accept
|
|
1136
|
+
// arbitrary jobKeys — that would let the UI cancel internal jobs we don't
|
|
1137
|
+
// want to be cancellable from the toolbar (e.g. future scheduling work).
|
|
1138
|
+
const CANCELLABLE_JOB_PREFIXES = new Set(['tour', 'summaries']);
|
|
1139
|
+
|
|
1140
|
+
/**
|
|
1141
|
+
* POST /api/reviews/:reviewId/jobs/:jobKey/cancel
|
|
1142
|
+
*
|
|
1143
|
+
* Cancel an in-flight background job (tour or summaries) for this review.
|
|
1144
|
+
* Aborts the per-job `AbortSignal`, which kills the upstream CLI child
|
|
1145
|
+
* process so we stop burning tokens immediately.
|
|
1146
|
+
*
|
|
1147
|
+
* Works for BOTH Local mode (`/local/:reviewId`) and PR mode (`/pr/...`)
|
|
1148
|
+
* because both modes write into the same `reviews` table and dispatch
|
|
1149
|
+
* jobs through the same `backgroundQueue` keyed by reviewId. The
|
|
1150
|
+
* separate `/api/local/...` cancel route below shares this handler so
|
|
1151
|
+
* the contract stays in one place.
|
|
1152
|
+
*
|
|
1153
|
+
* Request:
|
|
1154
|
+
* - `jobKey` path param: bare prefix (`tour` | `summaries`) or full
|
|
1155
|
+
* job key suffix (`summaries:<digest>`). Bare prefix cancels ALL
|
|
1156
|
+
* matching variants — what the toolbar actually wants.
|
|
1157
|
+
*
|
|
1158
|
+
* Responses:
|
|
1159
|
+
* - 200 `{ cancelled: true, count: N }` - aborted N job(s)
|
|
1160
|
+
* - 404 `{ cancelled: false }` - nothing in flight
|
|
1161
|
+
* - 400 - invalid jobKey
|
|
1162
|
+
*/
|
|
1163
|
+
async function handleJobCancel(req, res) {
|
|
1164
|
+
const rawKey = String(req.params.jobKey || '').trim();
|
|
1165
|
+
// Strip whitespace; reject if empty, contains slashes/control chars, or
|
|
1166
|
+
// does not start with an allow-listed prefix. We deliberately do NOT
|
|
1167
|
+
// accept arbitrary keys — see CANCELLABLE_JOB_PREFIXES comment.
|
|
1168
|
+
if (!rawKey || /[/\\\s]/.test(rawKey)) {
|
|
1169
|
+
return res.status(400).json({ error: 'Invalid jobKey' });
|
|
1170
|
+
}
|
|
1171
|
+
const prefix = rawKey.includes(':') ? rawKey.slice(0, rawKey.indexOf(':')) : rawKey;
|
|
1172
|
+
if (!CANCELLABLE_JOB_PREFIXES.has(prefix)) {
|
|
1173
|
+
return res.status(400).json({ error: `jobKey "${prefix}" is not cancellable` });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
const { cancelled } = backgroundQueue.cancel(req.reviewId, rawKey);
|
|
1177
|
+
if (cancelled === 0) {
|
|
1178
|
+
logger.info(`Cancel request for ${req.reviewId}:${rawKey} matched no in-flight job`);
|
|
1179
|
+
return res.status(404).json({ cancelled: false });
|
|
1180
|
+
}
|
|
1181
|
+
logger.info(`Cancelled ${cancelled} background job(s) for ${req.reviewId}:${rawKey}`);
|
|
1182
|
+
res.json({ cancelled: true, count: cancelled });
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
router.post('/api/reviews/:reviewId/jobs/:jobKey/cancel', validateReviewId, handleJobCancel);
|
|
1186
|
+
|
|
1073
1187
|
module.exports = router;
|
|
1188
|
+
module.exports.handleJobCancel = handleJobCancel;
|
|
1189
|
+
module.exports.CANCELLABLE_JOB_PREFIXES = CANCELLABLE_JOB_PREFIXES;
|
package/src/routes/setup.js
CHANGED
|
@@ -15,7 +15,7 @@ const crypto = require('crypto');
|
|
|
15
15
|
const { activeSetups, broadcastSetupProgress } = require('./shared');
|
|
16
16
|
const { setupPRReview } = require('../setup/pr-setup');
|
|
17
17
|
const { setupLocalReview } = require('../setup/local-setup');
|
|
18
|
-
const { getGitHubToken, expandPath } = require('../config');
|
|
18
|
+
const { getGitHubToken, expandPath, resolveBindingRepositoryFromPR } = require('../config');
|
|
19
19
|
const { queryOne, ReviewRepository } = require('../database');
|
|
20
20
|
const { normalizeRepository } = require('../utils/paths');
|
|
21
21
|
const { rejectUrlLikeLocalReviewPath } = require('../utils/local-path-input');
|
|
@@ -63,8 +63,12 @@ router.post('/api/setup/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
63
63
|
const db = req.app.get('db');
|
|
64
64
|
const config = req.app.get('config');
|
|
65
65
|
|
|
66
|
-
// GitHub token is required for PR setup
|
|
67
|
-
|
|
66
|
+
// GitHub token is required for PR setup. Resolve the binding key
|
|
67
|
+
// first so monorepo-style `repos[...]` entries (matched via
|
|
68
|
+
// `url_pattern` named captures) supply their per-repo token even when
|
|
69
|
+
// the captured owner/repo differs from the config key.
|
|
70
|
+
const repositoryForToken = resolveBindingRepositoryFromPR(owner, repo, config);
|
|
71
|
+
const githubToken = getGitHubToken(config, repositoryForToken);
|
|
68
72
|
if (!githubToken) {
|
|
69
73
|
return res.status(401).json({ error: 'GitHub token not configured' });
|
|
70
74
|
}
|
|
@@ -143,6 +147,7 @@ router.post('/api/setup/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
143
147
|
repo,
|
|
144
148
|
prNumber,
|
|
145
149
|
githubToken,
|
|
150
|
+
bindingRepository: repositoryForToken,
|
|
146
151
|
config,
|
|
147
152
|
poolLifecycle: req.app.get('poolLifecycle'),
|
|
148
153
|
restoreMetadata,
|
|
@@ -19,7 +19,7 @@ const { normalizeRepository } = require('../utils/paths');
|
|
|
19
19
|
const { mergeInstructions } = require('../utils/instructions');
|
|
20
20
|
const { GitWorktreeManager } = require('../git/worktree');
|
|
21
21
|
const { GitHubClient } = require('../github/client');
|
|
22
|
-
const { getGitHubToken, resolveLoadSkills, buildCouncilProviderOverrides } = require('../config');
|
|
22
|
+
const { getGitHubToken, resolveHostBinding, resolveBindingRepositoryFromPR, resolveLoadSkills, buildCouncilProviderOverrides } = require('../config');
|
|
23
23
|
const { setupStackPR } = require('../setup/stack-setup');
|
|
24
24
|
const Analyzer = require('../ai/analyzer');
|
|
25
25
|
const { getProviderClass, createProvider } = require('../ai/provider');
|
|
@@ -164,6 +164,8 @@ const defaults = {
|
|
|
164
164
|
GitWorktreeManager,
|
|
165
165
|
GitHubClient,
|
|
166
166
|
getGitHubToken,
|
|
167
|
+
resolveHostBinding,
|
|
168
|
+
resolveBindingRepositoryFromPR,
|
|
167
169
|
setupStackPR,
|
|
168
170
|
Analyzer,
|
|
169
171
|
getProviderClass,
|
|
@@ -229,10 +231,17 @@ async function executeStackAnalysis(params) {
|
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
// 3. Fetch all PR data from GitHub in parallel
|
|
232
|
-
|
|
234
|
+
// Use the per-repo binding so alt-host stack analyses target the right host.
|
|
235
|
+
// The PR identity (`${owner}/${repo}`) is used for DB rows and worktree
|
|
236
|
+
// identity. For host-binding lookups we MUST use the config-binding key,
|
|
237
|
+
// which differs from the PR identity for monorepo-style `url_pattern`
|
|
238
|
+
// configs (one `repos[...]` entry serves many captured owner/repo pairs).
|
|
239
|
+
const bindingRepository = deps.resolveBindingRepositoryFromPR(owner, repo, config);
|
|
240
|
+
const stackBinding = deps.resolveHostBinding(bindingRepository, config);
|
|
241
|
+
const githubToken = stackBinding.token;
|
|
233
242
|
const prDataMap = new Map();
|
|
234
243
|
if (githubToken) {
|
|
235
|
-
const githubClient = new deps.GitHubClient(
|
|
244
|
+
const githubClient = new deps.GitHubClient(stackBinding);
|
|
236
245
|
const fetchResults = await Promise.allSettled(
|
|
237
246
|
prNumbers.map(async (prNum) => {
|
|
238
247
|
const prData = await githubClient.fetchPullRequest(owner, repo, prNum);
|
|
@@ -294,10 +303,10 @@ async function executeStackAnalysis(params) {
|
|
|
294
303
|
};
|
|
295
304
|
|
|
296
305
|
return analyzeStackPR(deps, db, config, {
|
|
297
|
-
owner, repo, repository, prNum,
|
|
306
|
+
owner, repo, repository, bindingRepository, prNum,
|
|
298
307
|
worktreePath: worktreePathMap.get(prNum),
|
|
299
308
|
analysisConfig, stackAnalysisId, state,
|
|
300
|
-
githubToken, prData: prDataMap.get(prNum),
|
|
309
|
+
githubToken, binding: stackBinding, prData: prDataMap.get(prNum),
|
|
301
310
|
onAnalysisIdReady
|
|
302
311
|
}).then(result => {
|
|
303
312
|
state.prStatuses.set(prNum, {
|
|
@@ -335,15 +344,25 @@ async function executeStackAnalysis(params) {
|
|
|
335
344
|
* Called in parallel for all PRs.
|
|
336
345
|
*/
|
|
337
346
|
async function analyzeStackPR(deps, db, config, {
|
|
338
|
-
owner, repo, repository, prNum, worktreePath,
|
|
339
|
-
analysisConfig, stackAnalysisId, state, githubToken, prData,
|
|
347
|
+
owner, repo, repository, bindingRepository, prNum, worktreePath,
|
|
348
|
+
analysisConfig, stackAnalysisId, state, githubToken, binding, prData,
|
|
340
349
|
onAnalysisIdReady
|
|
341
350
|
}) {
|
|
351
|
+
// Build a GitHubClient for analyzer-side dedup pre-fetch. The stack
|
|
352
|
+
// analysis flow is PR mode only — local mode does not enter this path.
|
|
353
|
+
// Prefer the host-aware binding (alt-host capable) and fall back to the
|
|
354
|
+
// bare token for legacy callers.
|
|
355
|
+
const clientArg = binding || githubToken;
|
|
356
|
+
const stackGithubClient = clientArg ? new deps.GitHubClient(clientArg) : undefined;
|
|
357
|
+
if (stackGithubClient) {
|
|
358
|
+
logger.debug(`analyzer githubClient wired for ${owner}/${repo}#${prNum} (stack)`);
|
|
359
|
+
}
|
|
342
360
|
// 1. Setup PR (generates diff, stores metadata)
|
|
343
361
|
const worktreeManager = new deps.GitWorktreeManager(db);
|
|
344
362
|
await deps.setupStackPR({
|
|
345
363
|
db, owner, repo, prNumber: prNum,
|
|
346
|
-
githubToken,
|
|
364
|
+
githubToken, binding, bindingRepository,
|
|
365
|
+
worktreePath, worktreeManager, prData
|
|
347
366
|
});
|
|
348
367
|
|
|
349
368
|
// 2. Fetch prMetadata from DB
|
|
@@ -381,6 +400,7 @@ async function analyzeStackPR(deps, db, config, {
|
|
|
381
400
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
382
401
|
globalInstructions, repoInstructions, requestInstructions,
|
|
383
402
|
councilId, rawCouncilConfig, configType, onAnalysisIdReady,
|
|
403
|
+
githubClient: stackGithubClient,
|
|
384
404
|
providerOverrides: councilProviderOverrides,
|
|
385
405
|
providerOverridesMap: councilProviderOverridesMap
|
|
386
406
|
});
|
|
@@ -408,6 +428,7 @@ async function analyzeStackPR(deps, db, config, {
|
|
|
408
428
|
selectedProvider, selectedModel,
|
|
409
429
|
globalInstructions, repoInstructions, requestInstructions,
|
|
410
430
|
reqTier, reqEnabledLevels, onAnalysisIdReady,
|
|
431
|
+
githubClient: stackGithubClient,
|
|
411
432
|
providerOverrides
|
|
412
433
|
});
|
|
413
434
|
}
|
|
@@ -428,6 +449,7 @@ async function launchStackSingleAnalysis(deps, db, config, {
|
|
|
428
449
|
selectedProvider, selectedModel,
|
|
429
450
|
globalInstructions, repoInstructions, requestInstructions,
|
|
430
451
|
reqTier, reqEnabledLevels, onAnalysisIdReady,
|
|
452
|
+
githubClient,
|
|
431
453
|
providerOverrides = {}
|
|
432
454
|
}) {
|
|
433
455
|
const runId = uuidv4();
|
|
@@ -485,7 +507,7 @@ async function launchStackSingleAnalysis(deps, db, config, {
|
|
|
485
507
|
reviewId, worktreePath, prMetadata, progressCallback,
|
|
486
508
|
{ globalInstructions, repoInstructions, requestInstructions },
|
|
487
509
|
null,
|
|
488
|
-
{ analysisId, runId, skipRunCreation: true, tier, enabledLevels: levelsConfig }
|
|
510
|
+
{ analysisId, runId, skipRunCreation: true, tier, enabledLevels: levelsConfig, githubClient }
|
|
489
511
|
);
|
|
490
512
|
|
|
491
513
|
const completionInfo = determineCompletionInfo(result);
|
|
@@ -552,6 +574,7 @@ async function launchStackCouncilAnalysis(deps, db, config, {
|
|
|
552
574
|
reviewId, worktreePath, prMetadata, prNum, owner, repo, repository,
|
|
553
575
|
globalInstructions, repoInstructions, requestInstructions,
|
|
554
576
|
councilId, rawCouncilConfig, configType, onAnalysisIdReady,
|
|
577
|
+
githubClient,
|
|
555
578
|
providerOverrides = {},
|
|
556
579
|
providerOverridesMap = null
|
|
557
580
|
}) {
|
|
@@ -595,6 +618,7 @@ async function launchStackCouncilAnalysis(deps, db, config, {
|
|
|
595
618
|
logLabel: `Stack PR #${prNum}`,
|
|
596
619
|
initialStatusExtra: { prNumber: prNum, reviewType: 'pr' },
|
|
597
620
|
config,
|
|
621
|
+
githubClient,
|
|
598
622
|
providerOverrides,
|
|
599
623
|
providerOverridesMap,
|
|
600
624
|
hookContext: {
|
package/src/routes/worktrees.js
CHANGED
|
@@ -47,9 +47,10 @@ router.post('/api/worktrees/create', async (req, res) => {
|
|
|
47
47
|
const db = req.app.get('db');
|
|
48
48
|
const config = req.app.get('config');
|
|
49
49
|
|
|
50
|
-
// Validate GitHub token
|
|
50
|
+
// Validate GitHub token. Pass the owner/repo so alt-host repos
|
|
51
|
+
// resolve their own per-repo token.
|
|
51
52
|
const { getGitHubToken } = require('../config');
|
|
52
|
-
const githubToken = getGitHubToken(config);
|
|
53
|
+
const githubToken = getGitHubToken(config, `${owner}/${repo}`);
|
|
53
54
|
if (!githubToken) {
|
|
54
55
|
return res.status(500).json({
|
|
55
56
|
success: false,
|
package/src/server.js
CHANGED
|
@@ -348,6 +348,7 @@ async function startServer(sharedDb = null, sharedPoolLifecycle = null) {
|
|
|
348
348
|
const chatRoutes = require('./routes/chat');
|
|
349
349
|
const contextFilesRoutes = require('./routes/context-files');
|
|
350
350
|
const githubCollectionsRoutes = require('./routes/github-collections');
|
|
351
|
+
const bulkAnalysisConfigsRoutes = require('./routes/bulk-analysis-configs');
|
|
351
352
|
const stackAnalysisRoutes = require('./routes/stack-analysis');
|
|
352
353
|
const externalCommentsRoutes = require('./routes/external-comments');
|
|
353
354
|
const { createSoundRouter } = require('./routes/sound');
|
|
@@ -369,6 +370,7 @@ async function startServer(sharedDb = null, sharedPoolLifecycle = null) {
|
|
|
369
370
|
app.use('/', setupRoutes);
|
|
370
371
|
app.use('/', mcpRoutes);
|
|
371
372
|
app.use('/', githubCollectionsRoutes);
|
|
373
|
+
app.use('/', bulkAnalysisConfigsRoutes);
|
|
372
374
|
app.use('/', stackAnalysisRoutes);
|
|
373
375
|
// External-comments routes (GitHub PR review-comment sync + fetch) are
|
|
374
376
|
// gated by the `external_comments` config flag. When disabled, the
|
package/src/setup/pr-setup.js
CHANGED
|
@@ -17,7 +17,7 @@ const { WorktreePoolLifecycle } = require('../git/worktree-pool-lifecycle');
|
|
|
17
17
|
const { GitHubClient } = require('../github/client');
|
|
18
18
|
const { normalizeRepository } = require('../utils/paths');
|
|
19
19
|
const { findMainGitRoot } = require('../local-review');
|
|
20
|
-
const { getConfigDir, getRepoPath, resolveRepoOptions, resolvePoolConfig, getRepoResetScript, DEFAULT_CHECKOUT_TIMEOUT_MS } = require('../config');
|
|
20
|
+
const { getConfigDir, getRepoPath, resolveRepoOptions, resolvePoolConfig, getRepoResetScript, resolveHostBinding, resolveBindingRepositoryFromPR, DEFAULT_CHECKOUT_TIMEOUT_MS } = require('../config');
|
|
21
21
|
const logger = require('../utils/logger');
|
|
22
22
|
const { fireReviewStartedHook } = require('../hooks/payloads');
|
|
23
23
|
const simpleGit = require('simple-git');
|
|
@@ -213,9 +213,11 @@ async function registerRepositoryLocation(db, currentDir, owner, repo) {
|
|
|
213
213
|
* @param {Object} params.db - Database instance
|
|
214
214
|
* @param {string} params.owner - Repository owner
|
|
215
215
|
* @param {string} params.repo - Repository name
|
|
216
|
-
* @param {string} params.repository - Normalized "owner/repo"
|
|
216
|
+
* @param {string} params.repository - Normalized "owner/repo" PR identity (used for DB lookups: worktrees, repo_settings)
|
|
217
|
+
* @param {string} [params.bindingRepository] - `repos[...]` config-lookup key; defaults to `repository`. Differs for monorepo url_pattern configs.
|
|
217
218
|
* @param {number} params.prNumber - PR number (used for worktree lookup)
|
|
218
219
|
* @param {Object} [params.config] - Application config (used for monorepo path lookup)
|
|
220
|
+
* @param {string} [params.cloneUrl] - Alt-host clone URL from `prData.repository.clone_url`; falls back to github.com when omitted.
|
|
219
221
|
* @param {Function} [params.onProgress] - Optional progress callback
|
|
220
222
|
* @returns {Promise<{ repositoryPath: string, knownPath: string|null, worktreeSourcePath: string|null, checkoutScript: string|null, checkoutTimeout: number, worktreeConfig: Object|null }>}
|
|
221
223
|
* - repositoryPath: the main git root (bare repo or .git parent)
|
|
@@ -225,7 +227,10 @@ async function registerRepositoryLocation(db, currentDir, owner, repo) {
|
|
|
225
227
|
* - checkoutTimeout: timeout in ms for checkout script (default: 300000 = 5 minutes)
|
|
226
228
|
* - worktreeConfig: { worktreeBaseDir, nameTemplate } if configured, null otherwise
|
|
227
229
|
*/
|
|
228
|
-
async function findRepositoryPath({ db, owner, repo, repository, prNumber, config, onProgress }) {
|
|
230
|
+
async function findRepositoryPath({ db, owner, repo, repository, bindingRepository, prNumber, config, cloneUrl, onProgress }) {
|
|
231
|
+
// `repository` is the PR identity (DB key). `bindingRepository` is the
|
|
232
|
+
// `repos[...]` config-lookup key — they differ for monorepo url_pattern configs.
|
|
233
|
+
const configKey = bindingRepository || repository;
|
|
229
234
|
const worktreeManager = new GitWorktreeManager(db);
|
|
230
235
|
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
231
236
|
const worktreeRepo = new WorktreeRepository(db);
|
|
@@ -237,7 +242,7 @@ async function findRepositoryPath({ db, owner, repo, repository, prNumber, confi
|
|
|
237
242
|
// ------------------------------------------------------------------
|
|
238
243
|
// Tier -1: Explicit monorepo configuration (highest priority)
|
|
239
244
|
// ------------------------------------------------------------------
|
|
240
|
-
const monorepoPath = config ? getRepoPath(config,
|
|
245
|
+
const monorepoPath = config ? getRepoPath(config, configKey) : null;
|
|
241
246
|
|
|
242
247
|
if (monorepoPath) {
|
|
243
248
|
// The configured path might be a worktree or a regular/bare repo.
|
|
@@ -289,7 +294,7 @@ async function findRepositoryPath({ db, owner, repo, repository, prNumber, confi
|
|
|
289
294
|
// ------------------------------------------------------------------
|
|
290
295
|
// Resolve monorepo worktree options (checkout_script, worktree_directory, worktree_name_template)
|
|
291
296
|
// ------------------------------------------------------------------
|
|
292
|
-
const resolved = config ? resolveRepoOptions(config,
|
|
297
|
+
const resolved = config ? resolveRepoOptions(config, configKey, repoSettings) : { checkoutScript: null, checkoutTimeout: DEFAULT_CHECKOUT_TIMEOUT_MS, worktreeConfig: null };
|
|
293
298
|
const { checkoutScript, checkoutTimeout, worktreeConfig } = resolved;
|
|
294
299
|
|
|
295
300
|
// When a checkout script is configured, null out worktreeSourcePath —
|
|
@@ -357,8 +362,10 @@ async function findRepositoryPath({ db, owner, repo, repository, prNumber, confi
|
|
|
357
362
|
await fs.mkdir(path.dirname(cachedRepoPath), { recursive: true });
|
|
358
363
|
|
|
359
364
|
const git = simpleGit();
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
// Honor alt-host clone URL when callers thread it through; older
|
|
366
|
+
// restore snapshots / pre-alt-host callers fall back to github.com.
|
|
367
|
+
const resolvedCloneUrl = cloneUrl || `https://github.com/${owner}/${repo}.git`;
|
|
368
|
+
await git.clone(resolvedCloneUrl, cachedRepoPath, ['--filter=blob:none', '--no-checkout']);
|
|
362
369
|
repositoryPath = cachedRepoPath;
|
|
363
370
|
if (onProgress) {
|
|
364
371
|
onProgress({ step: 'repo', status: 'running', message: `Repository cloned to ${cachedRepoPath}` });
|
|
@@ -408,16 +415,31 @@ function isShaNotFoundError(err) {
|
|
|
408
415
|
* @param {string} params.repo - Repository name
|
|
409
416
|
* @param {number} params.prNumber - Pull request number
|
|
410
417
|
* @param {string} params.githubToken - GitHub PAT
|
|
418
|
+
* @param {string} [params.bindingRepository] - `repos[...]` config-lookup key; resolved internally when omitted
|
|
411
419
|
* @param {Object} [params.config] - Application config (for monorepo path lookup)
|
|
412
420
|
* @param {import('../git/worktree-pool-lifecycle').WorktreePoolLifecycle} [params.poolLifecycle] - Shared pool lifecycle instance (avoids creating a fresh singleton)
|
|
413
421
|
* @param {Object} [params.restoreMetadata] - Stored PR data for restore mode (skips GitHub fetch + diff)
|
|
414
422
|
* @param {Function} [params.onProgress] - Optional progress callback
|
|
415
423
|
* @returns {Promise<{ reviewUrl: string, title: string }>}
|
|
416
424
|
*/
|
|
417
|
-
async function setupPRReview({ db, owner, repo, prNumber, githubToken, config, onProgress, poolLifecycle: externalPoolLifecycle, restoreMetadata }) {
|
|
425
|
+
async function setupPRReview({ db, owner, repo, prNumber, githubToken, bindingRepository: externalBindingRepository, config, onProgress, poolLifecycle: externalPoolLifecycle, restoreMetadata }) {
|
|
418
426
|
const repository = normalizeRepository(owner, repo);
|
|
419
427
|
const progress = onProgress || (() => {});
|
|
420
428
|
|
|
429
|
+
// Resolve the per-repo host binding so alt-host setups talk to the
|
|
430
|
+
// configured `api_host`. Use `resolveBindingRepositoryFromPR` so
|
|
431
|
+
// monorepo-style configs (one `repos[...]` entry serving many
|
|
432
|
+
// captured owner/repo) find the right binding. Fall back to the bare
|
|
433
|
+
// token shape if no config is available (legacy invocation path) or
|
|
434
|
+
// the binding resolved no token (callers in this case already
|
|
435
|
+
// pre-resolved it).
|
|
436
|
+
const bindingRepository = externalBindingRepository
|
|
437
|
+
|| (config ? resolveBindingRepositoryFromPR(owner, repo, config) : repository);
|
|
438
|
+
const setupBinding = config ? resolveHostBinding(bindingRepository, config) : null;
|
|
439
|
+
const clientArg = (setupBinding && setupBinding.token)
|
|
440
|
+
? setupBinding
|
|
441
|
+
: githubToken;
|
|
442
|
+
|
|
421
443
|
const isRestore = !!(restoreMetadata && restoreMetadata.head_sha);
|
|
422
444
|
let prData;
|
|
423
445
|
let githubClient = null;
|
|
@@ -431,7 +453,7 @@ async function setupPRReview({ db, owner, repo, prNumber, githubToken, config, o
|
|
|
431
453
|
// Step: verify - Verify repository access
|
|
432
454
|
// ------------------------------------------------------------------
|
|
433
455
|
progress({ step: 'verify', status: 'running', message: 'Verifying repository access...' });
|
|
434
|
-
githubClient = new GitHubClient(
|
|
456
|
+
githubClient = new GitHubClient(clientArg);
|
|
435
457
|
const repoExists = await githubClient.repositoryExists(owner, repo);
|
|
436
458
|
if (!repoExists) {
|
|
437
459
|
throw new Error(`Repository ${owner}/${repo} not found`);
|
|
@@ -455,8 +477,10 @@ async function setupPRReview({ db, owner, repo, prNumber, githubToken, config, o
|
|
|
455
477
|
owner,
|
|
456
478
|
repo,
|
|
457
479
|
repository,
|
|
480
|
+
bindingRepository,
|
|
458
481
|
prNumber,
|
|
459
482
|
config,
|
|
483
|
+
cloneUrl: prData?.repository?.clone_url,
|
|
460
484
|
onProgress: progress
|
|
461
485
|
});
|
|
462
486
|
progress({ step: 'repo', status: 'completed', message: `Repository located at ${repositoryPath}` });
|
|
@@ -467,8 +491,10 @@ async function setupPRReview({ db, owner, repo, prNumber, githubToken, config, o
|
|
|
467
491
|
const prInfo = { owner, repo, number: prNumber };
|
|
468
492
|
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
469
493
|
const repoSettings = await repoSettingsRepo.getRepoSettings(repository);
|
|
470
|
-
|
|
471
|
-
|
|
494
|
+
// Pool/reset settings live under the config-lookup key (`bindingRepository`),
|
|
495
|
+
// not the PR identity, so monorepo `repos[...]` entries are honored.
|
|
496
|
+
const { poolSize } = resolvePoolConfig(config || {}, bindingRepository, repoSettings);
|
|
497
|
+
const resetScript = config ? getRepoResetScript(config, bindingRepository) : null;
|
|
472
498
|
|
|
473
499
|
let worktreePath;
|
|
474
500
|
let worktreeManager;
|
package/src/setup/stack-setup.js
CHANGED
|
@@ -23,17 +23,27 @@ const logger = require('../utils/logger');
|
|
|
23
23
|
* @param {string} params.owner - Repository owner
|
|
24
24
|
* @param {string} params.repo - Repository name
|
|
25
25
|
* @param {number} params.prNumber - Pull request number
|
|
26
|
-
* @param {string} params.githubToken - GitHub personal access token
|
|
26
|
+
* @param {string} [params.githubToken] - GitHub personal access token (legacy). Prefer `binding`.
|
|
27
|
+
* @param {Object} [params.binding] - Resolved host binding (`resolveHostBinding(bindingRepository, config)`).
|
|
28
|
+
* Required for alt-host repos so the right API host is used.
|
|
29
|
+
* @param {string} [params.bindingRepository] - `repos[...]` config-lookup key for this PR. Differs from
|
|
30
|
+
* `${owner}/${repo}` for monorepo `url_pattern` configs. Surfaced for downstream
|
|
31
|
+
* per-repo lookups (worktree pool, reset script) that key off the config entry.
|
|
27
32
|
* @param {string} params.worktreePath - Path to the per-PR worktree
|
|
28
33
|
* @param {import('../git/worktree').GitWorktreeManager} params.worktreeManager - Worktree manager instance
|
|
29
34
|
* @param {Object} [params.prData] - Pre-fetched PR data from GitHub (skips API call when provided)
|
|
30
35
|
* @returns {Promise<{ reviewId: number, prMetadata: Object, prData: Object, isNew: boolean }>}
|
|
31
36
|
*/
|
|
32
|
-
async function setupStackPR({ db, owner, repo, prNumber, githubToken, worktreePath, worktreeManager, prData: prefetchedPRData }) {
|
|
37
|
+
async function setupStackPR({ db, owner, repo, prNumber, githubToken, binding, bindingRepository, worktreePath, worktreeManager, prData: prefetchedPRData }) {
|
|
38
|
+
// `bindingRepository` is accepted so callers (e.g. `executeStackAnalysis`)
|
|
39
|
+
// can thread the resolved config-binding key through to any downstream
|
|
40
|
+
// per-repo lookups added in this function. Currently unused inside this
|
|
41
|
+
// function — `storePRData` keys off the PR identity.
|
|
42
|
+
void bindingRepository;
|
|
33
43
|
logger.info(`Setting up stack PR #${prNumber} for ${owner}/${repo}`);
|
|
34
44
|
|
|
35
45
|
// 1. Fetch PR data from GitHub (or use pre-fetched data)
|
|
36
|
-
const githubClient = new GitHubClient(githubToken);
|
|
46
|
+
const githubClient = new GitHubClient(binding || githubToken);
|
|
37
47
|
let prData;
|
|
38
48
|
if (prefetchedPRData) {
|
|
39
49
|
prData = prefetchedPRData;
|
package/src/single-port.js
CHANGED
|
@@ -116,12 +116,15 @@ function buildDelegationUrl(port, mode, context = {}) {
|
|
|
116
116
|
* Parse PR arguments for URL construction without starting a server.
|
|
117
117
|
* Reuses PRArgumentParser — synchronous for URLs, async for bare numbers.
|
|
118
118
|
* @param {string[]} prArgs - Raw CLI PR arguments
|
|
119
|
+
* @param {object} [config] - Pair-review config, passed to the parser so
|
|
120
|
+
* that per-repo `url_pattern` regexes are tried before the built-in
|
|
121
|
+
* GitHub/Graphite parsers. Pass null to disable config-driven matching.
|
|
119
122
|
* @param {object} [_deps] - Dependency overrides for testing
|
|
120
123
|
* @returns {Promise<{owner: string, repo: string, number: number}>}
|
|
121
124
|
*/
|
|
122
|
-
async function parsePRArgsForDelegation(prArgs, _deps) {
|
|
125
|
+
async function parsePRArgsForDelegation(prArgs, config = null, _deps) {
|
|
123
126
|
const deps = { ...defaults, ..._deps };
|
|
124
|
-
const parser = new deps.PRArgumentParser();
|
|
127
|
+
const parser = new deps.PRArgumentParser(config);
|
|
125
128
|
return parser.parsePRArguments(prArgs);
|
|
126
129
|
}
|
|
127
130
|
|
|
@@ -163,7 +166,7 @@ async function attemptDelegation(config, flags, prArgs, _deps) {
|
|
|
163
166
|
const targetPath = path.resolve(flags.localPath || process.cwd());
|
|
164
167
|
url = buildDelegationUrl(port, 'local', { localPath: targetPath, analyze: flags.ai });
|
|
165
168
|
} else if (prArgs.length > 0) {
|
|
166
|
-
const prInfo = await parsePRArgsForDelegation(prArgs, _deps);
|
|
169
|
+
const prInfo = await parsePRArgsForDelegation(prArgs, config, _deps);
|
|
167
170
|
url = buildDelegationUrl(port, 'pr', { ...prInfo, analyze: flags.ai });
|
|
168
171
|
} else {
|
|
169
172
|
url = buildDelegationUrl(port, 'server');
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
const { parseUnifiedDiffPatches } = require('./diff-file-list');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} Hunk
|
|
7
|
+
* @property {string} header - Hunk header line, e.g. "@@ -10,5 +10,7 @@".
|
|
8
|
+
* @property {string[]} lines - Diff lines including their leading marker
|
|
9
|
+
* ('+', '-', ' ', or the literal '\' marker).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Split a single file's patch text into per-hunk structures.
|
|
14
|
+
* @param {string} filePatch - Patch text for one file (with or without diff header).
|
|
15
|
+
* @returns {Hunk[]} Array of hunks; empty when the patch contains no `@@` lines.
|
|
16
|
+
*/
|
|
17
|
+
function parseHunks(filePatch) {
|
|
18
|
+
if (!filePatch) return [];
|
|
19
|
+
|
|
20
|
+
const lines = filePatch.split('\n');
|
|
21
|
+
const hunks = [];
|
|
22
|
+
let current = null;
|
|
23
|
+
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
if (line.startsWith('@@')) {
|
|
26
|
+
if (current) hunks.push(current);
|
|
27
|
+
current = { header: line, lines: [] };
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (current) {
|
|
31
|
+
current.lines.push(line);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (current) hunks.push(current);
|
|
36
|
+
|
|
37
|
+
for (const hunk of hunks) {
|
|
38
|
+
while (hunk.lines.length > 0 && hunk.lines[hunk.lines.length - 1] === '') {
|
|
39
|
+
hunk.lines.pop();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return hunks;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Parse a full unified diff into a Map of file path -> hunks.
|
|
48
|
+
* @param {string} diffText - Full unified diff text spanning many files.
|
|
49
|
+
* @returns {Map<string, Hunk[]>} Map keyed by the new path (or old path for deletions).
|
|
50
|
+
*/
|
|
51
|
+
function parseUnifiedDiffHunks(diffText) {
|
|
52
|
+
const result = new Map();
|
|
53
|
+
if (!diffText) return result;
|
|
54
|
+
|
|
55
|
+
const patches = parseUnifiedDiffPatches(diffText);
|
|
56
|
+
for (const [filePath, patch] of patches.entries()) {
|
|
57
|
+
const hunks = parseHunks(patch);
|
|
58
|
+
if (hunks.length === 0) continue;
|
|
59
|
+
result.set(filePath, hunks);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { parseHunks, parseUnifiedDiffHunks };
|
|
@@ -15,10 +15,13 @@ const logger = require('./logger');
|
|
|
15
15
|
*
|
|
16
16
|
* @param {string} response - Raw response text (may include preamble/postamble prose)
|
|
17
17
|
* @param {string|number} level - Level identifier for logging (e.g., 1, 2, 3, 'orchestration', 'unknown')
|
|
18
|
+
* @param {string} [logPrefix] - Custom log prefix to use instead of `[Level <level>]`.
|
|
19
|
+
* Used by callers (e.g., summary generation, council mode) that have a more
|
|
20
|
+
* meaningful identifier than a numeric analysis level.
|
|
18
21
|
* @returns {Object} Extraction result with success flag and data/error
|
|
19
22
|
*/
|
|
20
|
-
function extractJSON(response, level = 'unknown') {
|
|
21
|
-
const levelPrefix = `[Level ${level}]`;
|
|
23
|
+
function extractJSON(response, level = 'unknown', logPrefix) {
|
|
24
|
+
const levelPrefix = logPrefix || `[Level ${level}]`;
|
|
22
25
|
|
|
23
26
|
if (!response || !response.trim()) {
|
|
24
27
|
return { success: false, error: 'Empty response' };
|