pierre-review 0.1.28 → 0.1.30

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.
@@ -27,6 +27,7 @@ export function registerAccountContext(app) {
27
27
  id: LOCAL_ACCOUNT_ID,
28
28
  githubUserId: '',
29
29
  githubLogin: '',
30
+ displayName: null,
30
31
  avatarUrl: null,
31
32
  isLocal: true,
32
33
  };
@@ -92,6 +92,7 @@ export async function authRoutes(app) {
92
92
  const account = await upsertCloudAccount({
93
93
  githubUserId: user.node_id,
94
94
  githubLogin: user.login,
95
+ displayName: user.name ?? null,
95
96
  avatarUrl: user.avatar_url,
96
97
  accessTokenEnc: encryptToken(token),
97
98
  });
@@ -4,7 +4,7 @@ import { markFindingPosted, markReviewPosted, updateFinding, updateReviewDraft,
4
4
  import { getReviewStatus, listActiveReviews, requestReviewCancel, startReview, } from '../../review/review-manager.js';
5
5
  import { detectClaudeAuth } from '../../review/auth.js';
6
6
  import { hasUserAnthropicKey, setUserAnthropicKey, } from '../../review/local-settings.js';
7
- import { buildAnchorIndex, buildReview, fallbackAnchor, fetchCurrentHeadSha, fetchPrDiff, findingCommentBody, stripNoiseFromDiff, submitGithubComment, submitGithubReview, } from '../../review/post-review.js';
7
+ import { buildAnchorIndex, buildReview, fallbackAnchor, fetchCurrentHeadSha, fetchPrDiff, findingCommentBody, prLevelFindingBody, stripNoiseFromDiff, submitGithubComment, submitGithubIssueComment, submitGithubReview, } from '../../review/post-review.js';
8
8
  import { isNoiseFile } from '../../review/prompt.js';
9
9
  import { accountIdOf } from '../plugins/auth.js';
10
10
  const MODELS = ['claude-opus-4-8', 'claude-sonnet-4-6'];
@@ -233,8 +233,12 @@ export async function claudeReviewRoutes(app) {
233
233
  }
234
234
  return { status: 'ok' };
235
235
  });
