@rigour-labs/cli 6.7.5 → 6.7.6

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.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * `rigour review-post`: post a `rigour review --json` report on a pull request.
3
+ *
4
+ * The PR bot is the safety net behind the pre-PR review, so it stays quiet:
5
+ * at most `--max-comments` inline comments (default 2), the most severe
6
+ * first, each posted once across pushes, and one summary comment that is
7
+ * edited in place on every push rather than re-posted.
8
+ */
9
+ import { type ReviewRecord } from '@rigour-labs/core';
1
10
  /** Who posts with the workflow's GITHUB_TOKEN. Another identity (an app, a PAT) sets RIGOUR_BOT_LOGIN. */
2
11
  export declare const DEFAULT_BOT_LOGIN = "github-actions[bot]";
3
12
  export interface ReportFinding {
@@ -29,6 +38,10 @@ export interface ReviewReport {
29
38
  dismissed?: number;
30
39
  control_files_changed?: string[];
31
40
  receipt?: ReportReceipt;
41
+ /** The reviewer's part of `rigour review --json`, when it ran: its record is what the summary shows. */
42
+ reviewer?: {
43
+ record?: ReviewRecord | null;
44
+ };
32
45
  }
33
46
  /** The quality receipt as `rigour review --json` writes it (review-receipt.ts). */
34
47
  export interface ReportReceipt {
@@ -6,6 +6,7 @@
6
6
  * first, each posted once across pushes, and one summary comment that is
7
7
  * edited in place on every push rather than re-posted.
8
8
  */
9
+ import { recordLines } from '@rigour-labs/core';
9
10
  import crypto from 'crypto';
10
11
  import fs from 'fs';
11
12
  const SUMMARY_MARKER = '<!-- rigour:summary -->';
@@ -78,6 +79,8 @@ export function summaryBody(report, total, inline, alreadyPosted) {
78
79
  lines.push('', `${report.context_findings.length} note(s) elsewhere in changed files (not blocking).`);
79
80
  if (report.receipt && report.receipt.functions > 0)
80
81
  lines.push('', ...receiptLines(report.receipt));
82
+ if (report.reviewer?.record)
83
+ lines.push('', ...recordLines(report.reviewer.record));
81
84
  if (report.dismissed)
82
85
  lines.push('', `${report.dismissed} finding(s) on changed lines were dismissed as not a bug (.rigour/dismissed.json).`);
83
86
  if (report.control_files_changed?.length) {
@@ -1,5 +1,6 @@
1
+ import { buildRecord } from '@rigour-labs/core';
1
2
  import { describe, expect, it } from 'vitest';
2
- import { findingKey, postReview, rankFindings, receiptLines } from './review-post.js';
3
+ import { findingKey, postReview, rankFindings, receiptLines, summaryBody } from './review-post.js';
3
4
  const target = { token: 't', repo: 'acme/app', pr: 7, sha: 'abc123' };
4
5
  const BOT = { login: 'github-actions[bot]' };
5
6
  function finding(over) {
@@ -53,6 +54,18 @@ const report = {
53
54
  ],
54
55
  deep: { model: 'anthropic/claude-sonnet-5.5', cost_usd: 0.0421, router: { routed: 3, functions: 11, already_reviewed: 2 } },
55
56
  };
57
+ describe('the summary with a review record', () => {
58
+ it('carries the record: blocks, should-fixes, counts, judges and the hash', () => {
59
+ const record = buildRecord({ head: 'abcdef0123456789', base: '0123456789abcdef', scope: 'full', verdict: { prior_points: [], redundant: [], reads: [], scans: [], merge_impact: [], findings: [], carried: [], resolved_previous: [] },
60
+ accounted: { open: [{ id: 'i1', kind: 'finding', class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock' }], advisory: [], unverified: [], notes: [], resolved: [], answerInReply: [], disputed: [], dismissed: [] },
61
+ judges: [{ reviewer: 'claude', cost_usd: 0.5 }], lessonsServed: 0, humanReviews: 0 });
62
+ const body = summaryBody({ ...report, reviewer: { record } }, 1, 1, 0);
63
+ expect(body).toContain('**Review record** · 1 blocking · 0 should-fix');
64
+ expect(body).toContain('- **Blocking** `src/job.ts:2` returns before the lock');
65
+ expect(body).toContain(`Integrity \`${record.integrity.slice(0, 16)}\``);
66
+ expect(summaryBody(report, 1, 1, 0)).not.toContain('Review record');
67
+ });
68
+ });
56
69
  describe('rankFindings', () => {
57
70
  it('puts severity first, then the most provable source', () => {
58
71
  expect(rankFindings(report.failures).map(f => f.gate)).toEqual(['Leak', 'Race', 'NoLine', 'Style']);
@@ -2,6 +2,8 @@ import { type Config, type ReviewerResult, type ReviewResult, type RunChoice } f
2
2
  /** The base a branch review runs against: the one named, else where the branch left main. */
3
3
  export declare function reviewerBase(cwd: string, named: string | undefined): string | undefined;
4
4
  export declare function reviewerFor(cwd: string, base: string | undefined, config: Config, full: boolean, choice: RunChoice, review: ReviewResult): Promise<ReviewerResult>;
5
- export declare function printReviewer(result: ReviewerResult): void;
5
+ export declare function printReviewer(result: ReviewerResult, options?: {
6
+ notes?: boolean;
7
+ }): void;
6
8
  export declare function reviewerJson(result: ReviewerResult): Record<string, unknown>;
7
9
  export declare function printStatus(cwd: string, json: boolean): Promise<number>;
@@ -14,10 +14,12 @@ export function reviewerBase(cwd, named) {
14
14
  }
15
15
  export async function reviewerFor(cwd, base, config, full, choice, review) {
16
16
  if (!base)
17
- return { outcome: 'unavailable', items: [], unverified: [], resolved: [], answerInReply: [], notes: [], disputed: [], dropped: [], dismissed: [], reason: 'no base to review against: pass --base, or fetch the main branch', reviewers: [], cached: false };
17
+ return { outcome: 'unavailable', items: [], unverified: [], resolved: [], answerInReply: [], notes: [], advisory: [], disputed: [], dropped: [], dismissed: [], reason: 'no base to review against: pass --base, or fetch the main branch', reviewers: [], cached: false };
18
18
  return runReviewer(cwd, base, config, undefined, undefined, { trigger: 'review', full, choice, ...reviewerInputs(review) });
19
19
  }
20
- export function printReviewer(result) {
20
+ /** Verified should-fixes shown in full before the rest fold into a count. */
21
+ const ADVISORY_SHOWN = 5;
22
+ export function printReviewer(result, options = {}) {
21
23
  const who = result.reviewers.length ? result.reviewers.join(' + ') : 'no reviewer';
22
24
  console.log(chalk.bold('\n Reviewer') + chalk.dim(` ${who}${result.scope ? `, ${result.scope}${result.why ? ` (${result.why})` : ''}` : ''}${result.previousReview ? `; previous review: ${result.previousReview}` : '; no previous human review'}`));
23
25
  printMode(result);
@@ -36,18 +38,33 @@ export function printReviewer(result) {
36
38
  if (item.kind !== 'prior' && result.dismissable)
37
39
  console.log(chalk.dim(` not a bug? rigour dismiss ${item.id} --reason "…"`));
38
40
  }
39
- for (const item of result.disputed)
40
- console.log(chalk.yellow(` disputed, never blocks (no majority) ${itemLine(item)}`));
41
- for (const item of result.notes)
42
- console.log(chalk.dim(` note, never blocks (no wrong outcome or cost named) ${itemLine(item)}`));
43
- for (const item of result.dismissed)
44
- console.log(chalk.dim(` dismissed earlier as not a bug ${itemLine(item)}`));
45
- if (result.dropped.length)
46
- console.log(chalk.dim(` ${result.dropped.length} finding(s) refuted with evidence by the other judges (--json lists them)`));
47
- for (const item of result.unverified)
48
- console.log(chalk.dim(` unverified (names code the checkout does not have) ${itemLine(item)}`));
41
+ // What is shown gets the same discipline as what blocks: blocks in full, verified should-fixes capped, the rest one count.
42
+ const shown = result.advisory.slice(0, options.notes ? undefined : ADVISORY_SHOWN);
43
+ for (const item of shown)
44
+ console.log(chalk.yellow(` should fix (verified, never blocks) ${itemLine(item)}`));
49
45
  for (const point of result.answerInReply)
50
46
  console.log(chalk.dim(` answer in the reply ${point.point}${point.evidence ? `\n ${point.evidence}` : ''}`));
47
+ const folded = [
48
+ [result.advisory.length - shown.length, 'more should-fix'], [result.notes.length, 'working note'], [result.disputed.length, 'disputed'],
49
+ [result.unverified.length, 'unverified'], [result.dismissed.length, 'dismissed earlier'], [result.dropped.length, 'refuted by the other judges'],
50
+ ].filter(([n]) => n > 0);
51
+ if (options.notes) {
52
+ for (const item of result.disputed)
53
+ console.log(chalk.dim(` disputed, never blocks (no majority) ${itemLine(item)}`));
54
+ for (const item of result.notes)
55
+ console.log(chalk.dim(` note, never blocks (a working step, or no wrong outcome named) ${itemLine(item)}`));
56
+ for (const item of result.dismissed)
57
+ console.log(chalk.dim(` dismissed earlier as not a bug ${itemLine(item)}`));
58
+ for (const item of result.unverified)
59
+ console.log(chalk.dim(` unverified (a quote or a file the checkout does not have) ${itemLine(item)}`));
60
+ }
61
+ else if (folded.length) {
62
+ console.log(chalk.dim(` Also seen, never blocking: ${folded.map(([n, what]) => `${n} ${what}${n === 1 || what.startsWith('more') || what === 'disputed' || what === 'unverified' ? '' : 's'}`).join(', ')} (rigour review --reviewer --notes lists them)`));
63
+ }
64
+ if (result.rules?.checked)
65
+ console.log(chalk.dim(` repository rules answered: ${result.rules.checked} (${result.rules.broken} broken, ${result.rules.followed} followed, ${result.rules.notApplicable} not applicable)`));
66
+ if (result.record && result.recordPath)
67
+ console.log(chalk.dim(` record: ${result.recordPath} (integrity ${result.record.integrity.slice(0, 16)})`));
51
68
  const tokens = result.tokens ? `, ${(result.tokens.input + result.tokens.output).toLocaleString('en-US')} tokens` : '';
52
69
  const cost = `${result.costUsd !== undefined ? `, $${result.costUsd.toFixed(2)}` : ''}${tokens}`;
53
70
  console.log(` ${result.items.length} open item(s)${result.cached ? chalk.dim(' (cached for this commit)') : cost}\n`);
@@ -76,6 +93,8 @@ export function reviewerJson(result) {
76
93
  resolved: result.resolved,
77
94
  answer_in_reply: result.answerInReply,
78
95
  notes: result.notes,
96
+ advisory: result.advisory,
97
+ shown: { blocking: result.items.length, should_fix: Math.min(result.advisory.length, ADVISORY_SHOWN), folded: result.advisory.length - Math.min(result.advisory.length, ADVISORY_SHOWN) + result.notes.length + result.disputed.length + result.unverified.length + result.dismissed.length + result.dropped.length },
79
98
  disputed: result.disputed,
80
99
  dropped: result.dropped,
81
100
  dismissed: result.dismissed,
@@ -85,6 +104,7 @@ export function reviewerJson(result) {
85
104
  tokens: result.tokens ?? null,
86
105
  cached: result.cached,
87
106
  previous_review: result.previousReview ?? null,
107
+ record: result.record ?? null,
88
108
  pr: result.pr ?? null,
89
109
  };
90
110
  }
@@ -9,7 +9,7 @@ afterEach(() => {
9
9
  vi.restoreAllMocks();
10
10
  });
11
11
  const LOCK = { id: 'abcdef0123', kind: 'finding', class: 'correctness', file: 'src/job.ts', line: 2, issue: 'returns before the lock', consequence: 'two runs send the same email', reviewer: 'claude+codex' };
12
- const base = { outcome: 'findings', items: [LOCK], unverified: [], resolved: [], answerInReply: [], notes: [{ ...LOCK, id: 'n1', issue: 'could be shorter', consequence: undefined }], disputed: [{ ...LOCK, id: 'd1', issue: 'maybe slow' }], dropped: [{ ...LOCK, id: 'x1' }], dismissed: [], reviewers: ['claude', 'codex'], scope: 'full', cached: false, dismissable: true, costUsd: 0.42, tokens: { input: 26565, output: 24 } };
12
+ const base = { outcome: 'findings', items: [LOCK], unverified: [], resolved: [], answerInReply: [], notes: [{ ...LOCK, id: 'n1', issue: 'could be shorter', consequence: undefined }], advisory: [], disputed: [{ ...LOCK, id: 'd1', issue: 'maybe slow' }], dropped: [{ ...LOCK, id: 'x1' }], dismissed: [], reviewers: ['claude', 'codex'], scope: 'full', cached: false, dismissable: true, costUsd: 0.42, tokens: { input: 26565, output: 24 } };
13
13
  describe('the reviewer as the CLI prints it', () => {
14
14
  it('shows what ran against what was asked, why, what was refused, and only confirmed findings as open', () => {
15
15
  printReviewer({ ...base, mode: { asked: 'panel', ran: 'single', source: 'user', degraded: '3 judges asked, claude could run (not installed: codex)', refused: ['mode single (flag) refused: rigour.yml sets review.reviewer.panel: required'] } });
@@ -19,13 +19,26 @@ describe('the reviewer as the CLI prints it', () => {
19
19
  expect(text).toContain('OPEN');
20
20
  expect(text).toContain('consequence: two runs send the same email');
21
21
  expect(text).toContain('not a bug? rigour dismiss abcdef0123');
22
- expect(text).toContain('disputed, never blocks (no majority)');
23
- expect(text).toContain('note, never blocks');
24
- expect(text).toContain('1 finding(s) refuted with evidence by the other judges');
22
+ expect(text).not.toContain('disputed, never blocks'); // folded: what is shown gets the discipline of what blocks
23
+ expect(text).toMatch(/Also seen, never blocking: .*1 working note.*1 disputed.*1 refuted by the other judges.*--notes lists them/);
25
24
  expect(text).toContain('1 open item(s), $0.42, 26,589 tokens'); // a Codex judge reports tokens, not dollars
26
25
  });
26
+ it('shows at most five verified should-fixes, folds the rest, and lists everything with --notes', () => {
27
+ const advisory = Array.from({ length: 6 }, (_, i) => ({ ...LOCK, id: `s${i}`, issue: `should fix ${i}` }));
28
+ printReviewer({ ...base, advisory });
29
+ const text = out.join('\n');
30
+ expect(text.match(/should fix \(verified, never blocks\)/g)).toHaveLength(5);
31
+ expect(text).toContain('1 more should-fix');
32
+ out = [];
33
+ printReviewer({ ...base, advisory }, { notes: true });
34
+ const all = out.join('\n');
35
+ expect(all.match(/should fix \(verified, never blocks\)/g)).toHaveLength(6);
36
+ expect(all).toContain('disputed, never blocks (no majority)');
37
+ expect(all).toContain('note, never blocks');
38
+ expect(all).not.toContain('Also seen, never blocking');
39
+ });
27
40
  it('returns every decision in JSON', () => {
28
- expect(reviewerJson({ ...base, mode: { asked: 'panel', ran: 'panel', source: 'team' } })).toMatchObject({ blocks: true, mode: { ran: 'panel' }, disputed: [{ id: 'd1' }], notes: [{ id: 'n1' }], dropped: [{ id: 'x1' }] });
41
+ expect(reviewerJson({ ...base, mode: { asked: 'panel', ran: 'panel', source: 'team' } })).toMatchObject({ blocks: true, mode: { ran: 'panel' }, disputed: [{ id: 'd1' }], notes: [{ id: 'n1' }], shown: { blocking: 1, should_fix: 0 }, dropped: [{ id: 'x1' }] });
29
42
  });
30
43
  it('offers "not a bug" only where the team allows dismissals', () => {
31
44
  printReviewer({ ...base, dismissable: false });
@@ -161,7 +161,7 @@ async function print(result, options, receipt, reviewer, context, checked) {
161
161
  }
162
162
  printHuman(result, { ...context, receipt, all: options.all, notes: options.notes, showReceipt: options.receipt });
163
163
  if (reviewer)
164
- printReviewer(reviewer);
164
+ printReviewer(reviewer, { notes: options.notes });
165
165
  }
166
166
  function scopeOf(options) {
167
167
  if (options.diff)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/cli",
3
- "version": "6.7.5",
3
+ "version": "6.7.6",
4
4
  "description": "Code review that happens while your agent writes: deterministic checks on the lines it changed, quiet unless it can prove a defect, and learning from your team's fixes and dismissals. Works with Claude Code, Cursor, Codex, Cline and Windsurf.",
5
5
  "engines": {
6
6
  "node": ">=22.13"
@@ -56,13 +56,13 @@
56
56
  "inquirer": "9.2.16",
57
57
  "ora": "^8.0.1",
58
58
  "yaml": "^2.8.2",
59
- "@rigour-labs/core": "6.7.5"
59
+ "@rigour-labs/core": "6.7.6"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/fs-extra": "^11.0.4",
63
63
  "@types/inquirer": "9.0.7",
64
64
  "@types/node": "^25.0.3",
65
- "@rigour-labs/studio": "6.7.5"
65
+ "@rigour-labs/studio": "6.7.6"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc && pnpm bundle-studio",