@rigour-labs/core 6.6.3 → 6.6.5
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/hooks/session-state.d.ts +1 -1
- package/dist/hooks/session-state.js +7 -3
- package/dist/hooks/stop-review.js +1 -1
- package/dist/review/code-patterns.test.js +58 -0
- package/dist/review/duplicate-functions.js +7 -2
- package/dist/review/loop-copies.d.ts +2 -0
- package/dist/review/loop-copies.js +116 -0
- package/dist/review/migration-order.js +5 -1
- package/dist/review/orphan-files.js +5 -1
- package/dist/review/partial-fixes.d.ts +2 -0
- package/dist/review/partial-fixes.js +138 -0
- package/dist/review/partial-wiring.d.ts +2 -0
- package/dist/review/partial-wiring.js +201 -0
- package/dist/review/quiet.js +1 -1
- package/dist/review/review.js +4 -1
- package/dist/review/review.test.js +1 -1
- package/dist/review/reviewer.js +10 -2
- package/dist/templates/universal-config.js +3 -0
- package/dist/types/index.d.ts +34 -0
- package/dist/types/index.js +2 -0
- package/package.json +6 -6
|
@@ -7,7 +7,7 @@ export declare function sessionBaseline(cwd: string, session: string): string |
|
|
|
7
7
|
* A stop in the same state as the last review (a turn that only read, or only talked) has
|
|
8
8
|
* nothing new to review, so the agent is not told the same thing again.
|
|
9
9
|
*/
|
|
10
|
-
export declare function workFingerprint(cwd: string): string | undefined;
|
|
10
|
+
export declare function workFingerprint(cwd: string, ownOutputs?: string[]): string | undefined;
|
|
11
11
|
/** Whether the work is in the state the last stop review saw. */
|
|
12
12
|
export declare function alreadyReviewed(cwd: string, session: string, fingerprint: string | undefined): boolean;
|
|
13
13
|
export declare function recordReviewed(cwd: string, session: string, fingerprint: string | undefined): void;
|
|
@@ -56,11 +56,13 @@ export function sessionBaseline(cwd, session) {
|
|
|
56
56
|
* A stop in the same state as the last review (a turn that only read, or only talked) has
|
|
57
57
|
* nothing new to review, so the agent is not told the same thing again.
|
|
58
58
|
*/
|
|
59
|
-
export function workFingerprint(cwd) {
|
|
59
|
+
export function workFingerprint(cwd, ownOutputs = DEFAULT_OWN_OUTPUTS) {
|
|
60
60
|
const run = (args) => spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
61
|
+
// Rigour's own files change on every review; where a repository does not ignore them, they must not count as work.
|
|
62
|
+
const pathspec = ['--', '.', ...ownOutputs.map(own => `:(exclude)${own}`)];
|
|
61
63
|
const head = run(['rev-parse', '--verify', '--quiet', 'HEAD']);
|
|
62
|
-
const diff = run(['diff', 'HEAD', '--no-color', '--no-ext-diff']);
|
|
63
|
-
const untracked = run(['ls-files', '--others', '--exclude-standard', '-z']);
|
|
64
|
+
const diff = run(['diff', 'HEAD', '--no-color', '--no-ext-diff', ...pathspec]);
|
|
65
|
+
const untracked = run(['ls-files', '--others', '--exclude-standard', '-z', ...pathspec]);
|
|
64
66
|
if (diff.status !== 0 || untracked.status !== 0)
|
|
65
67
|
return undefined;
|
|
66
68
|
const hash = createHash('sha256').update(head.stdout).update(diff.stdout);
|
|
@@ -75,6 +77,8 @@ export function workFingerprint(cwd) {
|
|
|
75
77
|
}
|
|
76
78
|
return hash.digest('hex');
|
|
77
79
|
}
|
|
80
|
+
/** What a review writes into the repository: its state folder and its reports. */
|
|
81
|
+
const DEFAULT_OWN_OUTPUTS = ['.rigour', 'rigour-report.json', 'rigour-fix-packet.json'];
|
|
78
82
|
/** Whether the work is in the state the last stop review saw. */
|
|
79
83
|
export function alreadyReviewed(cwd, session, fingerprint) {
|
|
80
84
|
return !!fingerprint && readStore(cwd)[session]?.reviewed === fingerprint;
|
|
@@ -42,7 +42,7 @@ export function reviewAckMessage(items, attempt) {
|
|
|
42
42
|
].join('\n');
|
|
43
43
|
}
|
|
44
44
|
/** Certain from the code alone, and quick to fix (each measured with no false alarms before it blocks). */
|
|
45
|
-
const MUST_FIX = new Set(['unused-export', 'orphan-file', 'offset-paging', 'unbounded-window', 'duplicate-function']);
|
|
45
|
+
const MUST_FIX = new Set(['unused-export', 'orphan-file', 'offset-paging', 'unbounded-window', 'duplicate-function', 'partial-fix', 'partial-wiring']);
|
|
46
46
|
/** Critical; high and either proven by the semantic engine or a security finding; or a certain, local finding the change added (MUST_FIX). */
|
|
47
47
|
export function blocksStop(finding) {
|
|
48
48
|
const severity = (finding.severity || 'medium');
|
|
@@ -7,6 +7,9 @@ import { ConfigSchema } from '../types/index.js';
|
|
|
7
7
|
import { parseDiff } from '../utils/diff.js';
|
|
8
8
|
import { diffFromGit } from './git-diff.js';
|
|
9
9
|
import { duplicateFunctionFailures } from './duplicate-functions.js';
|
|
10
|
+
import { loopCopyFailures } from './loop-copies.js';
|
|
11
|
+
import { partialFixFailures } from './partial-fixes.js';
|
|
12
|
+
import { partialWiringFailures } from './partial-wiring.js';
|
|
10
13
|
import { optionalParamFailures } from './optional-params.js';
|
|
11
14
|
import { queryPatternFailures } from './query-patterns.js';
|
|
12
15
|
let repo;
|
|
@@ -82,3 +85,58 @@ describe('duplicate functions', () => {
|
|
|
82
85
|
expect(duplicateFunctionFailures(repo, changed(), config)).toEqual([]);
|
|
83
86
|
});
|
|
84
87
|
});
|
|
88
|
+
describe('quadratic copies', () => {
|
|
89
|
+
it('reports an accumulator spread into a new copy on every step, not a push', () => {
|
|
90
|
+
write('src/group.ts', [
|
|
91
|
+
'export function group(items: { day: string }[]) {',
|
|
92
|
+
' const groups = new Map<string, { day: string }[]>();',
|
|
93
|
+
' for (const item of items) groups.set(item.day, [...(groups.get(item.day) ?? []), item]);',
|
|
94
|
+
' const ids = items.reduce((acc: string[], item) => [...acc, item.day], []);',
|
|
95
|
+
' const fast = new Map<string, string[]>();',
|
|
96
|
+
' for (const item of items) (fast.get(item.day) ?? fast.set(item.day, []).get(item.day)!).push(item.day);',
|
|
97
|
+
' return { groups, ids, fast };',
|
|
98
|
+
'}',
|
|
99
|
+
].join('\n'));
|
|
100
|
+
expect(loopCopyFailures(repo, changed(), config).map(f => f.line)).toEqual([3, 4]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('partial fixes', () => {
|
|
104
|
+
it('names the places that still test the narrower condition a new predicate widened', () => {
|
|
105
|
+
write('src/routes/session/page.server.ts', 'export function load(snapshot: any) {\n if (snapshot.answeredCount > 0) return 1;\n return 0;\n}\n');
|
|
106
|
+
write('src/routes/session/page.svelte', '<script lang="ts">\n const show = data.snapshot.answeredCount > 0;\n</script>\n');
|
|
107
|
+
git('add', '-A');
|
|
108
|
+
git('commit', '-qm', 'before');
|
|
109
|
+
write('src/routes/session/page.server.ts', 'export function load(snapshot: any) {\n if (snapshot.answeredCount > 0) return 1;\n return 0;\n}\nexport const resume = (snapshot: any) => {\n const hasSaved = snapshot.answers.length > 0 || snapshot.answeredCount > 0;\n return hasSaved;\n};\n');
|
|
110
|
+
const found = partialFixFailures(repo, changed(), config);
|
|
111
|
+
expect(found.map(f => [f.id, f.line])).toEqual([['partial-fix', 6]]);
|
|
112
|
+
expect(found[0].details).toContain('page.server.ts:2');
|
|
113
|
+
expect(found[0].details).toContain('page.svelte:2');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('partial wiring', () => {
|
|
117
|
+
const mount = (extra) => `<script lang="ts">\n import Player from '$lib/Player.svelte';\n</script>\n<Player\n mode="quiz"\n {questions}${extra}\n/>\n`;
|
|
118
|
+
it('reports a callback added to most same-kind mounts of a local component but not one', () => {
|
|
119
|
+
write('src/lib/Player.svelte', '<script lang="ts">let { onResume } = $props();</script>\n');
|
|
120
|
+
for (const route of ['a', 'b', 'c'])
|
|
121
|
+
write(`src/routes/${route}/+page.svelte`, mount(''));
|
|
122
|
+
write('src/routes/d/+page.svelte', mount('').replace('mode="quiz"', 'mode="exam"'));
|
|
123
|
+
git('add', '-A');
|
|
124
|
+
git('commit', '-qm', 'mounts');
|
|
125
|
+
write('src/routes/a/+page.svelte', mount('\n {onResume}'));
|
|
126
|
+
write('src/routes/b/+page.svelte', mount('\n {onResume}'));
|
|
127
|
+
const found = partialWiringFailures(repo, changed(), config);
|
|
128
|
+
expect(found).toHaveLength(1);
|
|
129
|
+
expect(found[0].details).toContain('src/routes/c/+page.svelte');
|
|
130
|
+
expect(found[0].details).not.toContain('routes/d'); // another kind of mount
|
|
131
|
+
});
|
|
132
|
+
it('ignores styling props and components from packages', () => {
|
|
133
|
+
const icon = (extra) => `<script lang="ts">\n import { Star } from 'lucide-svelte';\n</script>\n<Star class="h-4"${extra} />\n`;
|
|
134
|
+
for (const route of ['a', 'b', 'c'])
|
|
135
|
+
write(`src/routes/${route}/+page.svelte`, icon(''));
|
|
136
|
+
git('add', '-A');
|
|
137
|
+
git('commit', '-qm', 'icons');
|
|
138
|
+
write('src/routes/a/+page.svelte', icon(' onHover={go}'));
|
|
139
|
+
write('src/routes/b/+page.svelte', icon(' onHover={go}'));
|
|
140
|
+
expect(partialWiringFailures(repo, changed(), config)).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -21,8 +21,13 @@ export function duplicateFunctionFailures(cwd, changedLines, config) {
|
|
|
21
21
|
bodies.push(...bodiesIn(cwd, file, lines));
|
|
22
22
|
}
|
|
23
23
|
const byKey = new Map();
|
|
24
|
-
for (const body of bodies)
|
|
25
|
-
|
|
24
|
+
for (const body of bodies) {
|
|
25
|
+
const same = byKey.get(body.key);
|
|
26
|
+
if (same)
|
|
27
|
+
same.push(body);
|
|
28
|
+
else
|
|
29
|
+
byKey.set(body.key, [body]);
|
|
30
|
+
}
|
|
26
31
|
const failures = [];
|
|
27
32
|
for (const same of byKey.values()) {
|
|
28
33
|
if (same.length < 2)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quadratic copy: an accumulator copied whole on every step of a loop.
|
|
3
|
+
* `groups.set(k, [...(groups.get(k) ?? []), item])`, `acc = [...acc, item]`, or a reduce that
|
|
4
|
+
* returns `[...acc, item]` / `{ ...acc, [k]: v }` copies everything gathered so far each time, so
|
|
5
|
+
* n items cost O(n²). Pushing into the existing array (or assigning the key) is linear.
|
|
6
|
+
* Reported only on lines a change adds, from the syntax tree.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
const CODE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
|
|
12
|
+
const BUILD_OUTPUT = /(^|\/)(dist|build|out|coverage|\.next|[\w-]+-dist)\//;
|
|
13
|
+
export function loopCopyFailures(cwd, changedLines, config) {
|
|
14
|
+
if (!config.gates.change_sweep?.enabled)
|
|
15
|
+
return [];
|
|
16
|
+
const failures = [];
|
|
17
|
+
for (const [file, lines] of Object.entries(changedLines)) {
|
|
18
|
+
if (!CODE.test(file) || BUILD_OUTPUT.test(file) || lines.size === 0)
|
|
19
|
+
continue;
|
|
20
|
+
let source;
|
|
21
|
+
try {
|
|
22
|
+
const text = fs.readFileSync(path.join(cwd, file), 'utf8');
|
|
23
|
+
if (!text.includes('...'))
|
|
24
|
+
continue;
|
|
25
|
+
source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const visit = (node) => {
|
|
31
|
+
const copied = copiedAccumulator(node, source);
|
|
32
|
+
if (copied) {
|
|
33
|
+
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
34
|
+
if (lines.has(line))
|
|
35
|
+
failures.push(quadratic(file, line, copied));
|
|
36
|
+
}
|
|
37
|
+
ts.forEachChild(node, visit);
|
|
38
|
+
};
|
|
39
|
+
visit(source);
|
|
40
|
+
}
|
|
41
|
+
return failures;
|
|
42
|
+
}
|
|
43
|
+
/** The accumulator a node copies whole, inside a loop or a reduce, or undefined. */
|
|
44
|
+
function copiedAccumulator(node, source) {
|
|
45
|
+
// m.set(k, [...(m.get(k) ?? []), v]) in a loop
|
|
46
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'set' && node.arguments.length === 2) {
|
|
47
|
+
const map = node.expression.expression.getText(source);
|
|
48
|
+
const value = node.arguments[1];
|
|
49
|
+
if (ts.isArrayLiteralExpression(value) && value.elements.some(e => ts.isSpreadElement(e) && e.expression.getText(source).replace(/\s+/g, '').startsWith(`(${map}.get(`))) {
|
|
50
|
+
return insideLoop(node) ? `${map}'s list` : undefined;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// acc = [...acc, v] / acc = { ...acc, k: v } in a loop
|
|
54
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isIdentifier(node.left)) {
|
|
55
|
+
const name = node.left.text;
|
|
56
|
+
if (spreadsItself(node.right, name, source) && insideLoop(node))
|
|
57
|
+
return name;
|
|
58
|
+
}
|
|
59
|
+
// reduce((acc, v) => [...acc, v]) or { ...acc, [k]: v }
|
|
60
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'reduce') {
|
|
61
|
+
const reducer = node.arguments[0];
|
|
62
|
+
if (reducer && (ts.isArrowFunction(reducer) || ts.isFunctionExpression(reducer)) && reducer.parameters[0] && ts.isIdentifier(reducer.parameters[0].name)) {
|
|
63
|
+
const acc = reducer.parameters[0].name.text;
|
|
64
|
+
const results = ts.isBlock(reducer.body)
|
|
65
|
+
? collectReturns(reducer.body)
|
|
66
|
+
: [ts.isParenthesizedExpression(reducer.body) ? reducer.body.expression : reducer.body];
|
|
67
|
+
if (results.some(r => spreadsItself(r, acc, source)))
|
|
68
|
+
return acc;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function spreadsItself(expr, name, source) {
|
|
74
|
+
const unwrapped = ts.isParenthesizedExpression(expr) ? expr.expression : expr;
|
|
75
|
+
if (ts.isArrayLiteralExpression(unwrapped))
|
|
76
|
+
return unwrapped.elements.some(e => ts.isSpreadElement(e) && e.expression.getText(source) === name);
|
|
77
|
+
if (ts.isObjectLiteralExpression(unwrapped))
|
|
78
|
+
return unwrapped.properties.some(p => ts.isSpreadAssignment(p) && p.expression.getText(source) === name);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function collectReturns(body) {
|
|
82
|
+
const found = [];
|
|
83
|
+
const walk = (node) => {
|
|
84
|
+
if (ts.isFunctionLike(node))
|
|
85
|
+
return;
|
|
86
|
+
if (ts.isReturnStatement(node) && node.expression)
|
|
87
|
+
found.push(node.expression);
|
|
88
|
+
ts.forEachChild(node, walk);
|
|
89
|
+
};
|
|
90
|
+
body.statements.forEach(walk);
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
/** A loop (or an array callback that runs per item) between the node and its function. */
|
|
94
|
+
function insideLoop(node) {
|
|
95
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
96
|
+
if (ts.isForStatement(current) || ts.isForOfStatement(current) || ts.isForInStatement(current) || ts.isWhileStatement(current) || ts.isDoStatement(current))
|
|
97
|
+
return true;
|
|
98
|
+
if (ts.isFunctionLike(current)) {
|
|
99
|
+
const call = current.parent;
|
|
100
|
+
return !!call && ts.isCallExpression(call) && ts.isPropertyAccessExpression(call.expression) && /^(forEach|map|flatMap)$/.test(call.expression.name.text);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function quadratic(file, line, accumulator) {
|
|
106
|
+
return {
|
|
107
|
+
id: 'quadratic-copy',
|
|
108
|
+
title: 'Accumulator copied on every step',
|
|
109
|
+
details: `${accumulator} is copied whole on every step (spread into a new array or object), so n items cost O(n²) work and allocations.`,
|
|
110
|
+
severity: 'medium',
|
|
111
|
+
provenance: 'traditional',
|
|
112
|
+
files: [file],
|
|
113
|
+
line,
|
|
114
|
+
hint: 'Mutate the accumulator you already own: push onto the existing list (`(m.get(k) ?? m.set(k, []).get(k)!).push(v)`), or assign the key.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -24,7 +24,11 @@ export function migrationOrderFailures(cwd, diff, source, config) {
|
|
|
24
24
|
const directory = path.posix.dirname(file);
|
|
25
25
|
if (!file.endsWith('.sql') || !micromatch.isMatch(directory, settings.dirs))
|
|
26
26
|
continue;
|
|
27
|
-
|
|
27
|
+
const files = byDirectory.get(directory);
|
|
28
|
+
if (files)
|
|
29
|
+
files.push(file);
|
|
30
|
+
else
|
|
31
|
+
byDirectory.set(directory, [file]);
|
|
28
32
|
}
|
|
29
33
|
const failures = [];
|
|
30
34
|
for (const [directory, added] of byDirectory) {
|
|
@@ -53,7 +53,11 @@ function referrersOf(cwd, added, files) {
|
|
|
53
53
|
const underFolder = new Map();
|
|
54
54
|
for (const file of added) {
|
|
55
55
|
for (let dir = path.posix.dirname(file); dir !== '.' && dir !== '/'; dir = path.posix.dirname(dir)) {
|
|
56
|
-
|
|
56
|
+
const files = underFolder.get(dir);
|
|
57
|
+
if (files)
|
|
58
|
+
files.push(file);
|
|
59
|
+
else
|
|
60
|
+
underFolder.set(dir, [file]);
|
|
57
61
|
}
|
|
58
62
|
}
|
|
59
63
|
const referrers = new Map(added.map(file => [file, new Set()]));
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partial fix: a change names a broader condition (`const hasSaved = a.length > 0 || count > 0`)
|
|
3
|
+
* and uses it at some call sites, while the narrower condition it replaces (`count > 0`) is still
|
|
4
|
+
* tested on its own elsewhere in the same folder (the same route's other file, a sibling page).
|
|
5
|
+
* "Fixed" means every case, so the places still using the old form are named. Reported on the
|
|
6
|
+
* line that adds the named condition, since the leftovers are, by definition, lines it did not touch.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import ts from 'typescript';
|
|
11
|
+
const CODE = /\.(ts|tsx|js|jsx|mjs|cjs|svelte)$/;
|
|
12
|
+
const SKIPPED = /\.(test|spec)\.|\.d\.ts$/;
|
|
13
|
+
export function partialFixFailures(cwd, changedLines, config) {
|
|
14
|
+
if (!config.gates.change_sweep?.enabled)
|
|
15
|
+
return [];
|
|
16
|
+
const failures = [];
|
|
17
|
+
for (const [file, lines] of Object.entries(changedLines)) {
|
|
18
|
+
if (!/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(file) || SKIPPED.test(file) || lines.size === 0)
|
|
19
|
+
continue;
|
|
20
|
+
const source = parse(cwd, file);
|
|
21
|
+
if (!source)
|
|
22
|
+
continue;
|
|
23
|
+
const visit = (node) => {
|
|
24
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isOr(node.initializer)) {
|
|
25
|
+
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
26
|
+
if (lines.has(line)) {
|
|
27
|
+
const leftovers = narrowerElsewhere(cwd, file, line, parts(node.initializer, source), changedLines);
|
|
28
|
+
if (leftovers.length)
|
|
29
|
+
failures.push(partial(file, line, node.name.text, leftovers));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(node, visit);
|
|
33
|
+
};
|
|
34
|
+
visit(source);
|
|
35
|
+
}
|
|
36
|
+
return failures;
|
|
37
|
+
}
|
|
38
|
+
function isOr(expr) {
|
|
39
|
+
const e = ts.isParenthesizedExpression(expr) ? expr.expression : expr;
|
|
40
|
+
return ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.BarBarToken;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The comparisons a broader condition ORs together. A comparison on a property (`x.count > 0`)
|
|
44
|
+
* matches the same property, operator and value on any receiver (`data.x.count > 0`).
|
|
45
|
+
*/
|
|
46
|
+
function parts(expr, source) {
|
|
47
|
+
const found = [];
|
|
48
|
+
const walk = (e) => {
|
|
49
|
+
const inner = ts.isParenthesizedExpression(e) ? e.expression : e;
|
|
50
|
+
if (ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.BarBarToken) {
|
|
51
|
+
walk(inner.left);
|
|
52
|
+
walk(inner.right);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (ts.isBinaryExpression(inner) && isComparison(inner.operatorToken.kind) && ts.isPropertyAccessExpression(inner.left) && isLiteral(inner.right)) {
|
|
56
|
+
// The receiver's last name and the property (`answers.length`, `snapshot.answeredCount`): specific
|
|
57
|
+
// enough that an unrelated `.length > 0` never matches, loose enough for `data.snapshot.answeredCount`.
|
|
58
|
+
const receiver = inner.left.expression;
|
|
59
|
+
const owner = ts.isIdentifier(receiver) ? receiver.text : ts.isPropertyAccessExpression(receiver) ? receiver.name.text : undefined;
|
|
60
|
+
if (!owner)
|
|
61
|
+
return;
|
|
62
|
+
const tail = `${owner}.${inner.left.name.text}`;
|
|
63
|
+
const operator = inner.operatorToken.getText(source);
|
|
64
|
+
const value = inner.right.getText(source);
|
|
65
|
+
found.push({
|
|
66
|
+
text: inner.getText(source),
|
|
67
|
+
pattern: new RegExp(`(?<![\\w$])${escape(tail)}\\s*${escape(operator)}\\s*${escape(value)}(?![\\w.])`),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
walk(expr);
|
|
72
|
+
return found;
|
|
73
|
+
}
|
|
74
|
+
function isComparison(kind) {
|
|
75
|
+
return [ts.SyntaxKind.GreaterThanToken, ts.SyntaxKind.GreaterThanEqualsToken, ts.SyntaxKind.LessThanToken, ts.SyntaxKind.LessThanEqualsToken,
|
|
76
|
+
ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(kind);
|
|
77
|
+
}
|
|
78
|
+
function isLiteral(e) {
|
|
79
|
+
return ts.isNumericLiteral(e) || ts.isStringLiteral(e) || e.kind === ts.SyntaxKind.NullKeyword || e.kind === ts.SyntaxKind.TrueKeyword || e.kind === ts.SyntaxKind.FalseKeyword;
|
|
80
|
+
}
|
|
81
|
+
/** `file:line` of each remaining standalone use of a part, in the same folder, outside the broader condition. */
|
|
82
|
+
function narrowerElsewhere(cwd, file, declLine, found, changed) {
|
|
83
|
+
if (found.length < 2)
|
|
84
|
+
return [];
|
|
85
|
+
const dir = path.posix.dirname(file);
|
|
86
|
+
let siblings;
|
|
87
|
+
try {
|
|
88
|
+
siblings = fs.readdirSync(path.join(cwd, dir)).filter(name => CODE.test(name) && !SKIPPED.test(name)).map(name => path.posix.join(dir, name));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const leftovers = [];
|
|
94
|
+
for (const sibling of siblings) {
|
|
95
|
+
let text;
|
|
96
|
+
try {
|
|
97
|
+
text = fs.readFileSync(path.join(cwd, sibling), 'utf8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
text.split('\n').forEach((content, index) => {
|
|
103
|
+
const line = index + 1;
|
|
104
|
+
if (sibling === file && line === declLine)
|
|
105
|
+
return;
|
|
106
|
+
// A line the change itself added is the author's current intent, not a leftover.
|
|
107
|
+
if (changed[sibling]?.has(line) && sibling === file)
|
|
108
|
+
return;
|
|
109
|
+
if (found.some(part => part.pattern.test(content)) && !found.every(part => part.pattern.test(content)))
|
|
110
|
+
leftovers.push(`${sibling}:${line}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return leftovers;
|
|
114
|
+
}
|
|
115
|
+
function parse(cwd, file) {
|
|
116
|
+
try {
|
|
117
|
+
return ts.createSourceFile(file, fs.readFileSync(path.join(cwd, file), 'utf8'), ts.ScriptTarget.Latest, true);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function escape(text) {
|
|
124
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
125
|
+
}
|
|
126
|
+
function partial(file, line, name, leftovers) {
|
|
127
|
+
const listed = leftovers.slice(0, 5).map(l => `\`${l}\``).join(', ');
|
|
128
|
+
return {
|
|
129
|
+
id: 'partial-fix',
|
|
130
|
+
title: 'Partial fix',
|
|
131
|
+
details: `\`${name}\` widens a condition, but the narrower form is still tested on its own at ${listed}${leftovers.length > 5 ? ` and ${leftovers.length - 5} more` : ''}.`,
|
|
132
|
+
severity: 'medium',
|
|
133
|
+
provenance: 'traditional',
|
|
134
|
+
files: [file],
|
|
135
|
+
line,
|
|
136
|
+
hint: `Use \`${name}\` (or the same condition) at each of those places too, or say in a comment why the narrower test is right there.`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partial wiring: a change adds a prop to some mounts of a component, and a sibling mount of the
|
|
3
|
+
* same kind (agreeing on a literal prop, such as `mode="quiz"`, in at least three files) still lacks it, so that place keeps the
|
|
4
|
+
* old behaviour (a handler never called, events missing). Svelte and JSX mounts are read with a
|
|
5
|
+
* brace-aware scanner; a mount that spreads props (`{...rest}`) is given the benefit of the doubt.
|
|
6
|
+
* Reported on the line that adds the prop.
|
|
7
|
+
*/
|
|
8
|
+
import { spawnSync } from 'child_process';
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
const MOUNTING = /\.(svelte|tsx|jsx)$/;
|
|
12
|
+
const SKIPPED = /\.(test|spec|stories)\.|(^|\/)(__tests__|tests?)\//;
|
|
13
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
14
|
+
export function partialWiringFailures(cwd, changedLines, config) {
|
|
15
|
+
if (!config.gates.change_sweep?.enabled)
|
|
16
|
+
return [];
|
|
17
|
+
const failures = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const [file, lines] of Object.entries(changedLines)) {
|
|
20
|
+
if (!MOUNTING.test(file) || SKIPPED.test(file) || lines.size === 0)
|
|
21
|
+
continue;
|
|
22
|
+
for (const mount of mountsIn(cwd, file)) {
|
|
23
|
+
for (const { prop, line } of addedProps(cwd, mount, lines)) {
|
|
24
|
+
const key = `${mount.component}\0${prop}`;
|
|
25
|
+
if (seen.has(key))
|
|
26
|
+
continue;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
// A missed wiring is a callback the project's own component offers (`onResume`), among mounts
|
|
29
|
+
// of the same kind (a literal they agree on, `mode="quiz"`) in several files. UI-kit and icon
|
|
30
|
+
// components, styling props and DOM events take different values everywhere by design.
|
|
31
|
+
if (!/^on[A-Z]/.test(prop) || mount.literals.size === 0 || !isLocalComponent(cwd, mount))
|
|
32
|
+
continue;
|
|
33
|
+
const kin = mountsOf(cwd, mount.component).filter(m => sameKind(m, mount));
|
|
34
|
+
if (new Set(kin.map(m => m.file)).size < 3)
|
|
35
|
+
continue;
|
|
36
|
+
const missing = kin.filter(m => !m.props.has(prop) && !m.spreads);
|
|
37
|
+
const having = kin.filter(m => m.props.has(prop));
|
|
38
|
+
if (kin.length >= 3 && having.length >= 2 && missing.length >= 1 && missing.length < having.length) {
|
|
39
|
+
failures.push(unwired(mount, prop, line, missing));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return failures;
|
|
45
|
+
}
|
|
46
|
+
/** Props on the mount written on a line the change added, with that line (where a finding belongs). */
|
|
47
|
+
function addedProps(cwd, mount, lines) {
|
|
48
|
+
const text = fs.readFileSync(path.join(cwd, mount.file), 'utf8').split('\n');
|
|
49
|
+
const added = new Map();
|
|
50
|
+
for (let line = mount.line; line <= mount.endLine; line++) {
|
|
51
|
+
if (!lines.has(line))
|
|
52
|
+
continue;
|
|
53
|
+
for (const prop of mount.props) {
|
|
54
|
+
if (!added.has(prop) && new RegExp(`(^|[\\s{])${prop.replace(/[$]/g, '\\$')}(\\s*=|\\s*}|\\s|$)`).test(text[line - 1] ?? ''))
|
|
55
|
+
added.set(prop, line);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return [...added].map(([prop, line]) => ({ prop, line }));
|
|
59
|
+
}
|
|
60
|
+
/** The component is imported from the project itself (a relative path or a project alias), not a package. */
|
|
61
|
+
function isLocalComponent(cwd, mount) {
|
|
62
|
+
const text = fs.readFileSync(path.join(cwd, mount.file), 'utf8');
|
|
63
|
+
const name = mount.component.split('.')[0];
|
|
64
|
+
const imported = new RegExp(`import\\s+(?:type\\s+)?(?:${name}\\b[^;]*?|\\{[^}]*\\b${name}\\b[^}]*\\})\\s*from\\s*['"]([^'"]+)['"]`).exec(text);
|
|
65
|
+
return !!imported && /^(\.|\$lib\/|@\/|~\/|src\/)/.test(imported[1]);
|
|
66
|
+
}
|
|
67
|
+
function sameKind(a, b) {
|
|
68
|
+
return [...b.literals].every(([name, value]) => a.literals.get(name) === value);
|
|
69
|
+
}
|
|
70
|
+
const mountCache = new Map();
|
|
71
|
+
/** Every mount of `component` in the repository's Svelte and JSX files. */
|
|
72
|
+
function mountsOf(cwd, component) {
|
|
73
|
+
const key = `${cwd}\0${component}`;
|
|
74
|
+
const cached = mountCache.get(key);
|
|
75
|
+
if (cached)
|
|
76
|
+
return cached;
|
|
77
|
+
const grep = spawnSync('git', ['grep', '--untracked', '-l', '-F', '-e', `<${component}`], { cwd, encoding: 'utf8', timeout: GIT_TIMEOUT_MS });
|
|
78
|
+
const files = grep.status === 0 ? grep.stdout.split('\n').filter(f => f && MOUNTING.test(f) && !SKIPPED.test(f)) : [];
|
|
79
|
+
const mounts = files.flatMap(file => mountsIn(cwd, file).filter(m => m.component === component));
|
|
80
|
+
mountCache.set(key, mounts);
|
|
81
|
+
return mounts;
|
|
82
|
+
}
|
|
83
|
+
/** Component tags (capitalised) in a file, with their props, read without breaking on `>` inside braces or quotes. */
|
|
84
|
+
function mountsIn(cwd, file) {
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
text = fs.readFileSync(path.join(cwd, file), 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const mounts = [];
|
|
93
|
+
const tag = /<([A-Z][\w.]*)(?=[\s/>])/g;
|
|
94
|
+
for (let match = tag.exec(text); match; match = tag.exec(text)) {
|
|
95
|
+
const end = tagEnd(text, match.index + match[0].length);
|
|
96
|
+
if (end < 0)
|
|
97
|
+
continue;
|
|
98
|
+
const body = text.slice(match.index + match[0].length, end);
|
|
99
|
+
const line = text.slice(0, match.index).split('\n').length;
|
|
100
|
+
mounts.push({ file, line, endLine: line + body.split('\n').length - 1, component: match[1], ...props(body) });
|
|
101
|
+
}
|
|
102
|
+
return mounts;
|
|
103
|
+
}
|
|
104
|
+
/** Index of the `>` that closes the tag starting at `from`, skipping braces and quotes. */
|
|
105
|
+
function tagEnd(text, from) {
|
|
106
|
+
let depth = 0;
|
|
107
|
+
let quote = '';
|
|
108
|
+
for (let i = from; i < text.length; i++) {
|
|
109
|
+
const c = text[i];
|
|
110
|
+
if (quote) {
|
|
111
|
+
if (c === quote)
|
|
112
|
+
quote = '';
|
|
113
|
+
}
|
|
114
|
+
else if (c === '"' || c === "'" || c === '`') {
|
|
115
|
+
quote = c;
|
|
116
|
+
}
|
|
117
|
+
else if (c === '{') {
|
|
118
|
+
depth++;
|
|
119
|
+
}
|
|
120
|
+
else if (c === '}') {
|
|
121
|
+
depth--;
|
|
122
|
+
}
|
|
123
|
+
else if (c === '>' && depth === 0) {
|
|
124
|
+
return i;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return -1;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* A tag's props: `name="literal"`, `name={…}`, the shorthand `{name}`, and whether it spreads
|
|
131
|
+
* `{...rest}`. Text inside braces is code, not props, and never read as one.
|
|
132
|
+
*/
|
|
133
|
+
function props(body) {
|
|
134
|
+
let top = '';
|
|
135
|
+
let depth = 0;
|
|
136
|
+
let quote = '';
|
|
137
|
+
let inner = '';
|
|
138
|
+
for (const c of body) {
|
|
139
|
+
if (quote) {
|
|
140
|
+
if (depth === 0)
|
|
141
|
+
top += c;
|
|
142
|
+
else
|
|
143
|
+
inner += c;
|
|
144
|
+
if (c === quote)
|
|
145
|
+
quote = '';
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
149
|
+
quote = c;
|
|
150
|
+
if (depth === 0)
|
|
151
|
+
top += c;
|
|
152
|
+
else
|
|
153
|
+
inner += c;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (c === '{') {
|
|
157
|
+
if (depth > 0)
|
|
158
|
+
inner += c;
|
|
159
|
+
depth++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (c === '}') {
|
|
163
|
+
depth--;
|
|
164
|
+
if (depth > 0) {
|
|
165
|
+
inner += c;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const content = inner.trim();
|
|
169
|
+
top += /^\.\.\./.test(content) ? '{...}' : /^[A-Za-z_$][\w$]*$/.test(content) ? `{${content}}` : '{}';
|
|
170
|
+
inner = '';
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (depth === 0)
|
|
174
|
+
top += c;
|
|
175
|
+
else
|
|
176
|
+
inner += c;
|
|
177
|
+
}
|
|
178
|
+
const names = new Set();
|
|
179
|
+
const literals = new Map();
|
|
180
|
+
for (const m of top.matchAll(/([A-Za-z_$][\w$:-]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{)/g)) {
|
|
181
|
+
names.add(m[1]);
|
|
182
|
+
if (m[2] !== undefined || m[3] !== undefined)
|
|
183
|
+
literals.set(m[1], m[2] ?? m[3]);
|
|
184
|
+
}
|
|
185
|
+
for (const m of top.matchAll(/(?<![=\w$])\{([A-Za-z_$][\w$]*)\}/g))
|
|
186
|
+
names.add(m[1]);
|
|
187
|
+
return { props: names, literals, spreads: top.includes('{...}') };
|
|
188
|
+
}
|
|
189
|
+
function unwired(mount, prop, line, missing) {
|
|
190
|
+
const where = missing.slice(0, 5).map(m => `\`${m.file}:${m.line}\``).join(', ');
|
|
191
|
+
return {
|
|
192
|
+
id: 'partial-wiring',
|
|
193
|
+
title: 'Wired in some places, not all',
|
|
194
|
+
details: `\`${prop}\` is passed to \`<${mount.component}>\` here and in other mounts of the same kind, but not at ${where}, so that place keeps the old behaviour.`,
|
|
195
|
+
severity: 'medium',
|
|
196
|
+
provenance: 'traditional',
|
|
197
|
+
files: [mount.file],
|
|
198
|
+
line,
|
|
199
|
+
hint: `Pass \`${prop}\` there too, or say why that mount should not have it.`,
|
|
200
|
+
};
|
|
201
|
+
}
|
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']);
|
|
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']);
|
|
24
24
|
export const DISMISSED_FILE = path.join('.rigour', 'dismissed.json');
|
|
25
25
|
export function isProven(failure) {
|
|
26
26
|
return PROVEN_GATES.has(failure.id);
|
package/dist/review/review.js
CHANGED
|
@@ -24,6 +24,9 @@ import { unusedExportFailures } from './unused-exports.js';
|
|
|
24
24
|
import { queryPatternFailures } from './query-patterns.js';
|
|
25
25
|
import { optionalParamFailures } from './optional-params.js';
|
|
26
26
|
import { duplicateFunctionFailures } from './duplicate-functions.js';
|
|
27
|
+
import { loopCopyFailures } from './loop-copies.js';
|
|
28
|
+
import { partialFixFailures } from './partial-fixes.js';
|
|
29
|
+
import { partialWiringFailures } from './partial-wiring.js';
|
|
27
30
|
import { isControlFile } from './trusted-state.js';
|
|
28
31
|
import { baseCommit, baseFindings, splitIntroduced } from './baseline.js';
|
|
29
32
|
export async function reviewChange(input) {
|
|
@@ -40,7 +43,7 @@ export async function reviewChange(input) {
|
|
|
40
43
|
report.failures.push(...await diffTestFailures(input.cwd, input.source, deep));
|
|
41
44
|
report.failures.push(...migrationOrderFailures(input.cwd, diff, input.source, input.config));
|
|
42
45
|
report.failures.push(...unusedExportFailures(input.cwd, diff, input.config), ...orphanFileFailures(input.cwd, diff, input.config));
|
|
43
|
-
report.failures.push(...queryPatternFailures(input.cwd, changedLines, input.config), ...optionalParamFailures(input.cwd, changedLines, input.config), ...duplicateFunctionFailures(input.cwd, changedLines, input.config));
|
|
46
|
+
report.failures.push(...queryPatternFailures(input.cwd, changedLines, input.config), ...optionalParamFailures(input.cwd, changedLines, input.config), ...duplicateFunctionFailures(input.cwd, changedLines, input.config), ...loopCopyFailures(input.cwd, changedLines, input.config), ...partialFixFailures(input.cwd, changedLines, input.config), ...partialWiringFailures(input.cwd, changedLines, input.config));
|
|
44
47
|
const split = splitByChangedLines(report.failures, changedLines, deep ? changedFunctionSpans(input.cwd, changedLines) : {}, removedByFile(diff));
|
|
45
48
|
const deepError = deepAnalysisError(report);
|
|
46
49
|
const quiet = quietSplit(input.cwd, split.findings, input.config.review?.include_heuristics, input.trustedRef);
|
|
@@ -140,7 +140,7 @@ describe('git-backed review', () => {
|
|
|
140
140
|
const shown = [...all.findings, ...all.fileFindings, ...all.advisory].filter(f => f.id === 'AST_COMPLEXITY');
|
|
141
141
|
expect(shown.map(f => f.files?.[0]).sort()).toEqual(['src/fresh.ts', 'src/old.ts']);
|
|
142
142
|
expect(all.preexisting).toBe(0);
|
|
143
|
-
});
|
|
143
|
+
}, 30_000); // two whole reviews, each running the rules on the change and on the base tree
|
|
144
144
|
it('lets a change dismiss its own finding only when the review trusts the working tree', async () => {
|
|
145
145
|
write('src/old.ts', 'export const a = 1;\n');
|
|
146
146
|
write('.gitignore', '.rigour/*\n!.rigour/dismissed.json\n');
|
package/dist/review/reviewer.js
CHANGED
|
@@ -168,8 +168,16 @@ Do this in order.
|
|
|
168
168
|
"never", "only") must be true of the code; a claim the code does not make true is blocking.
|
|
169
169
|
4. Then review the diff the way that reviewer would: correctness, dead code and unreferenced exports,
|
|
170
170
|
duplicated logic, every comment and claim still true of the code, and the repository's rules.
|
|
171
|
-
5.
|
|
172
|
-
|
|
171
|
+
5. Sweep the author's own fixes. For each condition, helper or field the change introduces or changes:
|
|
172
|
+
is the old form still used elsewhere in the module or its sibling routes (a fix applied to some
|
|
173
|
+
places only)? Does an equivalent already exist nearby (a helper written again)? Is a field the
|
|
174
|
+
producers fill overwritten before anything reads it (a later spread, a merge), and does its comment
|
|
175
|
+
name where it really comes from? When a fix depends on what a third-party library does (for
|
|
176
|
+
example, a call that silently does nothing for an unknown id), cite the library's own types or docs,
|
|
177
|
+
and check that a test fake can fail the way the library does.
|
|
178
|
+
6. Blocking is decided by the kind of finding, not by how severe it feels. These are always blocking:
|
|
179
|
+
a previous blocking point not fully resolved; a fix applied to some of the places that need it;
|
|
180
|
+
state recorded as done before an unconfirmed library call; a read before a filter known before it; an unbounded window;
|
|
173
181
|
OFFSET paging in a loop; a read with no deadline in a scheduled job; a lock or kill switch checked
|
|
174
182
|
after work starts; a request hook that sets or clears cookies and then returns a response it built
|
|
175
183
|
itself without those cookies reaching it (check whether anything adds queued cookies afterwards); dead code or an unreferenced export the change adds; a comment, doc or PR claim
|
package/dist/types/index.d.ts
CHANGED
|
@@ -537,6 +537,14 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
537
537
|
}, {
|
|
538
538
|
enabled?: boolean | undefined;
|
|
539
539
|
}>>>;
|
|
540
|
+
/** What a fix leaves half done: the narrower condition still used elsewhere, a prop wired into some sibling mounts only, an accumulator copied every step (review/partial-fixes.ts, partial-wiring.ts, loop-copies.ts). */
|
|
541
|
+
change_sweep: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
542
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
543
|
+
}, "strip", z.ZodTypeAny, {
|
|
544
|
+
enabled: boolean;
|
|
545
|
+
}, {
|
|
546
|
+
enabled?: boolean | undefined;
|
|
547
|
+
}>>>;
|
|
540
548
|
/** A parameter the change adds as optional that only tests omit (review/optional-params.ts). */
|
|
541
549
|
optional_params: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
542
550
|
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
@@ -1008,6 +1016,9 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
1008
1016
|
query_patterns: {
|
|
1009
1017
|
enabled: boolean;
|
|
1010
1018
|
};
|
|
1019
|
+
change_sweep: {
|
|
1020
|
+
enabled: boolean;
|
|
1021
|
+
};
|
|
1011
1022
|
optional_params: {
|
|
1012
1023
|
enabled: boolean;
|
|
1013
1024
|
};
|
|
@@ -1280,6 +1291,9 @@ export declare const GatesSchema: z.ZodObject<{
|
|
|
1280
1291
|
query_patterns?: {
|
|
1281
1292
|
enabled?: boolean | undefined;
|
|
1282
1293
|
} | undefined;
|
|
1294
|
+
change_sweep?: {
|
|
1295
|
+
enabled?: boolean | undefined;
|
|
1296
|
+
} | undefined;
|
|
1283
1297
|
optional_params?: {
|
|
1284
1298
|
enabled?: boolean | undefined;
|
|
1285
1299
|
} | undefined;
|
|
@@ -1925,6 +1939,14 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
1925
1939
|
}, {
|
|
1926
1940
|
enabled?: boolean | undefined;
|
|
1927
1941
|
}>>>;
|
|
1942
|
+
/** What a fix leaves half done: the narrower condition still used elsewhere, a prop wired into some sibling mounts only, an accumulator copied every step (review/partial-fixes.ts, partial-wiring.ts, loop-copies.ts). */
|
|
1943
|
+
change_sweep: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
1944
|
+
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
1945
|
+
}, "strip", z.ZodTypeAny, {
|
|
1946
|
+
enabled: boolean;
|
|
1947
|
+
}, {
|
|
1948
|
+
enabled?: boolean | undefined;
|
|
1949
|
+
}>>>;
|
|
1928
1950
|
/** A parameter the change adds as optional that only tests omit (review/optional-params.ts). */
|
|
1929
1951
|
optional_params: z.ZodDefault<z.ZodOptional<z.ZodObject<{
|
|
1930
1952
|
enabled: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
@@ -2396,6 +2418,9 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
2396
2418
|
query_patterns: {
|
|
2397
2419
|
enabled: boolean;
|
|
2398
2420
|
};
|
|
2421
|
+
change_sweep: {
|
|
2422
|
+
enabled: boolean;
|
|
2423
|
+
};
|
|
2399
2424
|
optional_params: {
|
|
2400
2425
|
enabled: boolean;
|
|
2401
2426
|
};
|
|
@@ -2668,6 +2693,9 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
2668
2693
|
query_patterns?: {
|
|
2669
2694
|
enabled?: boolean | undefined;
|
|
2670
2695
|
} | undefined;
|
|
2696
|
+
change_sweep?: {
|
|
2697
|
+
enabled?: boolean | undefined;
|
|
2698
|
+
} | undefined;
|
|
2671
2699
|
optional_params?: {
|
|
2672
2700
|
enabled?: boolean | undefined;
|
|
2673
2701
|
} | undefined;
|
|
@@ -3032,6 +3060,9 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
3032
3060
|
query_patterns: {
|
|
3033
3061
|
enabled: boolean;
|
|
3034
3062
|
};
|
|
3063
|
+
change_sweep: {
|
|
3064
|
+
enabled: boolean;
|
|
3065
|
+
};
|
|
3035
3066
|
optional_params: {
|
|
3036
3067
|
enabled: boolean;
|
|
3037
3068
|
};
|
|
@@ -3342,6 +3373,9 @@ export declare const ConfigSchema: z.ZodObject<{
|
|
|
3342
3373
|
query_patterns?: {
|
|
3343
3374
|
enabled?: boolean | undefined;
|
|
3344
3375
|
} | undefined;
|
|
3376
|
+
change_sweep?: {
|
|
3377
|
+
enabled?: boolean | undefined;
|
|
3378
|
+
} | undefined;
|
|
3345
3379
|
optional_params?: {
|
|
3346
3380
|
enabled?: boolean | undefined;
|
|
3347
3381
|
} | undefined;
|
package/dist/types/index.js
CHANGED
|
@@ -288,6 +288,8 @@ export const GatesSchema = z.object({
|
|
|
288
288
|
}).optional().default({}),
|
|
289
289
|
/** Query shapes that cost production: offset paging in a loop, a time window with no upper bound (review/query-patterns.ts). */
|
|
290
290
|
query_patterns: z.object({ enabled: z.boolean().optional().default(true) }).optional().default({}),
|
|
291
|
+
/** What a fix leaves half done: the narrower condition still used elsewhere, a prop wired into some sibling mounts only, an accumulator copied every step (review/partial-fixes.ts, partial-wiring.ts, loop-copies.ts). */
|
|
292
|
+
change_sweep: z.object({ enabled: z.boolean().optional().default(true) }).optional().default({}),
|
|
291
293
|
/** A parameter the change adds as optional that only tests omit (review/optional-params.ts). */
|
|
292
294
|
optional_params: z.object({ enabled: z.boolean().optional().default(true) }).optional().default({}),
|
|
293
295
|
/** A changed function whose body duplicates another in the files the change touched (review/duplicate-functions.ts). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.6.
|
|
3
|
+
"version": "6.6.5",
|
|
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"
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"pg": "^8.16.3",
|
|
75
75
|
"sqlite3": "^5.1.7",
|
|
76
76
|
"openai": "^4.104.0",
|
|
77
|
-
"@rigour-labs/brain-darwin-arm64": "6.6.
|
|
78
|
-
"@rigour-labs/brain-darwin-x64": "6.6.
|
|
79
|
-
"@rigour-labs/brain-linux-arm64": "6.6.
|
|
80
|
-
"@rigour-labs/brain-linux-x64": "6.6.
|
|
81
|
-
"@rigour-labs/brain-win-x64": "6.6.
|
|
77
|
+
"@rigour-labs/brain-darwin-arm64": "6.6.5",
|
|
78
|
+
"@rigour-labs/brain-darwin-x64": "6.6.5",
|
|
79
|
+
"@rigour-labs/brain-linux-arm64": "6.6.5",
|
|
80
|
+
"@rigour-labs/brain-linux-x64": "6.6.5",
|
|
81
|
+
"@rigour-labs/brain-win-x64": "6.6.5"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"@types/fs-extra": "^11.0.4",
|