@rigour-labs/core 6.9.0-rc.3 → 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 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
+ }
@@ -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;
@@ -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
- return { switch: resolved, outcomes: run.outcomes, read: run.read, ...(lessons ? { lessons } : {}), ...(stopped ? { stopped } : {}) };
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' || !Array.isArray(e.lessons_applied))
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
- const result = applyOutcomeEvidence(lessons, Object.values(readPrOutcomes(cwd).outcomes), applied, { demoteAfter, git: gitIn(cwd), mainRef, deadline: Date.now() + LESSON_DEADLINE_MS });
51
- if (result.added)
52
- writeLessons(cwd, lessons);
53
- return result;
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 } : {}) });
@@ -109,6 +109,12 @@ export declare function matchLessons(lessons: ReviewLesson[], change: ChangeShap
109
109
  /** RIGOUR_REVIEW_LESSONS points at a lessons file outside the clone (CI, or a team's shared copy). */
110
110
  export declare function lessonsPath(cwd: string): string;
111
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;
112
118
  export declare function writeLessons(cwd: string, lessons: ReviewLesson[]): void;
113
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. */
114
120
  export declare function decideLesson(cwd: string, id: string, decision: 'accepted' | 'rejected' | 'dismissed', by: string, why?: string): ReviewLesson | undefined;
@@ -233,6 +233,17 @@ export function readLessons(cwd) {
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
+ }
236
247
  /** Why a lesson an outcome alone had promoted is a candidate again. */
237
248
  const RECLASSIFIED = 'promoted by the exact-line rule, which no longer promotes on its own';
238
249
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "6.9.0-rc.3",
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.3",
79
- "@rigour-labs/brain-darwin-x64": "6.9.0-rc.3",
80
- "@rigour-labs/brain-linux-arm64": "6.9.0-rc.3",
81
- "@rigour-labs/brain-win-x64": "6.9.0-rc.3",
82
- "@rigour-labs/brain-linux-x64": "6.9.0-rc.3"
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",