@rigour-labs/core 6.10.1-rc.1 → 6.11.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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.d.ts +3 -0
- package/dist/review/reviewer.js +1 -1
- 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 +12 -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 +4 -0
- package/dist/types/index.d.ts +54 -1
- package/dist/types/index.js +5 -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 => {
|
|
@@ -5,6 +5,7 @@ import { type PanelItem } from './reviewer/panel.js';
|
|
|
5
5
|
import { type RunChoice, type Source } from './reviewer/settings.js';
|
|
6
6
|
import { type ReviewRecord } from './reviewer/record.js';
|
|
7
7
|
import { type Accounting, type OpenItem, type PriorPoint } from './reviewer/verdict.js';
|
|
8
|
+
import type { CoveredLesson } from './settled-checks.js';
|
|
8
9
|
export { defaultExec, githubEnv, githubToken, parseJsonArrays, type Exec, type Progress } from './reviewer/exec.js';
|
|
9
10
|
export { itemLine, type OpenItem } from './reviewer/verdict.js';
|
|
10
11
|
export type ReviewerOutcome = 'passed' | 'findings' | 'unavailable' | 'skipped';
|
|
@@ -29,6 +30,8 @@ export interface ReviewerOptions {
|
|
|
29
30
|
hints?: string;
|
|
30
31
|
/** What Rigour's checks already found on this change: settled, so no judge spends a turn finding it again. */
|
|
31
32
|
checks?: string[];
|
|
33
|
+
/** Lessons the team's compiled checks covered on this change (review.ts): left out of the judge's lessons, and said so. */
|
|
34
|
+
covered?: CoveredLesson[];
|
|
32
35
|
/** Where the team's state lives (.rigour: dismissals, reviewed functions) when cwd is a worktree that lacks it. */
|
|
33
36
|
stateRoot?: string;
|
|
34
37
|
/** The branch the commit was pushed from, when reviewing it in a detached worktree (background.ts). */
|
package/dist/review/reviewer.js
CHANGED
|
@@ -177,7 +177,7 @@ async function review(cwd, base, config, exec, progress, options) {
|
|
|
177
177
|
const sincePrevious = previousIsAncestor ? new Set((await git(['diff', '--name-only', `${previous.head}..HEAD`])).split('\n').filter(Boolean)) : new Set();
|
|
178
178
|
const changedFiles = [...fullDiff.matchAll(/^diff --git a\/.* b\/(.*)$/gm)].map(m => m[1]);
|
|
179
179
|
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 ?? [],
|
|
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 ?? [], covered: options.covered ?? [],
|
|
181
181
|
previousPanel: previousIsAncestor ? store.readJson(previous.verdict)?.panel?.items : undefined,
|
|
182
182
|
docs: await relatedDocs(cwd, changedFiles, exec),
|
|
183
183
|
});
|
|
@@ -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;
|
|
@@ -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
|
/**
|
|
@@ -43,5 +42,14 @@ export function settledLine(check) {
|
|
|
43
42
|
}
|
|
44
43
|
/** The prompt section; empty when there is nothing settled. */
|
|
45
44
|
export function settledSection(lines) {
|
|
46
|
-
return lines.length ? `## Already found by Rigour's checks
|
|
45
|
+
return lines.length ? `## Already found by Rigour's checks on this change: do not report them again\n${lines.slice(0, MAX_SETTLED).map(c => `- ${c}`).join('\n')}` : '';
|
|
46
|
+
}
|
|
47
|
+
/** A model reviewer's lessons without those a compiled check covered on this change: the check said them for free. */
|
|
48
|
+
export function withoutCovered(lessons, covered) {
|
|
49
|
+
const ids = new Set(covered.map(c => c.lessonId));
|
|
50
|
+
return ids.size ? lessons.filter(l => !ids.has(l.id)) : lessons;
|
|
51
|
+
}
|
|
52
|
+
/** The prompt section saying which lessons a compiled check covered instead; empty when none did. */
|
|
53
|
+
export function coveredSection(covered) {
|
|
54
|
+
return covered.length ? `## Lessons the team's compiled checks covered on this change (their findings are listed as already found)\n${covered.map(c => `- covered by compiled check ${c.checkId} for lesson ${c.lessonId}: ${c.message}`).join('\n')}` : '';
|
|
47
55
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Config, Failure } from '../types/index.js';
|
|
2
|
+
import { type ReviewLesson } from './lessons.js';
|
|
3
|
+
import type { CoveredLesson } from '../review/settled-checks.js';
|
|
4
|
+
export interface CompiledCheck {
|
|
5
|
+
id: string;
|
|
6
|
+
lessonId: string;
|
|
7
|
+
/** The files it applies to: the lesson's file, or a glob a person widened it to. */
|
|
8
|
+
files: string;
|
|
9
|
+
kind: 'forbid' | 'require';
|
|
10
|
+
/** The symbol that triggers it. */
|
|
11
|
+
symbol: string;
|
|
12
|
+
/** For `require`: the symbol that must be near the trigger. */
|
|
13
|
+
with?: string;
|
|
14
|
+
message: string;
|
|
15
|
+
/** Proposed by `compileLesson`; runs only once `active` (a person approved it); `withdrawn` when taken back. */
|
|
16
|
+
state: 'proposed' | 'active' | 'withdrawn';
|
|
17
|
+
/** Who made the latest decision, and when (the full trail is `history`). */
|
|
18
|
+
by?: string;
|
|
19
|
+
at: string;
|
|
20
|
+
/** Every decision on it, oldest first: nothing a later decision overwrites is lost. */
|
|
21
|
+
history?: Array<{
|
|
22
|
+
state: 'active' | 'withdrawn';
|
|
23
|
+
by: string;
|
|
24
|
+
at: string;
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* The check run over the main branch's history, for the person who approves it: on merged pull requests a review
|
|
28
|
+
* found the lesson repeating in (it should fire), and on every other merged change to its files (each fire is a
|
|
29
|
+
* false one, or a catch the review missed). Counts, never a rate below RATE_MIN.
|
|
30
|
+
*/
|
|
31
|
+
backtest?: {
|
|
32
|
+
repeating: BacktestShare;
|
|
33
|
+
other: BacktestShare;
|
|
34
|
+
commits: number;
|
|
35
|
+
at: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Fires out of n, with the rate computed here, the one place RATE_MIN applies: null below it. */
|
|
39
|
+
export interface BacktestShare {
|
|
40
|
+
fired: number;
|
|
41
|
+
n: number;
|
|
42
|
+
rate: number | null;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Proposes a check for every compilable lesson that has none yet, each backtested against the main branch's history
|
|
46
|
+
* (new proposals only); returns them. A decided check is never re-proposed.
|
|
47
|
+
*/
|
|
48
|
+
export declare function proposeCompiledChecks(cwd: string): CompiledCheck[];
|
|
49
|
+
/** A person's decision on a compiled check: approve it (it runs) or take it back (it stops). Undefined when there is no such check. */
|
|
50
|
+
export declare function decideCompiledCheck(cwd: string, id: string, state: 'active' | 'withdrawn', by: string): CompiledCheck | undefined;
|
|
51
|
+
/** Whether an approved check is suspended, and why: its lesson no longer qualifies. */
|
|
52
|
+
export declare function suspension(check: CompiledCheck, lessons: Map<string, ReviewLesson>): string | undefined;
|
|
53
|
+
export declare function readCompiledChecks(cwd: string): CompiledCheck[];
|
|
54
|
+
/**
|
|
55
|
+
* The running compiled checks on a change: their findings on its changed lines (notes unless the team blocks), and the
|
|
56
|
+
* lessons they covered (each check whose files the change touched, whether or not it found anything). A model reviewer
|
|
57
|
+
* may leave a covered lesson out of its prompt; nothing else.
|
|
58
|
+
*/
|
|
59
|
+
export declare function compiledChecksOn(cwd: string, changedLines: Record<string, Set<number>>, config: Config): {
|
|
60
|
+
failures: Failure[];
|
|
61
|
+
covered: CoveredLesson[];
|
|
62
|
+
};
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A verified lesson turned into a check that runs without a model. Only lessons a person confirmed (accepted, a
|
|
3
|
+
* correction) or that recurred across pull requests qualify, never one an outcome alone promoted. Compilation is a
|
|
4
|
+
* template, not a model: a lesson compiles only when it names its file and its symbols in backticks and says, in so many
|
|
5
|
+
* words, what is wrong:
|
|
6
|
+
*
|
|
7
|
+
* forbid "never `a`", "avoid `a`", "do not use `a`", "use `b` instead of `a`" → `a` on a changed line is reported
|
|
8
|
+
* require "always …", "must …", "every …" naming `a` then `b` → `a` on a changed line with no `b`
|
|
9
|
+
* within REQUIRE_REACH lines is reported
|
|
10
|
+
*
|
|
11
|
+
* A compiled check is stored in .rigour/compiled-checks.json (committed and reviewed with the code) and runs only once a
|
|
12
|
+
* person approved it (`active`). It keeps its lesson's id, never blocks unless the team turns `block` on, and a person
|
|
13
|
+
* can take it back (`withdrawn`), which hands the lesson back to the model reviewer. Every decision is kept, on the
|
|
14
|
+
* check and as evidence on its lesson. An approved check whose lesson no longer qualifies (rejected, taken back) is
|
|
15
|
+
* suspended: it does not run, and the lesson goes back to the model reviewer.
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import micromatch from 'micromatch';
|
|
20
|
+
import { execFileSync } from 'child_process';
|
|
21
|
+
import { lessonState, readLessons, writeLessons } from './lessons.js';
|
|
22
|
+
import { branchBase } from '../gates/logic-drift-git-base.js';
|
|
23
|
+
import { threadReviews } from '../outcomes/run.js';
|
|
24
|
+
import { parseDiff } from '../utils/diff.js';
|
|
25
|
+
const STORE = path.join('.rigour', 'compiled-checks.json');
|
|
26
|
+
/** How far from the trigger line the required symbol may sit for a `require` check. */
|
|
27
|
+
const REQUIRE_REACH = 3;
|
|
28
|
+
/** Fewer than this: a count, never a rate. */
|
|
29
|
+
const RATE_MIN = 10;
|
|
30
|
+
/** Merged changes to a check's files read back, newest first. */
|
|
31
|
+
const BACKTEST_COMMITS = 100;
|
|
32
|
+
/** Each backtest stops here and keeps what it counted. */
|
|
33
|
+
const BACKTEST_DEADLINE_MS = 20_000;
|
|
34
|
+
/** All of one proposal round's backtests stop here; a check not reached is proposed without its counts. */
|
|
35
|
+
const PROPOSE_DEADLINE_MS = 30_000;
|
|
36
|
+
const FORBID = [
|
|
37
|
+
/\b(?:use|prefer)\s+`([^`]+)`\s+(?:instead of|over|rather than)\s+`([^`]+)`/i, // the second is forbidden
|
|
38
|
+
// "never forget to call `x`" asks for `x`: a negation followed by one of these is not a ban.
|
|
39
|
+
/\b(?:never|avoid|don't|do not|must not|should not|no longer)\b(?!\s+(?:forget|remove|skip|miss|omit|drop|leave out|lose|stop)\b)[^.`]*`([^`]+)`/i,
|
|
40
|
+
];
|
|
41
|
+
const REQUIRE = /\b(?:always|must|every|needs? to|has to)\b/i;
|
|
42
|
+
/** Whether a lesson may be compiled: verified by a person, a correction or recurrence, never by an outcome alone. */
|
|
43
|
+
function compilable(lesson) {
|
|
44
|
+
if (!lesson)
|
|
45
|
+
return false;
|
|
46
|
+
const { state, promotedBy } = lessonState(lesson);
|
|
47
|
+
return state === 'verified' && (promotedBy === 'person' || promotedBy === 'correction' || promotedBy === 'recurrence') && !!lesson.file && lesson.symbols.length > 0;
|
|
48
|
+
}
|
|
49
|
+
/** The check a lesson compiles to, proposed for a person to approve; undefined when no template fits it. */
|
|
50
|
+
function compileLesson(lesson, at = new Date().toISOString()) {
|
|
51
|
+
if (!compilable(lesson))
|
|
52
|
+
return undefined;
|
|
53
|
+
const named = (s) => (s && lesson.symbols.includes(s.replace(/\(\)$/, '')) ? s.replace(/\(\)$/, '') : undefined);
|
|
54
|
+
const base = { id: `c-${lesson.id}`, lessonId: lesson.id, files: lesson.file, message: lesson.text, state: 'proposed', at };
|
|
55
|
+
const instead = FORBID[0].exec(lesson.text);
|
|
56
|
+
const forbidden = instead ? named(instead[2]) : named(FORBID[1].exec(lesson.text)?.[1]);
|
|
57
|
+
if (forbidden)
|
|
58
|
+
return { ...base, kind: 'forbid', symbol: forbidden };
|
|
59
|
+
if (REQUIRE.test(lesson.text)) {
|
|
60
|
+
const ticked = [...lesson.text.matchAll(/`([^`]+)`/g)].map(m => named(m[1])).filter((s) => !!s);
|
|
61
|
+
if (ticked.length >= 2 && ticked[0] !== ticked[1])
|
|
62
|
+
return { ...base, kind: 'require', symbol: ticked[0], with: ticked[1] };
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Proposes a check for every compilable lesson that has none yet, each backtested against the main branch's history
|
|
68
|
+
* (new proposals only); returns them. A decided check is never re-proposed.
|
|
69
|
+
*/
|
|
70
|
+
export function proposeCompiledChecks(cwd) {
|
|
71
|
+
const checks = readCompiledChecks(cwd);
|
|
72
|
+
const known = new Set(checks.map(c => c.lessonId));
|
|
73
|
+
const proposed = readLessons(cwd).filter(l => !known.has(l.id)).map(l => compileLesson(l)).filter((c) => !!c);
|
|
74
|
+
if (proposed.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
withBacktests(cwd, proposed); // only the new ones: a decided or earlier proposal keeps the counts it was shown with
|
|
77
|
+
writeCompiledChecks(cwd, [...checks, ...proposed]);
|
|
78
|
+
return proposed;
|
|
79
|
+
}
|
|
80
|
+
/** A person's decision on a compiled check: approve it (it runs) or take it back (it stops). Undefined when there is no such check. */
|
|
81
|
+
export function decideCompiledCheck(cwd, id, state, by) {
|
|
82
|
+
if (!by.trim())
|
|
83
|
+
throw new Error('a compiled check is decided by a named person: their git email is committed with it');
|
|
84
|
+
const checks = readCompiledChecks(cwd);
|
|
85
|
+
const check = checks.find(c => c.id === id);
|
|
86
|
+
if (!check)
|
|
87
|
+
return undefined;
|
|
88
|
+
const lessons = readLessons(cwd);
|
|
89
|
+
const lesson = lessons.find(l => l.id === check.lessonId);
|
|
90
|
+
if (state === 'active' && !(lesson && compilable(lesson)))
|
|
91
|
+
throw new Error(`lesson ${check.lessonId} no longer qualifies (rejected, taken back, or gone): its check cannot run`);
|
|
92
|
+
const at = new Date().toISOString();
|
|
93
|
+
check.history = [...(check.history ?? []), { state, by, at }];
|
|
94
|
+
Object.assign(check, { state, by, at });
|
|
95
|
+
writeCompiledChecks(cwd, checks);
|
|
96
|
+
// The lesson keeps the decision too: what its check did is part of its trail.
|
|
97
|
+
if (lesson) {
|
|
98
|
+
lesson.evidence.push({ kind: 'compiled', pr: lesson.evidence[0]?.pr ?? 0, comment: `compiled-${check.id}-${at}`, author: by, detail: `${state === 'active' ? 'approved' : 'took back'} compiled check ${check.id}`, at });
|
|
99
|
+
writeLessons(cwd, lessons);
|
|
100
|
+
}
|
|
101
|
+
return check;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The approved checks that run: an approved check whose lesson no longer qualifies (a person rejected it, evidence
|
|
105
|
+
* took it back, it is gone) is suspended, and its lesson goes back to the model reviewer.
|
|
106
|
+
*/
|
|
107
|
+
function runningChecks(cwd) {
|
|
108
|
+
const lessons = new Map(readLessons(cwd).map(l => [l.id, l]));
|
|
109
|
+
return readCompiledChecks(cwd).filter(c => c.state === 'active' && compilable(lessons.get(c.lessonId)));
|
|
110
|
+
}
|
|
111
|
+
/** Whether an approved check is suspended, and why: its lesson no longer qualifies. */
|
|
112
|
+
export function suspension(check, lessons) {
|
|
113
|
+
if (check.state !== 'active')
|
|
114
|
+
return undefined;
|
|
115
|
+
const lesson = lessons.get(check.lessonId);
|
|
116
|
+
if (!lesson)
|
|
117
|
+
return `lesson ${check.lessonId} is gone`;
|
|
118
|
+
return compilable(lesson) ? undefined : `lesson ${check.lessonId} no longer qualifies (${lessonState(lesson).state}): suspended, the lesson is back with the model reviewer`;
|
|
119
|
+
}
|
|
120
|
+
export function readCompiledChecks(cwd) {
|
|
121
|
+
try {
|
|
122
|
+
const data = JSON.parse(fs.readFileSync(path.join(cwd, STORE), 'utf8'));
|
|
123
|
+
return Array.isArray(data?.checks) ? data.checks : [];
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function writeCompiledChecks(cwd, checks) {
|
|
130
|
+
fs.mkdirSync(path.join(cwd, '.rigour'), { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(cwd, STORE), `${JSON.stringify({ version: 1, checks }, null, 2)}\n`);
|
|
132
|
+
}
|
|
133
|
+
const word = (symbol) => new RegExp(`(^|[^\\w$])${symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\w$]|$)`);
|
|
134
|
+
/** Whether a check applies to a file: the file itself, or a glob it was widened to. */
|
|
135
|
+
function covers(check, file) {
|
|
136
|
+
return /[*?[\]{}]/.test(check.files) ? micromatch.isMatch(file, check.files, { dot: true }) : check.files === file;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The running compiled checks on a change: their findings on its changed lines (notes unless the team blocks), and the
|
|
140
|
+
* lessons they covered (each check whose files the change touched, whether or not it found anything). A model reviewer
|
|
141
|
+
* may leave a covered lesson out of its prompt; nothing else.
|
|
142
|
+
*/
|
|
143
|
+
export function compiledChecksOn(cwd, changedLines, config) {
|
|
144
|
+
const settings = config.gates.compiled_lessons;
|
|
145
|
+
if (!settings?.enabled)
|
|
146
|
+
return { failures: [], covered: [] };
|
|
147
|
+
const checks = runningChecks(cwd);
|
|
148
|
+
if (checks.length === 0)
|
|
149
|
+
return { failures: [], covered: [] };
|
|
150
|
+
const failures = [];
|
|
151
|
+
const ran = new Set();
|
|
152
|
+
for (const [file, lines] of Object.entries(changedLines)) {
|
|
153
|
+
const applying = checks.filter(c => covers(c, file));
|
|
154
|
+
if (applying.length === 0)
|
|
155
|
+
continue;
|
|
156
|
+
applying.forEach(c => ran.add(c));
|
|
157
|
+
let text;
|
|
158
|
+
try {
|
|
159
|
+
text = fs.readFileSync(path.join(cwd, file), 'utf8').split('\n');
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
for (const check of applying) {
|
|
165
|
+
for (const line of firesOn(check, text, lines)) {
|
|
166
|
+
failures.push({
|
|
167
|
+
id: 'compiled-lesson',
|
|
168
|
+
...(settings.block ? {} : { advisory: true }),
|
|
169
|
+
title: check.kind === 'forbid' ? `\`${check.symbol}\`: the team's lesson says not to` : `\`${check.symbol}\` without \`${check.with}\`: the team's lesson says to pair them`,
|
|
170
|
+
details: `${check.message} (compiled from lesson ${check.lessonId}; a person approved it)`,
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
provenance: 'traditional',
|
|
173
|
+
files: [file],
|
|
174
|
+
line,
|
|
175
|
+
hint: `Follow the lesson, or take the compiled check back in Studio if it no longer holds (lesson ${check.lessonId}).`,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return { failures, covered: [...ran].map(c => ({ checkId: c.id, lessonId: c.lessonId, message: c.message })) };
|
|
181
|
+
}
|
|
182
|
+
/** The changed lines a check fires on, in a file's text. */
|
|
183
|
+
function firesOn(check, text, lines) {
|
|
184
|
+
const trigger = word(check.symbol);
|
|
185
|
+
return [...lines].sort((a, b) => a - b).filter(line => {
|
|
186
|
+
if (!trigger.test(text[line - 1] ?? ''))
|
|
187
|
+
return false;
|
|
188
|
+
if (check.kind === 'forbid')
|
|
189
|
+
return true;
|
|
190
|
+
return !word(check.with).test(text.slice(Math.max(0, line - 1 - REQUIRE_REACH), line + REQUIRE_REACH).join('\n'));
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Runs a check over the main branch's last BACKTEST_COMMITS changes to its files: per change, whether it fires on the
|
|
195
|
+
* lines that change added, split by whether a review found the lesson repeating in that pull request. Read-only, no model.
|
|
196
|
+
*/
|
|
197
|
+
function backtestCheck(cwd, check, mainRef, applied) {
|
|
198
|
+
const git = (args) => execFileSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, timeout: 10_000 });
|
|
199
|
+
const spec = /[*?[\]{}]/.test(check.files) ? `:(glob)${check.files}` : check.files;
|
|
200
|
+
const result = { repeating: { fired: 0, n: 0, rate: null }, other: { fired: 0, n: 0, rate: null }, commits: 0, at: new Date().toISOString() };
|
|
201
|
+
const deadline = Date.now() + BACKTEST_DEADLINE_MS;
|
|
202
|
+
let log = '';
|
|
203
|
+
try {
|
|
204
|
+
log = git(['log', '--first-parent', '-n', String(BACKTEST_COMMITS), '--format=%H %s', mainRef, '--', spec]);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return withRates(result);
|
|
208
|
+
}
|
|
209
|
+
for (const entry of log.split('\n').filter(Boolean)) {
|
|
210
|
+
if (Date.now() > deadline)
|
|
211
|
+
break;
|
|
212
|
+
const [sha, ...subject] = entry.split(' ');
|
|
213
|
+
const pr = Number(/\(#(\d+)\)\s*$|Merge pull request #(\d+)/.exec(subject.join(' '))?.slice(1).find(Boolean));
|
|
214
|
+
let fired = false;
|
|
215
|
+
try {
|
|
216
|
+
for (const [file, lines] of Object.entries(parseDiff(git(['diff', '-U0', `${sha}^1`, sha, '--', spec])))) {
|
|
217
|
+
if (firesOn(check, git(['show', `${sha}:${file}`]).split('\n'), lines).length)
|
|
218
|
+
fired = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
continue; // a root commit or a file the commit removed: nothing to count
|
|
223
|
+
}
|
|
224
|
+
const bucket = pr && applied.get(pr)?.has(check.lessonId) ? result.repeating : result.other;
|
|
225
|
+
bucket.n++;
|
|
226
|
+
if (fired)
|
|
227
|
+
bucket.fired++;
|
|
228
|
+
result.commits++;
|
|
229
|
+
}
|
|
230
|
+
return withRates(result);
|
|
231
|
+
}
|
|
232
|
+
function withRates(result) {
|
|
233
|
+
for (const share of [result.repeating, result.other])
|
|
234
|
+
share.rate = share.n >= RATE_MIN ? Math.round((share.fired / share.n) * 100) / 100 : null;
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
/** Backtests new proposals, all within PROPOSE_DEADLINE_MS: what the person approving each one reads. */
|
|
238
|
+
function withBacktests(cwd, checks) {
|
|
239
|
+
const mainRef = branchBase(cwd)?.mainRef;
|
|
240
|
+
if (!mainRef)
|
|
241
|
+
return;
|
|
242
|
+
const { applied } = threadReviews(cwd);
|
|
243
|
+
const deadline = Date.now() + PROPOSE_DEADLINE_MS;
|
|
244
|
+
for (const check of checks) {
|
|
245
|
+
if (Date.now() > deadline)
|
|
246
|
+
break;
|
|
247
|
+
check.backtest = backtestCheck(cwd, check, mainRef, applied);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -18,7 +18,7 @@ import type { Git, ReviewBody, ReviewComment } from './acted-on.js';
|
|
|
18
18
|
* until a person promotes it.
|
|
19
19
|
* A record from before evidence kinds has none: it is a `point`.
|
|
20
20
|
*/
|
|
21
|
-
export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified';
|
|
21
|
+
export type EvidenceKind = 'point' | 'outcome' | 'counter' | 'correction' | 'accepted' | 'rejected' | 'norule' | 'followup' | 'lines' | 'dismissed' | 'against' | 'demoted' | 'reclassified' | 'compiled';
|
|
22
22
|
export interface LessonEvidence {
|
|
23
23
|
kind?: EvidenceKind;
|
|
24
24
|
pr: number;
|
package/dist/switches.d.ts
CHANGED
|
@@ -227,6 +227,10 @@ export declare const SWITCHES: {
|
|
|
227
227
|
allow: string[];
|
|
228
228
|
block: boolean;
|
|
229
229
|
};
|
|
230
|
+
compiled_lessons: {
|
|
231
|
+
enabled: boolean;
|
|
232
|
+
block: boolean;
|
|
233
|
+
};
|
|
230
234
|
migration_order: {
|
|
231
235
|
enabled: boolean;
|
|
232
236
|
dirs: string[];
|
|
@@ -536,6 +540,10 @@ export declare const SWITCHES: {
|
|
|
536
540
|
allow: string[];
|
|
537
541
|
block: boolean;
|
|
538
542
|
};
|
|
543
|
+
compiled_lessons: {
|
|
544
|
+
enabled: boolean;
|
|
545
|
+
block: boolean;
|
|
546
|
+
};
|
|
539
547
|
migration_order: {
|
|
540
548
|
enabled: boolean;
|
|
541
549
|
dirs: string[];
|
|
@@ -845,6 +853,10 @@ export declare const SWITCHES: {
|
|
|
845
853
|
allow: string[];
|
|
846
854
|
block: boolean;
|
|
847
855
|
};
|
|
856
|
+
compiled_lessons: {
|
|
857
|
+
enabled: boolean;
|
|
858
|
+
block: boolean;
|
|
859
|
+
};
|
|
848
860
|
migration_order: {
|
|
849
861
|
enabled: boolean;
|
|
850
862
|
dirs: string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check ids opt-in telemetry may send: Rigour's own, and nothing else. A team's own check (a `commands:` entry, a
|
|
3
|
+
* plugin, anything named in config) can carry a product or customer name, so it is sent only as `custom`. The list is
|
|
4
|
+
* the gate registry's ids, the review's own checks and the hook's: check-ids.test.ts builds the registry and fails
|
|
5
|
+
* when a gate is missing here.
|
|
6
|
+
*/
|
|
7
|
+
const BUILT_IN = new Set([
|
|
8
|
+
// Gates (gates/runner.ts registers them)
|
|
9
|
+
'agent-team', 'ast-analysis', 'checkpoint', 'content-check', 'context-drift', 'context-window-artifacts', 'coverage-guard', 'deep-analysis',
|
|
10
|
+
'dependency-guardian', 'deprecated-apis', 'deprecated-dependencies', 'duplication-drift', 'environment-alignment', 'file-guard', 'file-size',
|
|
11
|
+
'frontend-secret-exposure', 'hallucinated-imports', 'inconsistent-error-handling', 'logic-drift', 'phantom-apis', 'promise-safety',
|
|
12
|
+
'retry_loop_breaker', 'security-patterns', 'semantic-bugs', 'side-effect-analysis', 'structure-check', 'style-drift', 'test-quality', 'unindexed-reads',
|
|
13
|
+
// The review's own checks (review/review.ts) and their typed checks
|
|
14
|
+
'unused-export', 'orphan-file', 'offset-paging', 'unbounded-window', 'duplicate-function', 'partial-fix', 'partial-wiring', 'quadratic-copy',
|
|
15
|
+
'migration-order', 'diff-tests', 'compiled-lesson', 'goal-scope', 'goal-done-when', 'duplicate-null-filter', 'nullable-filtered-column',
|
|
16
|
+
'optional-always-supplied', 'write-only-property', 'typed-checks-unavailable',
|
|
17
|
+
// The per-edit hook's (hooks/checker.ts)
|
|
18
|
+
'agent-scope', 'governance', 'governance-dlp', 'governance-skills',
|
|
19
|
+
]);
|
|
20
|
+
/** A check id as telemetry may send it: one of Rigour's own, else `custom`. */
|
|
21
|
+
export function telemetryCheckId(id) {
|
|
22
|
+
return BUILT_IN.has(id) ? id : 'custom';
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the learning loop did in a repository, for opt-in telemetry: read from the two places that already say it, the
|
|
3
|
+
* switches' resolver and the outcome numbers (outcomes/metrics.ts), never counted again here. Enums and counts only; a
|
|
4
|
+
* rate only where outcomeMetrics gives one (ten or more records), else none.
|
|
5
|
+
*/
|
|
6
|
+
import type { Config } from '../types/index.js';
|
|
7
|
+
export declare function learningUsage(cwd: string, config: Config): Record<string, unknown>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolveSwitch, SWITCHES } from '../switches.js';
|
|
2
|
+
import { localOutcomeMetrics } from '../outcomes/run.js';
|
|
3
|
+
export function learningUsage(cwd, config) {
|
|
4
|
+
const usage = {};
|
|
5
|
+
for (const name of Object.keys(SWITCHES)) {
|
|
6
|
+
const resolved = resolveSwitch(name, config);
|
|
7
|
+
usage[`switch_${name}`] = resolved.required ? 'required' : resolved.enabled ? 'on' : 'off';
|
|
8
|
+
usage[`switch_${name}_set_by`] = resolved.source; // team (rigour.yml), user (personal settings), env or flag
|
|
9
|
+
}
|
|
10
|
+
const metrics = localOutcomeMetrics(cwd);
|
|
11
|
+
if (!metrics)
|
|
12
|
+
return usage;
|
|
13
|
+
const share = (prefix, s) => {
|
|
14
|
+
usage[`${prefix}_count`] = s.count;
|
|
15
|
+
usage[`${prefix}_of`] = s.of;
|
|
16
|
+
if (s.rate !== null)
|
|
17
|
+
usage[`${prefix}_rate`] = s.rate;
|
|
18
|
+
};
|
|
19
|
+
usage.outcomes_merged = metrics.records.merged;
|
|
20
|
+
usage.outcomes_settled = metrics.records.settled;
|
|
21
|
+
share('outcomes_ci_regressed', metrics.settled.ciRegressed);
|
|
22
|
+
share('outcomes_reverted', metrics.settled.reverted);
|
|
23
|
+
share('outcomes_fixed_later', metrics.settled.fixedLater);
|
|
24
|
+
usage.outcomes_reviewed_prs = metrics.settled.reviewed.prs;
|
|
25
|
+
share('outcomes_reviewed_fixed_later', metrics.settled.reviewed.fixedLater);
|
|
26
|
+
usage.outcomes_not_reviewed_prs = metrics.settled.notReviewed.prs;
|
|
27
|
+
share('outcomes_not_reviewed_fixed_later', metrics.settled.notReviewed.fixedLater);
|
|
28
|
+
usage.lessons_awaiting_decision = metrics.lessons.awaitingDecision;
|
|
29
|
+
usage.lessons_promoted_from_evidence = metrics.lessons.promotedFromEvidence;
|
|
30
|
+
usage.lessons_dismissed = metrics.lessons.dismissed;
|
|
31
|
+
usage.lessons_taken_back = metrics.lessons.takenBack;
|
|
32
|
+
return usage;
|
|
33
|
+
}
|
|
@@ -4,6 +4,8 @@ export interface TelemetryState {
|
|
|
4
4
|
/** undefined until the person has been asked. */
|
|
5
5
|
enabled?: boolean;
|
|
6
6
|
installId: string;
|
|
7
|
+
/** When the person was first asked (ms): how old the install is, bucketed. Absent in files written before it was kept. */
|
|
8
|
+
firstAt?: number;
|
|
7
9
|
}
|
|
8
10
|
export interface TelemetryDeps {
|
|
9
11
|
env?: Env;
|
|
@@ -11,7 +13,13 @@ export interface TelemetryDeps {
|
|
|
11
13
|
fetch?: Fetch;
|
|
12
14
|
version?: string;
|
|
13
15
|
now?: number;
|
|
16
|
+
/** The agent a hook ran for, when the hook knows (its own tool name): wins over the environment. */
|
|
17
|
+
agent?: string;
|
|
18
|
+
/** More of the daily event, read only when it is sent (learning-usage.ts): the learning loop of the repository the command ran in. */
|
|
19
|
+
daily?: () => Record<string, unknown>;
|
|
14
20
|
}
|
|
21
|
+
/** The agent hosts telemetry names, and nothing else: anything unknown is `other`, no agent is `none`. */
|
|
22
|
+
export type AgentHost = 'claude-code' | 'cursor' | 'cline' | 'windsurf' | 'codex' | 'other' | 'none';
|
|
15
23
|
export declare function telemetryToken(env?: Env): string;
|
|
16
24
|
export declare function readTelemetryState(home?: string): TelemetryState;
|
|
17
25
|
export declare function setTelemetryEnabled(enabled: boolean, home?: string): TelemetryState;
|
|
@@ -22,6 +22,44 @@ import { MIXPANEL_TOKEN } from './token.js';
|
|
|
22
22
|
const ENDPOINT = 'https://api.mixpanel.com/track?ip=0';
|
|
23
23
|
const TIMEOUT_MS = 2000;
|
|
24
24
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
const HOOK_TOOLS = { claude: 'claude-code', 'claude-code': 'claude-code', cursor: 'cursor', cline: 'cline', windsurf: 'windsurf', codex: 'codex' };
|
|
26
|
+
/**
|
|
27
|
+
* Which agent Rigour ran under: the hook's own tool name when it is one of the known; else the variable each sets in
|
|
28
|
+
* the processes it starts (CLAUDECODE=1 for Claude Code, CURSOR_TRACE_ID for Cursor's terminal, CODEX_SANDBOX for
|
|
29
|
+
* Codex); else `other` for a hint naming an agent not on the list, `none` without one. Cline and Windsurf are known
|
|
30
|
+
* only from their hooks.
|
|
31
|
+
*/
|
|
32
|
+
function agentHost(env, hint) {
|
|
33
|
+
const known = hint ? HOOK_TOOLS[hint.toLowerCase()] : undefined;
|
|
34
|
+
if (known)
|
|
35
|
+
return known;
|
|
36
|
+
if (env.CLAUDECODE === '1')
|
|
37
|
+
return 'claude-code';
|
|
38
|
+
if (env.CURSOR_TRACE_ID)
|
|
39
|
+
return 'cursor';
|
|
40
|
+
if (env.CODEX_SANDBOX)
|
|
41
|
+
return 'codex';
|
|
42
|
+
return hint ? 'other' : 'none';
|
|
43
|
+
}
|
|
44
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
45
|
+
/** When telemetry.json was first written, for a file from before `firstAt` was kept: its creation time, else its last write. */
|
|
46
|
+
function firstWritten(home) {
|
|
47
|
+
try {
|
|
48
|
+
const stat = fs.statSync(file(home, 'telemetry.json'));
|
|
49
|
+
return stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** How long ago the person was first asked, bucketed: 0, 1, 2-4, 5-12 or 13+ weeks; undefined before they were. */
|
|
56
|
+
function installAgeWeeks(home, now) {
|
|
57
|
+
const first = readTelemetryState(home).firstAt ?? firstWritten(home);
|
|
58
|
+
if (first === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const weeks = Math.floor(Math.max(0, now - first) / WEEK_MS);
|
|
61
|
+
return weeks === 0 ? '0' : weeks === 1 ? '1' : weeks <= 4 ? '2-4' : weeks <= 12 ? '5-12' : '13+';
|
|
62
|
+
}
|
|
25
63
|
export function telemetryToken(env = process.env) {
|
|
26
64
|
return env.RIGOUR_MIXPANEL_TOKEN?.trim() || MIXPANEL_TOKEN;
|
|
27
65
|
}
|
|
@@ -32,7 +70,7 @@ export function readTelemetryState(home = rigourHome()) {
|
|
|
32
70
|
try {
|
|
33
71
|
const parsed = JSON.parse(fs.readFileSync(file(home, 'telemetry.json'), 'utf8'));
|
|
34
72
|
if (typeof parsed?.installId === 'string')
|
|
35
|
-
return { installId: parsed.installId, enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : undefined };
|
|
73
|
+
return { installId: parsed.installId, enabled: typeof parsed.enabled === 'boolean' ? parsed.enabled : undefined, ...(typeof parsed.firstAt === 'number' ? { firstAt: parsed.firstAt } : {}) };
|
|
36
74
|
}
|
|
37
75
|
catch {
|
|
38
76
|
// Not asked yet.
|
|
@@ -40,7 +78,8 @@ export function readTelemetryState(home = rigourHome()) {
|
|
|
40
78
|
return { installId: crypto.randomUUID() };
|
|
41
79
|
}
|
|
42
80
|
export function setTelemetryEnabled(enabled, home = rigourHome()) {
|
|
43
|
-
const
|
|
81
|
+
const current = readTelemetryState(home);
|
|
82
|
+
const state = { ...current, enabled, firstAt: current.firstAt ?? firstWritten(home) ?? Date.now() };
|
|
44
83
|
writeJson(file(home, 'telemetry.json'), state);
|
|
45
84
|
return state;
|
|
46
85
|
}
|
|
@@ -87,6 +126,8 @@ export async function trackUsage(event, properties, deps = {}) {
|
|
|
87
126
|
os: process.platform,
|
|
88
127
|
node_major: Number(process.versions.node.split('.')[0]),
|
|
89
128
|
ci: isCi(env),
|
|
129
|
+
agent_host: agentHost(env, deps.agent),
|
|
130
|
+
install_age_weeks: installAgeWeeks(deps.home ?? rigourHome(), deps.now ?? Date.now()),
|
|
90
131
|
...properties,
|
|
91
132
|
},
|
|
92
133
|
}];
|
|
@@ -110,6 +151,9 @@ export function countUsage(name, by = 1, deps = {}) {
|
|
|
110
151
|
const home = deps.home ?? rigourHome();
|
|
111
152
|
const counters = readCounters(home, deps.now ?? Date.now());
|
|
112
153
|
counters.counts[name] = (counters.counts[name] ?? 0) + by;
|
|
154
|
+
// What each agent drives: one count per agent host, beside the thing counted.
|
|
155
|
+
const host = `agent_host:${agentHost(deps.env ?? process.env, deps.agent)}`;
|
|
156
|
+
counters.counts[host] = (counters.counts[host] ?? 0) + by;
|
|
113
157
|
writeJson(file(home, 'telemetry-counters.json'), counters);
|
|
114
158
|
}
|
|
115
159
|
catch {
|
|
@@ -123,10 +167,20 @@ export async function flushDailyUsage(deps = {}) {
|
|
|
123
167
|
const home = deps.home ?? rigourHome();
|
|
124
168
|
const now = deps.now ?? Date.now();
|
|
125
169
|
const counters = readCounters(home, now);
|
|
126
|
-
|
|
170
|
+
// The day starts the first time anything could be sent, so a day with no counts still ends.
|
|
171
|
+
if (deps.daily && !fs.existsSync(file(home, 'telemetry-counters.json')))
|
|
172
|
+
writeJson(file(home, 'telemetry-counters.json'), counters);
|
|
173
|
+
if (now - counters.since < DAY_MS || (Object.keys(counters.counts).length === 0 && !deps.daily))
|
|
127
174
|
return false;
|
|
128
175
|
writeJson(file(home, 'telemetry-counters.json'), { since: now, counts: {} });
|
|
129
|
-
|
|
176
|
+
let daily = {};
|
|
177
|
+
try {
|
|
178
|
+
daily = deps.daily?.() ?? {};
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// What the learning loop did is extra: the day's counts go out without it.
|
|
182
|
+
}
|
|
183
|
+
await trackUsage('daily_usage', { ...counters.counts, ...daily }, deps);
|
|
130
184
|
return true;
|
|
131
185
|
}
|
|
132
186
|
function readCounters(home, now) {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -503,6 +503,17 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
503
503
|
allow?: string[] | undefined;
|
|
504
504
|
block?: boolean | undefined;
|
|
505
505
|
}>>>;
|
|
506
|
+
/** Verified lessons a person compiled into checks (review-learning/compiled-lessons.ts): notes unless `block`. */
|
|
507
|
+
compiled_lessons: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
508
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
509
|
+
block: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
510
|
+
}, "strip", z.ZodTypeAny, {
|
|
511
|
+
enabled: boolean;
|
|
512
|
+
block: boolean;
|
|
513
|
+
}, {
|
|
514
|
+
enabled?: boolean | undefined;
|
|
515
|
+
block?: boolean | undefined;
|
|
516
|
+
}>>>;
|
|
506
517
|
migration_order: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
507
518
|
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
508
519
|
dirs: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
@@ -897,6 +908,10 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
897
908
|
allow: string[];
|
|
898
909
|
block: boolean;
|
|
899
910
|
};
|
|
911
|
+
compiled_lessons: {
|
|
912
|
+
enabled: boolean;
|
|
913
|
+
block: boolean;
|
|
914
|
+
};
|
|
900
915
|
migration_order: {
|
|
901
916
|
enabled: boolean;
|
|
902
917
|
dirs: string[];
|
|
@@ -1136,6 +1151,10 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
1136
1151
|
allow?: string[] | undefined;
|
|
1137
1152
|
block?: boolean | undefined;
|
|
1138
1153
|
} | undefined;
|
|
1154
|
+
compiled_lessons?: {
|
|
1155
|
+
enabled?: boolean | undefined;
|
|
1156
|
+
block?: boolean | undefined;
|
|
1157
|
+
} | undefined;
|
|
1139
1158
|
migration_order?: {
|
|
1140
1159
|
enabled?: boolean | undefined;
|
|
1141
1160
|
dirs?: string[] | undefined;
|
|
@@ -1717,6 +1736,17 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
1717
1736
|
allow?: string[] | undefined;
|
|
1718
1737
|
block?: boolean | undefined;
|
|
1719
1738
|
}>>>;
|
|
1739
|
+
/** Verified lessons a person compiled into checks (review-learning/compiled-lessons.ts): notes unless `block`. */
|
|
1740
|
+
compiled_lessons: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
1741
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
1742
|
+
block: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
1743
|
+
}, "strip", z.ZodTypeAny, {
|
|
1744
|
+
enabled: boolean;
|
|
1745
|
+
block: boolean;
|
|
1746
|
+
}, {
|
|
1747
|
+
enabled?: boolean | undefined;
|
|
1748
|
+
block?: boolean | undefined;
|
|
1749
|
+
}>>>;
|
|
1720
1750
|
migration_order: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
1721
1751
|
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
1722
1752
|
dirs: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString, "many">>>;
|
|
@@ -2111,6 +2141,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
2111
2141
|
allow: string[];
|
|
2112
2142
|
block: boolean;
|
|
2113
2143
|
};
|
|
2144
|
+
compiled_lessons: {
|
|
2145
|
+
enabled: boolean;
|
|
2146
|
+
block: boolean;
|
|
2147
|
+
};
|
|
2114
2148
|
migration_order: {
|
|
2115
2149
|
enabled: boolean;
|
|
2116
2150
|
dirs: string[];
|
|
@@ -2350,6 +2384,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
2350
2384
|
allow?: string[] | undefined;
|
|
2351
2385
|
block?: boolean | undefined;
|
|
2352
2386
|
} | undefined;
|
|
2387
|
+
compiled_lessons?: {
|
|
2388
|
+
enabled?: boolean | undefined;
|
|
2389
|
+
block?: boolean | undefined;
|
|
2390
|
+
} | undefined;
|
|
2353
2391
|
migration_order?: {
|
|
2354
2392
|
enabled?: boolean | undefined;
|
|
2355
2393
|
dirs?: string[] | undefined;
|
|
@@ -2895,6 +2933,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
2895
2933
|
allow: string[];
|
|
2896
2934
|
block: boolean;
|
|
2897
2935
|
};
|
|
2936
|
+
compiled_lessons: {
|
|
2937
|
+
enabled: boolean;
|
|
2938
|
+
block: boolean;
|
|
2939
|
+
};
|
|
2898
2940
|
migration_order: {
|
|
2899
2941
|
enabled: boolean;
|
|
2900
2942
|
dirs: string[];
|
|
@@ -3200,6 +3242,10 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
3200
3242
|
allow?: string[] | undefined;
|
|
3201
3243
|
block?: boolean | undefined;
|
|
3202
3244
|
} | undefined;
|
|
3245
|
+
compiled_lessons?: {
|
|
3246
|
+
enabled?: boolean | undefined;
|
|
3247
|
+
block?: boolean | undefined;
|
|
3248
|
+
} | undefined;
|
|
3203
3249
|
migration_order?: {
|
|
3204
3250
|
enabled?: boolean | undefined;
|
|
3205
3251
|
dirs?: string[] | undefined;
|
|
@@ -3784,10 +3830,17 @@ export interface DeepOptions {
|
|
|
3784
3830
|
independent?: boolean;
|
|
3785
3831
|
/** The change's unified diff (from reviewChange): a cloud agentic review reads the PR as a whole. */
|
|
3786
3832
|
diff?: string;
|
|
3787
|
-
/** What Rigour's checks already found on the change's lines (the runner
|
|
3833
|
+
/** What Rigour's checks already found on the change's lines (the review and the runner fill it): settled, never reported again. */
|
|
3788
3834
|
settled?: Array<{
|
|
3789
3835
|
file: string;
|
|
3790
3836
|
line?: number;
|
|
3791
3837
|
title: string;
|
|
3838
|
+
kind?: string;
|
|
3839
|
+
}>;
|
|
3840
|
+
/** The lessons the team's compiled checks covered on this change: the deep review is told so instead of the lesson. */
|
|
3841
|
+
covered?: Array<{
|
|
3842
|
+
checkId: string;
|
|
3843
|
+
lessonId: string;
|
|
3844
|
+
message: string;
|
|
3792
3845
|
}>;
|
|
3793
3846
|
}
|
package/dist/types/index.js
CHANGED
|
@@ -282,6 +282,11 @@ export const GatesSchema = z.object({
|
|
|
282
282
|
// Block on them. Off by default, like unused_exports.block: a team whose reviewers block on dead files turns it on.
|
|
283
283
|
block: z.boolean().optional().default(false),
|
|
284
284
|
}).optional().default({}),
|
|
285
|
+
/** Verified lessons a person compiled into checks (review-learning/compiled-lessons.ts): notes unless `block`. */
|
|
286
|
+
compiled_lessons: z.object({
|
|
287
|
+
enabled: z.boolean().optional().default(true),
|
|
288
|
+
block: z.boolean().optional().default(false),
|
|
289
|
+
}).optional().default({}),
|
|
285
290
|
migration_order: z.object({
|
|
286
291
|
enabled: z.boolean().optional().default(false),
|
|
287
292
|
dirs: z.array(z.string()).optional().default(['**/supabase/migrations']),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.11.0-rc.1",
|
|
4
4
|
"description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.13"
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"@anthropic-ai/sdk": "^0.132.1",
|
|
76
76
|
"pg": "^8.16.3",
|
|
77
77
|
"openai": "^5.23.2",
|
|
78
|
-
"@rigour-labs/brain-darwin-
|
|
79
|
-
"@rigour-labs/brain-
|
|
80
|
-
"@rigour-labs/brain-linux-
|
|
81
|
-
"@rigour-labs/brain-
|
|
82
|
-
"@rigour-labs/brain-win-x64": "6.
|
|
78
|
+
"@rigour-labs/brain-darwin-x64": "6.11.0-rc.1",
|
|
79
|
+
"@rigour-labs/brain-linux-arm64": "6.11.0-rc.1",
|
|
80
|
+
"@rigour-labs/brain-linux-x64": "6.11.0-rc.1",
|
|
81
|
+
"@rigour-labs/brain-darwin-arm64": "6.11.0-rc.1",
|
|
82
|
+
"@rigour-labs/brain-win-x64": "6.11.0-rc.1"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/fs-extra": "^11.0.4",
|