@rigour-labs/core 6.7.4 → 6.7.5
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.
|
@@ -21,8 +21,8 @@ const MAX_TEXT = 220;
|
|
|
21
21
|
const MAX_SYMBOLS = 8;
|
|
22
22
|
/**
|
|
23
23
|
* Recurrence is weak evidence, and only when independent: the same point on this many pull requests,
|
|
24
|
-
* by this many different authors, raised by different reviewers or by one
|
|
25
|
-
* (a senior re-raising a standard counts; a bot
|
|
24
|
+
* by this many different authors, raised by different reviewers or by one person in different words
|
|
25
|
+
* (a senior re-raising a standard counts; a bot rewording its own point on every pull request does not).
|
|
26
26
|
*/
|
|
27
27
|
const RECUR_PRS = 2;
|
|
28
28
|
const RECUR_AUTHORS = 2;
|
|
@@ -61,7 +61,8 @@ export function lessonState(lesson) {
|
|
|
61
61
|
const prs = new Set(points.map(e => e.pr)).size;
|
|
62
62
|
const authors = new Set(points.map(e => e.prAuthor).filter(Boolean)).size;
|
|
63
63
|
const reviewers = new Set(points.map(e => e.author).filter(Boolean)).size;
|
|
64
|
-
|
|
64
|
+
// One person raising it again in their own words is a standard; a bot rewording its own point on every pull request is not.
|
|
65
|
+
const wordings = new Set(points.filter(e => e.source !== 'bot').map(e => normalize(e.text ?? '')).filter(Boolean)).size;
|
|
65
66
|
const independent = reviewers >= 2 || wordings >= 2;
|
|
66
67
|
return prs >= RECUR_PRS && authors >= RECUR_AUTHORS && independent ? { state: 'verified', promotedBy: 'recurrence' } : { state: 'candidate' };
|
|
67
68
|
}
|
|
@@ -77,7 +78,15 @@ export function lessonText(body) {
|
|
|
77
78
|
.join('\n');
|
|
78
79
|
const bold = /\*\*(.+?)\*\*/.exec(cleaned)?.[1]?.trim();
|
|
79
80
|
const text = bold || cleaned.replace(/\s+/g, ' ').trim().split(/(?<=[.!?])\s/)[0] || '';
|
|
80
|
-
return text.replace(/[*_]/g, '').slice(0, MAX_TEXT).trim();
|
|
81
|
+
return pointText(text.replace(/[*_]/g, '')).slice(0, MAX_TEXT).trim();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A point's words without a review bot's scaffolding (`In src/a.ts around lines 10-12:`), or '' when
|
|
85
|
+
* nothing but a path or a location is left: a file name alone teaches nothing.
|
|
86
|
+
*/
|
|
87
|
+
function pointText(text) {
|
|
88
|
+
const bare = text.trim().replace(/^(in\s+\S+\s+)?around\s+lines?\s+[\d\s,–-]+:\s*/i, '').trim();
|
|
89
|
+
return /^[`'"]?[\w@.~/[\]()+-]+[`'"]?:?$/.test(bare) ? '' : bare;
|
|
81
90
|
}
|
|
82
91
|
/** Identifiers a lesson is about: those the comment names, then those on the lines it pointed at. */
|
|
83
92
|
export function lessonSymbols(body, codeLines) {
|
|
@@ -118,7 +127,7 @@ export function lessonFromComment(git, comment, at = new Date().toISOString()) {
|
|
|
118
127
|
*/
|
|
119
128
|
export function lessonsFromReview(review, changedAfter, at = new Date().toISOString()) {
|
|
120
129
|
return bodyPoints(review.body).flatMap((point, i) => {
|
|
121
|
-
const text = point.replace(/[*_]/g, '').slice(0, MAX_TEXT).trim();
|
|
130
|
+
const text = pointText(point.replace(/[*_]/g, '')).slice(0, MAX_TEXT).trim();
|
|
122
131
|
if (text.length < 12)
|
|
123
132
|
return [];
|
|
124
133
|
const named = /(?:^|[\s`(])((?:[\w.-]+\/)+[\w.-]+\.\w+)/.exec(point)?.[1];
|
|
@@ -8,7 +8,7 @@ import { learnFromReviews } from './learn-from-reviews.js';
|
|
|
8
8
|
import { outcomeFor } from './outcomes.js';
|
|
9
9
|
import { rulesFromReviews } from './rules-from-reviews.js';
|
|
10
10
|
import { describeLesson, lessonView } from './team-lessons.js';
|
|
11
|
-
import { decideLesson, isSpecific, lessonState, lessonText, matchLessons, mergeLessons, readLessons, writeLessons } from './lessons.js';
|
|
11
|
+
import { decideLesson, isSpecific, lessonState, lessonText, lessonsFromReview, matchLessons, mergeLessons, readLessons, writeLessons } from './lessons.js';
|
|
12
12
|
import { lessonsForDiff, lessonsSection } from './team-lessons.js';
|
|
13
13
|
let repo;
|
|
14
14
|
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' }).trim();
|
|
@@ -40,6 +40,14 @@ describe('acted on', () => {
|
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
describe('lessons', () => {
|
|
43
|
+
it('never makes a lesson of a bare path or a review bot\'s line-range scaffolding', () => {
|
|
44
|
+
expect(lessonText('src/routes/learner/+layout.ts')).toBe('');
|
|
45
|
+
expect(lessonText('`src/routes/learner/api/[planId]/+server.ts`')).toBe('');
|
|
46
|
+
expect(lessonText('In src/a.ts around lines 10-12: Check the lock before the first read.')).toBe('Check the lock before the first read.');
|
|
47
|
+
expect(lessonText('Around line 124-141: Add a test for the failed save.')).toBe('Add a test for the failed save.');
|
|
48
|
+
expect(lessonsFromReview({ id: '1', prNumber: 3, commit: 'c', author: 'rabbit[bot]', source: 'bot', body: '- `src/a/b.ts`\n- Bound both ends of the time window.' }, ['src/a/b.ts']).map(l => l.text))
|
|
49
|
+
.toEqual(['Bound both ends of the time window.']);
|
|
50
|
+
});
|
|
43
51
|
it('keeps the point of a review comment and drops tool output and markup', () => {
|
|
44
52
|
const body = '_⚠️ Potential issue_ | _🟠 Major_\n\n**Use an upsert keyed on `id`.**\n\n<details>\n<summary>🏁 Script executed</summary>\nrg -n insert\n</details>';
|
|
45
53
|
expect(lessonText(body)).toBe('Use an upsert keyed on `id`.');
|
|
@@ -53,6 +61,7 @@ describe('lessons', () => {
|
|
|
53
61
|
expect(lessonState(lesson(point(1, 'ann'), point(2, 'ann')))).toEqual({ state: 'candidate' }); // two PRs, one author: weak, not enough
|
|
54
62
|
expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob', 'bot')))).toEqual({ state: 'verified', promotedBy: 'recurrence' }); // a bot's point counts the same
|
|
55
63
|
const said = (e, text) => ({ ...e, text });
|
|
64
|
+
expect(lessonState(lesson(said(point(1, 'ann', 'bot', 'helper[bot]'), 'Around line 60-103: update the callers.'), said(point(2, 'bob', 'bot', 'helper[bot]'), 'Around line 12-14: update the loader.')))).toEqual({ state: 'candidate' }); // a bot rewording its own point is one source
|
|
56
65
|
expect(lessonState(lesson(said(point(1, 'ann', 'bot', 'helper[bot]'), 'Consider more tests.'), said(point(2, 'bob', 'bot', 'helper[bot]'), 'Consider more tests.')))).toEqual({ state: 'candidate' }); // one bot's template on every PR is one source
|
|
57
66
|
expect(lessonState(lesson(said(point(1, 'ann', 'person', 'lead'), 'Regenerate the API client after changing the schema.'), said(point(2, 'bob', 'person', 'lead'), 'The generated client is stale again.')))).toEqual({ state: 'verified', promotedBy: 'recurrence' }); // a senior re-raising it in their own words
|
|
58
67
|
expect(lessonState(lesson(point(1, 'ann'), point(2, 'bob'), { kind: 'norule', pr: 1, comment: 'n', author: '' }))).toEqual({ state: 'candidate' }); // no rule in it: never promoted again
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.7.
|
|
3
|
+
"version": "6.7.5",
|
|
4
4
|
"description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.13"
|
|
@@ -72,11 +72,11 @@
|
|
|
72
72
|
"@anthropic-ai/sdk": "^0.30.1",
|
|
73
73
|
"pg": "^8.16.3",
|
|
74
74
|
"openai": "^5.23.2",
|
|
75
|
-
"@rigour-labs/brain-darwin-
|
|
76
|
-
"@rigour-labs/brain-
|
|
77
|
-
"@rigour-labs/brain-
|
|
78
|
-
"@rigour-labs/brain-
|
|
79
|
-
"@rigour-labs/brain-
|
|
75
|
+
"@rigour-labs/brain-darwin-x64": "6.7.5",
|
|
76
|
+
"@rigour-labs/brain-linux-arm64": "6.7.5",
|
|
77
|
+
"@rigour-labs/brain-win-x64": "6.7.5",
|
|
78
|
+
"@rigour-labs/brain-darwin-arm64": "6.7.5",
|
|
79
|
+
"@rigour-labs/brain-linux-x64": "6.7.5"
|
|
80
80
|
},
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@types/fs-extra": "^11.0.4",
|