236
- // Post a single anchored finding as a standalone inline comment (no review
237
- // submitted). Pins to the run's head SHA; 409 if the PR head has since moved.
236
+ // Post a single finding as a standalone comment (no review submitted). The
237
+ // destination is chosen AUTOMATICALLY from the live diff: anchorable on its own
238
+ // line → an inline comment there; unanchored but its file is in the diff → an
239
+ // inline comment on the file's first change; its file is NOT in the diff (e.g. a
240
+ // deep review on an unchanged file) → a standalone PR-level comment marked as
241
+ // outside the PR's diff. Pins to the run's head SHA; 409 if the PR head has moved.
238
242
  app.post('/api/claude-findings/:findingId/post', { schema: findingIdParam }, async (req, reply) => {
239
243
  if (!config.claudeReviewEnabled)
240
244
  return featureOff(reply);
@@ -254,47 +258,61 @@ export async function claudeReviewRoutes(app) {
254
258
  message: 'The PR head has moved since this review. Re-review before posting.',
255
259
  };
256
260
  }
257
- // Anchor: the finding's own line if addable, else the file's first change
258
- // (added preferred). A finding whose file isn't in the diff can't inline.
259
- let line;
260
- let side = f.side;
261
- let onFallback = false;
261
+ const postedAt = new Date().toISOString();
262
+ // Anchorable on its own line inline comment there.
262
263
  if (f.line != null && f.anchored) {
263
- line = f.line;
264
+ const { commentId } = await submitGithubComment({
265
+ owner: ctx.owner,
266
+ name: ctx.name,
267
+ prNumber: ctx.prNumber,
268
+ commitId: ctx.reviewHeadSha,
269
+ path: f.path,
270
+ line: f.line,
271
+ side: f.side,
272
+ body: findingCommentBody({
273
+ body: f.body,
274
+ editedBody: f.editedBody,
275
+ suggestion: f.suggestion,
276
+ }),
277
+ });
278
+ await markFindingPosted(findingId, commentId, 'inline');
279
+ const result = { githubCommentId: commentId, postedAt };
280
+ return result;
264
281
  }
265
- else {
266
- const { diff } = stripNoiseFromDiff(await fetchPrDiff(ctx.owner, ctx.name, ctx.prNumber), isNoiseFile);
267
- const fb = fallbackAnchor(buildAnchorIndex(diff), f.path);
268
- if (!fb) {
269
- reply.status(400);
270
- return {
271
- error: 'NotAnchored',
272
- message: "This finding's file has no changes in the PR diff, so it can't post inline.",
273
- };
274
- }
275
- line = fb.line;
276
- side = fb.side;
277
- onFallback = true;
282
+ // Otherwise consult the diff. If the file IS in the diff, re-anchor to its
283
+ // first change (inline). If it isn't, post a standalone PR-level comment.
284
+ const { diff } = stripNoiseFromDiff(await fetchPrDiff(ctx.owner, ctx.name, ctx.prNumber), isNoiseFile);
285
+ const fb = fallbackAnchor(buildAnchorIndex(diff), f.path);
286
+ if (fb) {
287
+ const { commentId } = await submitGithubComment({
288
+ owner: ctx.owner,
289
+ name: ctx.name,
290
+ prNumber: ctx.prNumber,
291
+ commitId: ctx.reviewHeadSha,
292
+ path: f.path,
293
+ line: fb.line,
294
+ side: fb.side,
295
+ body: findingCommentBody({ body: f.body, editedBody: f.editedBody, suggestion: f.suggestion }, { fallbackNote: true }),
296
+ });
297
+ await markFindingPosted(findingId, commentId, 'inline');
298
+ const result = { githubCommentId: commentId, postedAt };
299
+ return result;
278
300
  }
279
- const { commentId } = await submitGithubComment({
301
+ // File outside the PR's diff → standalone PR-level (issue) comment.
302
+ const { commentId } = await submitGithubIssueComment({
280
303
  owner: ctx.owner,
281
304
  name: ctx.name,
282
305
  prNumber: ctx.prNumber,
283
- commitId: ctx.reviewHeadSha,
284
- path: f.path,
285
- line,
286
- side,
287
- body: findingCommentBody({
306
+ body: prLevelFindingBody({
307
+ path: f.path,
308
+ line: f.line,
288
309
  body: f.body,
289
310
  editedBody: f.editedBody,
290
311
  suggestion: f.suggestion,
291
- }, onFallback ? { fallbackNote: true } : undefined),
312
+ }),
292
313
  });
293
- await markFindingPosted(findingId, commentId);
294
- const result = {
295
- githubCommentId: commentId,
296
- postedAt: new Date().toISOString(),
297
- };
314
+ await markFindingPosted(findingId, commentId, 'pr_comment');
315
+ const result = { githubCommentId: commentId, postedAt };
298
316
  return result;
299
317
  }
300
318
  catch (err) {
@@ -356,12 +374,36 @@ export async function claudeReviewRoutes(app) {
356
374
  event: userVerdict,
357
375
  comments: built.preview.comments,
358
376
  });
359
- await markReviewPosted(reviewId, ghReviewId, built.postedFindingIds);
377
+ // The review is now LIVE on GitHub and can't be un-posted. Findings whose
378
+ // file isn't in the diff post as standalone PR-level comments alongside it
379
+ // (one issue comment each), so a deep review's findings on unchanged files
380
+ // still land rather than being dropped. Each post is best-effort: a single
381
+ // failed comment must NOT strand the already-posted review (that would leave
382
+ // the run unstamped and tempt a duplicate re-post), so we collect what lands
383
+ // and ALWAYS stamp afterwards. Findings that fail simply stay un-posted and
384
+ // can be posted individually from their row.
385
+ const prCommentResults = [];
386
+ for (const pc of built.preview.prComments) {
387
+ try {
388
+ const { commentId } = await submitGithubIssueComment({
389
+ owner: ctx.owner,
390
+ name: ctx.name,
391
+ prNumber: ctx.prNumber,
392
+ body: pc.body,
393
+ });
394
+ prCommentResults.push({ findingId: pc.findingId, commentId });
395
+ }
396
+ catch (err) {
397
+ req.log.warn({ err, findingId: pc.findingId, path: pc.path }, 'failed to post a PR-level comment for an off-diff finding');
398
+ }
399
+ }
400
+ await markReviewPosted(reviewId, ghReviewId, built.inlineFindingIds, prCommentResults);
360
401
  const result = {
361
402
  postedReviewId: ghReviewId,
362
403
  postedAt: new Date().toISOString(),
363
404
  postedCommentCount: built.preview.comments.length,
364
- skippedUnanchored: built.preview.skippedUnanchored,
405
+ // Count what actually posted (a comment may have failed best-effort above).
406
+ prCommentCount: prCommentResults.length,
365
407
  };
366
408
  return result;
367
409
  }
@@ -15,6 +15,7 @@ function fetchFromGh() {
15
15
  return {
16
16
  login: parsed.login,
17
17
  node_id: parsed.node_id,
18
+ name: parsed.name ?? null,
18
19
  avatar_url: parsed.avatar_url ?? null,
19
20
  };
20
21
  }
@@ -28,6 +29,7 @@ function rowToAccount(row) {
28
29
  id: row.id,
29
30
  githubUserId: row.githubUserId,
30
31
  githubLogin: row.githubLogin,
32
+ displayName: row.displayName,
31
33
  avatarUrl: row.avatarUrl,
32
34
  isLocal: row.isLocal,
33
35
  };
@@ -53,7 +55,11 @@ export async function ensureLocalAccount() {
53
55
  const fresh = existing &&
54
56
  existing.githubUserId !== '' &&
55
57
  existing.lastLoginAt != null &&
56
- Date.now() - existing.lastLoginAt.getTime() < STALE_MS;
58
+ Date.now() - existing.lastLoginAt.getTime() < STALE_MS &&
59
+ // Backfill the display name on the first run after it was added (older rows have
60
+ // it NULL). A genuinely name-less GitHub user re-fetches each startup — cheap;
61
+ // the daily refresh would repopulate it anyway.
62
+ existing.displayName != null;
57
63
  if (existing && fresh) {
58
64
  cachedLocalAccount = rowToAccount(existing);
59
65
  return cachedLocalAccount;
@@ -70,6 +76,7 @@ export async function ensureLocalAccount() {
70
76
  id: LOCAL_ACCOUNT_ID,
71
77
  githubUserId: gh.node_id,
72
78
  githubLogin: gh.login,
79
+ displayName: gh.name,
73
80
  avatarUrl: gh.avatar_url,
74
81
  isLocal: true,
75
82
  lastLoginAt: new Date(),
@@ -79,6 +86,7 @@ export async function ensureLocalAccount() {
79
86
  set: {
80
87
  githubUserId: gh.node_id,
81
88
  githubLogin: gh.login,
89
+ displayName: gh.name,
82
90
  avatarUrl: gh.avatar_url,
83
91
  isLocal: true,
84
92
  lastLoginAt: new Date(),
@@ -106,6 +114,7 @@ export async function upsertCloudAccount(input) {
106
114
  .values({
107
115
  githubUserId: input.githubUserId,
108
116
  githubLogin: input.githubLogin,
117
+ displayName: input.displayName,
109
118
  avatarUrl: input.avatarUrl,
110
119
  accessTokenEnc: input.accessTokenEnc,
111
120
  isLocal: false,
@@ -118,6 +127,7 @@ export async function upsertCloudAccount(input) {
118
127
  target: accounts.githubUserId,
119
128
  set: {
120
129
  githubLogin: input.githubLogin,
130
+ displayName: input.displayName,
121
131
  avatarUrl: input.avatarUrl,
122
132
  accessTokenEnc: input.accessTokenEnc,
123
133
  lastLoginAt: now,
@@ -218,6 +228,7 @@ export function accountToLocalUser(account) {
218
228
  login: account.githubLogin,
219
229
  githubId: account.githubUserId,
220
230
  avatarUrl: account.avatarUrl,
231
+ displayName: account.displayName,
221
232
  };
222
233
  }
223
234
  //# sourceMappingURL=account.js.map
@@ -0,0 +1,7 @@
1
+ -- The signed-in user's GitHub display name (additive). `display_name` captures the
2
+ -- `name` field from `gh api user` (local) / OAuth `GET /user` (cloud), shown wherever
3
+ -- the logged-in identity appears (header, greeting) in place of the @login. Nullable;
4
+ -- existing rows stay NULL until the next identity refresh repopulates them (local:
5
+ -- ensureLocalAccount refetches when display_name is NULL; cloud: on next sign-in).
6
+ -- Postgres baseline is regenerated separately via db:generate:pg.
7
+ ALTER TABLE `accounts` ADD `display_name` text;
@@ -0,0 +1,7 @@
1
+ -- Records how a posted Claude-review finding was attached to the PR (additive):
2
+ -- 'inline' = a review comment on a diff line, 'pr_comment' = a standalone PR-level
3
+ -- issue comment (used when the user posts an UNANCHORED finding individually rather
4
+ -- than forcing it onto a diff line). Null until posted; the UI uses it to build the
5
+ -- correct GitHub permalink (#discussion_r vs #issuecomment). SQLite-only — Claude
6
+ -- Review is force-disabled in cloud, so the Postgres table is never populated.
7
+ ALTER TABLE `claude_review_findings` ADD `posted_comment_kind` text;
@@ -0,0 +1,7 @@
1
+ -- Whether a Claude-review finding's file is part of the PR's diff (additive). true ⇒
2
+ -- an unanchored finding posts inline on the file's first change; false ⇒ the file is
3
+ -- outside the PR's diff (e.g. a deep review on an unchanged file) so it posts as a
4
+ -- standalone PR-level comment instead of being forced onto a diff line. NOT NULL
5
+ -- DEFAULT 1 so pre-existing findings keep the inline-on-first-change behavior.
6
+ -- SQLite-only — Claude Review is force-disabled in cloud.
7
+ ALTER TABLE `claude_review_findings` ADD `file_in_diff` integer NOT NULL DEFAULT 1;
@@ -120,6 +120,27 @@
120
120
  "when": 1780800000007,
121
121
  "tag": "0016_repo_viewer_permission",
122
122
  "breakpoints": true
123
+ },
124
+ {
125
+ "idx": 17,
126
+ "version": "6",
127
+ "when": 1780800000008,
128
+ "tag": "0017_account_display_name",
129
+ "breakpoints": true
130
+ },
131
+ {
132
+ "idx": 18,
133
+ "version": "6",
134
+ "when": 1780800000009,
135
+ "tag": "0018_finding_comment_kind",
136
+ "breakpoints": true
137
+ },
138
+ {
139
+ "idx": 19,
140
+ "version": "6",
141
+ "when": 1780800000010,
142
+ "tag": "0019_finding_file_in_diff",
143
+ "breakpoints": true
123
144
  }
124
145
  ]
125
146
  }
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "accounts" ADD COLUMN "display_name" text;--> statement-breakpoint
2
+ ALTER TABLE "claude_review_findings" ADD COLUMN "posted_comment_kind" text;
@@ -0,0 +1 @@
1
+ ALTER TABLE "claude_review_findings" ADD COLUMN "file_in_diff" boolean DEFAULT true NOT NULL;