ensemble-review 1.0.35 → 1.0.36
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 +5 -2
- package/dist/ai/check-tool.js +0 -6
- package/dist/api/routes/auth.js +0 -194
- package/dist/api/routes/billing.js +0 -209
- package/dist/api/routes/claude-review.js +0 -522
- package/dist/api/routes/contact.js +0 -318
- package/dist/api/routes/feed.js +0 -21
- package/dist/api/routes/inbox.js +0 -45
- package/dist/api/routes/mentions.js +0 -22
- package/dist/api/routes/teams.js +0 -150
- package/dist/api/routes/webhooks.js +0 -181
- package/dist/coding/activity.js +0 -80
- package/dist/coding/agent.js +0 -147
- package/dist/coding/ai-fix/checks-preview.js +0 -58
- package/dist/coding/ai-fix/comment-seed.js +0 -519
- package/dist/coding/ai-fix/prompts.js +0 -283
- package/dist/coding/ai-resolve/prompt.js +0 -161
- package/dist/coding/ai-resolve/routes.js +0 -97
- package/dist/coding/ai-resolve/run.js +0 -343
- package/dist/coding/ai-resolve/schema.js +0 -26
- package/dist/coding/ai-resolve/validate.js +0 -136
- package/dist/coding/checks/deny-scan.js +0 -75
- package/dist/coding/checks/discover.js +0 -541
- package/dist/coding/checks/docker.js +0 -38
- package/dist/coding/checks/env.js +0 -88
- package/dist/coding/checks/hooks.js +0 -91
- package/dist/coding/checks/index.js +0 -8
- package/dist/coding/checks/local.js +0 -40
- package/dist/coding/checks/sandbox.js +0 -206
- package/dist/coding/checks/session.js +0 -518
- package/dist/coding/checks/spawn.js +0 -123
- package/dist/coding/checks/spec.js +0 -140
- package/dist/coding/checks/types.js +0 -6
- package/dist/coding/partial-spend.js +0 -19
- package/dist/coding/safe-path.js +0 -75
- package/dist/coding/story-build/build-prompts.js +0 -178
- package/dist/coding/story-build/build.js +0 -332
- package/dist/coding/story-build/checkouts.js +0 -66
- package/dist/coding/story-build/docs-runner.js +0 -178
- package/dist/coding/story-build/docs.js +0 -334
- package/dist/coding/story-build/manager.js +0 -999
- package/dist/coding/story-build/plan-agent.js +0 -190
- package/dist/coding/story-build/plan.js +0 -338
- package/dist/coding/story-build/prompts.js +0 -147
- package/dist/coding/story-build/routes-docs.js +0 -113
- package/dist/coding/story-build/routes.js +0 -295
- package/dist/coding/story-build/schema.js +0 -53
- package/dist/db/automation-output.js +0 -349
- package/dist/db/credits.js +0 -79
- package/dist/db/schema.js +0 -321
- package/dist/db/schema.pg.js +0 -2197
- package/dist/db/sprint-repo-suggest.js +0 -257
- package/dist/db/sprint-stories.js +0 -42
- package/dist/db/sprint-story.js +0 -505
- package/dist/db/sprint-ticket-repos.js +0 -195
- package/dist/db/story-selection.js +0 -270
- package/dist/db/team-comparison.js +0 -124
- package/dist/db/workspace-comparison.js +0 -98
- package/dist/feed/fyi-provider.js +0 -10
- package/dist/github/local-user.js +0 -92
- package/dist/github/repo-files.js +0 -26
- package/dist/ml/worker.js +0 -31
- package/dist/platform/advisor/discovery/tier2.js +0 -88
- package/dist/platform/advisor/discovery/tier3.js +0 -57
- package/dist/platform/advisor/emitters/adapter.js +0 -5
- package/dist/platform/advisor/emitters/coderabbit.js +0 -175
- package/dist/platform/advisor/emitters/copilot-prose.js +0 -118
- package/dist/platform/advisor/emitters/greptile.js +0 -176
- package/dist/platform/advisor/emitters/lint.js +0 -37
- package/dist/platform/advisor/emitters/manifest.js +0 -239
- package/dist/platform/advisor/emitters/prose.js +0 -183
- package/dist/platform/advisor/emitters/registry.js +0 -20
- package/dist/platform/advisor/emitters/yaml-doc.js +0 -95
- package/dist/platform/advisor/evidence.js +0 -79
- package/dist/platform/advisor/findings.js +0 -15
- package/dist/platform/advisor/intents.js +0 -269
- package/dist/platform/advisor/measure/effect.js +0 -18
- package/dist/platform/advisor/outputs/brief.js +0 -68
- package/dist/platform/advisor/outputs/config-pr.js +0 -156
- package/dist/platform/advisor/refine/diff-guard.js +0 -38
- package/dist/platform/advisor/refine/refine.js +0 -35
- package/dist/platform/advisor/retro-check.js +0 -98
- package/dist/platform/advisor/routes.js +0 -648
- package/dist/platform/advisor/store.js +0 -321
- package/dist/platform/advisor/taxonomy.js +0 -67
- package/dist/platform/annotations/prompts.js +0 -84
- package/dist/platform/annotations/routes.js +0 -86
- package/dist/platform/annotations/runner.js +0 -234
- package/dist/platform/annotations/targets.js +0 -203
- package/dist/platform/insights/grounded-figures.js +0 -162
- package/dist/platform/insights/period-questions.js +0 -235
- package/dist/platform/work-plan/hash.js +0 -99
- package/dist/platform/work-plan/prompt.js +0 -209
- package/dist/platform/work-plan/routes.js +0 -350
- package/dist/pro/bind.js +0 -309
- package/dist/pro/contract.js +0 -36
- package/dist/pro/event-hooks.js +0 -37
- package/dist/pro/migrate.js +0 -64
- package/dist/review/events.js +0 -24
- package/dist/review/local-settings.js +0 -78
- package/dist/review/memory/capture.js +0 -0
- package/dist/review/memory/retrieval.js +0 -272
- package/dist/review/memory/routes.js +0 -78
- package/dist/review/persist.js +0 -203
- package/dist/review/plugin-providers.js +0 -12
- package/dist/review/prompt.js +0 -264
- package/dist/review/review-manager.js +0 -259
- package/dist/review/routing.js +0 -164
- package/dist/review/sprint-assess/agent.js +0 -145
- package/dist/review/sprint-assess/manager.js +0 -382
- package/dist/review/sprint-assess/prompts.js +0 -99
- package/dist/review/sprint-assess/routes.js +0 -109
- package/dist/review/sprint-assess/schema.js +0 -70
- package/dist/review/sprint-assess/validate.js +0 -180
- package/dist/review/sprint-story/manager.js +0 -98
- package/dist/review/sprint-story/prompt.js +0 -45
- package/dist/review/sprint-story/validate.js +0 -127
- package/dist/review/story-docs/build-prompts.js +0 -153
- package/dist/review/story-docs/build-validate.js +0 -162
- package/dist/review/story-report/agent.js +0 -203
- package/dist/review/story-report/fingerprint.js +0 -45
- package/dist/review/story-report/manager.js +0 -287
- package/dist/review/story-report/routes.js +0 -154
- package/dist/review/story-report/story-detail.js +0 -148
- package/dist/review/story-report/validate.js +0 -105
- package/dist/review/ticket-review/sweep-waits.js +0 -20
- package/dist/review/write-deny.js +0 -170
- package/dist/shared/conflict-ai.js +0 -24
- package/dist/sync/benchmark-rollup.js +0 -50
|
@@ -1,522 +0,0 @@
|
|
|
1
|
-
import { config } from '../../config.js';
|
|
2
|
-
import { getClaudeReviewById, getClaudeReviewContext, getFindingPostContext, getLatestClaudeReview, listAllClaudeReviews, listClaudeReviewHistory, } from '../../db/queries.js';
|
|
3
|
-
import { markFindingPosted, markReviewPosted, updateFinding, updateReviewDraft, } from '../../review/persist.js';
|
|
4
|
-
import { getReviewStatus, listActiveReviews, requestReviewCancel, startReview, subscribeReviewStream, } from '../../review/review-manager.js';
|
|
5
|
-
import { detectClaudeAuth } from '../../review/auth.js';
|
|
6
|
-
import { hasUserAnthropicKey, setUserAnthropicKey, } from '../../review/local-settings.js';
|
|
7
|
-
import { buildAnchorIndex, buildReview, fallbackAnchor, fetchCurrentHeadSha, fetchPrDiff, findingCommentBody, prLevelFindingBody, stripNoiseFromDiff, submitGithubComment, submitGithubIssueComment, submitGithubReview, } from '../../review/post-review.js';
|
|
8
|
-
import { isNoiseFile } from '../../review/prompt.js';
|
|
9
|
-
import { accountIdOf } from '../plugins/auth.js';
|
|
10
|
-
import { reviewEvents } from '../../review/events.js';
|
|
11
|
-
const MODELS = ['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-haiku-4-5'];
|
|
12
|
-
const VERDICTS = ['COMMENT', 'REQUEST_CHANGES', 'APPROVE'];
|
|
13
|
-
const REVIEW_MODES = ['auto', 'diff_only', 'worktree'];
|
|
14
|
-
const idParam = {
|
|
15
|
-
params: {
|
|
16
|
-
type: 'object',
|
|
17
|
-
required: ['id'],
|
|
18
|
-
properties: { id: { type: 'integer' } },
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
const reviewIdParam = {
|
|
22
|
-
params: {
|
|
23
|
-
type: 'object',
|
|
24
|
-
required: ['reviewId'],
|
|
25
|
-
properties: { reviewId: { type: 'integer' } },
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
const findingIdParam = {
|
|
29
|
-
params: {
|
|
30
|
-
type: 'object',
|
|
31
|
-
required: ['findingId'],
|
|
32
|
-
properties: { findingId: { type: 'integer' } },
|
|
33
|
-
},
|
|
34
|
-
};
|
|
35
|
-
const generateSchema = {
|
|
36
|
-
...idParam,
|
|
37
|
-
body: {
|
|
38
|
-
type: 'object',
|
|
39
|
-
required: ['model'],
|
|
40
|
-
additionalProperties: false,
|
|
41
|
-
properties: {
|
|
42
|
-
model: { type: 'string', enum: MODELS },
|
|
43
|
-
// Review depth. Omitted defaults to 'auto' (the router decides).
|
|
44
|
-
mode: { type: 'string', enum: REVIEW_MODES },
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
const updateReviewSchema = {
|
|
49
|
-
...reviewIdParam,
|
|
50
|
-
body: {
|
|
51
|
-
type: 'object',
|
|
52
|
-
additionalProperties: false,
|
|
53
|
-
properties: {
|
|
54
|
-
userBody: { type: 'string' },
|
|
55
|
-
userVerdict: { type: 'string', enum: VERDICTS },
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
const updateFindingSchema = {
|
|
60
|
-
...findingIdParam,
|
|
61
|
-
body: {
|
|
62
|
-
type: 'object',
|
|
63
|
-
additionalProperties: false,
|
|
64
|
-
properties: {
|
|
65
|
-
included: { type: 'boolean' },
|
|
66
|
-
editedBody: { type: 'string' },
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
const postSchema = {
|
|
71
|
-
...reviewIdParam,
|
|
72
|
-
body: {
|
|
73
|
-
type: 'object',
|
|
74
|
-
required: ['userVerdict'],
|
|
75
|
-
additionalProperties: false,
|
|
76
|
-
properties: { userVerdict: { type: 'string', enum: VERDICTS } },
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
// Feature is opt-in (ENABLE_CLAUDE_REVIEW). When off, mutating routes 404.
|
|
80
|
-
function featureOff(reply) {
|
|
81
|
-
reply.status(404);
|
|
82
|
-
return {
|
|
83
|
-
error: 'NotFound',
|
|
84
|
-
message: 'Claude Review is disabled (set ENABLE_CLAUDE_REVIEW=true).',
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
export async function claudeReviewRoutes(app) {
|
|
88
|
-
// Latest run + findings + history + auth + enabled.
|
|
89
|
-
app.get('/api/prs/:id/claude-review', { schema: idParam }, async (req) => {
|
|
90
|
-
const { id } = req.params;
|
|
91
|
-
if (!config.claudeReviewEnabled) {
|
|
92
|
-
return {
|
|
93
|
-
enabled: false,
|
|
94
|
-
auth: 'none',
|
|
95
|
-
hasUserKey: false,
|
|
96
|
-
review: null,
|
|
97
|
-
history: [],
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
const accountId = accountIdOf(req);
|
|
101
|
-
const auth = detectClaudeAuth();
|
|
102
|
-
return {
|
|
103
|
-
enabled: true,
|
|
104
|
-
auth: auth.status,
|
|
105
|
-
authMessage: auth.status === 'none' ? auth.message : undefined,
|
|
106
|
-
hasUserKey: hasUserAnthropicKey(),
|
|
107
|
-
review: await getLatestClaudeReview(id, accountId),
|
|
108
|
-
history: await listClaudeReviewHistory(id, accountId),
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
// Set or clear the locally-stored Anthropic API key (local mode only — the
|
|
112
|
-
// whole route file is unregistered in cloud). An empty `key` clears it.
|
|
113
|
-
app.put('/api/claude-review/key', {
|
|
114
|
-
schema: {
|
|
115
|
-
body: {
|
|
116
|
-
type: 'object',
|
|
117
|
-
required: ['key'],
|
|
118
|
-
additionalProperties: false,
|
|
119
|
-
properties: { key: { type: 'string' } },
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
}, async (req) => {
|
|
123
|
-
const { key } = req.body;
|
|
124
|
-
setUserAnthropicKey(key);
|
|
125
|
-
return { hasUserKey: hasUserAnthropicKey(), auth: detectClaudeAuth().status };
|
|
126
|
-
});
|
|
127
|
-
// Kick off a run. 404 disabled/unknown PR, 400 no auth / no head, 409 busy.
|
|
128
|
-
app.post('/api/prs/:id/claude-review', { schema: generateSchema }, async (req, reply) => {
|
|
129
|
-
const { id } = req.params;
|
|
130
|
-
const { model, mode } = req.body;
|
|
131
|
-
if (!config.claudeReviewEnabled)
|
|
132
|
-
return featureOff(reply);
|
|
133
|
-
const auth = detectClaudeAuth();
|
|
134
|
-
if (auth.status === 'none') {
|
|
135
|
-
reply.status(400);
|
|
136
|
-
return { error: 'NoClaudeAuth', message: auth.message };
|
|
137
|
-
}
|
|
138
|
-
const result = await startReview(id, model, mode ?? 'auto', app.log);
|
|
139
|
-
if (!result.ok) {
|
|
140
|
-
if (result.reason === 'not_found') {
|
|
141
|
-
reply.status(404);
|
|
142
|
-
return { error: 'NotFound', message: `PR ${id} not found` };
|
|
143
|
-
}
|
|
144
|
-
if (result.reason === 'no_head') {
|
|
145
|
-
reply.status(400);
|
|
146
|
-
return {
|
|
147
|
-
error: 'NoHead',
|
|
148
|
-
message: 'PR has no head commit to review yet.',
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
if (result.reason === 'disabled')
|
|
152
|
-
return featureOff(reply);
|
|
153
|
-
// already_running | busy
|
|
154
|
-
reply.status(409);
|
|
155
|
-
return {
|
|
156
|
-
error: 'Conflict',
|
|
157
|
-
message: result.reason === 'already_running'
|
|
158
|
-
? 'A review is already running or queued for this PR.'
|
|
159
|
-
: 'The review queue is full; try again once some finish.',
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
reviewEvents.emit({
|
|
163
|
-
type: 'review.requested',
|
|
164
|
-
accountId: accountIdOf(req),
|
|
165
|
-
prId: id,
|
|
166
|
-
model,
|
|
167
|
-
requestedMode: mode ?? 'auto',
|
|
168
|
-
});
|
|
169
|
-
reply.status(202);
|
|
170
|
-
return { reviewId: result.reviewId, status: 'queued' };
|
|
171
|
-
});
|
|
172
|
-
// Live progress poll target (kept as a fallback + for the initial snapshot).
|
|
173
|
-
app.get('/api/prs/:id/claude-review/status', { schema: idParam }, async (req) => {
|
|
174
|
-
const { id } = req.params;
|
|
175
|
-
if (!config.claudeReviewEnabled) {
|
|
176
|
-
return { status: 'idle', reviewId: null, progress: null };
|
|
177
|
-
}
|
|
178
|
-
return await getReviewStatus(id);
|
|
179
|
-
});
|
|
180
|
-
// Live progress STREAM (SSE) — pushes each phase/activity/usage change in real
|
|
181
|
-
// time and one terminal `done`, so the UI progress bar tracks the run without
|
|
182
|
-
// polling. Local-only (Claude Review is gated off in cloud). We `hijack()` the
|
|
183
|
-
// reply and own the raw socket for the connection's lifetime.
|
|
184
|
-
app.get('/api/prs/:id/claude-review/stream', { schema: idParam }, async (req, reply) => {
|
|
185
|
-
const { id } = req.params;
|
|
186
|
-
reply.hijack();
|
|
187
|
-
const raw = reply.raw;
|
|
188
|
-
raw.writeHead(200, {
|
|
189
|
-
'Content-Type': 'text/event-stream',
|
|
190
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
191
|
-
Connection: 'keep-alive',
|
|
192
|
-
// Disable proxy buffering (nginx) so frames flush immediately.
|
|
193
|
-
'X-Accel-Buffering': 'no',
|
|
194
|
-
});
|
|
195
|
-
const send = (data) => {
|
|
196
|
-
if (!raw.writableEnded)
|
|
197
|
-
raw.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
198
|
-
};
|
|
199
|
-
if (!config.claudeReviewEnabled) {
|
|
200
|
-
send({ type: 'done', status: 'idle', reviewId: null });
|
|
201
|
-
raw.end();
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
let closed = false;
|
|
205
|
-
let unsubscribe = null;
|
|
206
|
-
const heartbeat = setInterval(() => {
|
|
207
|
-
if (!raw.writableEnded)
|
|
208
|
-
raw.write(': hb\n\n');
|
|
209
|
-
}, 15000);
|
|
210
|
-
const cleanup = () => {
|
|
211
|
-
if (closed)
|
|
212
|
-
return;
|
|
213
|
-
closed = true;
|
|
214
|
-
clearInterval(heartbeat);
|
|
215
|
-
unsubscribe?.();
|
|
216
|
-
if (!raw.writableEnded)
|
|
217
|
-
raw.end();
|
|
218
|
-
};
|
|
219
|
-
// Subscribe BEFORE reading the snapshot so a terminal `done` emitted during
|
|
220
|
-
// setup can't slip through the gap (it'd be delivered to this callback). If
|
|
221
|
-
// the run already finished, the snapshot check below sends `done` itself.
|
|
222
|
-
unsubscribe = subscribeReviewStream(id, (e) => {
|
|
223
|
-
send(e);
|
|
224
|
-
if (e.type === 'done')
|
|
225
|
-
cleanup();
|
|
226
|
-
});
|
|
227
|
-
req.raw.on('close', cleanup);
|
|
228
|
-
// Initial snapshot so a client that connects mid-run gets current state at once.
|
|
229
|
-
const snap = await getReviewStatus(id);
|
|
230
|
-
send({ type: 'snapshot', ...snap });
|
|
231
|
-
if (snap.status !== 'running' && snap.status !== 'queued') {
|
|
232
|
-
send({ type: 'done', status: snap.status, reviewId: snap.reviewId });
|
|
233
|
-
cleanup();
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
app.post('/api/prs/:id/claude-review/cancel', { schema: idParam }, async (req, reply) => {
|
|
237
|
-
const { id } = req.params;
|
|
238
|
-
if (!config.claudeReviewEnabled)
|
|
239
|
-
return featureOff(reply);
|
|
240
|
-
const ok = requestReviewCancel(id);
|
|
241
|
-
if (!ok) {
|
|
242
|
-
reply.status(404);
|
|
243
|
-
return { error: 'NotFound', message: 'No running review for this PR.' };
|
|
244
|
-
}
|
|
245
|
-
return { status: 'cancelling' };
|
|
246
|
-
});
|
|
247
|
-
// Cross-PR list of prior Claude reviews (one entry per PR = its most-recent
|
|
248
|
-
// succeeded run, within the timeline window). Static path — registered before
|
|
249
|
-
// the /:reviewId param route so Fastify never mis-routes it.
|
|
250
|
-
app.get('/api/claude-reviews', async (req) => {
|
|
251
|
-
if (!config.claudeReviewEnabled)
|
|
252
|
-
return { reviews: [] };
|
|
253
|
-
return { reviews: await listAllClaudeReviews(accountIdOf(req)) };
|
|
254
|
-
});
|
|
255
|
-
// All in-flight reviews (global progress banner). Static path — Fastify
|
|
256
|
-
// prioritises it over the /:reviewId param route.
|
|
257
|
-
app.get('/api/claude-reviews/active', async () => {
|
|
258
|
-
if (!config.claudeReviewEnabled)
|
|
259
|
-
return { reviews: [] };
|
|
260
|
-
return { reviews: await listActiveReviews() };
|
|
261
|
-
});
|
|
262
|
-
// A specific past run (with findings) — drives the history selector.
|
|
263
|
-
app.get('/api/claude-reviews/:reviewId', { schema: reviewIdParam }, async (req, reply) => {
|
|
264
|
-
if (!config.claudeReviewEnabled)
|
|
265
|
-
return featureOff(reply);
|
|
266
|
-
const { reviewId } = req.params;
|
|
267
|
-
const review = await getClaudeReviewById(reviewId, accountIdOf(req));
|
|
268
|
-
if (!review) {
|
|
269
|
-
reply.status(404);
|
|
270
|
-
return { error: 'NotFound', message: `Review ${reviewId} not found` };
|
|
271
|
-
}
|
|
272
|
-
return review;
|
|
273
|
-
});
|
|
274
|
-
// Save the user's authored draft (never touches Claude's summary/verdict).
|
|
275
|
-
app.patch('/api/claude-reviews/:reviewId', { schema: updateReviewSchema }, async (req, reply) => {
|
|
276
|
-
if (!config.claudeReviewEnabled)
|
|
277
|
-
return featureOff(reply);
|
|
278
|
-
const { reviewId } = req.params;
|
|
279
|
-
const body = req.body;
|
|
280
|
-
const ok = await updateReviewDraft(reviewId, body);
|
|
281
|
-
if (!ok) {
|
|
282
|
-
reply.status(404);
|
|
283
|
-
return { error: 'NotFound', message: `Review ${reviewId} not found` };
|
|
284
|
-
}
|
|
285
|
-
reviewEvents.emit({
|
|
286
|
-
type: 'review.draftUpdated',
|
|
287
|
-
accountId: accountIdOf(req),
|
|
288
|
-
reviewId,
|
|
289
|
-
change: { userBody: body.userBody, userVerdict: body.userVerdict },
|
|
290
|
-
});
|
|
291
|
-
return { status: 'ok' };
|
|
292
|
-
});
|
|
293
|
-
// Tick a finding for inline posting and/or save the user's reworded body.
|
|
294
|
-
app.patch('/api/claude-findings/:findingId', { schema: updateFindingSchema }, async (req, reply) => {
|
|
295
|
-
if (!config.claudeReviewEnabled)
|
|
296
|
-
return featureOff(reply);
|
|
297
|
-
const { findingId } = req.params;
|
|
298
|
-
const body = req.body;
|
|
299
|
-
const ok = await updateFinding(findingId, body);
|
|
300
|
-
if (!ok) {
|
|
301
|
-
reply.status(404);
|
|
302
|
-
return { error: 'NotFound', message: `Finding ${findingId} not found` };
|
|
303
|
-
}
|
|
304
|
-
reviewEvents.emit({
|
|
305
|
-
type: 'finding.updated',
|
|
306
|
-
accountId: accountIdOf(req),
|
|
307
|
-
findingId,
|
|
308
|
-
change: { included: body.included, editedBody: body.editedBody },
|
|
309
|
-
});
|
|
310
|
-
return { status: 'ok' };
|
|
311
|
-
});
|
|
312
|
-
// Post a single finding as a standalone comment (no review submitted). The
|
|
313
|
-
// destination is chosen AUTOMATICALLY from the live diff: anchorable on its own
|
|
314
|
-
// line → an inline comment there; unanchored but its file is in the diff → an
|
|
315
|
-
// inline comment on the file's first change; its file is NOT in the diff (e.g. a
|
|
316
|
-
// deep review on an unchanged file) → a standalone PR-level comment marked as
|
|
317
|
-
// outside the PR's diff. Pins to the run's head SHA; 409 if the PR head has moved.
|
|
318
|
-
app.post('/api/claude-findings/:findingId/post', { schema: findingIdParam }, async (req, reply) => {
|
|
319
|
-
if (!config.claudeReviewEnabled)
|
|
320
|
-
return featureOff(reply);
|
|
321
|
-
const { findingId } = req.params;
|
|
322
|
-
const accountId = accountIdOf(req);
|
|
323
|
-
const ctx = await getFindingPostContext(findingId, accountId);
|
|
324
|
-
if (!ctx) {
|
|
325
|
-
reply.status(404);
|
|
326
|
-
return { error: 'NotFound', message: `Finding ${findingId} not found` };
|
|
327
|
-
}
|
|
328
|
-
const f = ctx.finding;
|
|
329
|
-
try {
|
|
330
|
-
const currentHead = await fetchCurrentHeadSha(ctx.owner, ctx.name, ctx.prNumber);
|
|
331
|
-
if (currentHead !== ctx.reviewHeadSha) {
|
|
332
|
-
reply.status(409);
|
|
333
|
-
return {
|
|
334
|
-
error: 'HeadMoved',
|
|
335
|
-
message: 'The PR head has moved since this review. Re-review before posting.',
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
const postedAt = new Date().toISOString();
|
|
339
|
-
// Anchorable on its own line → inline comment there.
|
|
340
|
-
if (f.line != null && f.anchored) {
|
|
341
|
-
const { commentId } = await submitGithubComment({
|
|
342
|
-
owner: ctx.owner,
|
|
343
|
-
name: ctx.name,
|
|
344
|
-
prNumber: ctx.prNumber,
|
|
345
|
-
commitId: ctx.reviewHeadSha,
|
|
346
|
-
path: f.path,
|
|
347
|
-
line: f.line,
|
|
348
|
-
side: f.side,
|
|
349
|
-
body: findingCommentBody({
|
|
350
|
-
body: f.body,
|
|
351
|
-
editedBody: f.editedBody,
|
|
352
|
-
suggestion: f.suggestion,
|
|
353
|
-
}),
|
|
354
|
-
});
|
|
355
|
-
await markFindingPosted(findingId, commentId, 'inline');
|
|
356
|
-
reviewEvents.emit({
|
|
357
|
-
type: 'finding.posted',
|
|
358
|
-
accountId,
|
|
359
|
-
findingId,
|
|
360
|
-
postedCommentKind: 'inline',
|
|
361
|
-
});
|
|
362
|
-
const result = { githubCommentId: commentId, postedAt };
|
|
363
|
-
return result;
|
|
364
|
-
}
|
|
365
|
-
// Otherwise consult the diff. If the file IS in the diff, re-anchor to its
|
|
366
|
-
// first change (inline). If it isn't, post a standalone PR-level comment.
|
|
367
|
-
const { diff } = stripNoiseFromDiff(await fetchPrDiff(ctx.owner, ctx.name, ctx.prNumber), isNoiseFile);
|
|
368
|
-
const fb = fallbackAnchor(buildAnchorIndex(diff), f.path);
|
|
369
|
-
if (fb) {
|
|
370
|
-
const { commentId } = await submitGithubComment({
|
|
371
|
-
owner: ctx.owner,
|
|
372
|
-
name: ctx.name,
|
|
373
|
-
prNumber: ctx.prNumber,
|
|
374
|
-
commitId: ctx.reviewHeadSha,
|
|
375
|
-
path: f.path,
|
|
376
|
-
line: fb.line,
|
|
377
|
-
side: fb.side,
|
|
378
|
-
body: findingCommentBody({ body: f.body, editedBody: f.editedBody, suggestion: f.suggestion }, { fallbackNote: true }),
|
|
379
|
-
});
|
|
380
|
-
await markFindingPosted(findingId, commentId, 'inline');
|
|
381
|
-
reviewEvents.emit({
|
|
382
|
-
type: 'finding.posted',
|
|
383
|
-
accountId,
|
|
384
|
-
findingId,
|
|
385
|
-
postedCommentKind: 'inline',
|
|
386
|
-
});
|
|
387
|
-
const result = { githubCommentId: commentId, postedAt };
|
|
388
|
-
return result;
|
|
389
|
-
}
|
|
390
|
-
// File outside the PR's diff → standalone PR-level (issue) comment.
|
|
391
|
-
const { commentId } = await submitGithubIssueComment({
|
|
392
|
-
owner: ctx.owner,
|
|
393
|
-
name: ctx.name,
|
|
394
|
-
prNumber: ctx.prNumber,
|
|
395
|
-
body: prLevelFindingBody({
|
|
396
|
-
path: f.path,
|
|
397
|
-
line: f.line,
|
|
398
|
-
body: f.body,
|
|
399
|
-
editedBody: f.editedBody,
|
|
400
|
-
suggestion: f.suggestion,
|
|
401
|
-
}),
|
|
402
|
-
});
|
|
403
|
-
await markFindingPosted(findingId, commentId, 'pr_comment');
|
|
404
|
-
reviewEvents.emit({
|
|
405
|
-
type: 'finding.posted',
|
|
406
|
-
accountId,
|
|
407
|
-
findingId,
|
|
408
|
-
postedCommentKind: 'pr_comment',
|
|
409
|
-
});
|
|
410
|
-
const result = { githubCommentId: commentId, postedAt };
|
|
411
|
-
return result;
|
|
412
|
-
}
|
|
413
|
-
catch (err) {
|
|
414
|
-
reply.status(502);
|
|
415
|
-
return {
|
|
416
|
-
error: 'GitHubError',
|
|
417
|
-
message: err instanceof Error ? err.message : String(err),
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
// Post a single GitHub review (or, with ?dryRun=true, return the exact payload
|
|
422
|
-
// without calling GitHub).
|
|
423
|
-
app.post('/api/claude-reviews/:reviewId/post', { schema: postSchema }, async (req, reply) => {
|
|
424
|
-
if (!config.claudeReviewEnabled)
|
|
425
|
-
return featureOff(reply);
|
|
426
|
-
const { reviewId } = req.params;
|
|
427
|
-
const { userVerdict } = req.body;
|
|
428
|
-
const dryRun = req.query.dryRun === 'true';
|
|
429
|
-
const accountId = accountIdOf(req);
|
|
430
|
-
const ctx = await getClaudeReviewContext(reviewId, accountId);
|
|
431
|
-
if (!ctx) {
|
|
432
|
-
reply.status(404);
|
|
433
|
-
return { error: 'NotFound', message: `Review ${reviewId} not found` };
|
|
434
|
-
}
|
|
435
|
-
const review = await getClaudeReviewById(reviewId, accountId);
|
|
436
|
-
if (!review) {
|
|
437
|
-
reply.status(404);
|
|
438
|
-
return { error: 'NotFound', message: `Review ${reviewId} not found` };
|
|
439
|
-
}
|
|
440
|
-
try {
|
|
441
|
-
// Re-validate against the live head — if it moved, a re-review is needed.
|
|
442
|
-
const currentHead = await fetchCurrentHeadSha(ctx.owner, ctx.name, ctx.prNumber);
|
|
443
|
-
if (currentHead !== ctx.review.headSha) {
|
|
444
|
-
reply.status(409);
|
|
445
|
-
return {
|
|
446
|
-
error: 'HeadMoved',
|
|
447
|
-
message: 'The PR head has moved since this review. Re-review before posting.',
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
// Persist the chosen verdict so the run records what was posted.
|
|
451
|
-
await updateReviewDraft(reviewId, { userVerdict });
|
|
452
|
-
const rawDiff = await fetchPrDiff(ctx.owner, ctx.name, ctx.prNumber);
|
|
453
|
-
const { diff } = stripNoiseFromDiff(rawDiff, isNoiseFile);
|
|
454
|
-
const built = buildReview({
|
|
455
|
-
commitId: ctx.review.headSha,
|
|
456
|
-
body: review.userBody ?? '',
|
|
457
|
-
event: userVerdict,
|
|
458
|
-
includedFindings: review.findings.filter((f) => f.included),
|
|
459
|
-
diff,
|
|
460
|
-
});
|
|
461
|
-
if (dryRun)
|
|
462
|
-
return built.preview;
|
|
463
|
-
const { reviewId: ghReviewId } = await submitGithubReview({
|
|
464
|
-
owner: ctx.owner,
|
|
465
|
-
name: ctx.name,
|
|
466
|
-
prNumber: ctx.prNumber,
|
|
467
|
-
commitId: ctx.review.headSha,
|
|
468
|
-
body: review.userBody ?? '',
|
|
469
|
-
event: userVerdict,
|
|
470
|
-
comments: built.preview.comments,
|
|
471
|
-
});
|
|
472
|
-
// The review is now LIVE on GitHub and can't be un-posted. Findings whose
|
|
473
|
-
// file isn't in the diff post as standalone PR-level comments alongside it
|
|
474
|
-
// (one issue comment each), so a deep review's findings on unchanged files
|
|
475
|
-
// still land rather than being dropped. Each post is best-effort: a single
|
|
476
|
-
// failed comment must NOT strand the already-posted review (that would leave
|
|
477
|
-
// the run unstamped and tempt a duplicate re-post), so we collect what lands
|
|
478
|
-
// and ALWAYS stamp afterwards. Findings that fail simply stay un-posted and
|
|
479
|
-
// can be posted individually from their row.
|
|
480
|
-
const prCommentResults = [];
|
|
481
|
-
for (const pc of built.preview.prComments) {
|
|
482
|
-
try {
|
|
483
|
-
const { commentId } = await submitGithubIssueComment({
|
|
484
|
-
owner: ctx.owner,
|
|
485
|
-
name: ctx.name,
|
|
486
|
-
prNumber: ctx.prNumber,
|
|
487
|
-
body: pc.body,
|
|
488
|
-
});
|
|
489
|
-
prCommentResults.push({ findingId: pc.findingId, commentId });
|
|
490
|
-
}
|
|
491
|
-
catch (err) {
|
|
492
|
-
req.log.warn({ err, findingId: pc.findingId, path: pc.path }, 'failed to post a PR-level comment for an off-diff finding');
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
await markReviewPosted(reviewId, ghReviewId, built.inlineFindingIds, prCommentResults);
|
|
496
|
-
reviewEvents.emit({
|
|
497
|
-
type: 'review.posted',
|
|
498
|
-
accountId,
|
|
499
|
-
reviewId,
|
|
500
|
-
userVerdict,
|
|
501
|
-
inlineFindingIds: built.inlineFindingIds,
|
|
502
|
-
prCommentFindingIds: prCommentResults.map((r) => r.findingId),
|
|
503
|
-
});
|
|
504
|
-
const result = {
|
|
505
|
-
postedReviewId: ghReviewId,
|
|
506
|
-
postedAt: new Date().toISOString(),
|
|
507
|
-
postedCommentCount: built.preview.comments.length,
|
|
508
|
-
// Count what actually posted (a comment may have failed best-effort above).
|
|
509
|
-
prCommentCount: prCommentResults.length,
|
|
510
|
-
};
|
|
511
|
-
return result;
|
|
512
|
-
}
|
|
513
|
-
catch (err) {
|
|
514
|
-
reply.status(502);
|
|
515
|
-
return {
|
|
516
|
-
error: 'GitHubError',
|
|
517
|
-
message: err instanceof Error ? err.message : String(err),
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
//# sourceMappingURL=claude-review.js.map
|