@rigour-labs/core 6.6.5 → 6.7.0
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 +16 -92
- package/dist/environment.test.js +0 -4
- package/dist/gates/agent-team.d.ts +0 -2
- package/dist/gates/agent-team.js +0 -8
- package/dist/gates/checkpoint.d.ts +0 -1
- package/dist/gates/checkpoint.js +0 -1
- package/dist/gates/context.d.ts +0 -4
- package/dist/gates/context.js +7 -17
- package/dist/gates/deep-analysis.d.ts +0 -1
- package/dist/gates/deep-runner.js +0 -1
- package/dist/gates/frontend-secret-exposure.d.ts +1 -1
- package/dist/gates/frontend-secret-exposure.js +9 -3
- package/dist/gates/frontend-secret-exposure.test.js +6 -3
- package/dist/gates/hallucinated-imports/index.d.ts +0 -2
- package/dist/gates/hallucinated-imports/index.js +2 -2
- package/dist/gates/hallucinated-imports/js-resolver.js +1 -0
- package/dist/gates/hallucinated-imports.test.js +10 -0
- package/dist/gates/runner.js +3 -4
- package/dist/gates/test-quality-matchers.d.ts +0 -4
- package/dist/gates/test-quality-matchers.js +0 -7
- package/dist/gates/test-quality.d.ts +0 -2
- package/dist/gates/test-quality.js +1 -10
- package/dist/hooks/checker.js +1 -1
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/index.js +1 -2
- package/dist/hooks/stop-review.d.ts +6 -9
- package/dist/hooks/stop-review.js +5 -10
- package/dist/hooks/types.d.ts +0 -14
- package/dist/hooks/types.js +1 -15
- package/dist/index.d.ts +11 -3
- package/dist/index.js +11 -3
- package/dist/inference/cloud-provider.d.ts +2 -0
- package/dist/inference/cloud-provider.js +16 -6
- package/dist/inference/cloud-provider.test.js +21 -0
- package/dist/pattern-index/embeddings.d.ts +0 -5
- package/dist/pattern-index/embeddings.js +11 -4
- package/dist/pattern-index/semantic-runtime.d.ts +10 -0
- package/dist/pattern-index/semantic-runtime.js +46 -0
- package/dist/pattern-index/semantic-runtime.test.d.ts +1 -0
- package/dist/pattern-index/semantic-runtime.test.js +31 -0
- package/dist/review/backtest-init.d.ts +8 -0
- package/dist/review/backtest-init.js +87 -0
- package/dist/review/backtest-init.test.d.ts +1 -0
- package/dist/review/backtest-init.test.js +74 -0
- package/dist/review/backtest-judges.d.ts +38 -0
- package/dist/review/backtest-judges.js +63 -0
- package/dist/review/backtest-judges.test.d.ts +1 -0
- package/dist/review/backtest-judges.test.js +32 -0
- package/dist/review/backtest.d.ts +320 -0
- package/dist/review/backtest.js +207 -0
- package/dist/review/backtest.test.d.ts +1 -0
- package/dist/review/backtest.test.js +129 -0
- package/dist/review/baseline.js +2 -1
- package/dist/review/dead-code.test.js +15 -0
- package/dist/review/fix-scope.d.ts +21 -0
- package/dist/review/fix-scope.js +94 -0
- package/dist/review/fix-scope.test.d.ts +1 -0
- package/dist/review/fix-scope.test.js +104 -0
- package/dist/review/lint-overlay.d.ts +21 -0
- package/dist/review/lint-overlay.js +89 -0
- package/dist/review/orphan-files.js +3 -2
- package/dist/review/partial-wiring.js +5 -3
- package/dist/review/query-patterns.js +3 -2
- package/dist/review/quiet.d.ts +6 -0
- package/dist/review/quiet.js +12 -2
- package/dist/review/quiet.test.js +11 -0
- package/dist/review/review.d.ts +6 -0
- package/dist/review/review.js +10 -2
- package/dist/review/reviewer/adapters.d.ts +51 -0
- package/dist/review/reviewer/adapters.js +181 -0
- package/dist/review/reviewer/adapters.test.d.ts +1 -0
- package/dist/review/reviewer/adapters.test.js +23 -0
- package/dist/review/reviewer/background.d.ts +41 -0
- package/dist/review/reviewer/background.js +153 -0
- package/dist/review/reviewer/background.test.d.ts +1 -0
- package/dist/review/reviewer/background.test.js +118 -0
- package/dist/review/reviewer/consensus.d.ts +36 -0
- package/dist/review/reviewer/consensus.js +144 -0
- package/dist/review/reviewer/context.d.ts +75 -0
- package/dist/review/reviewer/context.js +140 -0
- package/dist/review/reviewer/context.test.d.ts +1 -0
- package/dist/review/reviewer/context.test.js +18 -0
- package/dist/review/reviewer/exec.d.ts +17 -0
- package/dist/review/reviewer/exec.js +27 -0
- package/dist/review/reviewer/exec.test.d.ts +1 -0
- package/dist/review/reviewer/exec.test.js +23 -0
- package/dist/review/reviewer/inputs.d.ts +39 -0
- package/dist/review/reviewer/inputs.js +107 -0
- package/dist/review/reviewer/merge-impact.d.ts +2 -0
- package/dist/review/reviewer/merge-impact.js +64 -0
- package/dist/review/reviewer/panel.d.ts +67 -0
- package/dist/review/reviewer/panel.js +101 -0
- package/dist/review/reviewer/panel.test.d.ts +1 -0
- package/dist/review/reviewer/panel.test.js +110 -0
- package/dist/review/reviewer/prompt.d.ts +32 -0
- package/dist/review/reviewer/prompt.js +145 -0
- package/dist/review/reviewer/settings.d.ts +58 -0
- package/dist/review/reviewer/settings.js +152 -0
- package/dist/review/reviewer/settings.test.d.ts +1 -0
- package/dist/review/reviewer/settings.test.js +57 -0
- package/dist/review/reviewer/store.d.ts +49 -0
- package/dist/review/reviewer/store.js +99 -0
- package/dist/review/reviewer/usage.d.ts +2 -0
- package/dist/review/reviewer/usage.js +28 -0
- package/dist/review/reviewer/usage.test.d.ts +1 -0
- package/dist/review/reviewer/usage.test.js +14 -0
- package/dist/review/reviewer/verdict.d.ts +141 -0
- package/dist/review/reviewer/verdict.js +166 -0
- package/dist/review/reviewer.d.ts +84 -49
- package/dist/review/reviewer.js +364 -195
- package/dist/review/reviewer.test.js +316 -50
- package/dist/review/test-files.d.ts +5 -1
- package/dist/review/test-files.js +6 -2
- package/dist/review/toolchain.d.ts +5 -2
- package/dist/review/toolchain.js +89 -7
- package/dist/review/toolchain.test.js +45 -4
- package/dist/review/typed/program.d.ts +26 -0
- package/dist/review/typed/program.js +90 -0
- package/dist/review/typed/redundancy.d.ts +10 -0
- package/dist/review/typed/redundancy.js +378 -0
- package/dist/review/typed/redundancy.test.d.ts +1 -0
- package/dist/review/typed/redundancy.test.js +179 -0
- package/dist/review/typed/schema-nullability.d.ts +6 -0
- package/dist/review/typed/schema-nullability.js +218 -0
- package/dist/review/typed/schema-nullability.test.d.ts +1 -0
- package/dist/review/typed/schema-nullability.test.js +63 -0
- package/dist/review/unused-exports.js +10 -7
- package/dist/semantic/learn/types.d.ts +2 -2
- package/dist/services/adaptive-thresholds.d.ts +2 -52
- package/dist/services/adaptive-thresholds.js +2 -147
- package/dist/services/adaptive-thresholds.test.js +1 -81
- package/dist/settings.d.ts +14 -6
- package/dist/settings.js +0 -12
- package/dist/storage/team-diagnostics.js +1 -1
- package/dist/storage/team-schema.d.ts +1 -1
- package/dist/storage/team-schema.js +2 -0
- package/dist/storage/team-store.js +26 -3
- package/dist/storage/team-sync-scope.test.js +35 -2
- package/dist/telemetry/telemetry.d.ts +5 -0
- package/dist/telemetry/telemetry.js +11 -1
- package/dist/templates/paradigms.js +0 -8
- package/dist/templates/presets.d.ts +0 -1
- package/dist/templates/presets.js +0 -23
- package/dist/templates/universal-config.js +16 -46
- package/dist/types/index.d.ts +202 -574
- package/dist/types/index.js +64 -67
- package/dist/types/index.test.d.ts +1 -0
- package/dist/types/index.test.js +33 -0
- package/dist/utils/command-line.d.ts +8 -0
- package/dist/utils/command-line.js +8 -0
- package/dist/utils/command-line.test.d.ts +1 -0
- package/dist/utils/command-line.test.js +8 -0
- package/dist/utils/profile.js +1 -1
- package/package.json +6 -7
package/dist/review/toolchain.js
CHANGED
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
* The repository's own tools, run on what a change touched: the formatter, the linter, the type
|
|
3
3
|
* checker and the tests that import changed files. Teams already trust these; Rigour runs them at
|
|
4
4
|
* the moment that matters (before a push) so nobody has to remember. Only tools the project
|
|
5
|
-
* installed are run (node_modules/.bin, never a download)
|
|
6
|
-
*
|
|
5
|
+
* installed are run (node_modules/.bin, never a download). A tool the project never declared is
|
|
6
|
+
* skipped and said so; one declared in package.json but not installed FAILS, since a checkout
|
|
7
|
+
* without its dependencies cannot prove anything about the change. A tool the team configured
|
|
8
|
+
* under `commands:` is run as that command instead, as written (whole project, not per file).
|
|
7
9
|
*/
|
|
8
10
|
import { execa } from 'execa';
|
|
9
11
|
import fs from 'fs';
|
|
10
12
|
import path from 'path';
|
|
13
|
+
import { splitCommand } from '../utils/command-line.js';
|
|
11
14
|
import { installedBin } from '../utils/installed-bin.js';
|
|
15
|
+
import { onChangedLines, overlayUnavailable, writeOverlayConfig } from './lint-overlay.js';
|
|
12
16
|
import { ownOutputs } from './unused-exports.js';
|
|
13
17
|
const CODE = /\.(ts|tsx|js|jsx|mjs|cjs|svelte)$/;
|
|
14
18
|
const FORMATTED = /\.(ts|tsx|js|jsx|mjs|cjs|svelte|json|css|md|ya?ml|html)$/;
|
|
@@ -16,25 +20,90 @@ const FORMATTED = /\.(ts|tsx|js|jsx|mjs|cjs|svelte|json|css|md|ya?ml|html)$/;
|
|
|
16
20
|
const BATCH = 150;
|
|
17
21
|
const TIMEOUT_MS = 10 * 60_000;
|
|
18
22
|
const OUTPUT_LINES = 30;
|
|
19
|
-
|
|
23
|
+
/** `changedLines`: the lines the change touched, for the tools that report per line (the overlay blocks on those only). */
|
|
24
|
+
export async function runToolchain(cwd, changedFiles, config, changedLines = {}) {
|
|
20
25
|
const own = ownOutputs(config);
|
|
21
26
|
const files = changedFiles.filter(file => !own.some(o => file === o || file.startsWith(`${o}/`)) && fs.existsSync(path.join(cwd, file)));
|
|
22
27
|
const code = files.filter(file => CODE.test(file));
|
|
23
28
|
const configured = config.commands ?? {};
|
|
24
29
|
const results = [];
|
|
25
30
|
const run = async (tool, step) => {
|
|
26
|
-
|
|
31
|
+
const command = configured[tool];
|
|
32
|
+
if (!command)
|
|
33
|
+
results.push(await step());
|
|
34
|
+
else {
|
|
35
|
+
const { bin, args } = splitCommand(command);
|
|
36
|
+
results.push(await once(tool, cwd, bin, args));
|
|
37
|
+
}
|
|
27
38
|
};
|
|
28
39
|
await run('format', () => formatCheck(cwd, files.filter(file => FORMATTED.test(file))));
|
|
29
40
|
await run('lint', () => lint(cwd, code));
|
|
41
|
+
await run('overlay', () => lintOverlay(cwd, code, changedLines));
|
|
30
42
|
await run('typecheck', () => typecheck(cwd));
|
|
31
43
|
await run('test', () => relatedTests(cwd, code));
|
|
44
|
+
await run('knip', () => knip(cwd, files));
|
|
32
45
|
return results;
|
|
33
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Type-checked rules the repository does not enable (lint-overlay.ts), on changed lines only. The
|
|
49
|
+
* repository's own eslint and typescript-eslint run it; without them it is skipped and said so.
|
|
50
|
+
*/
|
|
51
|
+
async function lintOverlay(cwd, files, changedLines) {
|
|
52
|
+
const bin = installedBin(cwd, cwd, 'eslint');
|
|
53
|
+
if (!bin)
|
|
54
|
+
return skipped('overlay', 'eslint is not installed');
|
|
55
|
+
const unavailable = overlayUnavailable(cwd);
|
|
56
|
+
if (unavailable)
|
|
57
|
+
return skipped('overlay', unavailable);
|
|
58
|
+
const lintable = files.filter(file => /\.(ts|mts|cts|js|mjs|svelte)$/.test(file));
|
|
59
|
+
if (lintable.length === 0)
|
|
60
|
+
return { tool: 'overlay', status: 'pass', command: 'no lintable changed files' };
|
|
61
|
+
const configFile = writeOverlayConfig(cwd);
|
|
62
|
+
try {
|
|
63
|
+
const command = `eslint --config <typed overlay> --format json (${lintable.length} files)`;
|
|
64
|
+
const result = await execa(bin, ['--config', configFile, '--no-warn-ignored', '--format', 'json', ...lintable], { cwd, reject: false, timeout: TIMEOUT_MS, env: { ...process.env, CI: '1', FORCE_COLOR: '0' } });
|
|
65
|
+
const report = onChangedLines(String(result.stdout ?? ''), cwd, changedLines);
|
|
66
|
+
if ('error' in report)
|
|
67
|
+
return { tool: 'overlay', status: 'fail', command, output: `${report.error}\n${String(result.stderr ?? '').trim().split('\n').slice(-OUTPUT_LINES).join('\n')}` };
|
|
68
|
+
const lines = report.blocking.map(m => `${m.file}:${m.line}:${m.column} ${m.rule}: ${m.message}`);
|
|
69
|
+
const summary = `${command}: ${lines.length} on changed lines, ${report.preexisting} pre-existing (not blocking)`;
|
|
70
|
+
return lines.length ? { tool: 'overlay', status: 'fail', command: summary, output: lines.join('\n'), lines } : { tool: 'overlay', status: 'pass', command: summary };
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
fs.rmSync(path.dirname(configFile), { recursive: true, force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* knip, when the repository installs it, with the repository's own config: unused files and
|
|
78
|
+
* exports among the files the change touched (a pre-existing dead export elsewhere is backlog).
|
|
79
|
+
*/
|
|
80
|
+
async function knip(cwd, files) {
|
|
81
|
+
const bin = installedBin(cwd, cwd, 'knip');
|
|
82
|
+
if (!bin)
|
|
83
|
+
return notInstalled('knip', cwd, 'knip');
|
|
84
|
+
if (files.length === 0)
|
|
85
|
+
return { tool: 'knip', status: 'pass', command: 'no changed files' };
|
|
86
|
+
const command = 'knip --production --reporter json';
|
|
87
|
+
const result = await execa(bin, ['--production', '--no-progress', '--reporter', 'json'], { cwd, reject: false, timeout: TIMEOUT_MS, env: { ...process.env, CI: '1', FORCE_COLOR: '0' } });
|
|
88
|
+
let report;
|
|
89
|
+
try {
|
|
90
|
+
report = JSON.parse(String(result.stdout ?? ''));
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return { tool: 'knip', status: 'fail', command, output: `knip printed no JSON (exit ${result.exitCode})\n${String(result.stderr ?? '').trim().split('\n').slice(-OUTPUT_LINES).join('\n')}` };
|
|
94
|
+
}
|
|
95
|
+
const changed = new Set(files);
|
|
96
|
+
const normalize = (file) => path.isAbsolute(file) ? path.relative(cwd, file).split(path.sep).join('/') : file;
|
|
97
|
+
const lines = [
|
|
98
|
+
...(report.files ?? []).map(normalize).filter(file => changed.has(file)).map(file => `${file}: nothing imports or runs this file`),
|
|
99
|
+
...(report.issues ?? []).filter(issue => changed.has(normalize(issue.file))).flatMap(issue => [...(issue.exports ?? []), ...(issue.types ?? [])].map(e => `${normalize(issue.file)}${e.line ? `:${e.line}` : ''}: unused export ${e.name}`)),
|
|
100
|
+
];
|
|
101
|
+
return lines.length ? { tool: 'knip', status: 'fail', command: `${command}: ${lines.length} in changed files`, output: lines.join('\n'), lines } : { tool: 'knip', status: 'pass', command };
|
|
102
|
+
}
|
|
34
103
|
async function formatCheck(cwd, files) {
|
|
35
104
|
const bin = installedBin(cwd, cwd, 'prettier');
|
|
36
105
|
if (!bin)
|
|
37
|
-
return
|
|
106
|
+
return notInstalled('format', cwd, 'prettier');
|
|
38
107
|
if (files.length === 0)
|
|
39
108
|
return { tool: 'format', status: 'pass', command: 'no files to format' };
|
|
40
109
|
return batched('format', cwd, bin, ['--check', '--ignore-unknown'], files);
|
|
@@ -42,7 +111,7 @@ async function formatCheck(cwd, files) {
|
|
|
42
111
|
async function lint(cwd, files) {
|
|
43
112
|
const bin = installedBin(cwd, cwd, 'eslint');
|
|
44
113
|
if (!bin)
|
|
45
|
-
return
|
|
114
|
+
return notInstalled('lint', cwd, 'eslint');
|
|
46
115
|
if (files.length === 0)
|
|
47
116
|
return { tool: 'lint', status: 'pass', command: 'no code files' };
|
|
48
117
|
// ESLint 9 warns about an explicitly named ignored file, which --max-warnings=0 would count.
|
|
@@ -71,8 +140,21 @@ async function typecheck(cwd) {
|
|
|
71
140
|
const tsc = fs.existsSync(path.join(cwd, 'tsconfig.json')) && installedBin(cwd, cwd, 'tsc');
|
|
72
141
|
if (tsc)
|
|
73
142
|
return once('typecheck', cwd, tsc, ['--noEmit']);
|
|
143
|
+
for (const pkg of ['svelte-check', 'typescript'])
|
|
144
|
+
if (declared(cwd, pkg))
|
|
145
|
+
return notInstalled('typecheck', cwd, pkg);
|
|
74
146
|
return skipped('typecheck', 'no typecheck/check script, svelte-check or tsc with a tsconfig.json');
|
|
75
147
|
}
|
|
148
|
+
function declared(cwd, pkg) {
|
|
149
|
+
const manifest = readPackage(cwd);
|
|
150
|
+
return !!(manifest.dependencies?.[pkg] ?? manifest.devDependencies?.[pkg]);
|
|
151
|
+
}
|
|
152
|
+
/** Declared in package.json but absent from node_modules: a checkout that cannot prove anything fails; a tool never declared is skipped. */
|
|
153
|
+
function notInstalled(tool, cwd, pkg) {
|
|
154
|
+
return declared(cwd, pkg)
|
|
155
|
+
? { tool, status: 'fail', command: `${pkg} is in package.json but not installed`, output: `node_modules has no ${pkg}: install the dependencies (npm install, pnpm install, …) and run again` }
|
|
156
|
+
: skipped(tool, `${pkg} is not installed`);
|
|
157
|
+
}
|
|
76
158
|
function readPackage(cwd) {
|
|
77
159
|
try {
|
|
78
160
|
return JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
@@ -85,7 +167,7 @@ function readPackage(cwd) {
|
|
|
85
167
|
async function relatedTests(cwd, files) {
|
|
86
168
|
const bin = installedBin(cwd, cwd, 'vitest');
|
|
87
169
|
if (!bin)
|
|
88
|
-
return
|
|
170
|
+
return notInstalled('test', cwd, 'vitest');
|
|
89
171
|
if (files.length === 0)
|
|
90
172
|
return { tool: 'test', status: 'pass', command: 'no code files' };
|
|
91
173
|
const first = await once('test', cwd, bin, ['related', '--run', '--passWithNoTests', ...files]);
|
|
@@ -25,7 +25,45 @@ afterEach(() => { fs.rmSync(dir, { recursive: true, force: true }); });
|
|
|
25
25
|
describe('runToolchain', () => {
|
|
26
26
|
it('reports every tool the project did not install as skipped, never downloading one', async () => {
|
|
27
27
|
const results = await runToolchain(dir, ['src/a.ts'], config());
|
|
28
|
-
expect(results.map(r => [r.tool, r.status])).toEqual([['format', 'skipped'], ['lint', 'skipped'], ['typecheck', 'skipped'], ['test', 'skipped']]);
|
|
28
|
+
expect(results.map(r => [r.tool, r.status])).toEqual([['format', 'skipped'], ['lint', 'skipped'], ['overlay', 'skipped'], ['typecheck', 'skipped'], ['test', 'skipped'], ['knip', 'skipped']]);
|
|
29
|
+
});
|
|
30
|
+
it('fails, not skips, a tool the project declares but has not installed: a checkout without its dependencies proves nothing', async () => {
|
|
31
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ devDependencies: { eslint: '^9', typescript: '^5' } }));
|
|
32
|
+
const results = await runToolchain(dir, ['src/a.ts'], config());
|
|
33
|
+
expect(results.map(r => [r.tool, r.status])).toEqual([['format', 'skipped'], ['lint', 'fail'], ['overlay', 'skipped'], ['typecheck', 'fail'], ['test', 'skipped'], ['knip', 'skipped']]);
|
|
34
|
+
expect(results.find(r => r.tool === 'lint')).toMatchObject({ command: 'eslint is in package.json but not installed', output: expect.stringContaining('install the dependencies') });
|
|
35
|
+
});
|
|
36
|
+
it('runs type-checked rules the project does not enable, blocking on changed lines only, with the project\'s own eslint and config', async () => {
|
|
37
|
+
fs.writeFileSync(path.join(dir, 'eslint.config.js'), 'export default [];\n');
|
|
38
|
+
fs.mkdirSync(path.join(dir, 'node_modules/typescript-eslint'), { recursive: true });
|
|
39
|
+
fs.writeFileSync(path.join(dir, 'node_modules/typescript-eslint/package.json'), '{"name":"typescript-eslint","main":"index.js"}');
|
|
40
|
+
fs.writeFileSync(path.join(dir, 'node_modules/typescript-eslint/index.js'), 'module.exports = {};');
|
|
41
|
+
const report = JSON.stringify([{ filePath: path.join(dir, 'src/a.ts'), messages: [
|
|
42
|
+
{ line: 2, column: 5, ruleId: '@typescript-eslint/no-unnecessary-condition', message: 'Unnecessary conditional, value is always truthy.' },
|
|
43
|
+
{ line: 9, column: 1, ruleId: '@typescript-eslint/no-floating-promises', message: 'Promises must be awaited.' },
|
|
44
|
+
] }]);
|
|
45
|
+
tool('eslint', `if (args[0] === '--version') { console.log('v9.0.0'); process.exit(0); }\nif (args.includes('--format')) { console.log(${JSON.stringify(report)}); process.exit(1); }\nprocess.exit(0);`);
|
|
46
|
+
const results = await runToolchain(dir, ['src/a.ts'], config(), { 'src/a.ts': new Set([1, 2, 3]) });
|
|
47
|
+
const overlay = results.find(r => r.tool === 'overlay');
|
|
48
|
+
expect(overlay).toMatchObject({ status: 'fail', lines: ['src/a.ts:2:5 @typescript-eslint/no-unnecessary-condition: Unnecessary conditional, value is always truthy.'] });
|
|
49
|
+
expect(overlay.command).toContain('1 on changed lines, 1 pre-existing (not blocking)');
|
|
50
|
+
const call = calls().split('\n').find(line => line.includes('--format json'));
|
|
51
|
+
expect(call).toMatch(/eslint --config \S+eslint\.overlay\.config\.mjs --no-warn-ignored --format json src\/a\.ts/);
|
|
52
|
+
expect(fs.readdirSync(dir)).not.toContain('eslint.overlay.config.mjs'); // never written into the repository
|
|
53
|
+
// Nothing on a changed line: pass, backlog counted.
|
|
54
|
+
const clean = await runToolchain(dir, ['src/a.ts'], config(), { 'src/a.ts': new Set([7]) });
|
|
55
|
+
expect(clean.find(r => r.tool === 'overlay')).toMatchObject({ status: 'pass', command: expect.stringContaining('0 on changed lines, 2 pre-existing') });
|
|
56
|
+
});
|
|
57
|
+
it('runs knip when the project installs it, and reports only what is in the changed files', async () => {
|
|
58
|
+
const report = JSON.stringify({ files: [path.join(dir, 'src/dead.ts'), 'src/elsewhere.ts'], issues: [
|
|
59
|
+
{ file: 'src/a.ts', exports: [{ name: 'spare', line: 3, col: 1 }], types: [{ name: 'Spare', line: 4, col: 1 }] },
|
|
60
|
+
{ file: 'src/other.ts', exports: [{ name: 'old', line: 1, col: 1 }], types: [] },
|
|
61
|
+
] });
|
|
62
|
+
tool('knip', `console.log(${JSON.stringify(report)}); process.exit(1);`);
|
|
63
|
+
fs.writeFileSync(path.join(dir, 'src/dead.ts'), 'export const dead = 1;\n');
|
|
64
|
+
const results = await runToolchain(dir, ['src/a.ts', 'src/dead.ts'], config());
|
|
65
|
+
expect(results.find(r => r.tool === 'knip')).toMatchObject({ status: 'fail', lines: ['src/dead.ts: nothing imports or runs this file', 'src/a.ts:3: unused export spare', 'src/a.ts:4: unused export Spare'] });
|
|
66
|
+
expect(calls()).toContain('knip --production --no-progress --reporter json');
|
|
29
67
|
});
|
|
30
68
|
it('runs the formatter on changed files only and reports what it said', async () => {
|
|
31
69
|
tool('prettier', "const bad = args.filter(a => a.endsWith('.ts') && fs.readFileSync(a, 'utf8').includes('BAD'));\nif (bad.length) { console.log('[warn] ' + bad.join(' ')); process.exit(1); }");
|
|
@@ -35,12 +73,15 @@ describe('runToolchain', () => {
|
|
|
35
73
|
expect(calls()).toContain('prettier --check --ignore-unknown src/a.ts src/bad.ts');
|
|
36
74
|
expect(calls()).not.toContain('gone.ts');
|
|
37
75
|
});
|
|
38
|
-
it('quiets ESLint 9 about ignored files, and
|
|
76
|
+
it('quiets ESLint 9 about ignored files, and runs a tool set under commands: as that command, whose failure fails the push', async () => {
|
|
39
77
|
tool('eslint', "if (args[0] === '--version') { console.log('v9.1.0'); process.exit(0); }");
|
|
40
|
-
|
|
78
|
+
tool('prettier', 'process.exit(0);');
|
|
79
|
+
const results = await runToolchain(dir, ['src/a.ts'], config({ format: 'node -e "process.exit(4)"', test: 'node -e "process.exit(0)"' }));
|
|
41
80
|
expect(results.find(r => r.tool === 'lint')).toMatchObject({ status: 'pass' });
|
|
42
81
|
expect(calls()).toContain('eslint --max-warnings=0 --no-warn-ignored src/a.ts');
|
|
43
|
-
expect(results.find(r => r.tool === 'format')).toMatchObject({ status: '
|
|
82
|
+
expect(results.find(r => r.tool === 'format')).toMatchObject({ status: 'fail', command: expect.stringContaining('node -e') });
|
|
83
|
+
expect(calls()).not.toContain('prettier'); // the team's command replaces the detected tool
|
|
84
|
+
expect(results.find(r => r.tool === 'test')).toMatchObject({ status: 'pass' });
|
|
44
85
|
});
|
|
45
86
|
it("uses the project's own check script for types, and never hands a tool Rigour's own files", async () => {
|
|
46
87
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ scripts: { check: 'node -e "process.exit(3)"' } }));
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type * as TS from 'typescript';
|
|
2
|
+
export interface TextFile {
|
|
3
|
+
file: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export interface TypedProgram {
|
|
7
|
+
ts: typeof TS;
|
|
8
|
+
program: TS.Program;
|
|
9
|
+
checker: TS.TypeChecker;
|
|
10
|
+
root: string;
|
|
11
|
+
/** A source file's path relative to the root, posix. */
|
|
12
|
+
rel(sf: TS.SourceFile): string;
|
|
13
|
+
/** The sources (outside node_modules, no declaration files) that are not tests: where a host or a reader counts. */
|
|
14
|
+
appSources: TS.SourceFile[];
|
|
15
|
+
/** Files the program does not type, as text. */
|
|
16
|
+
textFiles: TextFile[];
|
|
17
|
+
inProgram: Set<string>;
|
|
18
|
+
}
|
|
19
|
+
/** The program, `skip` when this is not a TypeScript project, or `error` when it is one that cannot be loaded. */
|
|
20
|
+
export declare function loadProgram(root: string): {
|
|
21
|
+
program: TypedProgram;
|
|
22
|
+
} | {
|
|
23
|
+
skip: string;
|
|
24
|
+
} | {
|
|
25
|
+
error: string;
|
|
26
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The project's TypeScript program, built once per review from its own tsconfig.json and its own
|
|
3
|
+
* `typescript` (the compiler version and options then match what the project type-checks with;
|
|
4
|
+
* Rigour's bundled compiler is the fallback). What the type checker knows (which literal is typed
|
|
5
|
+
* as T, which property is read where) is what the typed checks reason from. Files outside the
|
|
6
|
+
* program (.svelte, a package under its own tsconfig) are kept as text and scanned by name only,
|
|
7
|
+
* which can suppress a finding and never adds one.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'child_process';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import { createRequire } from 'module';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import ownTs from 'typescript';
|
|
14
|
+
import { installedBin } from '../../utils/installed-bin.js';
|
|
15
|
+
import { isTestFile } from '../test-files.js';
|
|
16
|
+
const GIT_TIMEOUT_MS = 10_000;
|
|
17
|
+
/** The program, `skip` when this is not a TypeScript project, or `error` when it is one that cannot be loaded. */
|
|
18
|
+
export function loadProgram(root) {
|
|
19
|
+
const tsconfig = path.join(root, 'tsconfig.json');
|
|
20
|
+
if (!fs.existsSync(tsconfig))
|
|
21
|
+
return { skip: 'no tsconfig.json' };
|
|
22
|
+
const ts = projectTypescript(root);
|
|
23
|
+
let parsed = parseConfig(ts, tsconfig);
|
|
24
|
+
if ('error' in parsed && /\.svelte-kit\//.test(parsed.error) && syncSvelteKit(root))
|
|
25
|
+
parsed = parseConfig(ts, tsconfig);
|
|
26
|
+
if ('error' in parsed)
|
|
27
|
+
return { error: `tsconfig.json cannot be loaded: ${parsed.error}` };
|
|
28
|
+
const program = ts.createProgram({ rootNames: parsed.config.fileNames, options: { ...parsed.config.options, noEmit: true } });
|
|
29
|
+
const rel = (sf) => path.relative(root, sf.fileName).split(path.sep).join('/');
|
|
30
|
+
const sources = program.getSourceFiles().filter(sf => !sf.isDeclarationFile && !sf.fileName.includes('/node_modules/'));
|
|
31
|
+
const inProgram = new Set(sources.map(rel));
|
|
32
|
+
return {
|
|
33
|
+
program: {
|
|
34
|
+
ts, program, checker: program.getTypeChecker(), root, rel,
|
|
35
|
+
appSources: sources.filter(sf => !isTestFile(rel(sf))),
|
|
36
|
+
textFiles: textFiles(root, inProgram),
|
|
37
|
+
inProgram,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function projectTypescript(root) {
|
|
42
|
+
try {
|
|
43
|
+
return createRequire(path.join(root, 'package.json'))('typescript');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return ownTs;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function parseConfig(ts, tsconfig) {
|
|
50
|
+
try {
|
|
51
|
+
const config = ts.getParsedCommandLineOfConfigFile(tsconfig, {}, {
|
|
52
|
+
...ts.sys,
|
|
53
|
+
onUnRecoverableConfigFileDiagnostic: diagnostic => {
|
|
54
|
+
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'));
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
if (!config)
|
|
58
|
+
return { error: 'unreadable' };
|
|
59
|
+
// A missing `extends` target or an unknown option is a diagnostic, not an exception; tsc would fail on it too.
|
|
60
|
+
const errors = config.errors.filter(d => d.category === ts.DiagnosticCategory.Error).map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n'));
|
|
61
|
+
return errors.length ? { error: errors.join('; ') } : { config };
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return { error: String(error.message ?? error) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** A SvelteKit tsconfig extends a generated one; `svelte-kit sync` writes it (never downloaded: the project's own binary or nothing). */
|
|
68
|
+
function syncSvelteKit(root) {
|
|
69
|
+
const bin = installedBin(root, root, 'svelte-kit');
|
|
70
|
+
if (!bin)
|
|
71
|
+
return false;
|
|
72
|
+
return spawnSync(bin, ['sync'], { cwd: root, stdio: 'ignore', timeout: 120_000 }).status === 0;
|
|
73
|
+
}
|
|
74
|
+
function textFiles(root, inProgram) {
|
|
75
|
+
const listed = spawnSync('git', ['ls-files', '--', '*.svelte', '*.ts', '*.js'], { cwd: root, encoding: 'utf8', timeout: GIT_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024 });
|
|
76
|
+
if (listed.status !== 0)
|
|
77
|
+
return [];
|
|
78
|
+
const files = [];
|
|
79
|
+
for (const file of listed.stdout.split('\n')) {
|
|
80
|
+
if (!file || inProgram.has(file) || file.endsWith('.d.ts') || isTestFile(file))
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
files.push({ file, text: fs.readFileSync(path.join(root, file), 'utf8') });
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// deleted in the working tree
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Config, Failure } from '../../types/index.js';
|
|
2
|
+
export interface Redundancy {
|
|
3
|
+
failures: Failure[];
|
|
4
|
+
hints: string[];
|
|
5
|
+
/** A TypeScript project whose program cannot be built: the checks could not run, which is never a pass (review.ts turns it into a gate error). */
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const TYPED_CHECKS = "typed-checks-unavailable";
|
|
9
|
+
/** The typed checks on a change: nothing when it touches no TypeScript or the project has no tsconfig. */
|
|
10
|
+
export declare function typedChecks(cwd: string, changedLines: Record<string, Set<number>>, config: Config): Redundancy;
|