@rigour-labs/core 6.3.1 → 6.4.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/README.md +1 -1
- package/dist/deep/changed-functions.d.ts +15 -0
- package/dist/deep/changed-functions.js +34 -0
- package/dist/deep/code-pass.d.ts +10 -0
- package/dist/deep/code-pass.js +37 -2
- package/dist/deep/code-pass.test.d.ts +1 -0
- package/dist/deep/code-pass.test.js +69 -0
- package/dist/deep/code-review-prompt.d.ts +1 -1
- package/dist/deep/code-review-prompt.js +3 -2
- package/dist/deep/diff-tests/calls.d.ts +8 -0
- package/dist/deep/diff-tests/calls.js +85 -0
- package/dist/deep/diff-tests/calls.test.d.ts +1 -0
- package/dist/deep/diff-tests/calls.test.js +33 -0
- package/dist/deep/diff-tests/run.d.ts +21 -0
- package/dist/deep/diff-tests/run.js +178 -0
- package/dist/deep/diff-tests/run.test.d.ts +1 -0
- package/dist/deep/diff-tests/run.test.js +58 -0
- package/dist/deep/reference-pack.d.ts +19 -0
- package/dist/deep/reference-pack.js +101 -0
- package/dist/deep/reference-pack.test.d.ts +1 -0
- package/dist/deep/reference-pack.test.js +46 -0
- package/dist/deep/review-context-export.d.ts +11 -0
- package/dist/deep/review-context-export.js +31 -0
- package/dist/deep/review-context-export.test.d.ts +1 -0
- package/dist/deep/review-context-export.test.js +32 -0
- package/dist/deep/self-check.d.ts +21 -0
- package/dist/deep/self-check.js +49 -0
- package/dist/firewall/trusted-control.d.ts +1 -2
- package/dist/gates/deep-analysis.d.ts +2 -0
- package/dist/gates/deep-analysis.js +12 -0
- package/dist/gates/deep-runner.js +2 -1
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +7 -0
- package/dist/gates/hallucinated-imports/js-resolver.js +17 -1
- package/dist/gates/hallucinated-imports-sveltekit.test.js +24 -0
- package/dist/gates/semantic-bugs.js +4 -3
- package/dist/gates/semantic-bugs.test.js +11 -5
- package/dist/hooks/checker.d.ts +1 -2
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/stop-review.d.ts +25 -0
- package/dist/hooks/stop-review.js +31 -0
- package/dist/index.d.ts +26 -2
- package/dist/index.js +10 -6
- package/dist/inference/index.d.ts +1 -1
- package/dist/inference/index.js +3 -5
- package/dist/inference/local-model.test.d.ts +1 -0
- package/dist/inference/local-model.test.js +23 -0
- package/dist/inference/sidecar-provider.d.ts +10 -1
- package/dist/inference/sidecar-provider.js +28 -6
- package/dist/inference/types.d.ts +8 -1
- package/dist/inference/types.js +14 -0
- package/dist/inference/types.test.d.ts +1 -0
- package/dist/inference/types.test.js +19 -0
- package/dist/review/agent-fixes.d.ts +18 -0
- package/dist/review/agent-fixes.js +118 -0
- package/dist/review/agent-fixes.test.d.ts +1 -0
- package/dist/review/agent-fixes.test.js +32 -0
- package/dist/review/changed-lines.d.ts +19 -0
- package/dist/review/changed-lines.js +22 -0
- package/dist/review/diff-test-findings.d.ts +3 -0
- package/dist/review/diff-test-findings.js +41 -0
- package/dist/review/effectiveness.d.ts +34 -0
- package/dist/review/effectiveness.js +114 -0
- package/dist/review/effectiveness.test.d.ts +1 -0
- package/dist/review/effectiveness.test.js +42 -0
- package/dist/review/git-diff.d.ts +9 -0
- package/dist/review/git-diff.js +42 -0
- package/dist/review/review.d.ts +38 -0
- package/dist/review/review.js +52 -0
- package/dist/review/review.test.d.ts +1 -0
- package/dist/review/review.test.js +87 -0
- package/dist/semantic/benchmark.js +2 -2
- package/dist/semantic/benchmark.test.js +5 -1
- package/dist/semantic/engine.d.ts +3 -0
- package/dist/semantic/engine.js +8 -2
- package/dist/semantic/failure-handling.d.ts +12 -0
- package/dist/semantic/failure-handling.js +41 -0
- package/dist/semantic/intent/parallel-reads.js +2 -30
- package/dist/semantic/learn/learn.d.ts +2 -0
- package/dist/semantic/learn/learn.js +18 -0
- package/dist/semantic/project-facts.d.ts +25 -0
- package/dist/semantic/project-facts.js +104 -0
- package/dist/semantic/project-facts.test.d.ts +1 -0
- package/dist/semantic/project-facts.test.js +45 -0
- package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.js +71 -0
- package/dist/semantic/rules/ecosystem/deps-internal-symbol.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/deps-internal-symbol.js +36 -0
- package/dist/semantic/rules/ecosystem/dom-self-query-in-component.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/dom-self-query-in-component.js +60 -0
- package/dist/semantic/rules/ecosystem/env-bare-browser-global.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/env-bare-browser-global.js +57 -0
- package/dist/semantic/rules/ecosystem/exports-condition-parity.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/exports-condition-parity.js +57 -0
- package/dist/semantic/rules/ecosystem/exports-forgotten-type.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/exports-forgotten-type.js +128 -0
- package/dist/semantic/rules/ecosystem/imports-barrel-cycle.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/imports-barrel-cycle.js +78 -0
- package/dist/semantic/rules/ecosystem/index.d.ts +15 -0
- package/dist/semantic/rules/ecosystem/index.js +22 -0
- package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.js +87 -0
- package/dist/semantic/rules/ecosystem/react-inline-html-object.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/react-inline-html-object.js +30 -0
- package/dist/semantic/rules/ecosystem/shared.d.ts +18 -0
- package/dist/semantic/rules/ecosystem/shared.js +43 -0
- package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.js +36 -0
- package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.js +66 -0
- package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.js +31 -0
- package/dist/semantic/rules/ecosystem/vue-static-computed.d.ts +2 -0
- package/dist/semantic/rules/ecosystem/vue-static-computed.js +51 -0
- package/dist/semantic/sites/call-sites.d.ts +25 -0
- package/dist/semantic/sites/call-sites.js +69 -0
- package/dist/semantic/sites/call-sites.test.d.ts +1 -0
- package/dist/semantic/sites/call-sites.test.js +46 -0
- package/dist/semantic/sites/export.d.ts +2 -0
- package/dist/semantic/sites/export.js +22 -0
- package/dist/semantic/types.d.ts +4 -0
- package/dist/services/engineering-knowledge-graph.d.ts +1 -2
- package/dist/templates/universal-config.js +2 -1
- package/dist/types/index.d.ts +31 -8
- package/dist/types/index.js +4 -2
- package/dist/utils/diff-removed.test.d.ts +1 -0
- package/dist/utils/diff-removed.test.js +29 -0
- package/dist/utils/diff.d.ts +12 -0
- package/dist/utils/diff.js +26 -0
- package/package.json +6 -6
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { RemovedBlock } from '../utils/diff.js';
|
|
2
|
+
export interface ReferenceInput {
|
|
3
|
+
cwd: string;
|
|
4
|
+
/** cwd-relative path of the reviewed file. */
|
|
5
|
+
file: string;
|
|
6
|
+
focusLines?: number[];
|
|
7
|
+
removed?: RemovedBlock[];
|
|
8
|
+
prBody?: string;
|
|
9
|
+
maxChars: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ReferencePack {
|
|
12
|
+
/** Prompt block, empty when there is nothing to add. */
|
|
13
|
+
text: string;
|
|
14
|
+
/** Raw reference source, for identifier verification. */
|
|
15
|
+
source: string;
|
|
16
|
+
/** The prompt sections in priority order, for a second pass that reads them in reverse. */
|
|
17
|
+
sections: string[];
|
|
18
|
+
}
|
|
19
|
+
export declare function buildReferencePack(input: ReferenceInput): ReferencePack;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reference material for reviewing a changed file: what the change means,
|
|
3
|
+
* beyond the file itself.
|
|
4
|
+
*
|
|
5
|
+
* Most bugs reviewers miss need facts outside the changed lines: a guard the
|
|
6
|
+
* change deleted, what a called function actually does, who calls a changed
|
|
7
|
+
* export, what the PR says it intends. The pack carries, within a character
|
|
8
|
+
* budget and in this priority:
|
|
9
|
+
* 1. the PR description,
|
|
10
|
+
* 2. lines the change removed from this file,
|
|
11
|
+
* 3. definitions of functions the changed code calls (resolved by the type checker),
|
|
12
|
+
* 4. call sites of changed exported functions elsewhere in the repository.
|
|
13
|
+
*
|
|
14
|
+
* It is reference only: findings must still cite a line of the reviewed file.
|
|
15
|
+
* Its source joins the identifier check, so a finding may name a callee.
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'child_process';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import ts from 'typescript';
|
|
20
|
+
import { calledFunction, forEachNode, lineOf } from '../semantic/ast.js';
|
|
21
|
+
import { changedFunctions, functionName, isExported } from './changed-functions.js';
|
|
22
|
+
import { loadProjectConfig, programBatches } from '../semantic/program.js';
|
|
23
|
+
const PR_BODY_CHARS = 1500;
|
|
24
|
+
const MAX_CALLEES = 6;
|
|
25
|
+
const CALLEE_LINES = 40;
|
|
26
|
+
const MAX_CALLERS = 5;
|
|
27
|
+
const TS_FILE = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
|
|
28
|
+
export function buildReferencePack(input) {
|
|
29
|
+
const sections = [];
|
|
30
|
+
const sources = [];
|
|
31
|
+
let budget = input.maxChars;
|
|
32
|
+
const add = (title, body, source = body) => {
|
|
33
|
+
const block = `${title}\n${body}`;
|
|
34
|
+
if (!body.trim() || block.length > budget)
|
|
35
|
+
return;
|
|
36
|
+
sections.push(block);
|
|
37
|
+
sources.push(source);
|
|
38
|
+
budget -= block.length + 2;
|
|
39
|
+
};
|
|
40
|
+
if (input.prBody?.trim())
|
|
41
|
+
add('PR DESCRIPTION (what the author intends):', input.prBody.trim().slice(0, PR_BODY_CHARS));
|
|
42
|
+
for (const block of input.removed ?? []) {
|
|
43
|
+
add(`REMOVED by this change, just before line ${block.line} of ${input.file}:`, block.text.map(l => `- ${l}`).join('\n'), block.text.join('\n'));
|
|
44
|
+
}
|
|
45
|
+
const change = changeOf(input);
|
|
46
|
+
if (change) {
|
|
47
|
+
for (const callee of calleeDefinitions(input.cwd, change))
|
|
48
|
+
add(`CALLED: ${callee.label}`, callee.text);
|
|
49
|
+
for (const caller of callersOfChangedExports(input.cwd, input.file, change))
|
|
50
|
+
add(`CALLER of ${caller.name}:`, caller.text);
|
|
51
|
+
}
|
|
52
|
+
return { text: sections.join('\n\n'), source: sources.join('\n'), sections };
|
|
53
|
+
}
|
|
54
|
+
function changeOf(input) {
|
|
55
|
+
const focus = input.focusLines ?? [];
|
|
56
|
+
if (focus.length === 0 || !TS_FILE.test(input.file))
|
|
57
|
+
return undefined;
|
|
58
|
+
const absolute = path.resolve(input.cwd, input.file);
|
|
59
|
+
const [program] = programBatches([absolute], loadProjectConfig(input.cwd).options, 1);
|
|
60
|
+
const sourceFile = program?.getSourceFile(absolute);
|
|
61
|
+
if (!program || !sourceFile)
|
|
62
|
+
return undefined;
|
|
63
|
+
return { sourceFile, checker: program.getTypeChecker(), functions: changedFunctions(sourceFile, focus) };
|
|
64
|
+
}
|
|
65
|
+
/** Definitions, in other files, of functions called from the changed functions. */
|
|
66
|
+
function calleeDefinitions(cwd, change) {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const snippets = [];
|
|
69
|
+
for (const fn of change.functions) {
|
|
70
|
+
forEachNode(fn, (node) => {
|
|
71
|
+
if (snippets.length >= MAX_CALLEES || !ts.isCallExpression(node))
|
|
72
|
+
return;
|
|
73
|
+
const target = calledFunction(change.checker, node);
|
|
74
|
+
const where = target?.getSourceFile();
|
|
75
|
+
if (!target || !where || seen.has(target) || where === change.sourceFile || where.isDeclarationFile)
|
|
76
|
+
return;
|
|
77
|
+
seen.add(target);
|
|
78
|
+
const lines = target.getText(where).split('\n');
|
|
79
|
+
const text = lines.slice(0, CALLEE_LINES).join('\n') + (lines.length > CALLEE_LINES ? `\n… [${lines.length - CALLEE_LINES} more lines]` : '');
|
|
80
|
+
snippets.push({ label: `${path.relative(cwd, where.fileName).split(path.sep).join('/')}:${lineOf(target)}`, text });
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return snippets;
|
|
84
|
+
}
|
|
85
|
+
/** Call sites, outside this file, of exported functions the change touched. */
|
|
86
|
+
function callersOfChangedExports(cwd, file, change) {
|
|
87
|
+
const names = change.functions.filter(isExported).map(functionName).filter((n) => !!n);
|
|
88
|
+
return names.map(name => ({ name, text: grepCallers(cwd, file, name) })).filter(c => c.text);
|
|
89
|
+
}
|
|
90
|
+
function grepCallers(cwd, file, name) {
|
|
91
|
+
try {
|
|
92
|
+
const out = execFileSync('git', ['grep', '-n', '-E', '-e', `(^|[^A-Za-z0-9_$])${name}\\(`, '--', '*.ts', '*.tsx', '*.js', '*.jsx', '*.mjs', `:!${file}`], {
|
|
93
|
+
cwd, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'],
|
|
94
|
+
});
|
|
95
|
+
return out.split('\n').filter(Boolean).filter(l => !/\.(?:test|spec)\./.test(l)).slice(0, MAX_CALLERS)
|
|
96
|
+
.map(l => l.length > 200 ? `${l.slice(0, 200)}…` : l).join('\n');
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { buildReferencePack } from './reference-pack.js';
|
|
7
|
+
let repo;
|
|
8
|
+
afterEach(() => { if (repo)
|
|
9
|
+
fs.rmSync(repo, { recursive: true, force: true }); repo = undefined; });
|
|
10
|
+
function project() {
|
|
11
|
+
repo = fs.mkdtempSync(path.join(os.tmpdir(), 'reference-pack-'));
|
|
12
|
+
const files = {
|
|
13
|
+
'src/paths.ts': "export function cleanPath(p: string): string {\n return p.replace(/\\/+/g, '/');\n}\n",
|
|
14
|
+
'src/routes.ts': "import { cleanPath } from './paths';\nexport function routePath(dir: string, route: string): string {\n return cleanPath(`/${dir}${route}`);\n}\n",
|
|
15
|
+
'src/generator.ts': "import { routePath } from './routes';\nexport const home = routePath('.', '/');\n",
|
|
16
|
+
};
|
|
17
|
+
for (const [name, text] of Object.entries(files)) {
|
|
18
|
+
fs.mkdirSync(path.dirname(path.join(repo, name)), { recursive: true });
|
|
19
|
+
fs.writeFileSync(path.join(repo, name), text);
|
|
20
|
+
}
|
|
21
|
+
execFileSync('git', ['init', '-q'], { cwd: repo });
|
|
22
|
+
execFileSync('git', ['add', '-A'], { cwd: repo });
|
|
23
|
+
return repo;
|
|
24
|
+
}
|
|
25
|
+
describe('buildReferencePack', () => {
|
|
26
|
+
it('carries the intent, removed lines, callee definitions and callers of a changed export', () => {
|
|
27
|
+
const cwd = project();
|
|
28
|
+
const pack = buildReferencePack({
|
|
29
|
+
cwd, file: 'src/routes.ts', focusLines: [3], maxChars: 10_000,
|
|
30
|
+
prBody: 'Normalize virtual route paths.',
|
|
31
|
+
removed: [{ line: 3, text: [" return `/${dir}${route}`;"] }],
|
|
32
|
+
});
|
|
33
|
+
expect(pack.text).toContain('PR DESCRIPTION (what the author intends):\nNormalize virtual route paths.');
|
|
34
|
+
expect(pack.text).toContain('REMOVED by this change, just before line 3 of src/routes.ts:\n- return `/${dir}${route}`;');
|
|
35
|
+
expect(pack.text).toContain('CALLED: src/paths.ts:1\nexport function cleanPath');
|
|
36
|
+
expect(pack.text).toContain("CALLER of routePath:\nsrc/generator.ts:2:export const home = routePath('.', '/');");
|
|
37
|
+
expect(pack.source).toContain('cleanPath');
|
|
38
|
+
});
|
|
39
|
+
it('stays within its budget, keeping the highest-priority sections', () => {
|
|
40
|
+
const cwd = project();
|
|
41
|
+
const pack = buildReferencePack({ cwd, file: 'src/routes.ts', focusLines: [3], maxChars: 80, prBody: 'Normalize virtual route paths.' });
|
|
42
|
+
expect(pack.text).toContain('PR DESCRIPTION');
|
|
43
|
+
expect(pack.text).not.toContain('CALLED');
|
|
44
|
+
expect(pack.text.length).toBeLessThanOrEqual(80);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ReviewContextExport {
|
|
2
|
+
file: string;
|
|
3
|
+
focusLines: number[];
|
|
4
|
+
prompt: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ReviewContextOptions {
|
|
7
|
+
prBody?: string;
|
|
8
|
+
maxSourceChars?: number;
|
|
9
|
+
maxReferenceChars?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function exportReviewContexts(cwd: string, diff: string, options?: ReviewContextOptions): Promise<ReviewContextExport[]>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The exact prompt the max tier would review for each changed file of a diff.
|
|
3
|
+
*
|
|
4
|
+
* The driftbench review miner pairs these prompts with what the later fix
|
|
5
|
+
* changed, so a fine-tuned model learns from the input it will get at runtime,
|
|
6
|
+
* built by the same code (numbered source, focus segments, reference pack).
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { changedLinesByFile, parseDiff, removedByFile } from '../utils/diff.js';
|
|
11
|
+
import { buildCodeContext } from './code-context.js';
|
|
12
|
+
import { buildCodeReviewPrompt } from './code-review-prompt.js';
|
|
13
|
+
import { extractFacts } from './fact-extractor.js';
|
|
14
|
+
import { buildReferencePack } from './reference-pack.js';
|
|
15
|
+
export async function exportReviewContexts(cwd, diff, options = {}) {
|
|
16
|
+
const focus = changedLinesByFile(parseDiff(diff));
|
|
17
|
+
const removed = removedByFile(diff);
|
|
18
|
+
const files = Object.keys(focus).filter(f => focus[f].length > 0 && fs.existsSync(path.join(cwd, f)));
|
|
19
|
+
if (files.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
const facts = await extractFacts(cwd, [], files);
|
|
22
|
+
return facts.map(fileFacts => {
|
|
23
|
+
const content = fs.readFileSync(path.join(cwd, fileFacts.path), 'utf-8');
|
|
24
|
+
const context = buildCodeContext(fileFacts, content, { maxChars: options.maxSourceChars ?? 36_000, focusLines: focus[fileFacts.path] });
|
|
25
|
+
const pack = buildReferencePack({
|
|
26
|
+
cwd, file: fileFacts.path, focusLines: focus[fileFacts.path], removed: removed[fileFacts.path],
|
|
27
|
+
prBody: options.prBody, maxChars: options.maxReferenceChars ?? 12_000,
|
|
28
|
+
});
|
|
29
|
+
return { file: fileFacts.path, focusLines: focus[fileFacts.path], prompt: buildCodeReviewPrompt(context, pack.text) };
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { buildCodeReviewPrompt } from './code-review-prompt.js';
|
|
6
|
+
import { exportReviewContexts } from './review-context-export.js';
|
|
7
|
+
let dir;
|
|
8
|
+
afterEach(() => { if (dir)
|
|
9
|
+
fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
|
|
10
|
+
const DIFF = `diff --git a/src/total.ts b/src/total.ts
|
|
11
|
+
--- a/src/total.ts
|
|
12
|
+
+++ b/src/total.ts
|
|
13
|
+
@@ -2,2 +2,2 @@
|
|
14
|
+
let sum = 0;
|
|
15
|
+
- for (let i = 0; i < items.length; i++) sum += items[i];
|
|
16
|
+
+ for (let i = 0; i <= items.length; i++) sum += items[i];
|
|
17
|
+
`;
|
|
18
|
+
describe('exportReviewContexts', () => {
|
|
19
|
+
it('emits the review prompt for each changed file, with its focus lines and what the change removed', async () => {
|
|
20
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'review-context-'));
|
|
21
|
+
fs.mkdirSync(path.join(dir, 'src'));
|
|
22
|
+
fs.writeFileSync(path.join(dir, 'src/total.ts'), 'export function total(items: number[]) {\n let sum = 0;\n for (let i = 0; i <= items.length; i++) sum += items[i];\n return sum;\n}\n');
|
|
23
|
+
const [context, ...rest] = await exportReviewContexts(dir, DIFF, { prBody: 'Sum the basket.' });
|
|
24
|
+
expect(rest).toEqual([]);
|
|
25
|
+
expect(context.file).toBe('src/total.ts');
|
|
26
|
+
expect(context.focusLines).toEqual([3]);
|
|
27
|
+
expect(context.prompt.startsWith(buildCodeReviewPrompt({ file: '', language: '', text: '', ranges: [], source: '' }).slice(0, 60))).toBe(true);
|
|
28
|
+
expect(context.prompt).toContain('3| for (let i = 0; i <= items.length; i++) sum += items[i];');
|
|
29
|
+
expect(context.prompt).toContain('- for (let i = 0; i < items.length; i++) sum += items[i];');
|
|
30
|
+
expect(context.prompt).toContain('Sum the basket.');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-check: the model re-reads each finding against the code and confirms
|
|
3
|
+
* or withdraws it.
|
|
4
|
+
*
|
|
5
|
+
* Two review passes with the reference material in different orders raise
|
|
6
|
+
* recall (a finding one ordering misses, the other catches); asking again,
|
|
7
|
+
* finding by finding, whether it would misbehave at runtime keeps that extra
|
|
8
|
+
* recall from arriving as noise. A check that cannot run keeps the finding:
|
|
9
|
+
* an unavailable model is not evidence against it.
|
|
10
|
+
*/
|
|
11
|
+
import type { DeepFinding, InferenceOptions, InferenceProvider } from '../inference/types.js';
|
|
12
|
+
import type { CodeContext } from './code-context.js';
|
|
13
|
+
export interface SelfCheckResult {
|
|
14
|
+
kept: DeepFinding[];
|
|
15
|
+
withdrawn: number;
|
|
16
|
+
failed: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function selfCheck(provider: InferenceProvider, context: CodeContext, reference: string, findings: DeepFinding[], inference: InferenceOptions): Promise<SelfCheckResult>;
|
|
19
|
+
export declare function checkPrompt(context: CodeContext, reference: string, finding: DeepFinding): string;
|
|
20
|
+
/** true, false, or undefined when the reply is not a verdict. */
|
|
21
|
+
export declare function parseVerdict(reply: string): boolean | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const MAX_CHECKED = 8;
|
|
2
|
+
const VERDICT_SCHEMA = {
|
|
3
|
+
type: 'object',
|
|
4
|
+
properties: { real: { type: 'boolean' }, reason: { type: 'string' } },
|
|
5
|
+
required: ['real'],
|
|
6
|
+
};
|
|
7
|
+
export async function selfCheck(provider, context, reference, findings, inference) {
|
|
8
|
+
const result = { kept: [], withdrawn: 0, failed: 0 };
|
|
9
|
+
for (const [index, finding] of findings.entries()) {
|
|
10
|
+
if (index >= MAX_CHECKED) {
|
|
11
|
+
result.kept.push(finding);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const reply = await provider.analyze(checkPrompt(context, reference, finding), { ...inference, jsonSchema: VERDICT_SCHEMA });
|
|
16
|
+
if (parseVerdict(reply) === false)
|
|
17
|
+
result.withdrawn++;
|
|
18
|
+
else
|
|
19
|
+
result.kept.push(finding);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
result.failed++;
|
|
23
|
+
result.kept.push(finding);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
export function checkPrompt(context, reference, finding) {
|
|
29
|
+
return [
|
|
30
|
+
'You reported a defect in a code review. Check it against the code before it is posted.',
|
|
31
|
+
`SOURCE (line numbers on the left):\n${context.text}`,
|
|
32
|
+
reference ? `REFERENCE (not under review):\n${reference}` : '',
|
|
33
|
+
`REPORTED at ${context.file}:${finding.line}: ${finding.description}`,
|
|
34
|
+
'Is this a real defect: would this code misbehave at runtime or in production as described? '
|
|
35
|
+
+ 'Answer false if it is style, speculation about unseen callers, or already handled in the code shown. '
|
|
36
|
+
+ 'Respond ONLY with JSON: {"real": true|false, "reason": "..."}.',
|
|
37
|
+
].filter(Boolean).join('\n\n');
|
|
38
|
+
}
|
|
39
|
+
/** true, false, or undefined when the reply is not a verdict. */
|
|
40
|
+
export function parseVerdict(reply) {
|
|
41
|
+
const json = reply.slice(reply.indexOf('{'), reply.lastIndexOf('}') + 1);
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(json).real;
|
|
44
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -19,7 +19,7 @@ export declare function getGatewayPolicyHash(cwd: string, config: GatewayControl
|
|
|
19
19
|
export declare function validateGatewayConfig(value: unknown): GatewayControlConfig;
|
|
20
20
|
export declare function saveGatewayConfig(cwd: string, config: GatewayControlConfig, controlRoot?: string): Promise<string>;
|
|
21
21
|
export declare function loadGatewayConfig(cwd: string, controlRoot?: string): Promise<GatewayControlConfig | null>;
|
|
22
|
-
interface GrantInput {
|
|
22
|
+
export interface GrantInput {
|
|
23
23
|
issuerId: string;
|
|
24
24
|
subjectId: string;
|
|
25
25
|
taskId: string;
|
|
@@ -42,4 +42,3 @@ export declare function consumeTrustedGrant(cwd: string, input: {
|
|
|
42
42
|
reason: string;
|
|
43
43
|
grant?: CapabilityGrant;
|
|
44
44
|
}>;
|
|
45
|
-
export {};
|
|
@@ -47,6 +47,8 @@ export declare class DeepAnalysisGate extends Gate {
|
|
|
47
47
|
run(context: GateContext): Promise<Failure[]>;
|
|
48
48
|
private setupProvider;
|
|
49
49
|
private reviewCode;
|
|
50
|
+
/** The max and cloud tiers read reference material and review twice; the small models do not. */
|
|
51
|
+
private referenceOptions;
|
|
50
52
|
private analyzeFacts;
|
|
51
53
|
private recordPass;
|
|
52
54
|
private localMemoryFindings;
|
|
@@ -32,6 +32,9 @@ const CLOUD_SETUP_TIMEOUT_MS = 120_000;
|
|
|
32
32
|
*/
|
|
33
33
|
const LOCAL_SOURCE_CHARS = 36_000;
|
|
34
34
|
const CLOUD_SOURCE_CHARS = 60_000;
|
|
35
|
+
/** Reference material (removed lines, callees, callers, PR intent) for the max and cloud tiers. */
|
|
36
|
+
const LOCAL_REFERENCE_CHARS = 12_000;
|
|
37
|
+
const CLOUD_REFERENCE_CHARS = 24_000;
|
|
35
38
|
export class DeepAnalysisGate extends Gate {
|
|
36
39
|
config;
|
|
37
40
|
provider = null;
|
|
@@ -104,11 +107,20 @@ export class DeepAnalysisGate extends Gate {
|
|
|
104
107
|
inference: inferenceOptions(this.config),
|
|
105
108
|
maxSourceChars: isCloud(this.config.options) ? CLOUD_SOURCE_CHARS : LOCAL_SOURCE_CHARS,
|
|
106
109
|
focusLines: this.config.options.focusLines,
|
|
110
|
+
reference: this.referenceOptions(),
|
|
107
111
|
onProgress: this.config.onProgress,
|
|
108
112
|
});
|
|
109
113
|
this.recordPass(result);
|
|
110
114
|
return verifyCodeFindings(result.findings, result.contexts);
|
|
111
115
|
}
|
|
116
|
+
/** The max and cloud tiers read reference material and review twice; the small models do not. */
|
|
117
|
+
referenceOptions() {
|
|
118
|
+
const options = this.config.options;
|
|
119
|
+
const cloud = isCloud(options);
|
|
120
|
+
if (!cloud && !options.max)
|
|
121
|
+
return undefined;
|
|
122
|
+
return { maxChars: cloud ? CLOUD_REFERENCE_CHARS : LOCAL_REFERENCE_CHARS, removed: options.removedLines, prBody: options.prBody };
|
|
123
|
+
}
|
|
112
124
|
async analyzeFacts(cwd, facts) {
|
|
113
125
|
this.config.onProgress?.(` Found ${facts.length} files to analyze.`);
|
|
114
126
|
const memory = await this.localMemoryFindings(cwd, facts);
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* and failures, derived from the deep gate's outcome.
|
|
4
4
|
*/
|
|
5
5
|
import { DeepAnalysisGate } from './deep-analysis.js';
|
|
6
|
+
import { localTier } from '../inference/types.js';
|
|
6
7
|
import { Logger } from '../utils/logger.js';
|
|
7
8
|
/**
|
|
8
9
|
* Run the deep gate and turn its outcome into summary, stats and failures.
|
|
@@ -37,7 +38,7 @@ export async function runDeepAnalysis(config, context, deepOptions) {
|
|
|
37
38
|
enabled: true,
|
|
38
39
|
status: outcome.status,
|
|
39
40
|
mode: outcome.mode,
|
|
40
|
-
tier: isLocal ? (deepOptions
|
|
41
|
+
tier: isLocal ? localTier(deepOptions) : 'cloud',
|
|
41
42
|
model: outcome.model,
|
|
42
43
|
model_fallback: outcome.modelFallback,
|
|
43
44
|
total_ms: Date.now() - start,
|
|
@@ -29,6 +29,13 @@ export interface JsImportContext {
|
|
|
29
29
|
* Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
|
|
30
30
|
*/
|
|
31
31
|
export declare function checkJSImports(content: string, file: string, ctx: JsImportContext): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Bundler conventions: a query on a relative import (`./logo.svg?raw`, `./worker.ts?worker`)
|
|
34
|
+
* names a transform of that file, so the file is what must exist; a `virtual:` module or a
|
|
35
|
+
* bare specifier with a query (`page.tsx?tsr-split=component`) is produced by a plugin at
|
|
36
|
+
* build time and cannot be checked. Returns the path to check, or null to skip.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bundlerSpecifier(importPath: string): string | null;
|
|
32
39
|
/**
|
|
33
40
|
* Collect all import statements from JavaScript/TypeScript file content.
|
|
34
41
|
* Handles import, export, require(), and dynamic import() statements.
|
|
@@ -21,7 +21,9 @@ import { loadTsPathConfig } from './tsconfig-paths.js';
|
|
|
21
21
|
export async function checkJSImports(content, file, ctx) {
|
|
22
22
|
const depsForFile = await resolveJSDepsForFile(file, ctx.cwd, ctx.rootDeps, ctx.depCacheByDir);
|
|
23
23
|
const kitRoot = await ctx.kitRoots.rootFor(file);
|
|
24
|
-
for (const
|
|
24
|
+
for (const spec of collectJSImportSpecs(content, file)) {
|
|
25
|
+
const { line } = spec;
|
|
26
|
+
const importPath = bundlerSpecifier(spec.importPath);
|
|
25
27
|
if (!importPath || ctx.shouldIgnore(importPath))
|
|
26
28
|
continue;
|
|
27
29
|
if (kitRoot && isSvelteKitProvided(importPath))
|
|
@@ -38,6 +40,20 @@ export async function checkJSImports(content, file, ctx) {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Bundler conventions: a query on a relative import (`./logo.svg?raw`, `./worker.ts?worker`)
|
|
45
|
+
* names a transform of that file, so the file is what must exist; a `virtual:` module or a
|
|
46
|
+
* bare specifier with a query (`page.tsx?tsr-split=component`) is produced by a plugin at
|
|
47
|
+
* build time and cannot be checked. Returns the path to check, or null to skip.
|
|
48
|
+
*/
|
|
49
|
+
export function bundlerSpecifier(importPath) {
|
|
50
|
+
if (!importPath || importPath.startsWith('virtual:') || importPath.startsWith('\0'))
|
|
51
|
+
return null;
|
|
52
|
+
const query = importPath.search(/[?#]/);
|
|
53
|
+
if (query < 0)
|
|
54
|
+
return importPath;
|
|
55
|
+
return importPath.startsWith('.') ? importPath.slice(0, query) : null;
|
|
56
|
+
}
|
|
41
57
|
async function checkRelativeImport(file, importPath, ctx) {
|
|
42
58
|
if (ctx.resolveRelativeImport(file, importPath, ctx.projectFiles))
|
|
43
59
|
return null;
|
|
@@ -106,3 +106,27 @@ describe('HallucinatedImportsGate — tsconfig inheritance and rootDirs', () =>
|
|
|
106
106
|
expect(await missingImports(cwd)).toContain("'./nowhere'");
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
+
describe('HallucinatedImportsGate — bundler query and virtual imports', () => {
|
|
110
|
+
let cwd;
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'bundler-imports-'));
|
|
113
|
+
write(cwd, 'package.json', JSON.stringify({ dependencies: { '@tanstack/react-router': '1.0.0' } }));
|
|
114
|
+
write(cwd, 'src/logo.svg', '<svg/>');
|
|
115
|
+
write(cwd, 'src/worker.ts', 'export {};\n');
|
|
116
|
+
});
|
|
117
|
+
afterEach(() => { fs.rmSync(cwd, { recursive: true, force: true }); });
|
|
118
|
+
it('checks the file behind a query and skips plugin-provided modules', async () => {
|
|
119
|
+
write(cwd, 'src/route.tsx', [
|
|
120
|
+
"import raw from './logo.svg?raw';",
|
|
121
|
+
"import Worker from './worker.ts?worker&inline';",
|
|
122
|
+
"const split = () => import('route.tsx?tsr-split=component');",
|
|
123
|
+
"import { registerSW } from 'virtual:pwa-register';",
|
|
124
|
+
'export default { raw, Worker, split, registerSW };',
|
|
125
|
+
].join('\n'));
|
|
126
|
+
expect(await missingImports(cwd)).toBe('');
|
|
127
|
+
});
|
|
128
|
+
it('still reports a missing module behind a query', async () => {
|
|
129
|
+
write(cwd, 'src/route.tsx', "import Worker from './missing-worker.ts?worker';\nexport default Worker;\n");
|
|
130
|
+
expect(await missingImports(cwd)).toContain('./missing-worker.ts');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { Gate } from './base.js';
|
|
9
9
|
import { FileScanner } from '../utils/scanner.js';
|
|
10
10
|
import { Logger } from '../utils/logger.js';
|
|
11
|
-
import { analyzeFiles, BUILT_IN_RULES } from '../semantic/engine.js';
|
|
11
|
+
import { ALL_RULES, analyzeFiles, BUILT_IN_RULES } from '../semantic/engine.js';
|
|
12
12
|
import { compileLearnedRule, LEARNED_PREFIX } from '../semantic/learn/compile.js';
|
|
13
13
|
import { loadLearnedRules } from '../semantic/learn/store.js';
|
|
14
14
|
const SOURCE_PATTERNS = ['**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}'];
|
|
@@ -36,9 +36,10 @@ export class SemanticBugsGate extends Gate {
|
|
|
36
36
|
}
|
|
37
37
|
/** Built-in rules plus the repository's learned rules (`.rigour/rules/`), filtered by config. */
|
|
38
38
|
selectRules(cwd) {
|
|
39
|
-
const
|
|
39
|
+
const learned = loadLearnedRules(cwd).map(compileLearnedRule);
|
|
40
40
|
const wanted = this.config.rules;
|
|
41
|
-
|
|
41
|
+
// Named rules may include opt-in candidates; unnamed, only the default set runs.
|
|
42
|
+
return wanted?.length ? [...ALL_RULES, ...learned].filter(rule => wanted.includes(rule.id)) : [...BUILT_IN_RULES, ...learned];
|
|
42
43
|
}
|
|
43
44
|
toFailure(finding) {
|
|
44
45
|
const failure = this.createFailure(`${finding.message}\n${finding.evidence.map(e => ` at ${e}`).join('\n')}`, [finding.file], finding.hint, `[${finding.rule}] ${finding.message.split('. ')[0]}`.slice(0, 120), finding.line, undefined, finding.severity);
|
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
5
|
import { SemanticBugsGate } from './semantic-bugs.js';
|
|
6
6
|
import { GateRunner } from './runner.js';
|
|
7
|
+
import { ConfigSchema } from '../types/index.js';
|
|
7
8
|
const LEAKY = [
|
|
8
9
|
'export async function notify(endpoint: string, signature: string) {',
|
|
9
10
|
" return fetch(endpoint, { method: 'POST', headers: { 'x-hook-signature': signature } });",
|
|
@@ -17,6 +18,12 @@ describe('SemanticBugsGate', () => {
|
|
|
17
18
|
fs.mkdirSync(path.dirname(path.join(cwd, rel)), { recursive: true });
|
|
18
19
|
fs.writeFileSync(path.join(cwd, rel), body);
|
|
19
20
|
}
|
|
21
|
+
it('runs an unvalidated candidate rule only when it is named', async () => {
|
|
22
|
+
write('src/scroll.ts', "export function setup(fn: () => void) {\n window.addEventListener('a', fn);\n addEventListener('b', fn);\n}\n");
|
|
23
|
+
expect(await new SemanticBugsGate().run({ cwd })).toEqual([]);
|
|
24
|
+
const named = await new SemanticBugsGate({ rules: ['env/bare-browser-global'] }).run({ cwd });
|
|
25
|
+
expect(named.map(f => [f.category, f.line])).toEqual([['env/bare-browser-global', 3]]);
|
|
26
|
+
});
|
|
20
27
|
it('reports a proven finding with its evidence, rule and verified metadata', async () => {
|
|
21
28
|
write('src/notify.ts', LEAKY);
|
|
22
29
|
const [failure, ...rest] = await new SemanticBugsGate().run({ cwd });
|
|
@@ -59,12 +66,11 @@ describe('SemanticBugsGate', () => {
|
|
|
59
66
|
expect(failure.title).toContain('[learned/require-option-redirect-abcd1234]');
|
|
60
67
|
expect(failure.hint).toContain('fix abcdef01 in src/http.ts (post)');
|
|
61
68
|
});
|
|
62
|
-
it('is
|
|
69
|
+
it('is on by default and can be turned off in rigour.yml', async () => {
|
|
63
70
|
write('src/notify.ts', LEAKY);
|
|
64
|
-
const
|
|
65
|
-
const off = await new GateRunner(base).run(cwd);
|
|
66
|
-
expect(off.failures.some(f => f.id === 'semantic-bugs')).toBe(false);
|
|
67
|
-
const on = await new GateRunner({ ...base, gates: { ...base.gates, semantic_bugs: { enabled: true } } }).run(cwd);
|
|
71
|
+
const on = await new GateRunner(ConfigSchema.parse({ version: 1 })).run(cwd);
|
|
68
72
|
expect(on.failures.some(f => f.id === 'semantic-bugs')).toBe(true);
|
|
73
|
+
const off = await new GateRunner(ConfigSchema.parse({ version: 1, gates: { semantic_bugs: { enabled: false } } })).run(cwd);
|
|
74
|
+
expect(off.failures.some(f => f.id === 'semantic-bugs')).toBe(false);
|
|
69
75
|
});
|
|
70
76
|
});
|
package/dist/hooks/checker.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
*/
|
|
10
10
|
import type { HookCheckerResult } from './types.js';
|
|
11
|
-
interface CheckerOptions {
|
|
11
|
+
export interface CheckerOptions {
|
|
12
12
|
cwd: string;
|
|
13
13
|
files: string[];
|
|
14
14
|
timeout_ms?: number;
|
|
@@ -21,4 +21,3 @@ interface CheckerOptions {
|
|
|
21
21
|
* Returns structured JSON for hook consumers.
|
|
22
22
|
*/
|
|
23
23
|
export declare function runHookChecker(options: CheckerOptions): Promise<HookCheckerResult>;
|
|
24
|
-
export {};
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
*/
|
|
5
5
|
export { runHookChecker } from './checker.js';
|
|
6
|
+
export type { CheckerOptions } from './checker.js';
|
|
6
7
|
export { generateHookFiles } from './templates.js';
|
|
8
|
+
export type { GeneratedHookFile } from './templates.js';
|
|
7
9
|
export type { HookTool, HookConfig, HookCheckerResult } from './types.js';
|
|
8
10
|
export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
|
|
9
11
|
export { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';
|
|
@@ -12,3 +14,5 @@ export { recordDLPFeedback, allowLastDLPBlock, writeDLPBlockManifest, getLearned
|
|
|
12
14
|
export type { DLPFeedbackEntry, DLPFeedbackStore, DLPBlockManifest } from './dlp-feedback.js';
|
|
13
15
|
export { generateDLPHookFiles } from './dlp-templates.js';
|
|
14
16
|
export type { GeneratedDLPHookFile } from './dlp-templates.js';
|
|
17
|
+
export { stopReview, stopMessage, blocksStop, STOP_MAX_ATTEMPTS } from './stop-review.js';
|
|
18
|
+
export type { StopDecision } from './stop-review.js';
|
package/dist/hooks/index.js
CHANGED
|
@@ -9,3 +9,5 @@ export { DEFAULT_HOOK_CONFIG, FAST_GATE_IDS } from './types.js';
|
|
|
9
9
|
export { scanInputForCredentials, formatDLPAlert, createDLPAuditEntry } from './input-validator.js';
|
|
10
10
|
export { recordDLPFeedback, allowLastDLPBlock, writeDLPBlockManifest, getLearnedAllowCount, loadDLPFeedbackStore, loadDLPFeedbackStoreSync, fingerprintDetection, } from './dlp-feedback.js';
|
|
11
11
|
export { generateDLPHookFiles } from './dlp-templates.js';
|
|
12
|
+
// Stop review ("before you say done")
|
|
13
|
+
export { stopReview, stopMessage, blocksStop, STOP_MAX_ATTEMPTS } from './stop-review.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "before you say done" review: when an agent is about to stop, review the
|
|
3
|
+
* uncommitted change and, if it introduced serious findings, tell the agent
|
|
4
|
+
* what to fix instead of letting it finish.
|
|
5
|
+
*
|
|
6
|
+
* Only findings that deserve it block: critical ones, and high ones that are
|
|
7
|
+
* proven (the semantic engine traced them) or security findings. A high
|
|
8
|
+
* heuristic (a regex that sees `fetch` without `.catch`) is not enough to hold
|
|
9
|
+
* an agent back; callers also cap the number of attempts.
|
|
10
|
+
*/
|
|
11
|
+
import type { Config, Failure } from '../types/index.js';
|
|
12
|
+
export declare const STOP_MAX_ATTEMPTS = 3;
|
|
13
|
+
export interface StopDecision {
|
|
14
|
+
block: boolean;
|
|
15
|
+
/** What the agent is told when blocked; empty otherwise. */
|
|
16
|
+
message: string;
|
|
17
|
+
blocking: number;
|
|
18
|
+
/** Every finding on changed lines, and the files the review covered (for fix capture). */
|
|
19
|
+
findings: Failure[];
|
|
20
|
+
reviewedFiles: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare function stopReview(cwd: string, config: Config, attempt: number): Promise<StopDecision>;
|
|
23
|
+
/** Critical, or high and either proven by the semantic engine or a security finding. */
|
|
24
|
+
export declare function blocksStop(finding: Failure): boolean;
|
|
25
|
+
export declare function stopMessage(findings: Failure[], attempt: number): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { reviewChange } from '../review/review.js';
|
|
2
|
+
export const STOP_MAX_ATTEMPTS = 3;
|
|
3
|
+
const MAX_LISTED = 8;
|
|
4
|
+
export async function stopReview(cwd, config, attempt) {
|
|
5
|
+
const result = await reviewChange({ cwd, config, source: { mode: 'working' } });
|
|
6
|
+
const blocking = result.findings.filter(blocksStop);
|
|
7
|
+
const reviewed = { findings: result.findings, reviewedFiles: Object.keys(result.changedLines) };
|
|
8
|
+
if (blocking.length === 0)
|
|
9
|
+
return { block: false, message: '', blocking: 0, ...reviewed };
|
|
10
|
+
return { block: true, message: stopMessage(blocking, attempt), blocking: blocking.length, ...reviewed };
|
|
11
|
+
}
|
|
12
|
+
/** Critical, or high and either proven by the semantic engine or a security finding. */
|
|
13
|
+
export function blocksStop(finding) {
|
|
14
|
+
const severity = (finding.severity || 'medium');
|
|
15
|
+
if (severity === 'critical')
|
|
16
|
+
return true;
|
|
17
|
+
return severity === 'high' && (finding.verified === true || finding.provenance === 'security');
|
|
18
|
+
}
|
|
19
|
+
export function stopMessage(findings, attempt) {
|
|
20
|
+
const listed = findings.slice(0, MAX_LISTED).map(f => {
|
|
21
|
+
const where = `${f.files?.[0] ?? '?'}:${f.line ?? '?'}`;
|
|
22
|
+
return `- [${(f.severity || 'medium').toUpperCase()}] ${where} ${f.title}${f.hint ? `\n Fix: ${f.hint}` : ''}`;
|
|
23
|
+
});
|
|
24
|
+
const more = findings.length > MAX_LISTED ? [`- …and ${findings.length - MAX_LISTED} more (run \`rigour review\`).`] : [];
|
|
25
|
+
return [
|
|
26
|
+
`Rigour review found ${findings.length} serious issue(s) on the lines you changed (attempt ${attempt} of ${STOP_MAX_ATTEMPTS}).`,
|
|
27
|
+
'Fix them before finishing, or explain why a finding is wrong:',
|
|
28
|
+
...listed,
|
|
29
|
+
...more,
|
|
30
|
+
].join('\n');
|
|
31
|
+
}
|