@rigour-labs/core 6.12.2-rc.4 → 6.12.2-rc.6
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/gates/runner.d.ts +2 -0
- package/dist/gates/runner.js +8 -0
- package/dist/gates/test-quality-helpers.d.ts +6 -0
- package/dist/gates/test-quality-helpers.js +155 -0
- package/dist/gates/test-quality.js +14 -6
- package/dist/review/review.d.ts +2 -0
- package/dist/review/review.js +20 -4
- package/package.json +6 -6
package/dist/gates/runner.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Gate } from './base.js';
|
|
2
2
|
import { Failure, Config, Report, DeepOptions } from '../types/index.js';
|
|
3
|
+
/** The gate a finding came from, for a finding this runner produced; else undefined. */
|
|
4
|
+
export declare function gateOf(failure: Failure): string | undefined;
|
|
3
5
|
export declare class GateRunner {
|
|
4
6
|
private config;
|
|
5
7
|
private gates;
|
package/dist/gates/runner.js
CHANGED
|
@@ -36,6 +36,12 @@ import { execa } from 'execa';
|
|
|
36
36
|
import { Logger } from '../utils/logger.js';
|
|
37
37
|
import { FileSystemCache } from '../services/filesystem-cache.js';
|
|
38
38
|
import { onChangedLines, settledChecks } from '../review/settled-checks.js';
|
|
39
|
+
/** The gate each finding of a run came from (a gate may give its findings other ids, such as `AST_COMPLEXITY`). */
|
|
40
|
+
const GATE_OF = new WeakMap();
|
|
41
|
+
/** The gate a finding came from, for a finding this runner produced; else undefined. */
|
|
42
|
+
export function gateOf(failure) {
|
|
43
|
+
return GATE_OF.get(failure);
|
|
44
|
+
}
|
|
39
45
|
export class GateRunner {
|
|
40
46
|
config;
|
|
41
47
|
gates = [];
|
|
@@ -166,6 +172,8 @@ export class GateRunner {
|
|
|
166
172
|
let skipped;
|
|
167
173
|
const gateFailures = await gate.run({ cwd, record, ignore, patterns, fileCache, skip: reason => { skipped = reason; } });
|
|
168
174
|
if (gateFailures.length > 0) {
|
|
175
|
+
for (const f of gateFailures)
|
|
176
|
+
GATE_OF.set(f, gate.id);
|
|
169
177
|
failures.push(...gateFailures);
|
|
170
178
|
summary[gate.id] = 'FAIL';
|
|
171
179
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Names this test file can call that assert. `read` reads a module once per run (cached by the caller). */
|
|
2
|
+
export declare function assertionHelpers(cwd: string, file: string, content: string, read: (abs: string) => Promise<string | undefined>): Promise<Set<string>>;
|
|
3
|
+
/** A call to one of the names on this line; `member` also counts `self.name(` (a Python method). */
|
|
4
|
+
export declare function callsHelper(line: string, helpers: Set<string>, member?: boolean): boolean;
|
|
5
|
+
/** Python: names this test file can call that assert (see the file comment). */
|
|
6
|
+
export declare function pythonAssertionHelpers(cwd: string, file: string, content: string, read: (abs: string) => Promise<string | undefined>): Promise<Set<string>>;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
const HELPER_NAME = /^(?:expect|assert|should)[A-Z0-9_$]/;
|
|
3
|
+
const ASSERTS = /\bexpect\s*[(.]|\bassert\s*[.(]/;
|
|
4
|
+
const IMPORT = /import\s+(?:type\s+)?([\w$]+\s*,?\s*)?(?:\{([^}]*)\})?\s*from\s*['"]([^'"]+)['"]/g;
|
|
5
|
+
const EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '/index.ts', '/index.js'];
|
|
6
|
+
/** Names this test file can call that assert. `read` reads a module once per run (cached by the caller). */
|
|
7
|
+
export async function assertionHelpers(cwd, file, content, read) {
|
|
8
|
+
const helpers = new Set(definedAsserting(content));
|
|
9
|
+
for (const m of content.matchAll(IMPORT)) {
|
|
10
|
+
const names = importedNames(m[1], m[2]);
|
|
11
|
+
for (const [local, exported] of names)
|
|
12
|
+
if (HELPER_NAME.test(local) || HELPER_NAME.test(exported))
|
|
13
|
+
helpers.add(local);
|
|
14
|
+
if (!m[3].startsWith('.'))
|
|
15
|
+
continue;
|
|
16
|
+
const source = await moduleSource(path.join(cwd, path.dirname(file), m[3]), read);
|
|
17
|
+
if (!source)
|
|
18
|
+
continue;
|
|
19
|
+
const asserting = new Set(definedAsserting(source));
|
|
20
|
+
for (const [local, exported] of names)
|
|
21
|
+
if (asserting.has(exported))
|
|
22
|
+
helpers.add(local);
|
|
23
|
+
}
|
|
24
|
+
return helpers;
|
|
25
|
+
}
|
|
26
|
+
/** A call to one of the names on this line; `member` also counts `self.name(` (a Python method). */
|
|
27
|
+
export function callsHelper(line, helpers, member = false) {
|
|
28
|
+
const before = member ? '(?:^|[^\\w$.]|\\bself\\.)' : '(?:^|[^\\w$.])';
|
|
29
|
+
for (const name of helpers)
|
|
30
|
+
if (new RegExp(`${before}${name.replace(/\$/g, '\\$')}\\s*[(<]`).test(line))
|
|
31
|
+
return true;
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
/** `[local, exported]` pairs of an import's default and named bindings. */
|
|
35
|
+
function importedNames(defaultName, named) {
|
|
36
|
+
const pairs = [];
|
|
37
|
+
const d = defaultName?.replace(/[\s,]/g, '');
|
|
38
|
+
if (d)
|
|
39
|
+
pairs.push([d, 'default']);
|
|
40
|
+
for (const part of (named ?? '').split(',')) {
|
|
41
|
+
const [exported, local] = part.replace(/^\s*type\s+/, '').trim().split(/\s+as\s+/);
|
|
42
|
+
if (exported)
|
|
43
|
+
pairs.push([(local ?? exported).trim(), exported.trim()]);
|
|
44
|
+
}
|
|
45
|
+
return pairs;
|
|
46
|
+
}
|
|
47
|
+
async function moduleSource(base, read) {
|
|
48
|
+
// An ESM import names the compiled file (`./helpers.js`); the source is `./helpers.ts`.
|
|
49
|
+
const stems = [...new Set([base, base.replace(/\.[cm]?js$/, '')])];
|
|
50
|
+
for (const stem of stems)
|
|
51
|
+
for (const ext of EXTENSIONS) {
|
|
52
|
+
const text = await read(stem + ext);
|
|
53
|
+
if (text !== undefined)
|
|
54
|
+
return text;
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Functions defined in this source whose body calls expect or assert. */
|
|
59
|
+
function definedAsserting(source) {
|
|
60
|
+
const names = [];
|
|
61
|
+
const DEF = /function\s*\*?\s*([\w$]+)\s*[<(]|(?:const|let|var)\s+([\w$]+)\s*(?::[^=]+)?=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::[^=]+)?=>|[\w$]+\s*=>)/g;
|
|
62
|
+
for (const m of source.matchAll(DEF)) {
|
|
63
|
+
const from = m.index + m[0].length;
|
|
64
|
+
const body = m[1] || !m[0].endsWith('=>') ? blockAt(source, afterParams(source, m.index)) : arrowBodyAt(source, from);
|
|
65
|
+
if (ASSERTS.test(body))
|
|
66
|
+
names.push(m[1] ?? m[2]);
|
|
67
|
+
}
|
|
68
|
+
return names;
|
|
69
|
+
}
|
|
70
|
+
/** Just past a function's parameter list (which may hold `{ … }` types or defaults), from its `function` keyword. */
|
|
71
|
+
function afterParams(source, from) {
|
|
72
|
+
const open = source.indexOf('(', from);
|
|
73
|
+
if (open < 0)
|
|
74
|
+
return from;
|
|
75
|
+
let depth = 0;
|
|
76
|
+
for (let i = open; i < source.length; i++) {
|
|
77
|
+
if (source[i] === '(')
|
|
78
|
+
depth++;
|
|
79
|
+
else if (source[i] === ')' && --depth === 0)
|
|
80
|
+
return i + 1;
|
|
81
|
+
}
|
|
82
|
+
return source.length;
|
|
83
|
+
}
|
|
84
|
+
/** The first braced block from `from`, braces balanced. */
|
|
85
|
+
function blockAt(source, from) {
|
|
86
|
+
const open = source.indexOf('{', from);
|
|
87
|
+
if (open < 0)
|
|
88
|
+
return '';
|
|
89
|
+
let depth = 0;
|
|
90
|
+
for (let i = open; i < source.length; i++) {
|
|
91
|
+
if (source[i] === '{')
|
|
92
|
+
depth++;
|
|
93
|
+
else if (source[i] === '}' && --depth === 0)
|
|
94
|
+
return source.slice(open, i + 1);
|
|
95
|
+
}
|
|
96
|
+
return source.slice(open);
|
|
97
|
+
}
|
|
98
|
+
/** An arrow function's body: a block, else its expression up to the end of the statement. */
|
|
99
|
+
function arrowBodyAt(source, from) {
|
|
100
|
+
const rest = source.slice(from);
|
|
101
|
+
if (rest.trimStart().startsWith('{'))
|
|
102
|
+
return blockAt(source, from);
|
|
103
|
+
const end = rest.search(/;|\n\s*\n/);
|
|
104
|
+
return end < 0 ? rest : rest.slice(0, end);
|
|
105
|
+
}
|
|
106
|
+
const PY_ASSERTS = /\bassert\b|\bself\.assert\w*\s*\(|\braise\s+AssertionError\b|\bpytest\.(?:raises|fail)\b|\.assert_\w+\s*\(/;
|
|
107
|
+
const PY_FROM = /^\s*from\s+(\.*[\w.]*)\s+import\s+\(?([^)\n]*)/gm;
|
|
108
|
+
/** Python: names this test file can call that assert (see the file comment). */
|
|
109
|
+
export async function pythonAssertionHelpers(cwd, file, content, read) {
|
|
110
|
+
const helpers = new Set(pythonDefinedAsserting(content));
|
|
111
|
+
for (const m of content.matchAll(PY_FROM)) {
|
|
112
|
+
for (const part of m[2].split(',')) {
|
|
113
|
+
const [exported, local] = part.trim().split(/\s+as\s+/);
|
|
114
|
+
if (exported && HELPER_NAME_PY.test(exported))
|
|
115
|
+
helpers.add((local ?? exported).trim());
|
|
116
|
+
}
|
|
117
|
+
const source = await pythonModule(cwd, file, m[1], read);
|
|
118
|
+
for (const name of source ? pythonDefinedAsserting(source) : [])
|
|
119
|
+
helpers.add(name);
|
|
120
|
+
}
|
|
121
|
+
return helpers;
|
|
122
|
+
}
|
|
123
|
+
const HELPER_NAME_PY = /^(?:expect|assert|should)_?\w/;
|
|
124
|
+
async function pythonModule(cwd, file, module, read) {
|
|
125
|
+
const dots = module.match(/^\.*/)[0].length;
|
|
126
|
+
const parts = module.slice(dots).split('.').filter(Boolean);
|
|
127
|
+
let base = cwd;
|
|
128
|
+
if (dots) {
|
|
129
|
+
base = path.join(cwd, path.dirname(file));
|
|
130
|
+
for (let i = 1; i < dots; i++)
|
|
131
|
+
base = path.dirname(base);
|
|
132
|
+
}
|
|
133
|
+
const stem = path.join(base, ...parts);
|
|
134
|
+
return await read(`${stem}.py`) ?? await read(path.join(stem, '__init__.py'));
|
|
135
|
+
}
|
|
136
|
+
/** Python functions and methods in this source whose body asserts. */
|
|
137
|
+
function pythonDefinedAsserting(source) {
|
|
138
|
+
const names = [];
|
|
139
|
+
const lines = source.split('\n');
|
|
140
|
+
lines.forEach((line, i) => {
|
|
141
|
+
const def = line.match(/^(\s*)(?:async\s+)?def\s+(\w+)\s*\(/);
|
|
142
|
+
if (!def || def[2].startsWith('test_'))
|
|
143
|
+
return;
|
|
144
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
145
|
+
const next = lines[j];
|
|
146
|
+
if (next.trim() && next.search(/\S/) <= def[1].length && !/^\s*[)\]]/.test(next))
|
|
147
|
+
break;
|
|
148
|
+
if (PY_ASSERTS.test(next)) {
|
|
149
|
+
names.push(def[2]);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return names;
|
|
155
|
+
}
|
|
@@ -25,6 +25,7 @@ import { FileScanner } from '../utils/scanner.js';
|
|
|
25
25
|
import { Logger } from '../utils/logger.js';
|
|
26
26
|
import { languageAdapters } from './language-adapters/index.js';
|
|
27
27
|
import { checkGoTestQuality, checkJavaKotlinTestQuality } from './test-quality-lang.js';
|
|
28
|
+
import { assertionHelpers, callsHelper, pythonAssertionHelpers } from './test-quality-helpers.js';
|
|
28
29
|
import { JS_TEST_START_PATTERN, JS_ASSERTION_PATTERNS, JS_MOCK_PATTERNS, JS_TAUTOLOGICAL_PATTERNS, JS_VAR_TAUTOLOGY_PATTERN, PYTHON_TEST_FUNC_PATTERN, PYTHON_ASSERTION_PATTERNS, PYTHON_MOCK_PATTERNS, PYTHON_TAUTOLOGICAL_PATTERNS, PYTHON_FIXTURE_PATTERN, PYTHON_CONFTEST_NAME, } from './test-quality-matchers.js';
|
|
29
30
|
import fs from 'fs-extra';
|
|
30
31
|
import path from 'path';
|
|
@@ -66,6 +67,13 @@ export class TestQualityGate extends Gate {
|
|
|
66
67
|
'**/target/**', '**/.gradle/**', '**/out/**'],
|
|
67
68
|
});
|
|
68
69
|
Logger.info(`Test Quality: Scanning ${files.length} test files`);
|
|
70
|
+
// Helper modules several test files import are read once per run.
|
|
71
|
+
const modules = new Map();
|
|
72
|
+
const read = (abs) => {
|
|
73
|
+
if (!modules.has(abs))
|
|
74
|
+
modules.set(abs, Promise.resolve().then(() => fs.readFile(abs, 'utf-8')).catch(() => undefined));
|
|
75
|
+
return modules.get(abs);
|
|
76
|
+
};
|
|
69
77
|
const CONCURRENCY = 16;
|
|
70
78
|
for (let i = 0; i < files.length; i += CONCURRENCY) {
|
|
71
79
|
const batch = files.slice(i, i + CONCURRENCY);
|
|
@@ -79,10 +87,10 @@ export class TestQualityGate extends Gate {
|
|
|
79
87
|
const localIssues = [];
|
|
80
88
|
switch (adapter.id) {
|
|
81
89
|
case 'js':
|
|
82
|
-
this.checkJSTestQuality(content, file, localIssues);
|
|
90
|
+
this.checkJSTestQuality(content, file, localIssues, await assertionHelpers(context.cwd, file, content, read));
|
|
83
91
|
break;
|
|
84
92
|
case 'python':
|
|
85
|
-
this.checkPythonTestQuality(content, file, localIssues);
|
|
93
|
+
this.checkPythonTestQuality(content, file, localIssues, await pythonAssertionHelpers(context.cwd, file, content, read));
|
|
86
94
|
break;
|
|
87
95
|
case 'go':
|
|
88
96
|
checkGoTestQuality(content, file, localIssues, {
|
|
@@ -121,7 +129,7 @@ export class TestQualityGate extends Gate {
|
|
|
121
129
|
}
|
|
122
130
|
return failures;
|
|
123
131
|
}
|
|
124
|
-
checkJSTestQuality(content, file, issues) {
|
|
132
|
+
checkJSTestQuality(content, file, issues, helpers = new Set()) {
|
|
125
133
|
const lines = content.split('\n');
|
|
126
134
|
// Track test blocks for analysis
|
|
127
135
|
let inTestBlock = false;
|
|
@@ -170,7 +178,7 @@ export class TestQualityGate extends Gate {
|
|
|
170
178
|
braceDepth--;
|
|
171
179
|
}
|
|
172
180
|
// Check for assertions
|
|
173
|
-
if (JS_ASSERTION_PATTERNS.some(p => p.test(line))) {
|
|
181
|
+
if (JS_ASSERTION_PATTERNS.some(p => p.test(line)) || callsHelper(line, helpers)) {
|
|
174
182
|
hasAssertion = true;
|
|
175
183
|
}
|
|
176
184
|
// Check for mocks
|
|
@@ -244,7 +252,7 @@ export class TestQualityGate extends Gate {
|
|
|
244
252
|
});
|
|
245
253
|
}
|
|
246
254
|
}
|
|
247
|
-
checkPythonTestQuality(content, file, issues) {
|
|
255
|
+
checkPythonTestQuality(content, file, issues, helpers = new Set()) {
|
|
248
256
|
const lines = content.split('\n');
|
|
249
257
|
const basename = path.basename(file);
|
|
250
258
|
// Skip conftest.py entirely — these contain fixtures, not tests
|
|
@@ -309,7 +317,7 @@ export class TestQualityGate extends Gate {
|
|
|
309
317
|
}
|
|
310
318
|
testContent += line + '\n';
|
|
311
319
|
// Check for assertions
|
|
312
|
-
if (PYTHON_ASSERTION_PATTERNS.some(p => p.test(trimmed))) {
|
|
320
|
+
if (PYTHON_ASSERTION_PATTERNS.some(p => p.test(trimmed)) || callsHelper(trimmed, helpers, true)) {
|
|
313
321
|
hasAssertion = true;
|
|
314
322
|
}
|
|
315
323
|
// Check for mocks
|
package/dist/review/review.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface ReviewResult {
|
|
|
38
38
|
excludedOutsideChangedLines: number;
|
|
39
39
|
/** Findings the base already had: counted, not reported (baseline.ts). */
|
|
40
40
|
preexisting: number;
|
|
41
|
+
/** The same, by check. A check whose findings were all the base's reads PASS in the report's summary. */
|
|
42
|
+
preexistingByCheck: Record<string, number>;
|
|
41
43
|
/** A diff given with no base whose change HEAD already holds (committed work): nothing was compared, so nothing was dropped as the base's. */
|
|
42
44
|
baseUnknown?: boolean;
|
|
43
45
|
changedLines: Record<string, Set<number>>;
|
package/dist/review/review.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* and so does a proven gate that crashed (a heuristic one is only listed).
|
|
9
9
|
*/
|
|
10
10
|
import { spawnSync } from 'child_process';
|
|
11
|
-
import { GateRunner } from '../gates/runner.js';
|
|
11
|
+
import { GateRunner, gateOf } from '../gates/runner.js';
|
|
12
12
|
import { changedLinesByFile, parseDiff, removedByFile } from '../utils/diff.js';
|
|
13
13
|
import { normalizeScopePatterns } from '../utils/scope.js';
|
|
14
14
|
import { deepAnalysisError } from '../utils/deep-status.js';
|
|
@@ -39,14 +39,14 @@ export async function reviewChange(input) {
|
|
|
39
39
|
const changedLines = withoutGenerated(input.cwd, parseDiff(diff));
|
|
40
40
|
const targets = input.files?.length ? input.files : Object.keys(changedLines);
|
|
41
41
|
if (targets.length === 0) {
|
|
42
|
-
return { status: 'PASS', findings: [], fileFindings: [], contextFindings: [], advisory: [], muted: 0, dismissed: 0, dismissedByGate: {}, unlocated: 0, excludedOutsideChangedLines: 0, preexisting: 0, changedLines, report: null, gateErrors: [], controlFilesChanged: controlFiles(diff), hints: [], covered: [] };
|
|
42
|
+
return { status: 'PASS', findings: [], fileFindings: [], contextFindings: [], advisory: [], muted: 0, dismissed: 0, dismissedByGate: {}, unlocated: 0, excludedOutsideChangedLines: 0, preexisting: 0, preexistingByCheck: {}, changedLines, report: null, gateErrors: [], controlFilesChanged: controlFiles(diff), hints: [], covered: [] };
|
|
43
43
|
}
|
|
44
44
|
// The team's compiled checks run before the deep review, so it is told what they found and which lessons they covered.
|
|
45
45
|
const compiled = compiledChecksOn(input.cwd, changedLines, input.config);
|
|
46
46
|
const deep = input.deep ? { ...input.deep, focusLines: changedLinesByFile(changedLines), removedLines: removedByFile(diff), diff, settled: settledChecks(compiled.failures), covered: compiled.covered } : undefined;
|
|
47
47
|
// The team's `commands:` run at push (toolchain.ts), where a failure blocks; here they would only cost time.
|
|
48
48
|
const report = await new GateRunner({ ...input.config, commands: {} }).run(input.cwd, await normalizeScopePatterns(input.cwd, targets), deep);
|
|
49
|
-
const { preexisting, baseUnknown } = await dropPreexisting(input, report, targets);
|
|
49
|
+
const { preexisting, byCheck: preexistingByCheck = {}, baseUnknown } = await dropPreexisting(input, report, targets);
|
|
50
50
|
if (input.diffTests && deep)
|
|
51
51
|
report.failures.push(...await diffTestFailures(input.cwd, input.source, deep));
|
|
52
52
|
// The review's own checks, each recorded in the summary beside the gates, so a report says everything that ran.
|
|
@@ -96,6 +96,7 @@ export async function reviewChange(input) {
|
|
|
96
96
|
unlocated: split.unlocated,
|
|
97
97
|
excludedOutsideChangedLines: split.outside,
|
|
98
98
|
preexisting,
|
|
99
|
+
preexistingByCheck,
|
|
99
100
|
...(baseUnknown ? { baseUnknown } : {}),
|
|
100
101
|
changedLines,
|
|
101
102
|
report,
|
|
@@ -108,6 +109,21 @@ export async function reviewChange(input) {
|
|
|
108
109
|
...(checkedGoal ? { goal: checkedGoal } : {}),
|
|
109
110
|
};
|
|
110
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* A check that failed only on what the base already had passes for this change: its summary reads PASS, and the
|
|
114
|
+
* count, by check, says what it left out. A check with a finding the change introduced keeps FAIL.
|
|
115
|
+
*/
|
|
116
|
+
function passOnlyPreexisting(report, preexisting) {
|
|
117
|
+
const byCheck = {};
|
|
118
|
+
const check = (f) => gateOf(f) ?? f.id;
|
|
119
|
+
for (const f of preexisting)
|
|
120
|
+
byCheck[check(f)] = (byCheck[check(f)] ?? 0) + 1;
|
|
121
|
+
const remaining = new Set(report.failures.map(check));
|
|
122
|
+
for (const id of Object.keys(byCheck))
|
|
123
|
+
if (report.summary[id] === 'FAIL' && !remaining.has(id))
|
|
124
|
+
report.summary[id] = 'PASS';
|
|
125
|
+
return byCheck;
|
|
126
|
+
}
|
|
111
127
|
/**
|
|
112
128
|
* Drop the rules' findings the base already had; returns how many. Model findings stay: the model reviews only the change.
|
|
113
129
|
* A diff given with no `--base` is compared with HEAD, which is right for uncommitted work. When HEAD already holds the
|
|
@@ -127,7 +143,7 @@ async function dropPreexisting(input, report, targets) {
|
|
|
127
143
|
const { preexisting } = splitIntroduced(rules, await baseFindings(input.cwd, input.config, commit, targets));
|
|
128
144
|
const old = new Set(preexisting);
|
|
129
145
|
report.failures = report.failures.filter(f => !old.has(f));
|
|
130
|
-
return { preexisting: old.size };
|
|
146
|
+
return { preexisting: old.size, byCheck: passOnlyPreexisting(report, preexisting) };
|
|
131
147
|
}
|
|
132
148
|
catch {
|
|
133
149
|
return { preexisting: 0 }; // the comparison is a courtesy; it never fails a review
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.12.2-rc.
|
|
3
|
+
"version": "6.12.2-rc.6",
|
|
4
4
|
"description": "Rigour's review engine: deterministic gates on changed lines, rules and lessons learned from your team's fixes, and per-check precision from what you fix versus dismiss, across TypeScript, JavaScript, Python, Go, Ruby and C#.",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22.13"
|
|
@@ -75,11 +75,11 @@
|
|
|
75
75
|
"@anthropic-ai/sdk": "^0.132.1",
|
|
76
76
|
"pg": "^8.16.3",
|
|
77
77
|
"openai": "^5.23.2",
|
|
78
|
-
"@rigour-labs/brain-darwin-arm64": "6.12.2-rc.
|
|
79
|
-
"@rigour-labs/brain-
|
|
80
|
-
"@rigour-labs/brain-linux-arm64": "6.12.2-rc.
|
|
81
|
-
"@rigour-labs/brain-
|
|
82
|
-
"@rigour-labs/brain-
|
|
78
|
+
"@rigour-labs/brain-darwin-arm64": "6.12.2-rc.6",
|
|
79
|
+
"@rigour-labs/brain-darwin-x64": "6.12.2-rc.6",
|
|
80
|
+
"@rigour-labs/brain-linux-arm64": "6.12.2-rc.6",
|
|
81
|
+
"@rigour-labs/brain-linux-x64": "6.12.2-rc.6",
|
|
82
|
+
"@rigour-labs/brain-win-x64": "6.12.2-rc.6"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/fs-extra": "^11.0.4",
|