@rigour-labs/core 6.8.1 → 6.9.0-rc.2
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/goal/goal.d.ts +34 -0
- package/dist/goal/goal.js +188 -0
- package/dist/goal/hook.d.ts +7 -0
- package/dist/goal/hook.js +100 -0
- package/dist/hooks/stop-review.js +4 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/outcomes/outcome.d.ts +80 -0
- package/dist/outcomes/outcome.js +161 -0
- package/dist/outcomes/run.d.ts +26 -0
- package/dist/outcomes/run.js +63 -0
- package/dist/review/backtest-init.d.ts +9 -4
- package/dist/review/backtest-init.js +2 -2
- package/dist/review/quiet.js +2 -1
- package/dist/review/review.d.ts +5 -0
- package/dist/review/review.js +23 -2
- package/dist/review/reviewer/context.d.ts +11 -0
- package/dist/review/reviewer/context.js +10 -4
- package/dist/review/reviewer/prompt.d.ts +8 -1
- package/dist/review/reviewer/prompt.js +21 -2
- package/dist/review/reviewer/verdict.d.ts +12 -1
- package/dist/review/reviewer/verdict.js +8 -1
- package/dist/review/reviewer.d.ts +4 -0
- package/dist/review/reviewer.js +14 -6
- package/dist/review-learning/lessons.d.ts +11 -3
- package/dist/review-learning/lessons.js +3 -0
- package/dist/review-learning/outcome-evidence.d.ts +41 -0
- package/dist/review-learning/outcome-evidence.js +72 -0
- package/dist/review-learning/outcomes.d.ts +8 -0
- package/dist/review-learning/outcomes.js +11 -5
- package/dist/settings.d.ts +2 -0
- package/dist/switches.d.ts +640 -0
- package/dist/switches.js +36 -0
- package/dist/task/thread.d.ts +9 -1
- package/dist/task/thread.js +33 -1
- package/dist/templates/universal-config.js +4 -0
- package/dist/types/fix-packet.d.ts +1 -1
- package/dist/types/index.d.ts +56 -0
- package/dist/types/index.js +18 -0
- package/package.json +6 -6
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { lessonState } from './lessons.js';
|
|
2
|
+
import { outcomeFor } from './outcomes.js';
|
|
3
|
+
/** Lines either side of a point's own that count as its lines, and the most files a fixing commit may touch to say anything about one point. */
|
|
4
|
+
const POINT_SLACK = 3;
|
|
5
|
+
const MAX_FIX_FILES = 15;
|
|
6
|
+
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
|
7
|
+
/**
|
|
8
|
+
* Adds the evidence the settled and unsettled records give, to `lessons` in place, once each (by its comment key), and
|
|
9
|
+
* recomputes their states. `applied` is, per pull request, the lessons a review of it recorded as applying.
|
|
10
|
+
*/
|
|
11
|
+
export function applyOutcomeEvidence(lessons, records, applied, options) {
|
|
12
|
+
const byPr = new Map(records.map(r => [r.pr, r]));
|
|
13
|
+
const result = { added: 0, suggested: [], demoted: [] };
|
|
14
|
+
for (const lesson of lessons) {
|
|
15
|
+
const before = lesson.state;
|
|
16
|
+
const add = (evidence) => {
|
|
17
|
+
if (lesson.evidence.some(e => e.comment === evidence.comment))
|
|
18
|
+
return false;
|
|
19
|
+
lesson.evidence.push(evidence);
|
|
20
|
+
result.added++;
|
|
21
|
+
return true;
|
|
22
|
+
};
|
|
23
|
+
const own = new Set(lesson.evidence.filter(e => (e.kind ?? 'point') === 'point').map(e => e.pr));
|
|
24
|
+
// For it: a point its pull request left alone, whose own lines a later fix inside the window changed.
|
|
25
|
+
for (const point of lesson.evidence.filter(e => (e.kind ?? 'point') === 'point' && e.actedOn === false)) {
|
|
26
|
+
const record = byPr.get(point.pr);
|
|
27
|
+
// Only where the record shows a fix on the file at all: following lines costs a git walk, done only then.
|
|
28
|
+
const fileFix = lesson.file ? record?.followUps.find(f => f.fix && f.files.includes(lesson.file)) : undefined;
|
|
29
|
+
if (!record || !fileFix)
|
|
30
|
+
continue;
|
|
31
|
+
const context = [record.ci === 'failure' ? 'CI regressed on the merge commit' : '', record.reverted ? `the pull request was reverted by ${record.reverted.sha.slice(0, 9)}` : ''].filter(Boolean).join('; ');
|
|
32
|
+
const inTime = options.deadline === undefined || Date.now() <= options.deadline;
|
|
33
|
+
const lines = options.git && options.mainRef && lesson.at && inTime
|
|
34
|
+
? outcomeFor(options.git, lesson, { number: record.pr, mergeSha: record.mergeSha, mergedAt: record.mergedAt }, { mainRef: options.mainRef, until: record.windowEnd, slack: POINT_SLACK, maxFiles: MAX_FIX_FILES })
|
|
35
|
+
: undefined;
|
|
36
|
+
// Evidence for a person, never a promotion: a fix on the same lines is often unrelated work.
|
|
37
|
+
if (lines?.kind === 'outcome') {
|
|
38
|
+
const evidence = { ...lines, kind: 'lines', comment: `lines-${record.pr}-${lines.comment.replace(/^outcome-/, '')}`, detail: `${lines.detail}${context ? `; ${context}` : ''}` };
|
|
39
|
+
if (add(evidence) && !result.suggested.includes(lesson.id))
|
|
40
|
+
result.suggested.push(lesson.id);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
add({ kind: 'followup', pr: record.pr, comment: `followup-${record.pr}-${fileFix.sha.slice(0, 12)}`, author: '', detail: `${lesson.file} fixed later by ${fileFix.sha.slice(0, 9)} "${fileFix.subject}", not on the point's lines${context ? `; ${context}` : ''}`, at: fileFix.at });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Against it: later pull requests a review found repeating it, merged anyway, settled clean.
|
|
47
|
+
if (lesson.state === 'verified' && (lesson.promotedBy === 'outcome' || lesson.promotedBy === 'recurrence')) {
|
|
48
|
+
for (const [pr, ids] of applied) {
|
|
49
|
+
const record = byPr.get(pr);
|
|
50
|
+
if (own.has(pr) || !ids.has(lesson.id) || !record || !settledClean(record, lesson.file))
|
|
51
|
+
continue;
|
|
52
|
+
add({ kind: 'against', pr, comment: `against-${pr}`, author: '', prAuthor: record.author, detail: `a review found #${pr} repeating it; merged ${record.mergedAt.slice(0, 10)}, CI passed, no fix on ${lesson.file || 'its files'} within the window, no revert`, at: record.mergedAt });
|
|
53
|
+
}
|
|
54
|
+
const against = lesson.evidence.filter(e => e.kind === 'against');
|
|
55
|
+
const authors = new Set(against.map(e => e.prAuthor).filter(Boolean)).size;
|
|
56
|
+
// Independent: more than one author, or merged at least a week apart (a span, never calendar buckets: two merges a day apart across a week boundary are not).
|
|
57
|
+
const times = against.map(e => Date.parse(e.at ?? '')).filter(Number.isFinite);
|
|
58
|
+
const apart = times.length > 1 && Math.max(...times) - Math.min(...times) >= WEEK_MS;
|
|
59
|
+
if (new Set(against.map(e => e.pr)).size >= options.demoteAfter && (authors > 1 || apart)) {
|
|
60
|
+
add({ kind: 'demoted', pr: against.at(-1).pr, comment: `demoted-${against.map(e => e.pr).sort((a, b) => a - b).join('-')}`, author: '', detail: `taken back: ${against.map(e => `#${e.pr}`).join(', ')} repeated it and settled clean`, at: new Date().toISOString() });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
Object.assign(lesson, lessonState(lesson));
|
|
64
|
+
if (before === 'verified' && lesson.state === 'candidate')
|
|
65
|
+
result.demoted.push(lesson.id);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/** CI passed on the merge commit, no fix touched the file (any file, for a team standard) within the window, no revert. Settled only. */
|
|
70
|
+
function settledClean(record, file) {
|
|
71
|
+
return record.settled && record.ci === 'success' && !record.reverted && !record.followUps.some(f => f.fix && (!file || f.files.includes(file)));
|
|
72
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Git } from './acted-on.js';
|
|
9
9
|
import type { LessonEvidence, ReviewLesson } from './lessons.js';
|
|
10
|
+
/** Commit subjects that say the commit fixed something. */
|
|
11
|
+
export declare const FIX: RegExp;
|
|
10
12
|
export interface OutcomeOptions {
|
|
11
13
|
/** The main branch the pull request merged into. */
|
|
12
14
|
mainRef: string;
|
|
@@ -14,6 +16,10 @@ export interface OutcomeOptions {
|
|
|
14
16
|
until?: string;
|
|
15
17
|
/** How long unchanged lines must ship before that counts against the point. */
|
|
16
18
|
windowDays?: number;
|
|
19
|
+
/** Lines either side of the point's own that count as its lines (outcome-evidence.ts: 3). */
|
|
20
|
+
slack?: number;
|
|
21
|
+
/** A fixing commit that touches more files than this is a sweep, and says nothing about one point: no verdict. */
|
|
22
|
+
maxFiles?: number;
|
|
17
23
|
}
|
|
18
24
|
export interface MergedAt {
|
|
19
25
|
number: number;
|
|
@@ -24,3 +30,5 @@ export interface MergedAt {
|
|
|
24
30
|
export declare function outcomeFor(git: Git, lesson: ReviewLesson, pr: MergedAt, options: OutcomeOptions): LessonEvidence | undefined;
|
|
25
31
|
/** A revert of the pull request on main, before `until`: an outcome for every point it left unaddressed. */
|
|
26
32
|
export declare function revertOf(git: Git, pr: MergedAt, options: OutcomeOptions): LessonEvidence | undefined;
|
|
33
|
+
/** A commit subject that reverts the pull request: "Revert ..." naming its number or its merge commit. */
|
|
34
|
+
export declare function revertsPr(subject: string, pr: Pick<MergedAt, 'number' | 'mergeSha'>): boolean;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { changedHunks } from './acted-on.js';
|
|
2
2
|
/** Commit subjects that say the commit fixed something. */
|
|
3
|
-
const FIX = /\b(fix(e[sd])?|bug|hotfix|regression|revert|broke|broken)\b/i;
|
|
3
|
+
export const FIX = /\b(fix(e[sd])?|bug|hotfix|regression|revert|broke|broken)\b/i;
|
|
4
4
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
5
5
|
/** The outcome or counter evidence for an inline point the pull request did not act on, or undefined when there is none yet. */
|
|
6
6
|
export function outcomeFor(git, lesson, pr, options) {
|
|
7
7
|
const point = lesson.evidence.find(e => (e.kind ?? 'point') === 'point' && e.pr === pr.number);
|
|
8
8
|
if (!point || point.actedOn || !lesson.at || !lesson.file || !pr.mergedAt)
|
|
9
9
|
return undefined;
|
|
10
|
-
|
|
10
|
+
const slack = options.slack ?? 0;
|
|
11
|
+
let range = shift(changedHunks(git, lesson.at.commit, pr.mergeSha, lesson.file), [Math.max(1, lesson.at.start - slack), lesson.at.end + slack]);
|
|
11
12
|
if (!range)
|
|
12
13
|
return undefined; // the pull request changed them after all
|
|
13
14
|
const before = options.until ? [`--before=${options.until}`] : [];
|
|
@@ -26,8 +27,9 @@ export function outcomeFor(git, lesson, pr, options) {
|
|
|
26
27
|
continue;
|
|
27
28
|
}
|
|
28
29
|
const [subject, date] = git(['log', '-1', '--format=%s%x09%cI', sha]).trim().split('\t');
|
|
29
|
-
// Changed by a commit that says it fixed something: the point was right. Changed otherwise: no verdict.
|
|
30
|
-
|
|
30
|
+
// Changed by a commit that says it fixed something, and is not a sweep across the codebase: the point was right. Changed otherwise: no verdict.
|
|
31
|
+
const sweep = options.maxFiles !== undefined && git(['show', '--name-only', '--format=', sha]).split('\n').filter(Boolean).length > options.maxFiles;
|
|
32
|
+
return FIX.test(subject) && !sweep ? { kind: 'outcome', pr: pr.number, comment: `outcome-${sha.slice(0, 12)}`, author: '', detail: `fixed later by ${sha.slice(0, 9)} "${subject}"`, at: date } : undefined;
|
|
31
33
|
}
|
|
32
34
|
const now = options.until ? Date.parse(options.until) : Date.now();
|
|
33
35
|
const days = Math.floor((now - Date.parse(pr.mergedAt)) / DAY_MS);
|
|
@@ -46,12 +48,16 @@ export function revertOf(git, pr, options) {
|
|
|
46
48
|
}
|
|
47
49
|
for (const line of log.split('\n')) {
|
|
48
50
|
const [sha, date, subject] = line.split('\t');
|
|
49
|
-
if (sha &&
|
|
51
|
+
if (sha && revertsPr(subject ?? '', pr)) {
|
|
50
52
|
return { kind: 'outcome', pr: pr.number, comment: `revert-${sha.slice(0, 12)}`, author: '', detail: `the pull request was reverted by ${sha.slice(0, 9)} "${subject}"`, at: date };
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
return undefined;
|
|
54
56
|
}
|
|
57
|
+
/** A commit subject that reverts the pull request: "Revert ..." naming its number or its merge commit. */
|
|
58
|
+
export function revertsPr(subject, pr) {
|
|
59
|
+
return /^revert\b/i.test(subject) && (subject.includes(`#${pr.number}`) || subject.includes(pr.mergeSha.slice(0, 7)));
|
|
60
|
+
}
|
|
55
61
|
/** The range after a commit's hunks, or undefined when a hunk touched it. */
|
|
56
62
|
function shift(hunks, [start, end]) {
|
|
57
63
|
let offset = 0;
|