@rigour-labs/core 6.9.0-rc.2 → 6.9.0-rc.4
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 +3 -2
- package/dist/index.js +2 -2
- package/dist/outcomes/metrics.d.ts +56 -0
- package/dist/outcomes/metrics.js +38 -0
- package/dist/outcomes/run.d.ts +5 -0
- package/dist/outcomes/run.js +21 -6
- package/dist/review-learning/learn-from-reviews.js +3 -2
- package/dist/review-learning/lessons.d.ts +11 -4
- package/dist/review-learning/lessons.js +34 -6
- package/dist/review-learning/outcome-evidence.d.ts +2 -2
- package/dist/review-learning/outcome-evidence.js +1 -1
- package/package.json +6 -6
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,8 @@ export { dismissFinding, dismissedKeys, findingKey, isProven, mustFix, quietSpli
|
|
|
20
20
|
export { loadLedger, ledgerProblems, runBacktest, backtestPassed, formatBacktest, LEDGER_PATH, type Ledger, type RoundResult } from './review/backtest.js';
|
|
21
21
|
export { scaffoldLedger } from './review/backtest-init.js';
|
|
22
22
|
export type { CiResult, FollowUp, PrOutcome } from './outcomes/outcome.js';
|
|
23
|
-
export { runOutcomes, type OutcomesRun } from './outcomes/run.js';
|
|
23
|
+
export { localOutcomeMetrics, runOutcomes, type OutcomesRun } from './outcomes/run.js';
|
|
24
|
+
export type { OutcomeMetrics, Share } from './outcomes/metrics.js';
|
|
24
25
|
export { fixScope, type FixScope } from './review/fix-scope.js';
|
|
25
26
|
export { isGeneratedFile } from './review/generated-files.js';
|
|
26
27
|
export { costBucket, countUsage, doNotTrack, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
|
|
@@ -46,7 +47,7 @@ export { learnFromReviews, type LearnFromReviewsOptions, type LearnFromReviewsRe
|
|
|
46
47
|
export { ruleWriterFor } from './review/reviewer/rule-writer.js';
|
|
47
48
|
export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH, type LastReport } from './review/backtest-last.js';
|
|
48
49
|
export { buildRecord, recordLines, recordIntact, type ReviewRecord } from './review/reviewer/record.js';
|
|
49
|
-
export { readLessons, writeLessons, decideLesson, lessonState, matchLessons, lessonText, lessonsPath, type ReviewLesson, type LessonEvidence } from './review-learning/lessons.js';
|
|
50
|
+
export { readLessons, writeLessons, decideLesson, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath, type ReviewLesson, type LessonEvidence } from './review-learning/lessons.js';
|
|
50
51
|
export { lessonsForDiff, lessonsSection, type LessonMode } from './review-learning/team-lessons.js';
|
|
51
52
|
export { recordAgentWrites, captureHumanEdits } from './review-learning/human-edits.js';
|
|
52
53
|
export { readRepoRules, rulesForDiff, rulesSection, splitRules, type RepoRule } from './review-learning/repo-rules.js';
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ export { isControlFile, mergeBaseOf, readStateFile } from './review/trusted-stat
|
|
|
19
19
|
export { dismissFinding, dismissedKeys, findingKey, isProven, mustFix, quietSplit, DISMISSED_FILE } from './review/quiet.js';
|
|
20
20
|
export { loadLedger, ledgerProblems, runBacktest, backtestPassed, formatBacktest, LEDGER_PATH } from './review/backtest.js';
|
|
21
21
|
export { scaffoldLedger } from './review/backtest-init.js';
|
|
22
|
-
export { runOutcomes } from './outcomes/run.js';
|
|
22
|
+
export { localOutcomeMetrics, runOutcomes } from './outcomes/run.js';
|
|
23
23
|
export { fixScope } from './review/fix-scope.js';
|
|
24
24
|
export { isGeneratedFile } from './review/generated-files.js';
|
|
25
25
|
export { costBucket, countUsage, doNotTrack, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
|
|
@@ -45,7 +45,7 @@ export { learnFromReviews } from './review-learning/learn-from-reviews.js';
|
|
|
45
45
|
export { ruleWriterFor } from './review/reviewer/rule-writer.js';
|
|
46
46
|
export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH } from './review/backtest-last.js';
|
|
47
47
|
export { buildRecord, recordLines, recordIntact } from './review/reviewer/record.js';
|
|
48
|
-
export { readLessons, writeLessons, decideLesson, lessonState, matchLessons, lessonText, lessonsPath } from './review-learning/lessons.js';
|
|
48
|
+
export { readLessons, writeLessons, decideLesson, lessonState, pendingDecision, matchLessons, lessonText, lessonsPath } from './review-learning/lessons.js';
|
|
49
49
|
export { lessonsForDiff, lessonsSection } from './review-learning/team-lessons.js';
|
|
50
50
|
export { recordAgentWrites, captureHumanEdits } from './review-learning/human-edits.js';
|
|
51
51
|
export { readRepoRules, rulesForDiff, rulesSection, splitRules } from './review-learning/repo-rules.js';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The outcome loop's numbers: the one place they are counted (Studio, `rigour outcomes --json` and telemetry read this,
|
|
3
|
+
* never their own count). The shape is versioned and documented in docs/OUTCOMES.md, "Numbers".
|
|
4
|
+
*
|
|
5
|
+
* Counts only, over what this machine read. A rate is given only when at least MIN_FOR_RATE records are behind it;
|
|
6
|
+
* below that it is null with the reason. Pull requests a review by Rigour saw and pull requests it did not are counted
|
|
7
|
+
* side by side and never compared: teams choose which pull requests get reviewed, so the two groups differ, and a gap
|
|
8
|
+
* between them is not Rigour's effect.
|
|
9
|
+
*/
|
|
10
|
+
import type { ReviewLesson } from '../review-learning/lessons.js';
|
|
11
|
+
import type { PrOutcome } from './outcome.js';
|
|
12
|
+
export interface Share {
|
|
13
|
+
count: number;
|
|
14
|
+
of: number;
|
|
15
|
+
/** count / of, two decimals; null when `of` is under MIN_FOR_RATE. */
|
|
16
|
+
rate: number | null;
|
|
17
|
+
reason?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface OutcomeMetrics {
|
|
20
|
+
version: 1;
|
|
21
|
+
records: {
|
|
22
|
+
merged: number;
|
|
23
|
+
settled: number;
|
|
24
|
+
unsettled: number;
|
|
25
|
+
};
|
|
26
|
+
/** Over settled records only: an open window can still change. */
|
|
27
|
+
settled: {
|
|
28
|
+
/** Over settled records whose CI result is known (passed or failed): one with no CI to read never dilutes it. */
|
|
29
|
+
ciRegressed: Share;
|
|
30
|
+
/** Settled records with no CI result to read. */
|
|
31
|
+
ciUnknown: number;
|
|
32
|
+
reverted: Share;
|
|
33
|
+
/** A later commit on its files, inside the window, that says it fixes something. */
|
|
34
|
+
fixedLater: Share;
|
|
35
|
+
/** Side by side, never compared (see above). */
|
|
36
|
+
reviewed: {
|
|
37
|
+
prs: number;
|
|
38
|
+
fixedLater: Share;
|
|
39
|
+
};
|
|
40
|
+
notReviewed: {
|
|
41
|
+
prs: number;
|
|
42
|
+
fixedLater: Share;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
lessons: {
|
|
46
|
+
/** Candidates waiting on a person: a later fix on their lines, back to candidate, or taken back. */
|
|
47
|
+
awaitingDecision: number;
|
|
48
|
+
/** Promoted by a person after such evidence. */
|
|
49
|
+
promotedFromEvidence: number;
|
|
50
|
+
dismissed: number;
|
|
51
|
+
/** Taken back by evidence and not promoted again since. */
|
|
52
|
+
takenBack: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** `reviewed`: the pull requests a review by Rigour ran on (the threads' review events). */
|
|
56
|
+
export declare function outcomeMetrics(records: PrOutcome[], lessons: ReviewLesson[], reviewed: Set<number>): OutcomeMetrics;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { pendingDecision } from '../review-learning/lessons.js';
|
|
2
|
+
/** Fewer records than this give a count, never a percentage. */
|
|
3
|
+
const MIN_FOR_RATE = 10;
|
|
4
|
+
/** `reviewed`: the pull requests a review by Rigour ran on (the threads' review events). */
|
|
5
|
+
export function outcomeMetrics(records, lessons, reviewed) {
|
|
6
|
+
const settled = records.filter(r => r.settled);
|
|
7
|
+
const fixed = (r) => r.followUps.some(f => f.fix);
|
|
8
|
+
const inReview = settled.filter(r => reviewed.has(r.pr));
|
|
9
|
+
const notInReview = settled.filter(r => !reviewed.has(r.pr));
|
|
10
|
+
return {
|
|
11
|
+
version: 1,
|
|
12
|
+
records: { merged: records.length, settled: settled.length, unsettled: records.length - settled.length },
|
|
13
|
+
settled: {
|
|
14
|
+
ciRegressed: share(settled.filter(r => r.ci === 'failure').length, settled.filter(r => r.ci === 'success' || r.ci === 'failure').length),
|
|
15
|
+
ciUnknown: settled.filter(r => r.ci !== 'success' && r.ci !== 'failure').length,
|
|
16
|
+
reverted: share(settled.filter(r => !!r.reverted).length, settled.length),
|
|
17
|
+
fixedLater: share(settled.filter(fixed).length, settled.length),
|
|
18
|
+
reviewed: { prs: inReview.length, fixedLater: share(inReview.filter(fixed).length, inReview.length) },
|
|
19
|
+
notReviewed: { prs: notInReview.length, fixedLater: share(notInReview.filter(fixed).length, notInReview.length) },
|
|
20
|
+
},
|
|
21
|
+
lessons: {
|
|
22
|
+
awaitingDecision: lessons.filter(l => pendingDecision(l)).length,
|
|
23
|
+
promotedFromEvidence: lessons.filter(promotedAfterEvidence).length,
|
|
24
|
+
dismissed: lessons.filter(l => l.evidence.some(e => e.kind === 'dismissed')).length,
|
|
25
|
+
takenBack: lessons.filter(l => pendingDecision(l)?.kind === 'demoted').length,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function share(count, of) {
|
|
30
|
+
return of >= MIN_FOR_RATE
|
|
31
|
+
? { count, of, rate: Math.round((count / of) * 100) / 100 }
|
|
32
|
+
: { count, of, rate: null, reason: `fewer than ${MIN_FOR_RATE} records: a count, not a rate` };
|
|
33
|
+
}
|
|
34
|
+
/** A person's promotion that came after a later fix on its lines, a reclassification or a take-back. */
|
|
35
|
+
function promotedAfterEvidence(lesson) {
|
|
36
|
+
const evidenceAt = lesson.evidence.findIndex(e => e.kind === 'lines' || e.kind === 'reclassified' || e.kind === 'demoted');
|
|
37
|
+
return evidenceAt >= 0 && lesson.evidence.slice(evidenceAt + 1).some(e => e.kind === 'accepted') && lesson.state === 'verified';
|
|
38
|
+
}
|
package/dist/outcomes/run.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type Exec } from '../review/reviewer/exec.js';
|
|
|
8
8
|
import { type ResolvedSwitch } from '../switches.js';
|
|
9
9
|
import { type PrOutcome } from './outcome.js';
|
|
10
10
|
import { type OutcomeEvidenceResult } from '../review-learning/outcome-evidence.js';
|
|
11
|
+
import { type OutcomeMetrics } from './metrics.js';
|
|
11
12
|
export interface OutcomesRun {
|
|
12
13
|
switch: ResolvedSwitch;
|
|
13
14
|
outcomes: PrOutcome[];
|
|
@@ -17,6 +18,8 @@ export interface OutcomesRun {
|
|
|
17
18
|
stopped?: string;
|
|
18
19
|
/** What the records did to the team's review lessons (review-learning/outcome-evidence.ts). */
|
|
19
20
|
lessons?: OutcomeEvidenceResult;
|
|
21
|
+
/** The outcome numbers over every record kept (metrics.ts). */
|
|
22
|
+
metrics?: OutcomeMetrics;
|
|
20
23
|
}
|
|
21
24
|
export declare function runOutcomes(cwd: string, config: Config, options: {
|
|
22
25
|
flag?: boolean;
|
|
@@ -24,3 +27,5 @@ export declare function runOutcomes(cwd: string, config: Config, options: {
|
|
|
24
27
|
last?: number;
|
|
25
28
|
exec?: Exec;
|
|
26
29
|
}): Promise<OutcomesRun>;
|
|
30
|
+
/** The outcome numbers for this checkout (metrics.ts), over every record it keeps; undefined when it keeps none. Read-only. */
|
|
31
|
+
export declare function localOutcomeMetrics(cwd: string): OutcomeMetrics | undefined;
|
package/dist/outcomes/run.js
CHANGED
|
@@ -7,6 +7,7 @@ import { applyOutcomeEvidence } from '../review-learning/outcome-evidence.js';
|
|
|
7
7
|
import { readLessons, writeLessons } from '../review-learning/lessons.js';
|
|
8
8
|
import { gitIn } from '../review-learning/acted-on.js';
|
|
9
9
|
import { eventsOfKind } from '../task/thread.js';
|
|
10
|
+
import { outcomeMetrics } from './metrics.js';
|
|
10
11
|
/** How long one run may read before it stops and keeps what it has. */
|
|
11
12
|
const READ_DEADLINE_MS = 2 * 60_000;
|
|
12
13
|
/** How long one run may follow points' lines through history; the next run carries on. */
|
|
@@ -30,16 +31,28 @@ export async function runOutcomes(cwd, config, options) {
|
|
|
30
31
|
const incomplete = listed.incomplete ? 'the list of merged pull requests may be incomplete (gh listed too many updates to prove it)' : undefined;
|
|
31
32
|
const stopped = [run.stopped, incomplete].filter(Boolean).join('; ');
|
|
32
33
|
const lessons = lessonEvidence(cwd, mainRef, config.learning?.outcomes?.demote_after ?? 2);
|
|
33
|
-
|
|
34
|
+
const metrics = localOutcomeMetrics(cwd);
|
|
35
|
+
return { switch: resolved, outcomes: run.outcomes, read: run.read, ...(lessons ? { lessons } : {}), ...(metrics ? { metrics } : {}), ...(stopped ? { stopped } : {}) };
|
|
34
36
|
}
|
|
35
37
|
/** Every record kept (not only this run's) against the team's lessons, and the reviews that found a lesson repeated; written only when something changed. */
|
|
36
38
|
function lessonEvidence(cwd, mainRef, demoteAfter) {
|
|
37
39
|
const lessons = readLessons(cwd);
|
|
38
40
|
if (lessons.length === 0)
|
|
39
41
|
return undefined;
|
|
42
|
+
const result = applyOutcomeEvidence(lessons, Object.values(readPrOutcomes(cwd).outcomes), reviews(cwd).applied, { demoteAfter, git: gitIn(cwd), mainRef, deadline: Date.now() + LESSON_DEADLINE_MS });
|
|
43
|
+
if (result.added)
|
|
44
|
+
writeLessons(cwd, lessons);
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/** Per pull request, the lessons a review of it recorded as applying, and every pull request a review by Rigour ran on: from the threads. */
|
|
48
|
+
function reviews(cwd) {
|
|
40
49
|
const applied = new Map();
|
|
50
|
+
const reviewed = new Set();
|
|
41
51
|
for (const e of eventsOfKind(cwd, 'review')) {
|
|
42
|
-
if (typeof e.pr !== 'number'
|
|
52
|
+
if (typeof e.pr !== 'number')
|
|
53
|
+
continue;
|
|
54
|
+
reviewed.add(e.pr);
|
|
55
|
+
if (!Array.isArray(e.lessons_applied))
|
|
43
56
|
continue;
|
|
44
57
|
const ids = applied.get(e.pr) ?? new Set();
|
|
45
58
|
for (const id of e.lessons_applied)
|
|
@@ -47,10 +60,12 @@ function lessonEvidence(cwd, mainRef, demoteAfter) {
|
|
|
47
60
|
ids.add(id);
|
|
48
61
|
applied.set(e.pr, ids);
|
|
49
62
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
return { applied, reviewed };
|
|
64
|
+
}
|
|
65
|
+
/** The outcome numbers for this checkout (metrics.ts), over every record it keeps; undefined when it keeps none. Read-only. */
|
|
66
|
+
export function localOutcomeMetrics(cwd) {
|
|
67
|
+
const records = Object.values(readPrOutcomes(cwd).outcomes);
|
|
68
|
+
return records.length ? outcomeMetrics(records, readLessons(cwd), reviews(cwd).reviewed) : undefined;
|
|
54
69
|
}
|
|
55
70
|
async function onePr(cwd, pr, exec, env) {
|
|
56
71
|
const view = await exec('gh', ['pr', 'view', String(pr), '--json', 'number,mergedAt,mergeCommit,headRefName,author,state'], { cwd, timeoutMs: GH_TIMEOUT_MS, ...(env ? { env } : {}) });
|
|
@@ -46,7 +46,7 @@ export async function learnFromReviews(cwd, options) {
|
|
|
46
46
|
const byNumber = new Map(prs.filter(pr => pr.mergedAt).map(pr => [pr.number, pr]));
|
|
47
47
|
const reverts = new Map([...byNumber.values()].map(pr => [pr.number, revertOf(git, pr, { mainRef: options.mainRef, until: options.until })]));
|
|
48
48
|
for (const lesson of merged.lessons) {
|
|
49
|
-
if (lesson.state !== 'candidate' || lesson.evidence.some(e => e.kind === 'outcome' || e.kind === 'counter'))
|
|
49
|
+
if (lesson.state !== 'candidate' || lesson.evidence.some(e => e.kind === 'outcome' || e.kind === 'lines' || e.kind === 'counter'))
|
|
50
50
|
continue;
|
|
51
51
|
for (const point of lesson.evidence.filter(e => (e.kind ?? 'point') === 'point')) {
|
|
52
52
|
const pr = byNumber.get(point.pr);
|
|
@@ -54,7 +54,8 @@ export async function learnFromReviews(cwd, options) {
|
|
|
54
54
|
continue;
|
|
55
55
|
const found = (point.actedOn === false ? reverts.get(pr.number) : undefined) ?? outcomeFor(git, lesson, pr, { mainRef: options.mainRef, until: options.until, windowDays: options.windowDays });
|
|
56
56
|
if (found) {
|
|
57
|
-
|
|
57
|
+
// A fix on the point's lines, or a revert, is evidence for a person to promote in Studio, never a promotion (lessonState).
|
|
58
|
+
lesson.evidence.push(found.kind === 'outcome' ? { ...found, kind: 'lines' } : found);
|
|
58
59
|
break;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -12,12 +12,13 @@ import type { Git, ReviewBody, ReviewComment } from './acted-on.js';
|
|
|
12
12
|
* lines a later fix changed the point's own lines (outcome-evidence.ts): shown for a person to promote or dismiss,
|
|
13
13
|
* never a promotion on its own (a fix on the same lines is often unrelated work);
|
|
14
14
|
* dismissed a person looked at that evidence and set it aside: the lesson stays as it was;
|
|
15
|
+
* reclassified a lesson an outcome alone had promoted, back to a candidate when outcomes stopped promoting (once);
|
|
15
16
|
* against a later pull request a review found repeating the lesson merged anyway and settled clean;
|
|
16
17
|
* demoted enough independent `against` pull requests took back a lesson evidence had promoted: a candidate again,
|
|
17
18
|
* until a person promotes it.
|
|
18
19
|
* A record from before evidence kinds has none: it is a `point`.
|
|
19
20
|
*/
|
|
20
|
-
export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted';
|
|
21
|
+
export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified';
|
|
21
22
|
export interface LessonEvidence {
|
|
22
23
|
kind?: EvidenceKind;
|
|
23
24
|
pr: number;
|
|
@@ -59,9 +60,9 @@ export interface ReviewLesson {
|
|
|
59
60
|
updatedAt: string;
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
|
-
* A lesson's state from its evidence trail. A person's decision wins; then
|
|
63
|
-
* holds a candidate back; recurrence across pull requests and authors is enough only without it.
|
|
64
|
-
* from before evidence kinds keeps the state it had.
|
|
63
|
+
* A lesson's state from its evidence trail. A person's decision wins; then a person's correction; counter-evidence
|
|
64
|
+
* holds a candidate back; recurrence across pull requests and authors is enough only without it. An outcome is
|
|
65
|
+
* evidence, never a promotion. A record from before evidence kinds keeps the state it had.
|
|
65
66
|
*/
|
|
66
67
|
export declare function lessonState(lesson: ReviewLesson): Pick<ReviewLesson, 'state' | 'promotedBy'>;
|
|
67
68
|
/** The point of a review comment: its bold title, else its first sentence, without tool output or markup. */
|
|
@@ -108,6 +109,12 @@ export declare function matchLessons(lessons: ReviewLesson[], change: ChangeShap
|
|
|
108
109
|
/** RIGOUR_REVIEW_LESSONS points at a lessons file outside the clone (CI, or a team's shared copy). */
|
|
109
110
|
export declare function lessonsPath(cwd: string): string;
|
|
110
111
|
export declare function readLessons(cwd: string): ReviewLesson[];
|
|
112
|
+
/**
|
|
113
|
+
* What a person is asked to decide on a candidate, from the last of its evidence and decisions: taken back by evidence
|
|
114
|
+
* (`demoted`), back to a candidate when outcomes stopped promoting (`reclassified`), or a later fix on its lines
|
|
115
|
+
* (`lines`); undefined when nothing waits on a person. Studio and the outcome numbers read it, so they never disagree.
|
|
116
|
+
*/
|
|
117
|
+
export declare function pendingDecision(lesson: ReviewLesson): LessonEvidence | undefined;
|
|
111
118
|
export declare function writeLessons(cwd: string, lessons: ReviewLesson[]): void;
|
|
112
119
|
/** A person's decision on a lesson, kept as evidence: accepted makes it a lesson, rejected an anti-lesson. Undefined for an unknown id. */
|
|
113
120
|
export declare function decideLesson(cwd: string, id: string, decision: 'accepted' | 'rejected' | 'dismissed', by: string, why?: string): ReviewLesson | undefined;
|
|
@@ -35,9 +35,9 @@ const PLAIN_WORDS = new Set(['when', 'that', 'this', 'with', 'from', 'before', '
|
|
|
35
35
|
const NOT_CODE = /(\.(md|mdx|txt|ya?ml|json|lock|snap)$)|(\.(test|spec)\.[cm]?[jt]sx?$)|(^|\/)(__tests__|docs?|migrations|\.github)\//i;
|
|
36
36
|
const NOT_SYMBOLS = new Set(['this', 'that', 'with', 'from', 'return', 'const', 'await', 'async', 'function', 'true', 'false', 'null', 'undefined', 'string', 'number', 'boolean', 'export', 'import', 'type', 'interface', 'else', 'when', 'then', 'should', 'line', 'lines']);
|
|
37
37
|
/**
|
|
38
|
-
* A lesson's state from its evidence trail. A person's decision wins; then
|
|
39
|
-
* holds a candidate back; recurrence across pull requests and authors is enough only without it.
|
|
40
|
-
* from before evidence kinds keeps the state it had.
|
|
38
|
+
* A lesson's state from its evidence trail. A person's decision wins; then a person's correction; counter-evidence
|
|
39
|
+
* holds a candidate back; recurrence across pull requests and authors is enough only without it. An outcome is
|
|
40
|
+
* evidence, never a promotion. A record from before evidence kinds keeps the state it had.
|
|
41
41
|
*/
|
|
42
42
|
export function lessonState(lesson) {
|
|
43
43
|
const kinds = new Set(lesson.evidence.map(e => e.kind));
|
|
@@ -56,8 +56,8 @@ export function lessonState(lesson) {
|
|
|
56
56
|
return { state: 'candidate' };
|
|
57
57
|
if (kinds.has('correction'))
|
|
58
58
|
return { state: 'verified', promotedBy: 'correction' };
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// An outcome (a later fix on the point's lines, a revert) no longer promotes on its own: judged by a person against
|
|
60
|
+
// real history, it was most often unrelated work. It is shown in Studio for a person to promote.
|
|
61
61
|
if (kinds.has('counter'))
|
|
62
62
|
return { state: 'candidate' };
|
|
63
63
|
const points = lesson.evidence.filter(e => (e.kind ?? 'point') === 'point');
|
|
@@ -227,12 +227,40 @@ export function lessonsPath(cwd) {
|
|
|
227
227
|
export function readLessons(cwd) {
|
|
228
228
|
try {
|
|
229
229
|
const parsed = JSON.parse(fs.readFileSync(lessonsPath(cwd), 'utf8'));
|
|
230
|
-
return Array.isArray(parsed?.lessons) ? parsed.lessons : [];
|
|
230
|
+
return Array.isArray(parsed?.lessons) ? parsed.lessons.map(reclassified) : [];
|
|
231
231
|
}
|
|
232
232
|
catch {
|
|
233
233
|
return [];
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* What a person is asked to decide on a candidate, from the last of its evidence and decisions: taken back by evidence
|
|
238
|
+
* (`demoted`), back to a candidate when outcomes stopped promoting (`reclassified`), or a later fix on its lines
|
|
239
|
+
* (`lines`); undefined when nothing waits on a person. Studio and the outcome numbers read it, so they never disagree.
|
|
240
|
+
*/
|
|
241
|
+
export function pendingDecision(lesson) {
|
|
242
|
+
if (lesson.state !== 'candidate')
|
|
243
|
+
return undefined;
|
|
244
|
+
const last = lesson.evidence.filter(e => e.kind === 'demoted' || e.kind === 'lines' || e.kind === 'reclassified' || e.kind === 'accepted' || e.kind === 'rejected' || e.kind === 'dismissed').at(-1);
|
|
245
|
+
return last?.kind === 'demoted' || last?.kind === 'lines' || last?.kind === 'reclassified' ? last : undefined;
|
|
246
|
+
}
|
|
247
|
+
/** Why a lesson an outcome alone had promoted is a candidate again. */
|
|
248
|
+
const RECLASSIFIED = 'promoted by the exact-line rule, which no longer promotes on its own';
|
|
249
|
+
/**
|
|
250
|
+
* A lesson stored as verified by an outcome (a later fix on its lines, or a revert), as every reader sees it now that
|
|
251
|
+
* outcomes no longer promote: its state worked out again from its evidence, and, when that leaves it a candidate, one
|
|
252
|
+
* `reclassified` record saying why. Never deleted, nothing else changed; the next write keeps it, and a lesson that
|
|
253
|
+
* already has the record is left as it is. Recurrence, a correction or a person's decision keeps a lesson verified.
|
|
254
|
+
*/
|
|
255
|
+
function reclassified(lesson) {
|
|
256
|
+
if (lesson.state !== 'verified' || lesson.promotedBy !== 'outcome' || lesson.evidence.some(e => e.kind === 'reclassified'))
|
|
257
|
+
return lesson;
|
|
258
|
+
const next = lessonState(lesson);
|
|
259
|
+
if (next.state === 'verified')
|
|
260
|
+
return { ...lesson, ...next };
|
|
261
|
+
const at = new Date().toISOString();
|
|
262
|
+
return { ...lesson, state: 'candidate', promotedBy: undefined, evidence: [...lesson.evidence, { kind: 'reclassified', pr: lesson.evidence[0]?.pr ?? 0, comment: `reclassified-${lesson.id}`, author: '', detail: RECLASSIFIED, at }] };
|
|
263
|
+
}
|
|
236
264
|
export function writeLessons(cwd, lessons) {
|
|
237
265
|
const file = lessonsPath(cwd);
|
|
238
266
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* lesson warns against; the reviewer's lessons step, review event `lessons_applied`) and the pull request merged anyway
|
|
14
14
|
* and settled clean: CI passed on the merge commit, no fix touched the lesson's file in the window, and no revert. A
|
|
15
15
|
* pull request that followed the lesson, or that no review checked against it, never counts. `demote_after` such pull
|
|
16
|
-
* requests, independent (more than one author, or merged at least a week apart), take back a lesson
|
|
17
|
-
*
|
|
16
|
+
* requests, independent (more than one author, or merged at least a week apart), take back a lesson recurrence
|
|
17
|
+
* promoted: it is `demoted` to a candidate. A lesson a person promoted, or corrected into being,
|
|
18
18
|
* is not.
|
|
19
19
|
*/
|
|
20
20
|
import type { PrOutcome } from '../outcomes/outcome.js';
|
|
@@ -44,7 +44,7 @@ export function applyOutcomeEvidence(lessons, records, applied, options) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
// Against it: later pull requests a review found repeating it, merged anyway, settled clean.
|
|
47
|
-
if (lesson.state === 'verified' &&
|
|
47
|
+
if (lesson.state === 'verified' && lesson.promotedBy === 'recurrence') {
|
|
48
48
|
for (const [pr, ids] of applied) {
|
|
49
49
|
const record = byPr.get(pr);
|
|
50
50
|
if (own.has(pr) || !ids.has(lesson.id) || !record || !settledClean(record, lesson.file))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.9.0-rc.
|
|
3
|
+
"version": "6.9.0-rc.4",
|
|
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.9.0-rc.
|
|
79
|
-
"@rigour-labs/brain-darwin-x64": "6.9.0-rc.
|
|
80
|
-
"@rigour-labs/brain-
|
|
81
|
-
"@rigour-labs/brain-linux-
|
|
82
|
-
"@rigour-labs/brain-
|
|
78
|
+
"@rigour-labs/brain-darwin-arm64": "6.9.0-rc.4",
|
|
79
|
+
"@rigour-labs/brain-darwin-x64": "6.9.0-rc.4",
|
|
80
|
+
"@rigour-labs/brain-win-x64": "6.9.0-rc.4",
|
|
81
|
+
"@rigour-labs/brain-linux-arm64": "6.9.0-rc.4",
|
|
82
|
+
"@rigour-labs/brain-linux-x64": "6.9.0-rc.4"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/fs-extra": "^11.0.4",
|