@rigour-labs/core 6.10.1 → 6.11.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/deep/pr-review.d.ts +3 -1
- package/dist/deep/pr-review.js +2 -1
- package/dist/gates/deep-analysis.js +2 -1
- package/dist/gates/runner.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/outcomes/run.d.ts +6 -1
- package/dist/outcomes/run.js +3 -3
- package/dist/review/check-outcomes.js +8 -0
- package/dist/review/learning-events.js +5 -0
- package/dist/review/quiet.js +1 -1
- package/dist/review/review.d.ts +3 -0
- package/dist/review/review.js +8 -2
- package/dist/review/reviewer/context.d.ts +12 -0
- package/dist/review/reviewer/context.js +7 -3
- package/dist/review/reviewer/settings.d.ts +2 -0
- package/dist/review/reviewer/settings.js +2 -1
- package/dist/review/reviewer/store.d.ts +2 -0
- package/dist/review/reviewer/tiering.d.ts +38 -0
- package/dist/review/reviewer/tiering.js +43 -0
- package/dist/review/reviewer/triage.d.ts +2 -0
- package/dist/review/reviewer/triage.js +4 -0
- package/dist/review/reviewer/usage.js +1 -0
- package/dist/review/reviewer.d.ts +6 -0
- package/dist/review/reviewer.js +20 -4
- package/dist/review/settled-checks.d.ts +13 -2
- package/dist/review/settled-checks.js +11 -3
- package/dist/review-learning/compiled-lessons.d.ts +62 -0
- package/dist/review-learning/compiled-lessons.js +249 -0
- package/dist/review-learning/lessons.d.ts +1 -1
- package/dist/switches.d.ts +21 -0
- package/dist/telemetry/check-ids.d.ts +2 -0
- package/dist/telemetry/check-ids.js +23 -0
- package/dist/telemetry/learning-usage.d.ts +7 -0
- package/dist/telemetry/learning-usage.js +33 -0
- package/dist/telemetry/telemetry.d.ts +8 -0
- package/dist/telemetry/telemetry.js +58 -4
- package/dist/templates/universal-config.js +5 -0
- package/dist/types/index.d.ts +83 -1
- package/dist/types/index.js +10 -0
- package/package.json +6 -6
package/dist/deep/pr-review.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DeepFinding, InferenceOptions, InferenceProvider } from '../inference/types.js';
|
|
2
2
|
import type { CodeContext } from './code-context.js';
|
|
3
|
-
import { type SettledCheck } from '../review/settled-checks.js';
|
|
3
|
+
import { type CoveredLesson, type SettledCheck } from '../review/settled-checks.js';
|
|
4
4
|
import type { RelatedChange } from './related-changes.js';
|
|
5
5
|
export interface FocusItem {
|
|
6
6
|
file: string;
|
|
@@ -22,6 +22,8 @@ export interface PrReviewInput {
|
|
|
22
22
|
prBody?: string;
|
|
23
23
|
/** What Rigour's checks already found on the change: listed as settled; a finding of the same kind at one of their lines is dropped. */
|
|
24
24
|
settled?: SettledCheck[];
|
|
25
|
+
/** Lessons the team's compiled checks covered on this change, left out of `lessons`: the prompt says so. */
|
|
26
|
+
covered?: CoveredLesson[];
|
|
25
27
|
}
|
|
26
28
|
export interface PrReviewResult {
|
|
27
29
|
findings: DeepFinding[];
|
package/dist/deep/pr-review.js
CHANGED
|
@@ -14,7 +14,7 @@ import { REVIEW_CATEGORIES } from './code-review-prompt.js';
|
|
|
14
14
|
import { parseFindings } from './parse-findings.js';
|
|
15
15
|
import { diffSections } from './pr-diff.js';
|
|
16
16
|
import { runToolLoop } from './tool-loop.js';
|
|
17
|
-
import { againstSettled, settledLine, settledSection } from '../review/settled-checks.js';
|
|
17
|
+
import { againstSettled, coveredSection, settledLine, settledSection } from '../review/settled-checks.js';
|
|
18
18
|
const BUDGET = { maxToolCalls: 24, maxTurns: 14 };
|
|
19
19
|
const MAX_DIFF_CHARS = 80_000;
|
|
20
20
|
const MAX_FINDINGS = 5;
|
|
@@ -67,6 +67,7 @@ export function buildPrPrompt(input) {
|
|
|
67
67
|
input.prBody ? `PR DESCRIPTION (what the author intended):\n${input.prBody.slice(0, PR_BODY_CHARS)}` : '',
|
|
68
68
|
input.rules ?? '',
|
|
69
69
|
input.lessons ?? '',
|
|
70
|
+
coveredSection(input.covered ?? []),
|
|
70
71
|
settledSection((input.settled ?? []).map(settledLine)),
|
|
71
72
|
contracts ? `BOTH SIDES OF A CALL CHANGED (check these contracts first):\n${contracts}` : '',
|
|
72
73
|
focus ? `LOOK FIRST (riskiest changed functions, and what to check):\n${focus}` : '',
|
|
@@ -31,6 +31,7 @@ import { checkLocalPatterns } from '../storage/local-memory.js';
|
|
|
31
31
|
import { isScoped } from '../utils/scope.js';
|
|
32
32
|
import { Logger } from '../utils/logger.js';
|
|
33
33
|
import path from 'path';
|
|
34
|
+
import { withoutCovered } from '../review/settled-checks.js';
|
|
34
35
|
/** Cloud setup (API connection) must not hang a check. Local setup is bounded by download stall timeouts instead. */
|
|
35
36
|
const CLOUD_SETUP_TIMEOUT_MS = 120_000;
|
|
36
37
|
/**
|
|
@@ -158,7 +159,7 @@ export class DeepAnalysisGate extends Gate {
|
|
|
158
159
|
const related = relatedChanges(cwd, changedFiles, rankChangedFunctions(cwd, options.focusLines ?? {}, options.removedLines));
|
|
159
160
|
this.config.onProgress?.(` Reviewing the PR as a whole (${focus.length} risky function(s) first)...`);
|
|
160
161
|
try {
|
|
161
|
-
const result = await reviewPullRequest(this.provider, { cwd, diff: options.diff, focus, related, lessons: lessonsSection(lessonsForDiff(cwd, options.diff, this.config.reviewLessons)),
|
|
162
|
+
const result = await reviewPullRequest(this.provider, { cwd, diff: options.diff, focus, related, lessons: lessonsSection(withoutCovered(lessonsForDiff(cwd, options.diff, this.config.reviewLessons), options.covered ?? [])), covered: options.covered,
|
|
162
163
|
rules: rulesSection(rulesForDiff(cwd, options.diff, this.config.repoRules)), prBody: options.prBody, settled: options.settled }, inferenceOptions(this.config));
|
|
163
164
|
this.recordPass({ findings: [], chunksTotal: 1, chunksFailed: 0 });
|
|
164
165
|
this.outcome.findingsProposed = result.findings.length;
|
package/dist/gates/runner.js
CHANGED
|
@@ -215,7 +215,7 @@ export class GateRunner {
|
|
|
215
215
|
let deepStats = undefined;
|
|
216
216
|
if (deepOptions?.enabled) {
|
|
217
217
|
// What the checks found on the change's lines is settled: the model is told so and never reports it again.
|
|
218
|
-
const deep = await runDeepAnalysis(this.config, { cwd, ignore, patterns }, { ...deepOptions, settled: settledChecks(onChangedLines(failures, deepOptions.focusLines)) });
|
|
218
|
+
const deep = await runDeepAnalysis(this.config, { cwd, ignore, patterns }, { ...deepOptions, settled: [...(deepOptions.settled ?? []), ...settledChecks(onChangedLines(failures, deepOptions.focusLines))] });
|
|
219
219
|
failures.push(...deep.failures);
|
|
220
220
|
summary['deep-analysis'] = deep.summary;
|
|
221
221
|
deepStats = deep.stats;
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { loadLedger, ledgerProblems, runBacktest, backtestPassed, formatBacktest
|
|
|
21
21
|
export { scaffoldLedger } from './review/backtest-init.js';
|
|
22
22
|
export type { CiResult, FollowUp, PrOutcome } from './outcomes/outcome.js';
|
|
23
23
|
export { localOutcomeMetrics, runOutcomes, type OutcomesRun } from './outcomes/run.js';
|
|
24
|
+
export { learningUsage } from './telemetry/learning-usage.js';
|
|
24
25
|
export type { OutcomeMetrics, Share } from './outcomes/metrics.js';
|
|
25
26
|
export { fixScope, type FixScope } from './review/fix-scope.js';
|
|
26
27
|
export { isGeneratedFile } from './review/generated-files.js';
|
|
@@ -44,6 +45,7 @@ export { rankChangedFunctions, scoreRisk, functionHash, findFunction, type Funct
|
|
|
44
45
|
export { routeFiles, type RouterPolicy, type RouterStats } from './deep/router.js';
|
|
45
46
|
export { appendDeepRun, readDeepRuns, summarizeDeepRuns, type DeepRun, type DeepRunSummary } from './review/deep-runs.js';
|
|
46
47
|
export { learnFromReviews, type LearnFromReviewsOptions, type LearnFromReviewsResult } from './review-learning/learn-from-reviews.js';
|
|
48
|
+
export { decideCompiledCheck, proposeCompiledChecks, readCompiledChecks, suspension, type CompiledCheck } from './review-learning/compiled-lessons.js';
|
|
47
49
|
export { ruleWriterFor } from './review/reviewer/rule-writer.js';
|
|
48
50
|
export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH, type LastReport } from './review/backtest-last.js';
|
|
49
51
|
export { buildRecord, recordLines, recordIntact, type ReviewRecord } from './review/reviewer/record.js';
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export { dismissFinding, dismissedKeys, findingKey, isProven, mustFix, quietSpli
|
|
|
20
20
|
export { loadLedger, ledgerProblems, runBacktest, backtestPassed, formatBacktest, LEDGER_PATH } from './review/backtest.js';
|
|
21
21
|
export { scaffoldLedger } from './review/backtest-init.js';
|
|
22
22
|
export { localOutcomeMetrics, runOutcomes } from './outcomes/run.js';
|
|
23
|
+
export { learningUsage } from './telemetry/learning-usage.js';
|
|
23
24
|
export { fixScope } from './review/fix-scope.js';
|
|
24
25
|
export { isGeneratedFile } from './review/generated-files.js';
|
|
25
26
|
export { costBucket, countUsage, doNotTrack, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
|
|
@@ -42,6 +43,7 @@ export { rankChangedFunctions, scoreRisk, functionHash, findFunction } from './d
|
|
|
42
43
|
export { routeFiles } from './deep/router.js';
|
|
43
44
|
export { appendDeepRun, readDeepRuns, summarizeDeepRuns } from './review/deep-runs.js';
|
|
44
45
|
export { learnFromReviews } from './review-learning/learn-from-reviews.js';
|
|
46
|
+
export { decideCompiledCheck, proposeCompiledChecks, readCompiledChecks, suspension } from './review-learning/compiled-lessons.js';
|
|
45
47
|
export { ruleWriterFor } from './review/reviewer/rule-writer.js';
|
|
46
48
|
export { backtestLast, formatLast, scoreLast, LAST_LEDGER_PATH } from './review/backtest-last.js';
|
|
47
49
|
export { buildRecord, recordLines, recordIntact } from './review/reviewer/record.js';
|
package/dist/outcomes/run.d.ts
CHANGED
|
@@ -8,7 +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
|
+
import { type OutcomeMetrics, type PrReviews } from './metrics.js';
|
|
12
12
|
export interface OutcomesRun {
|
|
13
13
|
switch: ResolvedSwitch;
|
|
14
14
|
outcomes: PrOutcome[];
|
|
@@ -27,5 +27,10 @@ export declare function runOutcomes(cwd: string, config: Config, options: {
|
|
|
27
27
|
last?: number;
|
|
28
28
|
exec?: Exec;
|
|
29
29
|
}): Promise<OutcomesRun>;
|
|
30
|
+
/** Per pull request, the lessons a review of it recorded as applying, and every pull request a review by Rigour ran on with its rounds' dollars and its first review's findings: from the threads. */
|
|
31
|
+
export declare function threadReviews(cwd: string): {
|
|
32
|
+
applied: Map<number, Set<string>>;
|
|
33
|
+
reviewed: Map<number, PrReviews>;
|
|
34
|
+
};
|
|
30
35
|
/** The outcome numbers for this checkout (metrics.ts), over every record it keeps; undefined when it keeps none. Read-only. */
|
|
31
36
|
export declare function localOutcomeMetrics(cwd: string): OutcomeMetrics | undefined;
|
package/dist/outcomes/run.js
CHANGED
|
@@ -39,13 +39,13 @@ function lessonEvidence(cwd, mainRef, demoteAfter) {
|
|
|
39
39
|
const lessons = readLessons(cwd);
|
|
40
40
|
if (lessons.length === 0)
|
|
41
41
|
return undefined;
|
|
42
|
-
const result = applyOutcomeEvidence(lessons, Object.values(readPrOutcomes(cwd).outcomes),
|
|
42
|
+
const result = applyOutcomeEvidence(lessons, Object.values(readPrOutcomes(cwd).outcomes), threadReviews(cwd).applied, { demoteAfter, git: gitIn(cwd), mainRef, deadline: Date.now() + LESSON_DEADLINE_MS });
|
|
43
43
|
if (result.added)
|
|
44
44
|
writeLessons(cwd, lessons);
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
47
47
|
/** Per pull request, the lessons a review of it recorded as applying, and every pull request a review by Rigour ran on with its rounds' dollars and its first review's findings: from the threads. */
|
|
48
|
-
function
|
|
48
|
+
export function threadReviews(cwd) {
|
|
49
49
|
const applied = new Map();
|
|
50
50
|
const reviewed = new Map();
|
|
51
51
|
const count = (v) => typeof v === 'number' ? v : 0;
|
|
@@ -72,7 +72,7 @@ function reviews(cwd) {
|
|
|
72
72
|
/** The outcome numbers for this checkout (metrics.ts), over every record it keeps; undefined when it keeps none. Read-only. */
|
|
73
73
|
export function localOutcomeMetrics(cwd) {
|
|
74
74
|
const records = Object.values(readPrOutcomes(cwd).outcomes);
|
|
75
|
-
return records.length ? outcomeMetrics(records, readLessons(cwd),
|
|
75
|
+
return records.length ? outcomeMetrics(records, readLessons(cwd), threadReviews(cwd).reviewed) : undefined;
|
|
76
76
|
}
|
|
77
77
|
async function onePr(cwd, pr, exec, env) {
|
|
78
78
|
const view = await exec('gh', ['pr', 'view', String(pr), '--json', 'number,mergedAt,mergeCommit,headRefName,author,state'], { cwd, timeoutMs: GH_TIMEOUT_MS, ...(env ? { env } : {}) });
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
|
+
import { telemetryCheckId } from '../telemetry/check-ids.js';
|
|
17
|
+
import { countUsage } from '../telemetry/telemetry.js';
|
|
16
18
|
import { readStateFile } from './trusted-state.js';
|
|
17
19
|
const OUTCOMES_FILE = path.join('.rigour', 'check-outcomes.json');
|
|
18
20
|
const REPORTED_FILE = path.join('.rigour', 'reported-findings.json');
|
|
@@ -49,6 +51,12 @@ export function recordOutcome(cwd, check, kind) {
|
|
|
49
51
|
const current = outcomes[check] ?? { fixed: 0, dismissed: 0 };
|
|
50
52
|
outcomes[check] = { ...current, [kind]: current[kind] + 1 };
|
|
51
53
|
writeJson(path.join(cwd, OUTCOMES_FILE), outcomes);
|
|
54
|
+
// The same outcome, counted for opt-in telemetry by the check's gate id only, and only if it is one of Rigour's own.
|
|
55
|
+
countUsage(`finding_${kind}:${telemetryCheckId(gateOf(check))}`);
|
|
56
|
+
}
|
|
57
|
+
/** A check id's gate: `semantic-bugs` of `semantic-bugs: Credential header follows redirects`. */
|
|
58
|
+
function gateOf(check) {
|
|
59
|
+
return check.split(':')[0].trim();
|
|
52
60
|
}
|
|
53
61
|
/** Every check with an outcome, most dismissed first. */
|
|
54
62
|
export function checkPrecisions(cwd) {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { appendAgentEvent } from './effectiveness.js';
|
|
2
|
+
import { countUsage } from '../telemetry/telemetry.js';
|
|
3
|
+
import { telemetryCheckId } from '../telemetry/check-ids.js';
|
|
2
4
|
const MAX_LISTED = 20;
|
|
3
5
|
/** Lessons an agent was given before or while writing code. Nothing is written when none were. */
|
|
4
6
|
export function recordLessonsServed(cwd, via, subjects) {
|
|
@@ -10,6 +12,9 @@ export function recordLessonsServed(cwd, via, subjects) {
|
|
|
10
12
|
export function recordPrCatches(cwd, findings) {
|
|
11
13
|
if (findings.length === 0)
|
|
12
14
|
return;
|
|
15
|
+
// A finding that reached a pull request's review: counted for opt-in telemetry by its gate id only, if one of Rigour's own.
|
|
16
|
+
for (const finding of findings)
|
|
17
|
+
countUsage(`finding_pushed:${telemetryCheckId(finding.id)}`);
|
|
13
18
|
appendAgentEvent(cwd, {
|
|
14
19
|
type: 'pr_catches',
|
|
15
20
|
findings: findings.slice(0, MAX_LISTED).map(f => ({ rule: f.id, title: f.title, file: f.files?.[0] ?? '' })),
|
package/dist/review/quiet.js
CHANGED
|
@@ -20,7 +20,7 @@ import fs from 'fs';
|
|
|
20
20
|
import path from 'path';
|
|
21
21
|
import { checkId, isMuted, readOutcomes, recordOutcome, reportedCheck } from './check-outcomes.js';
|
|
22
22
|
import { readStateFile } from './trusted-state.js';
|
|
23
|
-
const PROVEN_GATES = new Set(['semantic-bugs', 'hallucinated-imports', 'security-patterns', 'deep-analysis', 'diff-tests', 'unused-export', 'orphan-file', 'offset-paging', 'unbounded-window', 'duplicate-function', 'partial-fix', 'partial-wiring', 'migration-order',
|
|
23
|
+
const PROVEN_GATES = new Set(['compiled-lesson', 'semantic-bugs', 'hallucinated-imports', 'security-patterns', 'deep-analysis', 'diff-tests', 'unused-export', 'orphan-file', 'offset-paging', 'unbounded-window', 'duplicate-function', 'partial-fix', 'partial-wiring', 'migration-order',
|
|
24
24
|
'duplicate-null-filter', 'nullable-filtered-column', 'optional-always-supplied', 'write-only-property', 'typed-checks-unavailable',
|
|
25
25
|
'goal-scope', 'goal-done-when']);
|
|
26
26
|
export const DISMISSED_FILE = path.join('.rigour', 'dismissed.json');
|
package/dist/review/review.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Config, DeepOptions, Failure, Report } from '../types/index.js';
|
|
2
2
|
import { type DiffSource } from './git-diff.js';
|
|
3
|
+
import { type CoveredLesson } from './settled-checks.js';
|
|
3
4
|
import { type Goal } from '../goal/goal.js';
|
|
4
5
|
export interface ReviewInput {
|
|
5
6
|
cwd: string;
|
|
@@ -52,6 +53,8 @@ export interface ReviewResult {
|
|
|
52
53
|
typedError?: string;
|
|
53
54
|
/** The goal the description declared, when the goal check ran on one with something to check. */
|
|
54
55
|
goal?: Goal;
|
|
56
|
+
/** The lessons the team's compiled checks covered on this change: a model reviewer is told so instead of the lesson. */
|
|
57
|
+
covered: CoveredLesson[];
|
|
55
58
|
}
|
|
56
59
|
export interface ReviewFinding {
|
|
57
60
|
id: string;
|
package/dist/review/review.js
CHANGED
|
@@ -20,6 +20,8 @@ import { checkId, rememberReported } from './check-outcomes.js';
|
|
|
20
20
|
import { diffFromGit } from './git-diff.js';
|
|
21
21
|
import { diffTestFailures } from './diff-test-findings.js';
|
|
22
22
|
import { migrationOrderFailures } from './migration-order.js';
|
|
23
|
+
import { compiledChecksOn } from '../review-learning/compiled-lessons.js';
|
|
24
|
+
import { settledChecks } from './settled-checks.js';
|
|
23
25
|
import { orphanFileFailures } from './orphan-files.js';
|
|
24
26
|
import { unusedExportFailures } from './unused-exports.js';
|
|
25
27
|
import { queryPatternFailures } from './query-patterns.js';
|
|
@@ -37,9 +39,11 @@ export async function reviewChange(input) {
|
|
|
37
39
|
const changedLines = withoutGenerated(input.cwd, parseDiff(diff));
|
|
38
40
|
const targets = input.files?.length ? input.files : Object.keys(changedLines);
|
|
39
41
|
if (targets.length === 0) {
|
|
40
|
-
return { status: 'PASS', findings: [], fileFindings: [], contextFindings: [], advisory: [], muted: 0, dismissed: 0, dismissedByGate: {}, unlocated: 0, excludedOutsideChangedLines: 0, preexisting: 0, changedLines, report: null, gateErrors: [], controlFilesChanged: controlFiles(diff), hints: [] };
|
|
42
|
+
return { status: 'PASS', findings: [], fileFindings: [], contextFindings: [], advisory: [], muted: 0, dismissed: 0, dismissedByGate: {}, unlocated: 0, excludedOutsideChangedLines: 0, preexisting: 0, changedLines, report: null, gateErrors: [], controlFilesChanged: controlFiles(diff), hints: [], covered: [] };
|
|
41
43
|
}
|
|
42
|
-
|
|
44
|
+
// The team's compiled checks run before the deep review, so it is told what they found and which lessons they covered.
|
|
45
|
+
const compiled = compiledChecksOn(input.cwd, changedLines, input.config);
|
|
46
|
+
const deep = input.deep ? { ...input.deep, focusLines: changedLinesByFile(changedLines), removedLines: removedByFile(diff), diff, settled: settledChecks(compiled.failures), covered: compiled.covered } : undefined;
|
|
43
47
|
// The team's `commands:` run at push (toolchain.ts), where a failure blocks; here they would only cost time.
|
|
44
48
|
const report = await new GateRunner({ ...input.config, commands: {} }).run(input.cwd, await normalizeScopePatterns(input.cwd, targets), deep);
|
|
45
49
|
const { preexisting, baseUnknown } = await dropPreexisting(input, report, targets);
|
|
@@ -54,6 +58,7 @@ export async function reviewChange(input) {
|
|
|
54
58
|
reviewCheck('migration-order', 'migration_order', migrationOrderFailures(input.cwd, diff, input.source, input.config));
|
|
55
59
|
reviewCheck('unused-exports', 'unused_exports', unusedExportFailures(input.cwd, diff, input.config));
|
|
56
60
|
reviewCheck('orphan-files', 'orphan_files', orphanFileFailures(input.cwd, diff, input.config));
|
|
61
|
+
reviewCheck('compiled-lessons', 'compiled_lessons', compiled.failures);
|
|
57
62
|
reviewCheck('query-patterns', 'query_patterns', queryPatternFailures(input.cwd, changedLines, input.config));
|
|
58
63
|
reviewCheck('optional-params', 'optional_params', optionalParamFailures(input.cwd, changedLines, input.config));
|
|
59
64
|
reviewCheck('duplicate-functions', 'duplicate_functions', duplicateFunctionFailures(input.cwd, changedLines, input.config));
|
|
@@ -97,6 +102,7 @@ export async function reviewChange(input) {
|
|
|
97
102
|
gateErrors,
|
|
98
103
|
controlFilesChanged: controlFiles(diff),
|
|
99
104
|
hints: typed.hints,
|
|
105
|
+
covered: compiled.covered,
|
|
100
106
|
...(deepError ? { deepError } : {}),
|
|
101
107
|
...(typed.error ? { typedError: typed.error } : {}),
|
|
102
108
|
...(checkedGoal ? { goal: checkedGoal } : {}),
|
|
@@ -3,6 +3,7 @@ import type { RouterPolicy } from '../../deep/router.js';
|
|
|
3
3
|
import type { PanelItem } from './panel.js';
|
|
4
4
|
import { type Exec } from './exec.js';
|
|
5
5
|
import type { OpenItem, ServedRule } from './verdict.js';
|
|
6
|
+
import { type CoveredLesson } from '../settled-checks.js';
|
|
6
7
|
export declare const REVIEW_DISMISSALS: string;
|
|
7
8
|
export interface ReviewDismissal {
|
|
8
9
|
id: string;
|
|
@@ -43,18 +44,29 @@ export interface ContextInput {
|
|
|
43
44
|
}>;
|
|
44
45
|
/** Findings Rigour's checks already report on this change, one line each. */
|
|
45
46
|
checks: string[];
|
|
47
|
+
/** Lessons the team's compiled checks covered on this change: left out of the lessons, and said so. */
|
|
48
|
+
covered?: CoveredLesson[];
|
|
46
49
|
}
|
|
47
50
|
/** A review's result as the reviewer's inputs: its hints, and what its checks found, as settled. */
|
|
48
51
|
export declare function reviewerInputs(review: {
|
|
49
52
|
hints: string[];
|
|
50
53
|
findings: Array<{
|
|
54
|
+
id?: string;
|
|
51
55
|
files?: string[];
|
|
52
56
|
line?: number;
|
|
53
57
|
title: string;
|
|
54
58
|
}>;
|
|
59
|
+
advisory?: Array<{
|
|
60
|
+
id?: string;
|
|
61
|
+
files?: string[];
|
|
62
|
+
line?: number;
|
|
63
|
+
title: string;
|
|
64
|
+
}>;
|
|
65
|
+
covered?: CoveredLesson[];
|
|
55
66
|
}): {
|
|
56
67
|
hints: string;
|
|
57
68
|
checks: string[];
|
|
69
|
+
covered: CoveredLesson[];
|
|
58
70
|
};
|
|
59
71
|
/** A lesson as the judge was shown it: its id, and the line it was listed as (the judge answers by that line). */
|
|
60
72
|
export interface ServedLesson {
|
|
@@ -20,7 +20,7 @@ import { reviewedKeys } from '../ledger.js';
|
|
|
20
20
|
import { textSimilarity } from './consensus.js';
|
|
21
21
|
import { defaultExec, GH_TIMEOUT_MS } from './exec.js';
|
|
22
22
|
import { VerdictStore } from './store.js';
|
|
23
|
-
import { MAX_SETTLED, settledChecks, settledLine, settledSection } from '../settled-checks.js';
|
|
23
|
+
import { coveredSection, MAX_SETTLED, settledChecks, settledLine, settledSection, withoutCovered } from '../settled-checks.js';
|
|
24
24
|
export const REVIEW_DISMISSALS = path.join('.rigour', 'dismissed-review-items.json');
|
|
25
25
|
const MAX_DOCS = 10;
|
|
26
26
|
/** Team standards a judge is shown with the lessons about the changed files. */
|
|
@@ -68,7 +68,9 @@ export function dismissedAs(item, dismissals) {
|
|
|
68
68
|
}
|
|
69
69
|
/** A review's result as the reviewer's inputs: its hints, and what its checks found, as settled. */
|
|
70
70
|
export function reviewerInputs(review) {
|
|
71
|
-
|
|
71
|
+
// A compiled check's findings are notes, and settled too: the judge no longer has the lesson, so it must have them.
|
|
72
|
+
const compiled = (review.advisory ?? []).filter(f => f.id === 'compiled-lesson');
|
|
73
|
+
return { hints: review.hints.join('\n'), checks: settledChecks([...review.findings, ...compiled]).map(settledLine), covered: review.covered ?? [] };
|
|
72
74
|
}
|
|
73
75
|
/** The ids of the served lessons the judge said this change repeats; an answer that names no served lesson says nothing. */
|
|
74
76
|
export function lessonsApplied(answers, served) {
|
|
@@ -87,13 +89,15 @@ export function buildContext(input) {
|
|
|
87
89
|
task = undefined;
|
|
88
90
|
}
|
|
89
91
|
// A judge reads the whole pull request: more of what the team taught fits than an agent's one question at the stop.
|
|
90
|
-
const servedLessons = input.lessons === 'off' ? [] : lessonsForDiff(input.cwd, input.diff, input.lessons, JUDGE_STANDARDS, JUDGE_FILE_LESSONS, JUDGE_LESSONS_PER_FILE, input.pr).map(l => ({ id: l.id, listed: describeLesson(lessonView(l)) }));
|
|
92
|
+
const servedLessons = input.lessons === 'off' ? [] : withoutCovered(lessonsForDiff(input.cwd, input.diff, input.lessons, JUDGE_STANDARDS, JUDGE_FILE_LESSONS, JUDGE_LESSONS_PER_FILE, input.pr), input.covered ?? []).map(l => ({ id: l.id, listed: describeLesson(lessonView(l)) }));
|
|
91
93
|
if (servedLessons.length)
|
|
92
94
|
sections.push(`## Lessons this team taught on earlier reviews, for what this change touches (context: a lesson never blocks on its own; a finding still needs its quote)\n${servedLessons.map(l => `- ${l.listed}`).join('\n')}`);
|
|
93
95
|
// The repository's own rules, always: the reviewer is the boundary, and what the team wrote is the standard it checks.
|
|
94
96
|
const rules = rulesForDiff(input.cwd, input.diff, true, JUDGE_RULES).map((r) => ({ id: r.id, source: r.source, text: r.text, requirement: r.requirement }));
|
|
95
97
|
if (rules.length)
|
|
96
98
|
sections.push(`## Rules this repository wrote for itself that apply to this change (answer every one in rules, by id)\n${rules.map(r => `- [${r.id}] (${r.source}, ${r.requirement ? 'requirement' : 'guidance'}) ${r.text}`).join('\n')}`);
|
|
99
|
+
if (input.covered?.length)
|
|
100
|
+
sections.push(coveredSection(input.covered));
|
|
97
101
|
if (input.checks.length)
|
|
98
102
|
sections.push(settledSection(input.checks));
|
|
99
103
|
const rejected = input.lessons === 'off' ? [] : rejectedForDiff(input.cwd, input.diff).map(l => {
|
|
@@ -35,6 +35,8 @@ export interface ResolvedReviewer {
|
|
|
35
35
|
judges: 2 | 3;
|
|
36
36
|
escalate: 'always' | 'risk';
|
|
37
37
|
cross_models: Record<string, string>;
|
|
38
|
+
/** The cheap model per reviewer name for cheap-model-first tiering; empty when it is off (team only). */
|
|
39
|
+
tiers: Record<string, string>;
|
|
38
40
|
/** Reasoning effort per reviewer name (codex, api). */
|
|
39
41
|
reasoning: Record<string, 'low' | 'medium' | 'high'>;
|
|
40
42
|
/** The API judge, when the team configured one (review.reviewer.api). */
|
|
@@ -13,7 +13,7 @@ const RANK = { single: 0, cross: 1, full: 2 };
|
|
|
13
13
|
/** How near a layer is to this run: the nearer wins. */
|
|
14
14
|
const NEAR = { flag: 0, env: 1, user: 2, team: 3 };
|
|
15
15
|
export function resolveReviewer(config, choice = {}, user = loadSettings().reviewer, env = process.env) {
|
|
16
|
-
const team = config.review?.reviewer ?? { enabled: false, on_push: 'background', reviewers: ['claude'], mode: 'single', models: {}, timeout_ms: 15 * 60_000, panel: 'off', mode_required: false, panel_max_items: 20, dismissals: false, orchestrator: 'off', judges: 2, escalate: 'always', cross_models: {}, judge_env: {}, reasoning: {} };
|
|
16
|
+
const team = config.review?.reviewer ?? { enabled: false, on_push: 'background', reviewers: ['claude'], mode: 'single', models: {}, timeout_ms: 15 * 60_000, panel: 'off', mode_required: false, panel_max_items: 20, dismissals: false, orchestrator: 'off', judges: 2, escalate: 'always', cross_models: {}, tiers: { cheap: {} }, judge_env: {}, reasoning: {} };
|
|
17
17
|
const refused = [];
|
|
18
18
|
const envMode = parseMode(env.RIGOUR_REVIEWER_MODE);
|
|
19
19
|
const envPanel = parseSwitch(env.RIGOUR_REVIEWER_PANEL);
|
|
@@ -78,6 +78,7 @@ export function resolveReviewer(config, choice = {}, user = loadSettings().revie
|
|
|
78
78
|
judges: floor ? Math.max(team.judges, user?.judges ?? team.judges) : user?.judges ?? team.judges,
|
|
79
79
|
escalate: requiredEscalation(team, user, refused),
|
|
80
80
|
cross_models: team.cross_models,
|
|
81
|
+
tiers: team.tiers?.cheap ?? {},
|
|
81
82
|
judge_env: team.judge_env ?? {},
|
|
82
83
|
reasoning: team.reasoning ?? {},
|
|
83
84
|
...(team.api ? { api: team.api } : {}),
|
|
@@ -43,6 +43,8 @@ export interface ReviewCost {
|
|
|
43
43
|
runs: number;
|
|
44
44
|
/** A verdict reused instead of run: for the same content on another commit (`content`). Runs and actual cost are 0. */
|
|
45
45
|
cache?: 'content';
|
|
46
|
+
/** The tier the review ran at, when cheap-model-first tiering is on (reviewer/tiering.ts). */
|
|
47
|
+
tier?: 'cheap' | 'strong';
|
|
46
48
|
}
|
|
47
49
|
/**
|
|
48
50
|
* A verdict, findable by the content it reviewed rather than its commit: a rebase, an amend, a cherry-pick or the same
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap-model-first tiering (off by default; review.reviewer.tiers.cheap names a cheaper model per judge). Which model
|
|
3
|
+
* reviews a change is decided BEFORE any run, from facts about the change only: never by a model, and never from a
|
|
4
|
+
* model's findings. A change with something a cheaper model may miss gets the team's model:
|
|
5
|
+
*
|
|
6
|
+
* a required floor, human reviews to check, open items carried from the last verdict, a migration, a security
|
|
7
|
+
* finding from the checks, a declared goal, or a risky changed function (the router's score)
|
|
8
|
+
*
|
|
9
|
+
* Anything else gets the cheap model. After the run, the only escalation is an answer that is not a valid verdict: the
|
|
10
|
+
* one retry the reviewer already makes uses the team's model. What blocks is unchanged: the same evidence contract.
|
|
11
|
+
*
|
|
12
|
+
* The rule a tier must keep: on average, a review costs no more than one judge would have. Over the last
|
|
13
|
+
* TIER_WINDOW reviews tiering ran on, actual cost against what one judge would have been given (the frozen baseline's
|
|
14
|
+
* dollars per character, else characters); above it, tiering turns itself off and the review record says so.
|
|
15
|
+
*/
|
|
16
|
+
import type { CostBaseline, ReviewCost } from './store.js';
|
|
17
|
+
export interface TierDecision {
|
|
18
|
+
tier: 'cheap' | 'strong';
|
|
19
|
+
/** The cheap model, when the tier is cheap. */
|
|
20
|
+
model?: string;
|
|
21
|
+
why: string;
|
|
22
|
+
/** Why tiering turned itself off, when it did. */
|
|
23
|
+
disabled?: string;
|
|
24
|
+
/** Why the cheap model's run was retried on the team's model: its answer was not a valid verdict. */
|
|
25
|
+
escalated?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface TierFacts {
|
|
28
|
+
required: boolean;
|
|
29
|
+
humanReviews: number;
|
|
30
|
+
carried: number;
|
|
31
|
+
migration: boolean;
|
|
32
|
+
securityChecks: number;
|
|
33
|
+
goal: boolean;
|
|
34
|
+
/** Risky changed functions (the router's score); undefined when it could not score the change. */
|
|
35
|
+
risky: number | undefined;
|
|
36
|
+
}
|
|
37
|
+
/** The tier for one judge's review; undefined when no cheap model is set for it (tiering is off). */
|
|
38
|
+
export declare function chooseTier(cheap: Record<string, string>, judge: string, facts: TierFacts, costs: ReviewCost[], baseline: CostBaseline | undefined): TierDecision | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Recent tiered reviews the self-disable rule reads. */
|
|
2
|
+
const TIER_WINDOW = 20;
|
|
3
|
+
/** Tiered reviews needed before the rule can turn tiering off. */
|
|
4
|
+
const TIER_MIN = 5;
|
|
5
|
+
/** The tier for one judge's review; undefined when no cheap model is set for it (tiering is off). */
|
|
6
|
+
export function chooseTier(cheap, judge, facts, costs, baseline) {
|
|
7
|
+
const model = cheap[judge];
|
|
8
|
+
if (!model)
|
|
9
|
+
return undefined;
|
|
10
|
+
const disabled = overBudget(costs, baseline);
|
|
11
|
+
if (disabled)
|
|
12
|
+
return { tier: 'strong', why: 'tiering turned itself off', disabled };
|
|
13
|
+
const strong = (why) => ({ tier: 'strong', why });
|
|
14
|
+
if (facts.required)
|
|
15
|
+
return strong('a required floor');
|
|
16
|
+
if (facts.humanReviews > 0)
|
|
17
|
+
return strong(`${facts.humanReviews} human review(s) to check`);
|
|
18
|
+
if (facts.carried > 0)
|
|
19
|
+
return strong(`${facts.carried} open item(s) carried from the last verdict`);
|
|
20
|
+
if (facts.migration)
|
|
21
|
+
return strong('the change adds or edits a migration');
|
|
22
|
+
if (facts.securityChecks > 0)
|
|
23
|
+
return strong(`${facts.securityChecks} security finding(s) from the checks`);
|
|
24
|
+
if (facts.goal)
|
|
25
|
+
return strong('the description declares a goal to check');
|
|
26
|
+
if (facts.risky === undefined)
|
|
27
|
+
return strong('the risk router could not score the change');
|
|
28
|
+
if (facts.risky > 0)
|
|
29
|
+
return strong(`${facts.risky} risky changed function(s)`);
|
|
30
|
+
return { tier: 'cheap', model, why: 'no risk signal: the cheap model' };
|
|
31
|
+
}
|
|
32
|
+
/** Why tiering must turn itself off: its recent reviews cost more, on average, than one judge would have. */
|
|
33
|
+
function overBudget(costs, baseline) {
|
|
34
|
+
const tiered = costs.filter(c => c.tier).slice(-TIER_WINDOW);
|
|
35
|
+
if (tiered.length < TIER_MIN)
|
|
36
|
+
return undefined;
|
|
37
|
+
const rate = baseline?.usdPerChar ?? null;
|
|
38
|
+
const single = tiered.reduce((sum, c) => sum + c.projectedSingleChars * (rate ?? 1), 0);
|
|
39
|
+
const actual = tiered.reduce((sum, c) => sum + (rate === null ? c.actualChars : c.actualUsd > 0 ? c.actualUsd : c.actualChars * rate), 0);
|
|
40
|
+
if (single <= 0 || actual <= single)
|
|
41
|
+
return undefined;
|
|
42
|
+
return `the last ${tiered.length} tiered reviews cost ${(actual / single).toFixed(2)}x what one judge would have (${rate === null ? 'characters' : 'dollars'})`;
|
|
43
|
+
}
|
|
@@ -31,6 +31,8 @@ export interface TriageContext {
|
|
|
31
31
|
}
|
|
32
32
|
/** Per specialist, the hunks it is for (by index); a specialist absent from the map is not needed. Prior points take the whole change. */
|
|
33
33
|
export declare function triage(hunks: Hunk[], context: TriageContext): Map<string, number[]>;
|
|
34
|
+
/** Whether a file is a migration or a schema (a change a cheaper model may miss the cost of). */
|
|
35
|
+
export declare function isMigration(file: string): boolean;
|
|
34
36
|
/** Whether no model reviews this file (SKIP). */
|
|
35
37
|
export declare function skipped(file: string): boolean;
|
|
36
38
|
export interface Pass {
|
|
@@ -79,6 +79,10 @@ export function triage(hunks, context) {
|
|
|
79
79
|
picked.set('prior-points', hunks.map((_, i) => i).filter(i => !skipped(hunks[i].file)));
|
|
80
80
|
return picked;
|
|
81
81
|
}
|
|
82
|
+
/** Whether a file is a migration or a schema (a change a cheaper model may miss the cost of). */
|
|
83
|
+
export function isMigration(file) {
|
|
84
|
+
return MIGRATION.test(file);
|
|
85
|
+
}
|
|
82
86
|
/** Whether no model reviews this file (SKIP). */
|
|
83
87
|
export function skipped(file) {
|
|
84
88
|
return SKIP.some(pattern => pattern.test(file));
|
|
@@ -18,6 +18,7 @@ export function reviewerUsage(result, trigger) {
|
|
|
18
18
|
judges: result.reviewers.length,
|
|
19
19
|
cached: result.cached,
|
|
20
20
|
...(result.cache ? { cache: result.cache } : {}),
|
|
21
|
+
...(mode?.tier ? { tier: mode.tier.tier, tier_disabled: !!mode.tier.disabled, tier_escalated: !!mode.tier.escalated } : {}),
|
|
21
22
|
confirmed: result.items.length,
|
|
22
23
|
disputed: result.disputed.length,
|
|
23
24
|
dropped: result.dropped.length,
|
|
@@ -2,9 +2,11 @@ import type { Config } from '../types/index.js';
|
|
|
2
2
|
import { type ReviewerName, type Tokens } from './reviewer/adapters.js';
|
|
3
3
|
import { type Exec, type Progress } from './reviewer/exec.js';
|
|
4
4
|
import { type PanelItem } from './reviewer/panel.js';
|
|
5
|
+
import { type TierDecision } from './reviewer/tiering.js';
|
|
5
6
|
import { type RunChoice, type Source } from './reviewer/settings.js';
|
|
6
7
|
import { type ReviewRecord } from './reviewer/record.js';
|
|
7
8
|
import { type Accounting, type OpenItem, type PriorPoint } from './reviewer/verdict.js';
|
|
9
|
+
import type { CoveredLesson } from './settled-checks.js';
|
|
8
10
|
export { defaultExec, githubEnv, githubToken, parseJsonArrays, type Exec, type Progress } from './reviewer/exec.js';
|
|
9
11
|
export { itemLine, type OpenItem } from './reviewer/verdict.js';
|
|
10
12
|
export type ReviewerOutcome = 'passed' | 'findings' | 'unavailable' | 'skipped';
|
|
@@ -29,6 +31,8 @@ export interface ReviewerOptions {
|
|
|
29
31
|
hints?: string;
|
|
30
32
|
/** What Rigour's checks already found on this change: settled, so no judge spends a turn finding it again. */
|
|
31
33
|
checks?: string[];
|
|
34
|
+
/** Lessons the team's compiled checks covered on this change (review.ts): left out of the judge's lessons, and said so. */
|
|
35
|
+
covered?: CoveredLesson[];
|
|
32
36
|
/** Where the team's state lives (.rigour: dismissals, reviewed functions) when cwd is a worktree that lacks it. */
|
|
33
37
|
stateRoot?: string;
|
|
34
38
|
/** The branch the commit was pushed from, when reviewing it in a detached worktree (background.ts). */
|
|
@@ -133,6 +137,8 @@ export interface ModeRecord {
|
|
|
133
137
|
refused?: string[];
|
|
134
138
|
/** With escalate: risk, why this review got one judge or all of them. */
|
|
135
139
|
escalation?: string;
|
|
140
|
+
/** With cheap-model-first tiering on: the tier, why, and whether it turned itself off or escalated (reviewer/tiering.ts). */
|
|
141
|
+
tier?: TierDecision;
|
|
136
142
|
}
|
|
137
143
|
/** Findings and no verdict stop a push; a skipped review does not, and is reported as owed. */
|
|
138
144
|
export declare function reviewerBlocks(result: ReviewerResult): boolean;
|
package/dist/review/reviewer.js
CHANGED
|
@@ -29,7 +29,8 @@ import { mergeImpact } from './reviewer/merge-impact.js';
|
|
|
29
29
|
import { applyPanel, parseAnswers, runPanel } from './reviewer/panel.js';
|
|
30
30
|
import { crossExamPrompt, deltaBlock, goalStep, mergeBlock, PROMPT_VERSION, renderPrompt } from './reviewer/prompt.js';
|
|
31
31
|
import { BASELINE_MIN_SINGLES, focusBlock, formatLedger, ledger, passLimit, runPasses, SPECIALISTS, SPECIALISTS_KEY, splitNeeds } from './reviewer/orchestrator.js';
|
|
32
|
-
import { MAX_PARTS, parseHunks, planPasses, reviewable, skipped, triage } from './reviewer/triage.js';
|
|
32
|
+
import { isMigration, MAX_PARTS, parseHunks, planPasses, reviewable, skipped, triage } from './reviewer/triage.js';
|
|
33
|
+
import { chooseTier } from './reviewer/tiering.js';
|
|
33
34
|
import { modelGoalItems, parseGoal } from '../goal/goal.js';
|
|
34
35
|
import { resolveSwitch } from '../switches.js';
|
|
35
36
|
import { resolveReviewer } from './reviewer/settings.js';
|
|
@@ -177,7 +178,7 @@ async function review(cwd, base, config, exec, progress, options) {
|
|
|
177
178
|
const sincePrevious = previousIsAncestor ? new Set((await git(['diff', '--name-only', `${previous.head}..HEAD`])).split('\n').filter(Boolean)) : new Set();
|
|
178
179
|
const changedFiles = [...fullDiff.matchAll(/^diff --git a\/.* b\/(.*)$/gm)].map(m => m[1]);
|
|
179
180
|
const context = buildContext({
|
|
180
|
-
cwd, stateRoot, dismissals, diff: fullDiff, router: config.gates.deep?.router, lessons: config.gates.deep?.review_lessons, ...(pr ? { pr: pr.number } : {}), touched: sincePrevious, checks: options.checks ?? [],
|
|
181
|
+
cwd, stateRoot, dismissals, diff: fullDiff, router: config.gates.deep?.router, lessons: config.gates.deep?.review_lessons, ...(pr ? { pr: pr.number } : {}), touched: sincePrevious, checks: options.checks ?? [], covered: options.covered ?? [],
|
|
181
182
|
previousPanel: previousIsAncestor ? store.readJson(previous.verdict)?.panel?.items : undefined,
|
|
182
183
|
docs: await relatedDocs(cwd, changedFiles, exec),
|
|
183
184
|
});
|
|
@@ -224,7 +225,17 @@ async function review(cwd, base, config, exec, progress, options) {
|
|
|
224
225
|
const previousOpen = scope === 'delta' ? store.readJson(store.openPath(previous.verdict)) ?? [] : undefined;
|
|
225
226
|
const verify = checkoutVerifier(cwd);
|
|
226
227
|
const prior = { approvals: reviews.approvals, inCheckout: checkoutSearch(cwd), changed: changedLinesOf(fullDiff), labels: reviews.labels };
|
|
227
|
-
|
|
228
|
+
// Cheap-model-first (reviewer/tiering.ts): decided before any run, from facts about the change only, for one judge.
|
|
229
|
+
let tier = reviewers.length === 1 && (modeRecord.ran === 'single' || modeRecord.ran === 'orchestrator')
|
|
230
|
+
? chooseTier(settings.tiers, reviewers[0], {
|
|
231
|
+
required: settings.required.mode || settings.required.panel || orchestrator.required, humanReviews: reviews.count, carried: previousOpen?.length ?? 0,
|
|
232
|
+
migration: changedFiles.some(isMigration), securityChecks: (options.checks ?? []).filter(c => /\bsecurity\b/i.test(c)).length, goal: goalItems.length > 0, risky: context.risky,
|
|
233
|
+
}, store.costs(), store.baseline())
|
|
234
|
+
: undefined;
|
|
235
|
+
if (tier)
|
|
236
|
+
modeRecord = { ...modeRecord, tier };
|
|
237
|
+
const cheapModel = () => (tier?.tier === 'cheap' && !tier.escalated ? tier.model : undefined);
|
|
238
|
+
const modelFor = (name) => (name === reviewers[0] ? cheapModel() : undefined) ?? settings.models[name] ?? (name === 'claude' ? settings.model : undefined);
|
|
228
239
|
// The record of the review, written beside the verdict once and rebuilt from the same verdict on a cached read.
|
|
229
240
|
// What a judge reads besides the repository and Rigour's inputs, on this machine: on the record, so "the same judge" is a claim it can check.
|
|
230
241
|
const outsideOf = (reviewer) => {
|
|
@@ -323,7 +334,7 @@ async function review(cwd, base, config, exec, progress, options) {
|
|
|
323
334
|
store.recordCost({
|
|
324
335
|
at: new Date().toISOString(), mode: orchestrated ? 'orchestrator' : 'single', lines: size.lines, projectedSingleChars: projectedSingle,
|
|
325
336
|
...(orchestrated ? { projectedChars: passes.reduce((sum, pass) => sum + shared + pass.diff.length, 0) } : {}),
|
|
326
|
-
actualChars: tally.chars, actualUsd: spentUsd(), runs: tally.runs,
|
|
337
|
+
actualChars: tally.chars, actualUsd: spentUsd(), runs: tally.runs, ...(tier ? { tier: tier.tier } : {}),
|
|
327
338
|
});
|
|
328
339
|
};
|
|
329
340
|
// One judge run, by CLI or by API: the same prompt, the same cost accounting, the same trace.
|
|
@@ -438,6 +449,11 @@ async function review(cwd, base, config, exec, progress, options) {
|
|
|
438
449
|
// No verdict, whether a malformed answer or a run that died, is a slip, not a decision: asked once more, inside the caps.
|
|
439
450
|
if (!once && (!first.verdict || 'error' in first.verdict) && !overBudget(store.spend(), settings, 1)) {
|
|
440
451
|
progress(`Rigour reviewer: ${name} gave no ${first.verdict ? 'valid verdict' : 'answer'}; asking once more`);
|
|
452
|
+
// The cheap model's slip is the one escalation: the retry runs on the team's model.
|
|
453
|
+
if (cheapModel()) {
|
|
454
|
+
tier = { ...tier, escalated: 'no valid verdict from the cheap model: retried on the team\'s model' };
|
|
455
|
+
modeRecord = { ...modeRecord, tier };
|
|
456
|
+
}
|
|
441
457
|
first = await ask();
|
|
442
458
|
}
|
|
443
459
|
return first.verdict ?? { error: `${name}: no answer (exit ${first.run.exitCode}): ${first.run.stderr.trim().slice(-200)}` };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What Rigour's checks already found on a change, as a model reviewer is told it: settled,
|
|
3
|
-
* to be reported again. The judge (reviewer/context.ts) and the deep PR review (deep/pr-review.ts) both say it this way,
|
|
2
|
+
* What Rigour's checks already found on a change, as a model reviewer is told it: settled, never to be reported again. The judge (reviewer/context.ts) and the deep PR review (deep/pr-review.ts) both say it this way,
|
|
4
3
|
* so neither spends a model's turn on what a free check proves.
|
|
5
4
|
*/
|
|
6
5
|
/** A check's finding where it sits; `kind` is the check's id. */
|
|
@@ -37,3 +36,15 @@ export declare function settledChecks(findings: Array<{
|
|
|
37
36
|
export declare function settledLine(check: SettledCheck): string;
|
|
38
37
|
/** The prompt section; empty when there is nothing settled. */
|
|
39
38
|
export declare function settledSection(lines: string[]): string;
|
|
39
|
+
/** A lesson a compiled check covered on this change: the check ran on its files, and its findings are settled. */
|
|
40
|
+
export interface CoveredLesson {
|
|
41
|
+
checkId: string;
|
|
42
|
+
lessonId: string;
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
/** A model reviewer's lessons without those a compiled check covered on this change: the check said them for free. */
|
|
46
|
+
export declare function withoutCovered<L extends {
|
|
47
|
+
id: string;
|
|
48
|
+
}>(lessons: L[], covered: CoveredLesson[]): L[];
|
|
49
|
+
/** The prompt section saying which lessons a compiled check covered instead; empty when none did. */
|
|
50
|
+
export declare function coveredSection(covered: CoveredLesson[]): string;
|