@rigour-labs/core 6.11.0 → 6.11.1-rc.1
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/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/review-learning/learn-from-reviews.js +3 -2
- package/dist/review-learning/lessons.d.ts +16 -4
- package/dist/review-learning/lessons.js +74 -11
- package/dist/review-learning/review-points.d.ts +3 -1
- package/dist/review-learning/review-points.js +7 -2
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -49,7 +49,7 @@ export { decideCompiledCheck, proposeCompiledChecks, readCompiledChecks, suspens
|
|
|
49
49
|
export { ruleWriterFor } from './review/reviewer/rule-writer.js';
|
|
50
50
|
export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH, type LastReport } from './review/backtest-last.js';
|
|
51
51
|
export { buildRecord, recordLines, recordIntact, type ReviewRecord } from './review/reviewer/record.js';
|
|
52
|
-
export { readLessons, writeLessons, decideLesson, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath, type ReviewLesson, type LessonEvidence } from './review-learning/lessons.js';
|
|
52
|
+
export { readLessons, writeLessons, decideLesson, acceptSuggestedText, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath, type ReviewLesson, type LessonEvidence } from './review-learning/lessons.js';
|
|
53
53
|
export { lessonsForDiff, lessonsSection, type LessonMode } from './review-learning/team-lessons.js';
|
|
54
54
|
export { recordAgentWrites, captureHumanEdits } from './review-learning/human-edits.js';
|
|
55
55
|
export { readRepoRules, rulesForDiff, rulesSection, splitRules, type RepoRule } from './review-learning/repo-rules.js';
|
package/dist/index.js
CHANGED
|
@@ -47,7 +47,7 @@ export { decideCompiledCheck, proposeCompiledChecks, readCompiledChecks, suspens
|
|
|
47
47
|
export { ruleWriterFor } from './review/reviewer/rule-writer.js';
|
|
48
48
|
export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH } from './review/backtest-last.js';
|
|
49
49
|
export { buildRecord, recordLines, recordIntact } from './review/reviewer/record.js';
|
|
50
|
-
export { readLessons, writeLessons, decideLesson, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath } from './review-learning/lessons.js';
|
|
50
|
+
export { readLessons, writeLessons, decideLesson, acceptSuggestedText, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath } from './review-learning/lessons.js';
|
|
51
51
|
export { lessonsForDiff, lessonsSection } from './review-learning/team-lessons.js';
|
|
52
52
|
export { recordAgentWrites, captureHumanEdits } from './review-learning/human-edits.js';
|
|
53
53
|
export { readRepoRules, rulesForDiff, rulesSection, splitRules } from './review-learning/repo-rules.js';
|
|
@@ -39,8 +39,9 @@ export async function learnFromReviews(cwd, options) {
|
|
|
39
39
|
}
|
|
40
40
|
const known = new Set(readLessons(cwd).flatMap(l => l.evidence.map(e => e.comment)));
|
|
41
41
|
const points = lessons.flatMap(l => l.evidence).filter(e => e.kind === 'point' && !known.has(e.comment));
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
// Comments read before go through too: each lands on its own lesson (mergeLessons matches the comment), which
|
|
43
|
+
// takes the text this version derives from it. Nothing is added twice.
|
|
44
|
+
const merged = mergeLessons(readLessons(cwd), lessons);
|
|
44
45
|
// Outcomes accrue after the merge: every candidate whose pull request was read this run is checked again.
|
|
45
46
|
if (options.mainRef) {
|
|
46
47
|
const byNumber = new Map(prs.filter(pr => pr.mergedAt).map(pr => [pr.number, pr]));
|
|
@@ -18,7 +18,7 @@ import type { Git, ReviewBody, ReviewComment } from './acted-on.js';
|
|
|
18
18
|
* until a person promotes it.
|
|
19
19
|
* A record from before evidence kinds has none: it is a `point`.
|
|
20
20
|
*/
|
|
21
|
-
export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified' | 'compiled';
|
|
21
|
+
export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified' | 'compiled' | 'reworded';
|
|
22
22
|
export interface LessonEvidence {
|
|
23
23
|
kind?: EvidenceKind;
|
|
24
24
|
pr: number;
|
|
@@ -50,6 +50,13 @@ export interface ReviewLesson {
|
|
|
50
50
|
/** Which evidence made it a lesson. */
|
|
51
51
|
promotedBy?: 'outcome' | 'correction' | 'person' | 'recurrence' | 'legacy';
|
|
52
52
|
evidence: LessonEvidence[];
|
|
53
|
+
/**
|
|
54
|
+
* A corrected wording a newer version derived from the lesson's own comment, waiting for a person. A lesson
|
|
55
|
+
* a person decided keeps the wording they decided on until they take this one (acceptSuggestedText).
|
|
56
|
+
*/
|
|
57
|
+
suggestedText?: string;
|
|
58
|
+
/** Why there is a suggestion: `parser fix`, a newer version reading the comment better. */
|
|
59
|
+
suggestedWhy?: string;
|
|
53
60
|
/** Where the point sits in the commit it was made on, for outcome evidence (inline comments only). */
|
|
54
61
|
at?: {
|
|
55
62
|
commit: string;
|
|
@@ -65,7 +72,10 @@ export interface ReviewLesson {
|
|
|
65
72
|
* evidence, never a promotion. A record from before evidence kinds keeps the state it had.
|
|
66
73
|
*/
|
|
67
74
|
export declare function lessonState(lesson: ReviewLesson): Pick<ReviewLesson, 'state' | 'promotedBy'>;
|
|
68
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* The point of a review comment: its bold title, else its first sentence and, when that only says what is
|
|
77
|
+
* wrong, the first later sentence that says what to do. Without tool output or markup.
|
|
78
|
+
*/
|
|
69
79
|
export declare function lessonText(body: string): string;
|
|
70
80
|
/** Identifiers a lesson is about: those the comment names, then those on the lines it pointed at. */
|
|
71
81
|
export declare function lessonSymbols(body: string, codeLines: string[]): string[];
|
|
@@ -78,8 +88,8 @@ export declare function lessonFromComment(git: Git, comment: ReviewComment, at?:
|
|
|
78
88
|
*/
|
|
79
89
|
export declare function lessonsFromReview(review: ReviewBody, changedAfter: string[], at?: string): ReviewLesson[];
|
|
80
90
|
/**
|
|
81
|
-
* Add lessons to the store. The same lesson again (same text,
|
|
82
|
-
* file with two shared symbols) adds evidence; acted on in enough PRs, it is
|
|
91
|
+
* Add lessons to the store. The same lesson again (isSameLesson: the same text, the same review
|
|
92
|
+
* comment, or the same file with two shared symbols) adds evidence; acted on in enough PRs, it is
|
|
83
93
|
* verified.
|
|
84
94
|
*/
|
|
85
95
|
export declare function mergeLessons(existing: ReviewLesson[], incoming: ReviewLesson[]): {
|
|
@@ -118,6 +128,8 @@ export declare function pendingDecision(lesson: ReviewLesson): LessonEvidence |
|
|
|
118
128
|
export declare function writeLessons(cwd: string, lessons: ReviewLesson[]): void;
|
|
119
129
|
/** A person's decision on a lesson, kept as evidence: accepted makes it a lesson, rejected an anti-lesson. Undefined for an unknown id. */
|
|
120
130
|
export declare function decideLesson(cwd: string, id: string, decision: 'accepted' | 'rejected' | 'dismissed', by: string, why?: string): ReviewLesson | undefined;
|
|
131
|
+
/** A person takes a decided lesson's suggested wording; who and the wording it replaced are kept as evidence. */
|
|
132
|
+
export declare function acceptSuggestedText(cwd: string, id: string, by: string): ReviewLesson | undefined;
|
|
121
133
|
/** An identifier specific enough to link two pieces of code: camelCase, snake_case, or long. */
|
|
122
134
|
export declare function isSpecific(symbol: string): boolean;
|
|
123
135
|
/** The meaningful words of a text or an identifier: `hasLaterAttempt` and "a later attempt" share later and attempt. */
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import crypto from 'crypto';
|
|
16
16
|
import fs from 'fs';
|
|
17
17
|
import path from 'path';
|
|
18
|
-
import { bodyPoints } from './review-points.js';
|
|
18
|
+
import { bodyPoints, withoutEmphasis } from './review-points.js';
|
|
19
19
|
const STORE = path.join('.rigour', 'review-lessons.json');
|
|
20
20
|
const MAX_TEXT = 220;
|
|
21
21
|
const MAX_SYMBOLS = 8;
|
|
@@ -69,7 +69,12 @@ export function lessonState(lesson) {
|
|
|
69
69
|
const independent = reviewers >= 2 || wordings >= 2;
|
|
70
70
|
return prs >= RECUR_PRS && authors >= RECUR_AUTHORS && independent ? { state: 'verified', promotedBy: 'recurrence' } : { state: 'candidate' };
|
|
71
71
|
}
|
|
72
|
-
/**
|
|
72
|
+
/** A sentence that tells the author what to do, not only what is wrong. */
|
|
73
|
+
const INSTRUCTION = /^(?:please\s+)?(?:use|pick|avoid|don'?t|do not|never|always|make|keep|move|read|filter|check|change|drop|add|remove|pass|split|prefer|replace|rename|derive|select|return|validate|extract|bound|lock)\b|\b(?:should|must|instead)\b/i;
|
|
74
|
+
/**
|
|
75
|
+
* The point of a review comment: its bold title, else its first sentence and, when that only says what is
|
|
76
|
+
* wrong, the first later sentence that says what to do. Without tool output or markup.
|
|
77
|
+
*/
|
|
73
78
|
export function lessonText(body) {
|
|
74
79
|
const cleaned = body
|
|
75
80
|
.replace(/<!--[\s\S]*?-->/g, '')
|
|
@@ -80,8 +85,10 @@ export function lessonText(body) {
|
|
|
80
85
|
.filter(line => !/^\s*#*\s*\**\s*(low|medium|high|critical)\s+severity\s*\**\s*$/i.test(line))
|
|
81
86
|
.join('\n');
|
|
82
87
|
const bold = /\*\*(.+?)\*\*/.exec(cleaned)?.[1]?.trim();
|
|
83
|
-
const
|
|
84
|
-
|
|
88
|
+
const sentences = cleaned.replace(/\s+/g, ' ').trim().split(/(?<=[.!?])\s/);
|
|
89
|
+
const instruction = INSTRUCTION.test(sentences[0] ?? '') ? undefined : sentences.slice(1).find(s => INSTRUCTION.test(s));
|
|
90
|
+
const text = bold || [sentences[0] ?? '', instruction ?? ''].filter(Boolean).join(' ');
|
|
91
|
+
return pointText(withoutEmphasis(text)).slice(0, MAX_TEXT).trim();
|
|
85
92
|
}
|
|
86
93
|
/**
|
|
87
94
|
* A point's words without a review bot's scaffolding (`In src/a.ts around lines 10-12:`), or '' when
|
|
@@ -130,7 +137,7 @@ export function lessonFromComment(git, comment, at = new Date().toISOString()) {
|
|
|
130
137
|
*/
|
|
131
138
|
export function lessonsFromReview(review, changedAfter, at = new Date().toISOString()) {
|
|
132
139
|
return bodyPoints(review.body).flatMap((point, i) => {
|
|
133
|
-
const text = pointText(point
|
|
140
|
+
const text = pointText(withoutEmphasis(point)).slice(0, MAX_TEXT).trim();
|
|
134
141
|
if (text.length < 12)
|
|
135
142
|
return [];
|
|
136
143
|
const named = /(?:^|[\s`(])((?:[\w.-]+\/)+[\w.-]+\.\w+)/.exec(point)?.[1];
|
|
@@ -143,18 +150,15 @@ export function lessonsFromReview(review, changedAfter, at = new Date().toISOStr
|
|
|
143
150
|
});
|
|
144
151
|
}
|
|
145
152
|
/**
|
|
146
|
-
* Add lessons to the store. The same lesson again (same text,
|
|
147
|
-
* file with two shared symbols) adds evidence; acted on in enough PRs, it is
|
|
153
|
+
* Add lessons to the store. The same lesson again (isSameLesson: the same text, the same review
|
|
154
|
+
* comment, or the same file with two shared symbols) adds evidence; acted on in enough PRs, it is
|
|
148
155
|
* verified.
|
|
149
156
|
*/
|
|
150
157
|
export function mergeLessons(existing, incoming) {
|
|
151
158
|
const lessons = existing.map(l => ({ ...l, evidence: [...l.evidence] }));
|
|
152
159
|
let added = 0;
|
|
153
160
|
for (const lesson of incoming) {
|
|
154
|
-
|
|
155
|
-
const otherPr = (l) => !l.evidence.some(e => lesson.evidence.some(x => x.pr === e.pr));
|
|
156
|
-
const same = lessons.find(l => l.id === lesson.id || (l.file === lesson.file && shared(l.symbols, lesson.symbols) >= 2 && otherPr(l))
|
|
157
|
-
|| (!l.file && !lesson.file && sameWords(l.text, lesson.text)));
|
|
161
|
+
const same = lessons.find(l => isSameLesson(l, lesson));
|
|
158
162
|
if (!same) {
|
|
159
163
|
lessons.push({ ...lesson, evidence: [...lesson.evidence] });
|
|
160
164
|
added++;
|
|
@@ -163,6 +167,7 @@ export function mergeLessons(existing, incoming) {
|
|
|
163
167
|
for (const e of lesson.evidence)
|
|
164
168
|
if (!same.evidence.some(x => x.comment === e.comment && x.kind === e.kind))
|
|
165
169
|
same.evidence.push(e);
|
|
170
|
+
refreshText(same, lesson);
|
|
166
171
|
same.updatedAt = lesson.updatedAt;
|
|
167
172
|
}
|
|
168
173
|
let verified = 0;
|
|
@@ -174,6 +179,49 @@ export function mergeLessons(existing, incoming) {
|
|
|
174
179
|
}
|
|
175
180
|
return { lessons, added, verified };
|
|
176
181
|
}
|
|
182
|
+
/** Whether a lesson carries the review point `point` (the same comment on the same pull request). */
|
|
183
|
+
function hasPoint(lesson, point) {
|
|
184
|
+
return lesson.evidence.some(x => x.kind === 'point' && x.pr === point.pr && x.comment === point.comment);
|
|
185
|
+
}
|
|
186
|
+
/** A person accepted, rejected or dismissed it, or approved or took back a check compiled from it. */
|
|
187
|
+
function decidedByPerson(lesson) {
|
|
188
|
+
return lesson.evidence.some(e => e.kind === 'accepted' || e.kind === 'rejected' || e.kind === 'dismissed' || e.kind === 'compiled');
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Re-read from the comment it was learned from, an undecided lesson takes the text this version derives; its id
|
|
192
|
+
* and evidence stay. One a person decided keeps the wording they decided on, and the new one waits as a
|
|
193
|
+
* suggestion for them. A later comment merged into a lesson never rewrites it.
|
|
194
|
+
*/
|
|
195
|
+
function refreshText(stored, lesson) {
|
|
196
|
+
const origin = stored.evidence.find(e => e.kind === 'point');
|
|
197
|
+
if (stored.id === lesson.id || !origin || !hasPoint(lesson, origin))
|
|
198
|
+
return;
|
|
199
|
+
if (lesson.text === stored.text) {
|
|
200
|
+
delete stored.suggestedText;
|
|
201
|
+
delete stored.suggestedWhy;
|
|
202
|
+
}
|
|
203
|
+
else if (decidedByPerson(stored)) {
|
|
204
|
+
stored.suggestedText = lesson.text;
|
|
205
|
+
stored.suggestedWhy = 'parser fix';
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
stored.text = lesson.text;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The same lesson: the same id; the same review comment read again, whatever text an earlier version took from it;
|
|
212
|
+
* shared names on another pull request (two points on one pull request are two points); or a team standard in the
|
|
213
|
+
* same words.
|
|
214
|
+
*/
|
|
215
|
+
function isSameLesson(stored, lesson) {
|
|
216
|
+
if (stored.id === lesson.id)
|
|
217
|
+
return true;
|
|
218
|
+
if (stored.evidence.some(e => e.kind === 'point' && hasPoint(lesson, e)))
|
|
219
|
+
return true;
|
|
220
|
+
const otherPr = !stored.evidence.some(e => lesson.evidence.some(x => x.pr === e.pr));
|
|
221
|
+
if (stored.file === lesson.file && shared(stored.symbols, lesson.symbols) >= 2 && otherPr)
|
|
222
|
+
return true;
|
|
223
|
+
return !stored.file && !lesson.file && sameWords(stored.text, lesson.text);
|
|
224
|
+
}
|
|
177
225
|
/**
|
|
178
226
|
* Lessons that apply to a change, best first. A lesson applies when the change
|
|
179
227
|
* touches its file, or shares at least two specific identifiers with it; a
|
|
@@ -278,6 +326,21 @@ export function decideLesson(cwd, id, decision, by, why = '') {
|
|
|
278
326
|
writeLessons(cwd, lessons);
|
|
279
327
|
return lesson;
|
|
280
328
|
}
|
|
329
|
+
/** A person takes a decided lesson's suggested wording; who and the wording it replaced are kept as evidence. */
|
|
330
|
+
export function acceptSuggestedText(cwd, id, by) {
|
|
331
|
+
const lessons = readLessons(cwd);
|
|
332
|
+
const lesson = lessons.find(l => l.id === id);
|
|
333
|
+
if (!lesson?.suggestedText)
|
|
334
|
+
return undefined;
|
|
335
|
+
const at = new Date().toISOString();
|
|
336
|
+
lesson.evidence.push({ kind: 'reworded', pr: lesson.evidence[0]?.pr ?? 0, comment: `reworded-${at}`, author: by, detail: `${lesson.suggestedWhy ?? 'reworded'}; was: ${lesson.text}`, at });
|
|
337
|
+
lesson.text = lesson.suggestedText;
|
|
338
|
+
delete lesson.suggestedText;
|
|
339
|
+
delete lesson.suggestedWhy;
|
|
340
|
+
lesson.updatedAt = at;
|
|
341
|
+
writeLessons(cwd, lessons);
|
|
342
|
+
return lesson;
|
|
343
|
+
}
|
|
281
344
|
/** An identifier specific enough to link two pieces of code: camelCase, snake_case, or long. */
|
|
282
345
|
export function isSpecific(symbol) {
|
|
283
346
|
return /[a-z][A-Z]|[A-Za-z]_[A-Za-z]/.test(symbol) || symbol.length >= 10;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** The bullet and numbered lines of a review body: the points a reviewer makes outside any one line. */
|
|
2
2
|
export declare function bodyPoints(body: string): string[];
|
|
3
|
-
/**
|
|
3
|
+
/** Markdown emphasis off, never the underscores of an identifier (`RESTOCK_CHUNK`) or anything inside a code span. */
|
|
4
|
+
export declare function withoutEmphasis(text: string): string;
|
|
5
|
+
/** A comment's first non-empty line, heading marks and emphasis removed, at most 140 characters. */
|
|
4
6
|
export declare function firstLine(text: string): string;
|
|
@@ -7,8 +7,13 @@ const BULLET = /^\s*(?:[-*]|\d+[.)])\s+(.{8,})$/;
|
|
|
7
7
|
export function bodyPoints(body) {
|
|
8
8
|
return body.split('\n').map(l => BULLET.exec(l)?.[1]).filter((l) => !!l).map(firstLine);
|
|
9
9
|
}
|
|
10
|
-
/**
|
|
10
|
+
/** Markdown emphasis off, never the underscores of an identifier (`RESTOCK_CHUNK`) or anything inside a code span. */
|
|
11
|
+
export function withoutEmphasis(text) {
|
|
12
|
+
return text.split(/(`[^`]*`)/).map(part => part.startsWith('`') ? part
|
|
13
|
+
: part.replace(/\*+/g, '').replace(/(^|[^\w])__?(?=\S)(.*?\S)__?(?=[^\w]|$)/g, '$1$2')).join('');
|
|
14
|
+
}
|
|
15
|
+
/** A comment's first non-empty line, heading marks and emphasis removed, at most 140 characters. */
|
|
11
16
|
export function firstLine(text) {
|
|
12
|
-
const line = text.split('\n').find(l => l.trim())?.replace(
|
|
17
|
+
const line = withoutEmphasis(text.split('\n').find(l => l.trim())?.replace(/^\s*#+\s*/, '') ?? '').trim();
|
|
13
18
|
return line.length > 140 ? `${line.slice(0, 137)}...` : line;
|
|
14
19
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.11.
|
|
3
|
+
"version": "6.11.1-rc.1",
|
|
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"
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"@anthropic-ai/sdk": "^0.132.1",
|
|
76
76
|
"pg": "^8.16.3",
|
|
77
77
|
"openai": "^5.23.2",
|
|
78
|
-
"@rigour-labs/brain-darwin-arm64": "6.11.
|
|
79
|
-
"@rigour-labs/brain-darwin-x64": "6.11.
|
|
80
|
-
"@rigour-labs/brain-linux-
|
|
81
|
-
"@rigour-labs/brain-
|
|
82
|
-
"@rigour-labs/brain-
|
|
78
|
+
"@rigour-labs/brain-darwin-arm64": "6.11.1-rc.1",
|
|
79
|
+
"@rigour-labs/brain-darwin-x64": "6.11.1-rc.1",
|
|
80
|
+
"@rigour-labs/brain-linux-x64": "6.11.1-rc.1",
|
|
81
|
+
"@rigour-labs/brain-linux-arm64": "6.11.1-rc.1",
|
|
82
|
+
"@rigour-labs/brain-win-x64": "6.11.1-rc.1"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/fs-extra": "^11.0.4",
|