@rigour-labs/core 6.5.4 → 6.6.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/context/automatic-index-cache.test.d.ts +1 -0
- package/dist/context/automatic-index-cache.test.js +30 -0
- package/dist/context/automatic-index.js +0 -4
- package/dist/context/cache-engine.d.ts +7 -38
- package/dist/context/cache-engine.js +7 -70
- package/dist/context/cache-engine.test.js +2 -46
- package/dist/context/index-bridge.d.ts +1 -7
- package/dist/context/index-bridge.js +1 -79
- package/dist/deep/risk.d.ts +6 -3
- package/dist/deep/risk.js +19 -3
- package/dist/deep/risk.test.js +28 -1
- package/dist/deep/router.d.ts +2 -1
- package/dist/deep/router.js +2 -2
- package/dist/firewall/execution-receipt.d.ts +5 -0
- package/dist/firewall/execution-receipt.js +10 -1
- package/dist/firewall/trusted-control.test.js +10 -0
- package/dist/gates/deep-analysis.js +3 -3
- package/dist/hooks/checker.js +18 -0
- package/dist/hooks/checker.test.js +10 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +6 -2
- package/dist/review/agent-fixes.d.ts +25 -2
- package/dist/review/agent-fixes.js +27 -6
- package/dist/review/effectiveness.d.ts +13 -0
- package/dist/review/learning-events.d.ts +14 -0
- package/dist/review/learning-events.js +21 -0
- package/dist/review/receipt.d.ts +63 -0
- package/dist/review/receipt.js +73 -0
- package/dist/review/receipt.test.d.ts +1 -0
- package/dist/review/receipt.test.js +60 -0
- package/dist/review/review-task.d.ts +3 -1
- package/dist/review/review-task.js +5 -4
- package/dist/review/review-task.test.js +9 -0
- package/dist/review/stories.d.ts +25 -0
- package/dist/review/stories.js +75 -0
- package/dist/review/stories.test.d.ts +1 -0
- package/dist/review/stories.test.js +58 -0
- package/dist/review-learning/team-lessons.d.ts +4 -0
- package/dist/review-learning/team-lessons.js +9 -1
- package/dist/storage/cache-cleanup.d.ts +19 -0
- package/dist/storage/cache-cleanup.js +90 -0
- package/dist/storage/cache-cleanup.test.d.ts +1 -0
- package/dist/storage/cache-cleanup.test.js +54 -0
- package/dist/storage/db.js +3 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/local-encryption.js +2 -2
- package/dist/storage/team-store.js +2 -2
- package/dist/types/index.js +1 -1
- package/dist/utils/user-state.js +1 -1
- package/package.json +6 -6
|
@@ -23,7 +23,7 @@ import { buildReviewTask } from '../review/review-task.js';
|
|
|
23
23
|
import { reviewPullRequest } from '../deep/pr-review.js';
|
|
24
24
|
import { relatedChanges } from '../deep/related-changes.js';
|
|
25
25
|
import { rankChangedFunctions } from '../deep/risk.js';
|
|
26
|
-
import { lessonsForDiff, lessonsSection } from '../review-learning/team-lessons.js';
|
|
26
|
+
import { activeLessons, lessonsForDiff, lessonsSection } from '../review-learning/team-lessons.js';
|
|
27
27
|
import { rulesForDiff, rulesSection } from '../review-learning/repo-rules.js';
|
|
28
28
|
import { verifyCodeFindings } from '../deep/code-verifier.js';
|
|
29
29
|
import { runIntentChecks } from './deep-intent.js';
|
|
@@ -153,7 +153,7 @@ export class DeepAnalysisGate extends Gate {
|
|
|
153
153
|
this.config.onProgress?.(' Router: no risky change for the model; the gates cover this PR.');
|
|
154
154
|
return [];
|
|
155
155
|
}
|
|
156
|
-
const focus = buildReviewTask(cwd, options.diff, this.config.router).items;
|
|
156
|
+
const focus = buildReviewTask(cwd, options.diff, this.config.router, this.config.reviewLessons, false, trustedReviews(cwd, options)).items;
|
|
157
157
|
const changedFiles = Object.keys(options.focusLines ?? {});
|
|
158
158
|
const related = relatedChanges(cwd, changedFiles, rankChangedFunctions(cwd, options.focusLines ?? {}, options.removedLines));
|
|
159
159
|
this.config.onProgress?.(` Reviewing the PR as a whole (${focus.length} risky function(s) first)...`);
|
|
@@ -179,7 +179,7 @@ export class DeepAnalysisGate extends Gate {
|
|
|
179
179
|
const { options } = this.config;
|
|
180
180
|
if (!isCloud(options) || this.config.router?.enabled === false || !options.focusLines)
|
|
181
181
|
return facts;
|
|
182
|
-
const routed = routeFiles(cwd, facts.map(f => f.path), options.focusLines, options.removedLines, this.config.router, trustedReviews(cwd, options));
|
|
182
|
+
const routed = routeFiles(cwd, facts.map(f => f.path), options.focusLines, options.removedLines, this.config.router, trustedReviews(cwd, options), activeLessons(cwd, this.config.reviewLessons));
|
|
183
183
|
this.outcome.router = routed.stats;
|
|
184
184
|
if (routed.stats.files_skipped > 0) {
|
|
185
185
|
this.config.onProgress?.(` Router: ${routed.stats.routed} of ${routed.stats.functions} changed function(s) to the model; ${routed.stats.files_skipped} file(s) left to the gates.`);
|
package/dist/hooks/checker.js
CHANGED
|
@@ -249,6 +249,24 @@ function checkSecurityPatterns(lines, relPath, failures) {
|
|
|
249
249
|
checkHardcodedSecrets(line, i, relPath, isTestFile, failures);
|
|
250
250
|
checkCommandInjection(line, i, relPath, failures);
|
|
251
251
|
}
|
|
252
|
+
if (!isTestFile)
|
|
253
|
+
checkVendorKeys(lines.join('\n'), relPath, failures);
|
|
254
|
+
}
|
|
255
|
+
/** Credential formats a vendor issues: unambiguous in code, unlike entropy or variable-name guesses. */
|
|
256
|
+
const VENDOR_KEYS = new Set(['aws_access_key', 'openai_key', 'anthropic_key', 'github_token', 'stripe_key', 'slack_token', 'sendgrid_key', 'private_key', 'private_key_full']);
|
|
257
|
+
/** A real vendor key written into code, whatever the variable is called. */
|
|
258
|
+
function checkVendorKeys(content, relPath, failures) {
|
|
259
|
+
for (const detection of scanInputForCredentials(content).detections) {
|
|
260
|
+
if (!VENDOR_KEYS.has(detection.type) || !detection.position)
|
|
261
|
+
continue;
|
|
262
|
+
const raw = content.slice(detection.position.start, detection.position.end);
|
|
263
|
+
if (/^pk_|_test_/.test(raw))
|
|
264
|
+
continue; // publishable and test keys are meant to be shared
|
|
265
|
+
const line = content.slice(0, detection.position.start).split('\n').length;
|
|
266
|
+
if (failures.some(f => f.gate === 'security-patterns' && f.line === line))
|
|
267
|
+
continue;
|
|
268
|
+
failures.push({ gate: 'security-patterns', file: relPath, message: `${detection.description} in code`, severity: 'critical', line });
|
|
269
|
+
}
|
|
252
270
|
}
|
|
253
271
|
function checkHardcodedSecrets(line, i, relPath, isTestFile, failures) {
|
|
254
272
|
if (isTestFile) {
|
|
@@ -51,6 +51,16 @@ describe('runHookChecker', () => {
|
|
|
51
51
|
expect(result.status).toBe('fail');
|
|
52
52
|
expect(result.failures[0]).toMatchObject({ gate: 'file-guard', severity: 'critical' });
|
|
53
53
|
});
|
|
54
|
+
it('catches a real vendor key whatever it is named, and leaves publishable and test keys alone', async () => {
|
|
55
|
+
// Built at runtime: a key-shaped literal in this source would be (rightly) refused by secret scanning.
|
|
56
|
+
const fake = (prefix) => `${prefix}_${'51HxQ'}${'abcdefghijklmnopqrstuv'}`;
|
|
57
|
+
fs.writeFileSync(path.join(testDir, 'pay.ts'), `export const stripe = {\n value: "${fake('sk_live')}",\n};\n`);
|
|
58
|
+
fs.writeFileSync(path.join(testDir, 'public.ts'), `export const key = "${fake('pk_live')}";\nexport const t = "${fake('sk_test')}";\n`);
|
|
59
|
+
const leaked = await runHookChecker({ cwd: testDir, files: ['pay.ts'] });
|
|
60
|
+
expect(leaked.failures).toEqual([expect.objectContaining({ gate: 'security-patterns', line: 2, severity: 'critical', message: 'Stripe API key detected in code' })]);
|
|
61
|
+
const fine = await runHookChecker({ cwd: testDir, files: ['public.ts'] });
|
|
62
|
+
expect(fine.failures.filter(f => f.gate === 'security-patterns')).toEqual([]);
|
|
63
|
+
});
|
|
54
64
|
it('should detect file size violations', async () => {
|
|
55
65
|
const filePath = path.join(testDir, 'big.ts');
|
|
56
66
|
const lines = Array.from({ length: 100 }, (_, i) => `export const v${i} = ${i};`);
|
package/dist/index.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export { dismissFinding, dismissedKeys, findingKey, isProven, quietSplit, DISMIS
|
|
|
19
19
|
export { isGeneratedFile } from './review/generated-files.js';
|
|
20
20
|
export { countUsage, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
|
|
21
21
|
export { buildReviewTask, type ReviewTask, type ReviewTaskItem } from './review/review-task.js';
|
|
22
|
+
export { buildQualityReceipt, receiptReport, type QualityReceipt, type ReceiptGap, type ReceiptOptions } from './review/receipt.js';
|
|
22
23
|
export { acknowledgeReview, type ReviewAck, type AckResult } from './review/review-ack.js';
|
|
23
24
|
export { exportReviewed, readLedger, reviewedKeys, isReviewed, recordReview, REVIEWED_FILE, type LedgerEntry, type ReviewedKey, type ReviewVerdict } from './review/ledger.js';
|
|
24
25
|
export { rankChangedFunctions, scoreRisk, functionHash, findFunction, type FunctionRisk, type RiskSignals } from './deep/risk.js';
|
|
@@ -29,8 +30,10 @@ export { readLessons, writeLessons, promoteLesson, matchLessons, lessonText, les
|
|
|
29
30
|
export { lessonsForDiff, lessonsSection, type LessonMode } from './review-learning/team-lessons.js';
|
|
30
31
|
export { readRepoRules, rulesForDiff, rulesSection, splitRules, type RepoRule } from './review-learning/repo-rules.js';
|
|
31
32
|
export { diffFromGit, GitDiffError, type DiffSource } from './review/git-diff.js';
|
|
32
|
-
export { checkPrecisions, checkId, precisionOf, isMuted, MUTE_MIN_OUTCOMES, MUTE_BELOW, type CheckPrecision, type CheckOutcome } from './review/check-outcomes.js';
|
|
33
|
-
export { recordReviewOutcome, listResolvedFixes, removeResolvedFix, openFindingCount, type ResolvedFix, type FixCapture } from './review/agent-fixes.js';
|
|
33
|
+
export { checkPrecisions, checkId, precisionOf, isMuted, readOutcomes, MUTE_MIN_OUTCOMES, MUTE_BELOW, type CheckPrecision, type CheckOutcome } from './review/check-outcomes.js';
|
|
34
|
+
export { recordReviewOutcome, listResolvedFixes, removeResolvedFix, openFindingCount, listOpenFindings, type ResolvedFix, type FixCapture } from './review/agent-fixes.js';
|
|
35
|
+
export { appendStory, readStories, compactDiff, STORIES_FILE, type Story, type CatchStage } from './review/stories.js';
|
|
36
|
+
export { recordLessonsServed, recordPrCatches, recordReuseSuggested, type LessonChannel } from './review/learning-events.js';
|
|
34
37
|
export { computeEffectiveness, readAgentEvents, appendAgentEvent, findingKeys, type AgentEvent, type ReviewEffectiveness } from './review/effectiveness.js';
|
|
35
38
|
export { parseDiff, changedLinesByFile } from './utils/diff.js';
|
|
36
39
|
export { FileScanner } from './utils/scanner.js';
|
|
@@ -46,6 +49,7 @@ export { isModelCached, getModelsDir, getModelInfo, getCachedModel, ensureModel
|
|
|
46
49
|
export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG } from './inference/llama-engine.js';
|
|
47
50
|
export { SidecarProvider } from './inference/sidecar-provider.js';
|
|
48
51
|
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
52
|
+
export { cleanContextCache, deadCacheRows, type CacheCleanupReport } from './storage/index.js';
|
|
49
53
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
50
54
|
export type { RigourDB, CompactResult } from './storage/index.js';
|
|
51
55
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,7 @@ export { dismissFinding, dismissedKeys, findingKey, isProven, quietSplit, DISMIS
|
|
|
19
19
|
export { isGeneratedFile } from './review/generated-files.js';
|
|
20
20
|
export { countUsage, durationBucket, flushDailyUsage, isTelemetryEnabled, readTelemetryState, setTelemetryEnabled, shouldAskTelemetry, telemetryToken, trackUsage } from './telemetry/telemetry.js';
|
|
21
21
|
export { buildReviewTask } from './review/review-task.js';
|
|
22
|
+
export { buildQualityReceipt, receiptReport } from './review/receipt.js';
|
|
22
23
|
export { acknowledgeReview } from './review/review-ack.js';
|
|
23
24
|
export { exportReviewed, readLedger, reviewedKeys, isReviewed, recordReview, REVIEWED_FILE } from './review/ledger.js';
|
|
24
25
|
export { rankChangedFunctions, scoreRisk, functionHash, findFunction } from './deep/risk.js';
|
|
@@ -29,8 +30,10 @@ export { readLessons, writeLessons, promoteLesson, matchLessons, lessonText, les
|
|
|
29
30
|
export { lessonsForDiff, lessonsSection } from './review-learning/team-lessons.js';
|
|
30
31
|
export { readRepoRules, rulesForDiff, rulesSection, splitRules } from './review-learning/repo-rules.js';
|
|
31
32
|
export { diffFromGit, GitDiffError } from './review/git-diff.js';
|
|
32
|
-
export { checkPrecisions, checkId, precisionOf, isMuted, MUTE_MIN_OUTCOMES, MUTE_BELOW } from './review/check-outcomes.js';
|
|
33
|
-
export { recordReviewOutcome, listResolvedFixes, removeResolvedFix, openFindingCount } from './review/agent-fixes.js';
|
|
33
|
+
export { checkPrecisions, checkId, precisionOf, isMuted, readOutcomes, MUTE_MIN_OUTCOMES, MUTE_BELOW } from './review/check-outcomes.js';
|
|
34
|
+
export { recordReviewOutcome, listResolvedFixes, removeResolvedFix, openFindingCount, listOpenFindings } from './review/agent-fixes.js';
|
|
35
|
+
export { appendStory, readStories, compactDiff, STORIES_FILE } from './review/stories.js';
|
|
36
|
+
export { recordLessonsServed, recordPrCatches, recordReuseSuggested } from './review/learning-events.js';
|
|
34
37
|
export { computeEffectiveness, readAgentEvents, appendAgentEvent, findingKeys } from './review/effectiveness.js';
|
|
35
38
|
export { parseDiff, changedLinesByFile } from './utils/diff.js';
|
|
36
39
|
export { FileScanner } from './utils/scanner.js';
|
|
@@ -47,6 +50,7 @@ export { installLlamaEngine, probeBinary, managedEnginePath, LLAMA_RELEASE_TAG }
|
|
|
47
50
|
export { SidecarProvider } from './inference/sidecar-provider.js';
|
|
48
51
|
export { extractFacts, factsToPromptString } from './deep/fact-extractor.js';
|
|
49
52
|
// Storage (SQLite Brain)
|
|
53
|
+
export { cleanContextCache, deadCacheRows } from './storage/index.js';
|
|
50
54
|
export { openDatabase, isSQLiteAvailable, compactDatabase, getDatabaseSize, resetDatabase, insertScan, insertFindings, getRecentScans, getScoreTrendFromDB, getTopIssues, reinforcePattern, getStrongPatterns } from './storage/index.js';
|
|
51
55
|
// Local Project Memory (hybrid intelligence — SQLite-backed per-project learning)
|
|
52
56
|
export { checkLocalPatterns, persistAndReinforce, getProjectStats } from './storage/index.js';
|
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import type { Failure } from '../types/index.js';
|
|
2
|
+
import { type CatchStage } from './stories.js';
|
|
3
|
+
interface OpenFinding {
|
|
4
|
+
file: string;
|
|
5
|
+
rule: string;
|
|
6
|
+
/** What the finding said; absent in entries captured before it was kept. */
|
|
7
|
+
title?: string;
|
|
8
|
+
details?: string;
|
|
9
|
+
before: string;
|
|
10
|
+
openedAt: string;
|
|
11
|
+
/** The stage that first reported it; absent in entries captured before stages were kept. */
|
|
12
|
+
stage?: CatchStage;
|
|
13
|
+
/** Its dismissal key (quiet.ts): a finding someone dismissed was not fixed. */
|
|
14
|
+
key?: string;
|
|
15
|
+
}
|
|
2
16
|
export interface ResolvedFix {
|
|
3
17
|
id: string;
|
|
4
18
|
file: string;
|
|
@@ -8,6 +22,9 @@ export interface ResolvedFix {
|
|
|
8
22
|
before: string;
|
|
9
23
|
after: string;
|
|
10
24
|
resolvedAt: string;
|
|
25
|
+
stage?: CatchStage;
|
|
26
|
+
/** When the finding was first reported, for time to fix. */
|
|
27
|
+
openedAt?: string;
|
|
11
28
|
}
|
|
12
29
|
export interface FixCapture {
|
|
13
30
|
opened: number;
|
|
@@ -15,8 +32,14 @@ export interface FixCapture {
|
|
|
15
32
|
/** The fixes this review resolved, for turning into lessons (storage/fix-lessons.ts). */
|
|
16
33
|
fixes: ResolvedFix[];
|
|
17
34
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Record a review's findings: open new ones, resolve open ones the review no longer reports.
|
|
37
|
+
* Each resolved fix is also kept as a story, credited to the stage that first reported it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function recordReviewOutcome(cwd: string, findings: Failure[], reviewedFiles: string[], stage?: CatchStage): FixCapture;
|
|
20
40
|
export declare function listResolvedFixes(cwd: string): ResolvedFix[];
|
|
21
41
|
export declare function removeResolvedFix(cwd: string, id: string): void;
|
|
22
42
|
export declare function openFindingCount(cwd: string): number;
|
|
43
|
+
/** Findings reported and not fixed yet, newest first: what still needs someone. */
|
|
44
|
+
export declare function listOpenFindings(cwd: string): Array<Omit<OpenFinding, 'before'>>;
|
|
45
|
+
export {};
|
|
@@ -14,24 +14,37 @@ import crypto from 'crypto';
|
|
|
14
14
|
import fs from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
16
|
import { checkId, recordOutcome } from './check-outcomes.js';
|
|
17
|
+
import { appendStory, compactDiff } from './stories.js';
|
|
18
|
+
import { dismissedKeys, findingKey } from './quiet.js';
|
|
17
19
|
const DIR = path.join('.rigour', 'agent-fixes');
|
|
18
20
|
const MAX_FILE_BYTES = 200_000;
|
|
19
21
|
const OPEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Record a review's findings: open new ones, resolve open ones the review no longer reports.
|
|
24
|
+
* Each resolved fix is also kept as a story, credited to the stage that first reported it.
|
|
25
|
+
*/
|
|
26
|
+
export function recordReviewOutcome(cwd, findings, reviewedFiles, stage = 'review') {
|
|
22
27
|
const open = readOpen(cwd);
|
|
23
28
|
const current = new Map(findings.flatMap(f => (f.files?.[0] ? [[`${f.id}:${f.files[0]}`, f]] : [])));
|
|
24
29
|
const fixes = resolveGone(cwd, open, current, new Set(reviewedFiles));
|
|
25
|
-
const
|
|
30
|
+
for (const fix of fixes) {
|
|
31
|
+
appendStory(cwd, { at: fix.resolvedAt, openedAt: fix.openedAt, stage: fix.stage ?? stage, file: fix.file, rule: fix.rule, title: fix.title ?? fix.rule, details: fix.details, diff: compactDiff(fix.before, fix.after) });
|
|
32
|
+
}
|
|
33
|
+
const opened = openNew(cwd, open, current, stage);
|
|
26
34
|
writeOpen(cwd, open);
|
|
27
35
|
return { opened, resolved: fixes.length, fixes };
|
|
28
36
|
}
|
|
29
37
|
/** Open findings this review no longer reports: resolved when their file was reviewed and changed. */
|
|
30
38
|
function resolveGone(cwd, open, current, reviewed) {
|
|
31
39
|
const resolved = [];
|
|
40
|
+
const dismissed = dismissedKeys(cwd);
|
|
32
41
|
for (const [key, entry] of Object.entries(open)) {
|
|
33
42
|
if (current.has(key))
|
|
34
43
|
continue;
|
|
44
|
+
if (entry.key && dismissed.has(entry.key)) {
|
|
45
|
+
delete open[key]; // gone because a person dismissed it, not because anyone fixed it
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
35
48
|
if (!reviewed.has(entry.file)) {
|
|
36
49
|
if (Date.now() - Date.parse(entry.openedAt) > OPEN_TTL_MS)
|
|
37
50
|
delete open[key];
|
|
@@ -46,7 +59,7 @@ function resolveGone(cwd, open, current, reviewed) {
|
|
|
46
59
|
}
|
|
47
60
|
return resolved;
|
|
48
61
|
}
|
|
49
|
-
function openNew(cwd, open, current) {
|
|
62
|
+
function openNew(cwd, open, current, stage) {
|
|
50
63
|
let opened = 0;
|
|
51
64
|
for (const [key, finding] of current) {
|
|
52
65
|
if (open[key])
|
|
@@ -55,7 +68,7 @@ function openNew(cwd, open, current) {
|
|
|
55
68
|
const before = readSmall(cwd, file);
|
|
56
69
|
if (before === null)
|
|
57
70
|
continue;
|
|
58
|
-
open[key] = { file, rule: finding.id, title: finding.title, details: finding.details, before, openedAt: new Date().toISOString() };
|
|
71
|
+
open[key] = { file, rule: finding.id, title: finding.title, details: finding.details, before, openedAt: new Date().toISOString(), stage, key: findingKey(finding) };
|
|
59
72
|
opened++;
|
|
60
73
|
}
|
|
61
74
|
return opened;
|
|
@@ -79,6 +92,14 @@ export function removeResolvedFix(cwd, id) {
|
|
|
79
92
|
export function openFindingCount(cwd) {
|
|
80
93
|
return Object.keys(readOpen(cwd)).length;
|
|
81
94
|
}
|
|
95
|
+
/** Findings reported and not fixed yet, newest first: what still needs someone. */
|
|
96
|
+
export function listOpenFindings(cwd) {
|
|
97
|
+
const dismissed = dismissedKeys(cwd);
|
|
98
|
+
return Object.values(readOpen(cwd))
|
|
99
|
+
.filter(entry => !(entry.key && dismissed.has(entry.key)))
|
|
100
|
+
.map(({ before: _before, ...rest }) => rest)
|
|
101
|
+
.sort((a, b) => b.openedAt.localeCompare(a.openedAt));
|
|
102
|
+
}
|
|
82
103
|
function readOpen(cwd) {
|
|
83
104
|
try {
|
|
84
105
|
return JSON.parse(fs.readFileSync(path.join(cwd, DIR, 'open.json'), 'utf8'));
|
|
@@ -98,7 +119,7 @@ function writeOpen(cwd, open) {
|
|
|
98
119
|
}
|
|
99
120
|
function writeResolved(cwd, entry, after) {
|
|
100
121
|
const id = crypto.createHash('sha256').update(`${entry.rule}\0${entry.file}\0${entry.before}\0${after}`).digest('hex').slice(0, 16);
|
|
101
|
-
const fix = { id, file: entry.file, rule: entry.rule, title: entry.title, details: entry.details, before: entry.before, after, resolvedAt: new Date().toISOString() };
|
|
122
|
+
const fix = { id, file: entry.file, rule: entry.rule, title: entry.title, details: entry.details, before: entry.before, after, resolvedAt: new Date().toISOString(), ...(entry.stage ? { stage: entry.stage } : {}), openedAt: entry.openedAt };
|
|
102
123
|
try {
|
|
103
124
|
fs.mkdirSync(path.join(cwd, DIR, 'resolved'), { recursive: true });
|
|
104
125
|
fs.writeFileSync(path.join(cwd, DIR, 'resolved', `${id}.json`), JSON.stringify(fix));
|
|
@@ -11,6 +11,19 @@ export interface AgentEvent {
|
|
|
11
11
|
blocked?: boolean;
|
|
12
12
|
/** Findings that blocked a stop. */
|
|
13
13
|
blocking?: number;
|
|
14
|
+
/** lessons_served: where the agent got them, and their subjects (learning-events.ts). */
|
|
15
|
+
via?: string;
|
|
16
|
+
lessons?: string[];
|
|
17
|
+
/** pr_catches: what a branch review reported. */
|
|
18
|
+
findings?: Array<{
|
|
19
|
+
rule: string;
|
|
20
|
+
title: string;
|
|
21
|
+
file: string;
|
|
22
|
+
}>;
|
|
23
|
+
/** reuse_suggested: what the agent was about to write, what already exists, and BLOCK or WARN. */
|
|
24
|
+
planned?: string;
|
|
25
|
+
existing?: string;
|
|
26
|
+
action?: string;
|
|
14
27
|
}
|
|
15
28
|
export interface ReviewEffectiveness {
|
|
16
29
|
toolCalls: Record<string, number>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two events the learning loop is measured by, written to the same local event log as every
|
|
3
|
+
* agent event: a lesson put in front of an agent, and findings a PR-scope review reported.
|
|
4
|
+
* Studio joins them with stories (stories.ts) to show each lesson's path from where it was
|
|
5
|
+
* learned to whether the same mistake still reaches a PR.
|
|
6
|
+
*/
|
|
7
|
+
import type { Failure } from '../types/index.js';
|
|
8
|
+
export type LessonChannel = 'recall' | 'context' | 'review';
|
|
9
|
+
/** Lessons an agent was given before or while writing code. Nothing is written when none were. */
|
|
10
|
+
export declare function recordLessonsServed(cwd: string, via: LessonChannel, subjects: string[]): void;
|
|
11
|
+
/** Findings a review of a whole branch (what a PR contains) reported. Nothing is written when clean. */
|
|
12
|
+
export declare function recordPrCatches(cwd: string, findings: Failure[]): void;
|
|
13
|
+
/** The agent was about to write something that already exists, and was pointed to it. */
|
|
14
|
+
export declare function recordReuseSuggested(cwd: string, planned: string, existing: string, action: 'BLOCK' | 'WARN'): void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { appendAgentEvent } from './effectiveness.js';
|
|
2
|
+
const MAX_LISTED = 20;
|
|
3
|
+
/** Lessons an agent was given before or while writing code. Nothing is written when none were. */
|
|
4
|
+
export function recordLessonsServed(cwd, via, subjects) {
|
|
5
|
+
const unique = [...new Set(subjects.filter(Boolean))].slice(0, MAX_LISTED);
|
|
6
|
+
if (unique.length)
|
|
7
|
+
appendAgentEvent(cwd, { type: 'lessons_served', via, lessons: unique });
|
|
8
|
+
}
|
|
9
|
+
/** Findings a review of a whole branch (what a PR contains) reported. Nothing is written when clean. */
|
|
10
|
+
export function recordPrCatches(cwd, findings) {
|
|
11
|
+
if (findings.length === 0)
|
|
12
|
+
return;
|
|
13
|
+
appendAgentEvent(cwd, {
|
|
14
|
+
type: 'pr_catches',
|
|
15
|
+
findings: findings.slice(0, MAX_LISTED).map(f => ({ rule: f.id, title: f.title, file: f.files?.[0] ?? '' })),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
/** The agent was about to write something that already exists, and was pointed to it. */
|
|
19
|
+
export function recordReuseSuggested(cwd, planned, existing, action) {
|
|
20
|
+
appendAgentEvent(cwd, { type: 'reuse_suggested', planned, existing, action });
|
|
21
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quality receipt for a change: for every changed function outside tests, what is known about
|
|
3
|
+
* it before a model looks at anything.
|
|
4
|
+
*
|
|
5
|
+
* - reviewed: risky, and reviewed at its current code during development (the ledger or a
|
|
6
|
+
* committed reviewed.json, keyed by the function's exact source);
|
|
7
|
+
* - changed since review: risky, reviewed once, then edited, so that review no longer counts;
|
|
8
|
+
* - low risk: below the router's bar, left to the deterministic checks;
|
|
9
|
+
* - not covered: risky with no review of this code. These are what a PR review should spend on.
|
|
10
|
+
*
|
|
11
|
+
* Reviews are agents' and people's own records. An independent (enforcing) review does not count
|
|
12
|
+
* them, and the receipt says how many it set aside rather than hiding them.
|
|
13
|
+
*/
|
|
14
|
+
import { type RouterPolicy } from '../deep/router.js';
|
|
15
|
+
import { type LessonMode } from '../review-learning/team-lessons.js';
|
|
16
|
+
export interface ReceiptGap {
|
|
17
|
+
file: string;
|
|
18
|
+
function: string;
|
|
19
|
+
start: number;
|
|
20
|
+
score: number;
|
|
21
|
+
/** Reviewed once, then edited. */
|
|
22
|
+
changedSinceReview: boolean;
|
|
23
|
+
/** The team lesson that made it risky, when one did. */
|
|
24
|
+
lesson?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface QualityReceipt {
|
|
27
|
+
functions: number;
|
|
28
|
+
reviewed: number;
|
|
29
|
+
changedSinceReview: number;
|
|
30
|
+
lowRisk: number;
|
|
31
|
+
notCovered: ReceiptGap[];
|
|
32
|
+
/** Who reviewed the functions counted as reviewed: agent, human, or byok:<model>. */
|
|
33
|
+
reviewers: Record<string, number>;
|
|
34
|
+
/** Changed files Rigour cannot split into functions (other languages, config): rules only. */
|
|
35
|
+
otherFiles: number;
|
|
36
|
+
/** Self-reported reviews an independent review did not count. */
|
|
37
|
+
setAside: number;
|
|
38
|
+
}
|
|
39
|
+
export interface ReceiptOptions {
|
|
40
|
+
policy?: RouterPolicy;
|
|
41
|
+
lessonMode?: LessonMode;
|
|
42
|
+
/** Trust nothing the change itself recorded (enforce mode). */
|
|
43
|
+
independent?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function buildQualityReceipt(cwd: string, diff: string, options?: ReceiptOptions): QualityReceipt;
|
|
46
|
+
/** The receipt as reports carry it (snake_case, like the rest of `rigour review --json` and the MCP tool). */
|
|
47
|
+
export declare function receiptReport(r: QualityReceipt): {
|
|
48
|
+
functions: number;
|
|
49
|
+
reviewed: number;
|
|
50
|
+
changed_since_review: number;
|
|
51
|
+
low_risk: number;
|
|
52
|
+
not_covered: {
|
|
53
|
+
lesson?: string | undefined;
|
|
54
|
+
file: string;
|
|
55
|
+
function: string;
|
|
56
|
+
line: number;
|
|
57
|
+
score: number;
|
|
58
|
+
changed_since_review: boolean;
|
|
59
|
+
}[];
|
|
60
|
+
reviewers: Record<string, number>;
|
|
61
|
+
other_files: number;
|
|
62
|
+
set_aside: number;
|
|
63
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quality receipt for a change: for every changed function outside tests, what is known about
|
|
3
|
+
* it before a model looks at anything.
|
|
4
|
+
*
|
|
5
|
+
* - reviewed: risky, and reviewed at its current code during development (the ledger or a
|
|
6
|
+
* committed reviewed.json, keyed by the function's exact source);
|
|
7
|
+
* - changed since review: risky, reviewed once, then edited, so that review no longer counts;
|
|
8
|
+
* - low risk: below the router's bar, left to the deterministic checks;
|
|
9
|
+
* - not covered: risky with no review of this code. These are what a PR review should spend on.
|
|
10
|
+
*
|
|
11
|
+
* Reviews are agents' and people's own records. An independent (enforcing) review does not count
|
|
12
|
+
* them, and the receipt says how many it set aside rather than hiding them.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_MIN_SCORE } from '../deep/router.js';
|
|
15
|
+
import { rankChangedFunctions } from '../deep/risk.js';
|
|
16
|
+
import { activeLessons, DEFAULT_LESSON_MODE } from '../review-learning/team-lessons.js';
|
|
17
|
+
import { changedLinesByFile, parseDiff, removedByFile } from '../utils/diff.js';
|
|
18
|
+
import { isReviewed, readLedger, reviewedKeys } from './ledger.js';
|
|
19
|
+
const PARSEABLE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
20
|
+
export function buildQualityReceipt(cwd, diff, options = {}) {
|
|
21
|
+
const changed = changedLinesByFile(parseDiff(diff));
|
|
22
|
+
const ranked = rankChangedFunctions(cwd, changed, removedByFile(diff), activeLessons(cwd, options.lessonMode ?? DEFAULT_LESSON_MODE));
|
|
23
|
+
const minScore = options.policy?.min_score ?? DEFAULT_MIN_SCORE;
|
|
24
|
+
const risky = ranked.filter(f => f.score >= minScore);
|
|
25
|
+
const recorded = reviewedKeys(cwd);
|
|
26
|
+
const trusted = options.independent ? [] : recorded;
|
|
27
|
+
const reviewedNow = risky.filter(f => isReviewed(trusted, keyOf(f)));
|
|
28
|
+
const gaps = risky.filter(f => !isReviewed(trusted, keyOf(f)));
|
|
29
|
+
return {
|
|
30
|
+
functions: ranked.length,
|
|
31
|
+
reviewed: reviewedNow.length,
|
|
32
|
+
changedSinceReview: gaps.filter(f => reviewedBefore(recorded, f)).length,
|
|
33
|
+
lowRisk: ranked.length - risky.length,
|
|
34
|
+
notCovered: gaps.map(f => ({
|
|
35
|
+
file: f.file, function: f.name, start: f.start, score: f.score,
|
|
36
|
+
changedSinceReview: reviewedBefore(recorded, f),
|
|
37
|
+
...(f.signals.lesson ? { lesson: f.signals.lesson } : {}),
|
|
38
|
+
})),
|
|
39
|
+
reviewers: reviewersOf(cwd, reviewedNow),
|
|
40
|
+
otherFiles: Object.keys(changed).filter(file => !PARSEABLE.test(file)).length,
|
|
41
|
+
setAside: options.independent ? risky.filter(f => isReviewed(recorded, keyOf(f))).length : 0,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function keyOf(f) {
|
|
45
|
+
return { file: f.file, function: f.name, hash: f.hash };
|
|
46
|
+
}
|
|
47
|
+
/** Some version of this function was reviewed, just not the code it has now. */
|
|
48
|
+
function reviewedBefore(recorded, f) {
|
|
49
|
+
return recorded.some(r => r.file === f.file && r.function === f.name && r.hash !== f.hash);
|
|
50
|
+
}
|
|
51
|
+
function reviewersOf(cwd, reviewed) {
|
|
52
|
+
const ledger = readLedger(cwd);
|
|
53
|
+
const counts = {};
|
|
54
|
+
for (const f of reviewed) {
|
|
55
|
+
const entry = [...ledger].reverse().find(e => e.file === f.file && e.function === f.name && e.hash === f.hash);
|
|
56
|
+
const who = entry ? (entry.reviewer.startsWith('byok:') ? 'model' : entry.reviewer) : 'recorded';
|
|
57
|
+
counts[who] = (counts[who] ?? 0) + 1;
|
|
58
|
+
}
|
|
59
|
+
return counts;
|
|
60
|
+
}
|
|
61
|
+
/** The receipt as reports carry it (snake_case, like the rest of `rigour review --json` and the MCP tool). */
|
|
62
|
+
export function receiptReport(r) {
|
|
63
|
+
return {
|
|
64
|
+
functions: r.functions,
|
|
65
|
+
reviewed: r.reviewed,
|
|
66
|
+
changed_since_review: r.changedSinceReview,
|
|
67
|
+
low_risk: r.lowRisk,
|
|
68
|
+
not_covered: r.notCovered.map(g => ({ file: g.file, function: g.function, line: g.start, score: g.score, changed_since_review: g.changedSinceReview, ...(g.lesson ? { lesson: g.lesson } : {}) })),
|
|
69
|
+
reviewers: r.reviewers,
|
|
70
|
+
other_files: r.otherFiles,
|
|
71
|
+
set_aside: r.setAside,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { findFunction } from '../deep/risk.js';
|
|
7
|
+
import { diffFromGit } from './git-diff.js';
|
|
8
|
+
import { recordReview } from './ledger.js';
|
|
9
|
+
import { buildQualityReceipt } from './receipt.js';
|
|
10
|
+
let repo;
|
|
11
|
+
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
12
|
+
const write = (rel, body) => { fs.mkdirSync(path.dirname(path.join(repo, rel)), { recursive: true }); fs.writeFileSync(path.join(repo, rel), body); };
|
|
13
|
+
const review = (file, fn, reviewer = 'agent') => {
|
|
14
|
+
const found = findFunction(repo, file, fn);
|
|
15
|
+
recordReview(repo, { file, function: fn, hash: found.hash, reviewer, verdict: 'no_issue', note: 'checked' });
|
|
16
|
+
};
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'receipt-'));
|
|
19
|
+
git('init', '-q', '-b', 'main');
|
|
20
|
+
git('config', 'user.email', 't@example.com');
|
|
21
|
+
git('config', 'user.name', 't');
|
|
22
|
+
git('config', 'commit.gpgsign', 'false');
|
|
23
|
+
write('.gitignore', '.rigour/\n');
|
|
24
|
+
write('src/pay.ts', 'export const x = 1;\n');
|
|
25
|
+
git('add', '-A');
|
|
26
|
+
git('commit', '-qm', 'init');
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => fs.rmSync(repo, { recursive: true, force: true }));
|
|
29
|
+
// Risky: a data write. Low risk: a formatter. Each in its own function.
|
|
30
|
+
const RISKY = (n) => `export async function saveRefund${n}(db: any, cents: number) {\n return db.insert({ cents: cents + ${n} });\n}\n`;
|
|
31
|
+
const PLAIN = 'export function label(name: string) {\n return name.trim();\n}\n';
|
|
32
|
+
describe('buildQualityReceipt', () => {
|
|
33
|
+
it('says what was reviewed at its current code, what changed after review, what is low risk, and what is not covered', () => {
|
|
34
|
+
write('src/pay.ts', RISKY(1) + RISKY(2) + RISKY(3) + PLAIN);
|
|
35
|
+
write('config/app.yml', 'a: 1\n');
|
|
36
|
+
review('src/pay.ts', 'saveRefund1');
|
|
37
|
+
review('src/pay.ts', 'saveRefund2');
|
|
38
|
+
write('src/pay.ts', RISKY(1) + RISKY(2).replace('cents + 2', 'cents + 20') + RISKY(3) + PLAIN); // edited after its review
|
|
39
|
+
const receipt = buildQualityReceipt(repo, diffFromGit(repo));
|
|
40
|
+
expect(receipt).toMatchObject({ functions: 4, reviewed: 1, changedSinceReview: 1, lowRisk: 1, otherFiles: 1, setAside: 0, reviewers: { agent: 1 } });
|
|
41
|
+
expect(receipt.notCovered.map(g => [g.function, g.changedSinceReview])).toEqual([['saveRefund2', true], ['saveRefund3', false]]);
|
|
42
|
+
});
|
|
43
|
+
it('counts no self-reported review when independent, and says how many it set aside', () => {
|
|
44
|
+
write('src/pay.ts', RISKY(1) + RISKY(2));
|
|
45
|
+
review('src/pay.ts', 'saveRefund1');
|
|
46
|
+
const receipt = buildQualityReceipt(repo, diffFromGit(repo), { independent: true });
|
|
47
|
+
expect(receipt).toMatchObject({ reviewed: 0, setAside: 1 });
|
|
48
|
+
expect(receipt.notCovered).toHaveLength(2);
|
|
49
|
+
});
|
|
50
|
+
it('treats a function matching a verified team lesson as risky, and says which lesson', () => {
|
|
51
|
+
write('src/pay.ts', 'export function formatTotal(totalCents: number) {\n return (totalCents / 100).toFixed(2);\n}\n');
|
|
52
|
+
write('.rigour/review-lessons.json', JSON.stringify({ version: 1, lessons: [{
|
|
53
|
+
id: 'l1', text: 'Format money from integer cents, never floats.', file: 'src/pay.ts', symbols: ['totalCents'],
|
|
54
|
+
state: 'verified', evidence: [{ pr: 12, comment: 'c', author: 'reviewer' }], createdAt: '', updatedAt: '',
|
|
55
|
+
}] }));
|
|
56
|
+
const withLesson = buildQualityReceipt(repo, diffFromGit(repo));
|
|
57
|
+
expect(withLesson.notCovered).toEqual([expect.objectContaining({ function: 'formatTotal', lesson: 'Format money from integer cents, never floats.' })]);
|
|
58
|
+
expect(buildQualityReceipt(repo, diffFromGit(repo), { lessonMode: 'off' }).notCovered).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* pre-empted four times as many acted-on review comments as chunked prompts.
|
|
10
10
|
*/
|
|
11
11
|
import { type RouterPolicy } from '../deep/router.js';
|
|
12
|
+
import { type ReviewedKey } from './ledger.js';
|
|
12
13
|
import { type LessonMode } from '../review-learning/team-lessons.js';
|
|
13
14
|
export interface ReviewTaskItem {
|
|
14
15
|
file: string;
|
|
@@ -37,4 +38,5 @@ export interface ReviewTask {
|
|
|
37
38
|
}>;
|
|
38
39
|
instructions: string;
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
/** `reviewed`: the reviews to trust; an independent (enforcing) review passes none, so self-reported reviews skip nothing. */
|
|
42
|
+
export declare function buildReviewTask(cwd: string, diff: string, policy?: RouterPolicy, lessonMode?: LessonMode, repoRules?: boolean, reviewed?: ReviewedKey[]): ReviewTask;
|
|
@@ -12,7 +12,7 @@ import { DEFAULT_MAX_FUNCTIONS, DEFAULT_MIN_SCORE } from '../deep/router.js';
|
|
|
12
12
|
import { rankChangedFunctions } from '../deep/risk.js';
|
|
13
13
|
import { changedLinesByFile, parseDiff, removedByFile } from '../utils/diff.js';
|
|
14
14
|
import { isReviewed, reviewedKeys } from './ledger.js';
|
|
15
|
-
import { lessonsForDiff } from '../review-learning/team-lessons.js';
|
|
15
|
+
import { activeLessons, DEFAULT_LESSON_MODE, lessonsForDiff } from '../review-learning/team-lessons.js';
|
|
16
16
|
import { rulesForDiff } from '../review-learning/repo-rules.js';
|
|
17
17
|
const QUESTIONS = {
|
|
18
18
|
'data-write': 'Is the write safe under concurrent calls (no check-then-act), idempotent on retry, and does an upsert/conflict clause do what the caller expects?',
|
|
@@ -22,11 +22,11 @@ const QUESTIONS = {
|
|
|
22
22
|
concurrency: 'What happens if two calls overlap, or one step fails half-way through?',
|
|
23
23
|
network: 'Are timeouts, aborts and non-2xx responses handled, and does a retry repeat a side effect?',
|
|
24
24
|
};
|
|
25
|
-
|
|
25
|
+
/** `reviewed`: the reviews to trust; an independent (enforcing) review passes none, so self-reported reviews skip nothing. */
|
|
26
|
+
export function buildReviewTask(cwd, diff, policy = {}, lessonMode = DEFAULT_LESSON_MODE, repoRules = false, reviewed = reviewedKeys(cwd)) {
|
|
26
27
|
const changed = parseDiff(diff);
|
|
27
|
-
const ranked = rankChangedFunctions(cwd, changedLinesByFile(changed), removedByFile(diff));
|
|
28
|
+
const ranked = rankChangedFunctions(cwd, changedLinesByFile(changed), removedByFile(diff), activeLessons(cwd, lessonMode));
|
|
28
29
|
const risky = ranked.filter(f => f.score >= (policy.min_score ?? DEFAULT_MIN_SCORE)).slice(0, policy.max_functions ?? DEFAULT_MAX_FUNCTIONS);
|
|
29
|
-
const reviewed = reviewedKeys(cwd);
|
|
30
30
|
const pending = risky.filter(f => !isReviewed(reviewed, { file: f.file, function: f.name, hash: f.hash }));
|
|
31
31
|
const lessons = lessonsForDiff(cwd, diff, lessonMode).map(l => ({ file: l.file, text: l.text, prs: [...new Set(l.evidence.map(e => e.pr))] }));
|
|
32
32
|
return {
|
|
@@ -42,6 +42,7 @@ export function buildReviewTask(cwd, diff, policy = {}, lessonMode = 'off', repo
|
|
|
42
42
|
}
|
|
43
43
|
function toItem(f) {
|
|
44
44
|
const questions = [
|
|
45
|
+
...(f.signals.lesson ? [`Your team flagged this before: "${f.signals.lesson}" Does this change repeat it?`] : []),
|
|
45
46
|
...(f.signals.removedGuard ? ['The change removed a condition, return or throw here: which inputs did it guard against, and are they still handled?'] : []),
|
|
46
47
|
...f.signals.sensitive.map(kind => QUESTIONS[kind]).filter(Boolean),
|
|
47
48
|
`Do the callers of \`${f.name}\` still get what they assume (types, empty and error cases)?`,
|
|
@@ -8,6 +8,7 @@ import { acknowledgeReview } from './review-ack.js';
|
|
|
8
8
|
import { buildReviewTask } from './review-task.js';
|
|
9
9
|
import { exportReviewed, readLedger, REVIEWED_FILE } from './ledger.js';
|
|
10
10
|
import { reviewAckMessage } from '../hooks/stop-review.js';
|
|
11
|
+
import { writeLessons } from '../review-learning/lessons.js';
|
|
11
12
|
let repo;
|
|
12
13
|
const git = (...args) => execFileSync('git', ['-C', repo, ...args], { encoding: 'utf8' });
|
|
13
14
|
const write = (body) => fs.writeFileSync(path.join(repo, 'sync.ts'), body);
|
|
@@ -51,6 +52,14 @@ describe('review task and acknowledgements', () => {
|
|
|
51
52
|
expect(task.items[0].questions[0]).toMatch(/concurrent calls/);
|
|
52
53
|
expect(task.items[0].questions.at(-1)).toContain('`syncOrders`');
|
|
53
54
|
});
|
|
55
|
+
it('sends a function a verified team lesson names to review, and asks whether the change repeats it', () => {
|
|
56
|
+
write(AFTER.replace('name.trim()', 'name.toLocaleUpperCase()'));
|
|
57
|
+
writeLessons(repo, [{ id: 'l1', text: 'Locale casing breaks Turkish names.', file: 'sync.ts', symbols: ['toLocaleUpperCase'], state: 'verified', evidence: [{ pr: 7, comment: 'c1', author: 'r' }], createdAt: '', updatedAt: '' }]);
|
|
58
|
+
const task = buildReviewTask(repo, diffFromGit(repo));
|
|
59
|
+
const label = task.items.find(i => i.function === 'label');
|
|
60
|
+
expect(label?.questions[0]).toContain('Locale casing breaks Turkish names.');
|
|
61
|
+
expect(buildReviewTask(repo, diffFromGit(repo), {}, 'off').items.map(i => i.function)).toEqual(['syncOrders']);
|
|
62
|
+
});
|
|
54
63
|
it('drops an acknowledged function from the task until its code changes again', () => {
|
|
55
64
|
const ack = acknowledgeReview(repo, { file: 'sync.ts', function: 'syncOrders', verdict: 'no_issue', note: 'upsert key is the unique order id; callers retry safely' });
|
|
56
65
|
expect(ack.ok).toBe(true);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Where a problem was caught: while the agent wrote, when it asked to review, when it tried to finish, at the PR. */
|
|
2
|
+
export type CatchStage = 'edit' | 'review' | 'stop' | 'pr';
|
|
3
|
+
export interface Story {
|
|
4
|
+
id: string;
|
|
5
|
+
/** When the agent fixed it. */
|
|
6
|
+
at: string;
|
|
7
|
+
/** When Rigour first reported it; absent in stories kept before this was recorded. */
|
|
8
|
+
openedAt?: string;
|
|
9
|
+
stage: CatchStage;
|
|
10
|
+
file: string;
|
|
11
|
+
rule: string;
|
|
12
|
+
title: string;
|
|
13
|
+
details?: string;
|
|
14
|
+
/** Changed lines with context, each prefixed ' ', '-' or '+'. */
|
|
15
|
+
diff: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const STORIES_FILE: string;
|
|
18
|
+
export declare function appendStory(cwd: string, story: Omit<Story, 'id'>): void;
|
|
19
|
+
/** Every story, oldest first; `since` keeps those at or after it. */
|
|
20
|
+
export declare function readStories(cwd: string, since?: Date): Story[];
|
|
21
|
+
/**
|
|
22
|
+
* The lines that changed between two versions of a file, with context: the common head and
|
|
23
|
+
* tail are trimmed and what is left between them is shown as removed then added.
|
|
24
|
+
*/
|
|
25
|
+
export declare function compactDiff(before: string, after: string): string[];
|