@rigour-labs/core 6.11.1-rc.1 → 6.11.1-rc.3
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/base.d.ts +2 -0
- package/dist/gates/language-adapters/python-adapter.js +2 -1
- package/dist/gates/language-adapters/types.d.ts +6 -3
- package/dist/gates/language-adapters/types.js +10 -2
- package/dist/gates/logic-drift-git-base.d.ts +11 -0
- package/dist/gates/logic-drift-git-base.js +38 -0
- package/dist/gates/logic-drift.js +2 -2
- package/dist/gates/runner.js +8 -1
- package/dist/gates/style-drift-rules.d.ts +0 -4
- package/dist/gates/style-drift-rules.js +0 -19
- package/dist/gates/style-drift.d.ts +8 -0
- package/dist/gates/style-drift.js +52 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/services/terminal-renderer.d.ts +2 -0
- package/dist/services/terminal-renderer.js +4 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/scanner.d.ts +2 -0
- package/dist/utils/scanner.js +7 -0
- package/dist/utils/user-state.d.ts +11 -1
- package/dist/utils/user-state.js +42 -2
- package/package.json +6 -6
package/dist/gates/base.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface GateContext {
|
|
|
8
8
|
patterns?: string[];
|
|
9
9
|
/** Shared file cache across gates — reduces memory ~80% on large repos */
|
|
10
10
|
fileCache?: FileSystemCache;
|
|
11
|
+
/** A gate that could not check (nothing to compare with) says why: the run shows SKIP, never PASS. */
|
|
12
|
+
skip?: (reason: string) => void;
|
|
11
13
|
}
|
|
12
14
|
export declare abstract class Gate {
|
|
13
15
|
readonly id: string;
|
|
@@ -104,7 +104,8 @@ class PythonAdapter {
|
|
|
104
104
|
if (match && !line.includes('def') && !line.includes('class')) {
|
|
105
105
|
const name = match[1];
|
|
106
106
|
// Distinguish constants from regular variables
|
|
107
|
-
|
|
107
|
+
// A private constant (`_RETRY_DELAYS`) is a constant too: the same reading classifyCasing gives it.
|
|
108
|
+
const kind = classifyCasing(name) === 'SCREAMING_SNAKE' ? 'constant' : 'variable';
|
|
108
109
|
patterns.push({
|
|
109
110
|
name, kind,
|
|
110
111
|
convention: classifyCasing(name),
|
|
@@ -53,8 +53,11 @@ export interface NamingPattern {
|
|
|
53
53
|
kind: 'function' | 'class' | 'variable' | 'constant' | 'method';
|
|
54
54
|
convention: NamingConvention;
|
|
55
55
|
}
|
|
56
|
-
export type NamingConvention = 'camelCase' | 'PascalCase' | 'snake_case' | 'SCREAMING_SNAKE' | 'kebab-case' | 'other'
|
|
56
|
+
export type NamingConvention = 'camelCase' | 'PascalCase' | 'snake_case' | 'SCREAMING_SNAKE' | 'kebab-case' | 'other'
|
|
57
|
+
/** One lowercase word (`run`, `data`): camelCase and snake_case alike, so evidence of neither. */
|
|
58
|
+
| 'ambiguous';
|
|
57
59
|
/**
|
|
58
|
-
* Classify a name into its casing convention.
|
|
60
|
+
* Classify a name into its casing convention. Leading underscores mark privacy, not casing (`_load_rows` is
|
|
61
|
+
* snake_case, `_Private` PascalCase), and a dunder (`__init__`) is the language's own name, so it is ambiguous.
|
|
59
62
|
*/
|
|
60
|
-
export declare function classifyCasing(
|
|
63
|
+
export declare function classifyCasing(raw: string): NamingConvention;
|
|
@@ -5,13 +5,21 @@
|
|
|
5
5
|
* Gates use these adapters instead of inline `if ext === '.py'` chains.
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* Classify a name into its casing convention.
|
|
8
|
+
* Classify a name into its casing convention. Leading underscores mark privacy, not casing (`_load_rows` is
|
|
9
|
+
* snake_case, `_Private` PascalCase), and a dunder (`__init__`) is the language's own name, so it is ambiguous.
|
|
9
10
|
*/
|
|
10
|
-
export function classifyCasing(
|
|
11
|
+
export function classifyCasing(raw) {
|
|
12
|
+
if (/^__\w+__$/.test(raw))
|
|
13
|
+
return 'ambiguous';
|
|
14
|
+
const name = raw.replace(/^_+/, '');
|
|
15
|
+
if (!name)
|
|
16
|
+
return 'other';
|
|
11
17
|
if (/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
12
18
|
return 'SCREAMING_SNAKE';
|
|
13
19
|
if (/^[A-Z][a-zA-Z0-9]*$/.test(name))
|
|
14
20
|
return 'PascalCase';
|
|
21
|
+
if (/^[a-z][a-z0-9]*$/.test(name))
|
|
22
|
+
return 'ambiguous';
|
|
15
23
|
if (/^[a-z][a-zA-Z0-9]*$/.test(name))
|
|
16
24
|
return 'camelCase';
|
|
17
25
|
if (/^[a-z][a-z0-9_]*$/.test(name))
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export interface GitLogicBase {
|
|
2
|
+
/** The commit compared against. */
|
|
3
|
+
base: string;
|
|
4
|
+
/** Tracked files that differ from the base (added, copied, modified, renamed). Untracked files are not in it. */
|
|
2
5
|
changedFiles: Set<string>;
|
|
3
6
|
readAtBase: (file: string) => string | null;
|
|
4
7
|
}
|
|
8
|
+
/** Why a check that compares with the main branch could not run in a git checkout (resolveGitLogicBase gave null). */
|
|
9
|
+
export declare const NO_GIT_BASE = "nothing to compare with: no main branch (GITHUB_BASE_REF, origin/main, main, origin/master or master), or run from below the repository root";
|
|
5
10
|
export declare function isGitWorktree(cwd: string): boolean;
|
|
6
11
|
export interface BranchBase {
|
|
7
12
|
/** The main branch as found: GITHUB_BASE_REF, else origin/main, main, origin/master, master. */
|
|
@@ -14,3 +19,9 @@ export interface BranchBase {
|
|
|
14
19
|
export declare function branchBase(cwd: string): BranchBase | null;
|
|
15
20
|
/** Use a fixed commit for the entire scan; never move the baseline on read. */
|
|
16
21
|
export declare function resolveGitLogicBase(cwd: string): GitLogicBase | null;
|
|
22
|
+
/** Files git does not track and does not ignore: what an agent just created. */
|
|
23
|
+
export declare function untrackedFiles(cwd: string): string[];
|
|
24
|
+
/** Every file committed at `commit`, repository-relative with `/`. */
|
|
25
|
+
export declare function filesAtCommit(cwd: string, commit: string): string[];
|
|
26
|
+
/** The contents of `files` at `commit`, read by one git process; a file missing there is left out. */
|
|
27
|
+
export declare function readManyAtCommit(cwd: string, commit: string, files: string[]): Map<string, string>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
+
/** Why a check that compares with the main branch could not run in a git checkout (resolveGitLogicBase gave null). */
|
|
3
|
+
export const NO_GIT_BASE = 'nothing to compare with: no main branch (GITHUB_BASE_REF, origin/main, main, origin/master or master), or run from below the repository root';
|
|
2
4
|
export function isGitWorktree(cwd) {
|
|
3
5
|
return git(cwd, ['rev-parse', '--is-inside-work-tree'])?.trim() === 'true';
|
|
4
6
|
}
|
|
@@ -40,7 +42,43 @@ export function resolveGitLogicBase(cwd) {
|
|
|
40
42
|
if (diff === null)
|
|
41
43
|
return null;
|
|
42
44
|
return {
|
|
45
|
+
base,
|
|
43
46
|
changedFiles: new Set(diff.split('\0').filter(Boolean)),
|
|
44
47
|
readAtBase: file => git(cwd, ['show', `${base}:${file}`]),
|
|
45
48
|
};
|
|
46
49
|
}
|
|
50
|
+
/** Files git does not track and does not ignore: what an agent just created. */
|
|
51
|
+
export function untrackedFiles(cwd) {
|
|
52
|
+
return (git(cwd, ['ls-files', '--others', '--exclude-standard', '-z']) ?? '').split('\0').filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
/** Every file committed at `commit`, repository-relative with `/`. */
|
|
55
|
+
export function filesAtCommit(cwd, commit) {
|
|
56
|
+
return (git(cwd, ['ls-tree', '-r', '--name-only', '-z', commit]) ?? '').split('\0').filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
/** The contents of `files` at `commit`, read by one git process; a file missing there is left out. */
|
|
59
|
+
export function readManyAtCommit(cwd, commit, files) {
|
|
60
|
+
const contents = new Map();
|
|
61
|
+
if (files.length === 0)
|
|
62
|
+
return contents;
|
|
63
|
+
let out;
|
|
64
|
+
try {
|
|
65
|
+
out = execFileSync('git', ['cat-file', '--batch'], { cwd, input: files.map(file => `${commit}:${file}`).join('\n') + '\n', stdio: ['pipe', 'pipe', 'ignore'], maxBuffer: 256 * 1024 * 1024 });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return contents;
|
|
69
|
+
}
|
|
70
|
+
let at = 0;
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const end = out.indexOf(0x0a, at);
|
|
73
|
+
if (end < 0)
|
|
74
|
+
break;
|
|
75
|
+
const header = out.subarray(at, end).toString('utf8').split(' ');
|
|
76
|
+
at = end + 1;
|
|
77
|
+
if (header[1] !== 'blob')
|
|
78
|
+
continue; // "<name> missing", or not a file
|
|
79
|
+
const size = Number(header[2]);
|
|
80
|
+
contents.set(file, out.subarray(at, at + size).toString('utf8'));
|
|
81
|
+
at += size + 1;
|
|
82
|
+
}
|
|
83
|
+
return contents;
|
|
84
|
+
}
|
|
@@ -26,7 +26,7 @@ import { FileScanner } from '../utils/scanner.js';
|
|
|
26
26
|
import { Logger } from '../utils/logger.js';
|
|
27
27
|
import { languageAdapters } from './language-adapters/index.js';
|
|
28
28
|
import { extractCallSequence, isDangerousMutation } from './logic-drift-extractors.js';
|
|
29
|
-
import { isGitWorktree, resolveGitLogicBase } from './logic-drift-git-base.js';
|
|
29
|
+
import { NO_GIT_BASE, isGitWorktree, resolveGitLogicBase } from './logic-drift-git-base.js';
|
|
30
30
|
import { isScoped } from '../utils/scope.js';
|
|
31
31
|
import fs from 'fs-extra';
|
|
32
32
|
import path from 'path';
|
|
@@ -60,7 +60,7 @@ export class LogicDriftGate extends Gate {
|
|
|
60
60
|
// In Git worktrees, compare only files changed from an immutable base.
|
|
61
61
|
const gitBase = resolveGitLogicBase(context.cwd);
|
|
62
62
|
if (!gitBase && isGitWorktree(context.cwd)) {
|
|
63
|
-
|
|
63
|
+
context.skip?.(NO_GIT_BASE);
|
|
64
64
|
return [];
|
|
65
65
|
}
|
|
66
66
|
const scanFiles = gitBase ? files.filter(file => gitBase.changedFiles.has(file)) : files;
|
package/dist/gates/runner.js
CHANGED
|
@@ -145,6 +145,7 @@ export class GateRunner {
|
|
|
145
145
|
const start = Date.now();
|
|
146
146
|
const failures = [];
|
|
147
147
|
const summary = {};
|
|
148
|
+
const skips = {};
|
|
148
149
|
const ignore = this.config.ignore;
|
|
149
150
|
// 0. Run Context Discovery
|
|
150
151
|
let record;
|
|
@@ -162,11 +163,16 @@ export class GateRunner {
|
|
|
162
163
|
gateIndex++;
|
|
163
164
|
try {
|
|
164
165
|
onProgress?.(` [${gateIndex}/${totalGates}] Running ${gate.id}...`);
|
|
165
|
-
|
|
166
|
+
let skipped;
|
|
167
|
+
const gateFailures = await gate.run({ cwd, record, ignore, patterns, fileCache, skip: reason => { skipped = reason; } });
|
|
166
168
|
if (gateFailures.length > 0) {
|
|
167
169
|
failures.push(...gateFailures);
|
|
168
170
|
summary[gate.id] = 'FAIL';
|
|
169
171
|
}
|
|
172
|
+
else if (skipped !== undefined) {
|
|
173
|
+
summary[gate.id] = 'SKIP';
|
|
174
|
+
skips[gate.id] = skipped;
|
|
175
|
+
}
|
|
170
176
|
else {
|
|
171
177
|
summary[gate.id] = 'PASS';
|
|
172
178
|
}
|
|
@@ -299,6 +305,7 @@ export class GateRunner {
|
|
|
299
305
|
const report = {
|
|
300
306
|
status,
|
|
301
307
|
summary,
|
|
308
|
+
...(Object.keys(skips).length ? { skips } : {}),
|
|
302
309
|
failures,
|
|
303
310
|
stats: {
|
|
304
311
|
duration_ms: Date.now() - start,
|
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Contains per-language naming convention rules and naming pattern regexes.
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
7
|
-
* Casing classification rules
|
|
8
|
-
*/
|
|
9
|
-
export declare function classifyCasing(name: string): 'camelCase' | 'snake_case' | 'PascalCase' | 'SCREAMING_SNAKE' | null;
|
|
10
6
|
/**
|
|
11
7
|
* Function name pattern for JavaScript
|
|
12
8
|
*/
|
|
@@ -3,25 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Contains per-language naming convention rules and naming pattern regexes.
|
|
5
5
|
*/
|
|
6
|
-
/**
|
|
7
|
-
* Casing classification rules
|
|
8
|
-
*/
|
|
9
|
-
export function classifyCasing(name) {
|
|
10
|
-
if (name.startsWith('_') || name.length <= 1)
|
|
11
|
-
return null;
|
|
12
|
-
if (/^[A-Z][A-Z0-9_]+$/.test(name)) {
|
|
13
|
-
return 'SCREAMING_SNAKE';
|
|
14
|
-
}
|
|
15
|
-
else if (/^[A-Z]/.test(name)) {
|
|
16
|
-
return 'PascalCase';
|
|
17
|
-
}
|
|
18
|
-
else if (name.includes('_')) {
|
|
19
|
-
return 'snake_case';
|
|
20
|
-
}
|
|
21
|
-
else {
|
|
22
|
-
return 'camelCase';
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
6
|
/**
|
|
26
7
|
* Function name pattern for JavaScript
|
|
27
8
|
*/
|
|
@@ -32,8 +32,16 @@ export declare class StyleDriftGate extends Gate {
|
|
|
32
32
|
constructor(config?: StyleDriftConfig);
|
|
33
33
|
protected get provenance(): Provenance;
|
|
34
34
|
run(context: GateContext): Promise<Failure[]>;
|
|
35
|
+
/**
|
|
36
|
+
* The changed and untracked files, each against a fingerprint of its language's files committed at the base.
|
|
37
|
+
* On the base branch with a clean tree nothing differs, so nothing is compared.
|
|
38
|
+
*/
|
|
39
|
+
private againstGitBase;
|
|
40
|
+
/** Each file against its own language's fingerprint. */
|
|
41
|
+
private compare;
|
|
35
42
|
private computePerLanguageBaseline;
|
|
36
43
|
private computeFingerprint;
|
|
44
|
+
private fingerprintOf;
|
|
37
45
|
private emptyFingerprint;
|
|
38
46
|
private analyzeFile;
|
|
39
47
|
private mergeIntoFingerprint;
|
|
@@ -25,6 +25,7 @@ import { Logger } from '../utils/logger.js';
|
|
|
25
25
|
import { languageAdapters } from './language-adapters/index.js';
|
|
26
26
|
import { extractComparableJsNames } from './js-style-context.js';
|
|
27
27
|
import { isScoped } from '../utils/scope.js';
|
|
28
|
+
import { filesAtCommit, isGitWorktree, NO_GIT_BASE, readManyAtCommit, resolveGitLogicBase, untrackedFiles } from './logic-drift-git-base.js';
|
|
28
29
|
import { TRY_CATCH_PATTERN, CATCH_PATTERN, RESULT_TYPE_PATTERN, NAMED_IMPORT_PATTERN, WILDCARD_IMPORT_PATTERN, SIDE_EFFECT_IMPORT_PATTERN, DEFAULT_IMPORT_PATTERN, countQuotes, } from './style-drift-rules.js';
|
|
29
30
|
import fs from 'fs-extra';
|
|
30
31
|
import path from 'path';
|
|
@@ -43,16 +44,26 @@ export class StyleDriftGate extends Gate {
|
|
|
43
44
|
async run(context) {
|
|
44
45
|
if (!this.config.enabled)
|
|
45
46
|
return [];
|
|
46
|
-
const failures = [];
|
|
47
47
|
const baselinePath = path.join(context.cwd, this.config.baseline_path);
|
|
48
48
|
// Find source files
|
|
49
|
+
const ignore = [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'];
|
|
49
50
|
const files = await FileScanner.findFiles({
|
|
50
51
|
cwd: context.cwd,
|
|
51
52
|
patterns: context.patterns || languageAdapters.getScanPatterns(),
|
|
52
|
-
ignore
|
|
53
|
+
ignore,
|
|
53
54
|
});
|
|
54
55
|
if (files.length === 0)
|
|
55
56
|
return [];
|
|
57
|
+
// In a git checkout the project's style is the code committed at the base, read fresh on every run, and
|
|
58
|
+
// only the files that differ from it are compared. Nothing is cached, so no earlier run on a dirty tree
|
|
59
|
+
// can change a verdict, and a .rigour/style-baseline.json left by an older version is never read.
|
|
60
|
+
const gitBase = resolveGitLogicBase(context.cwd);
|
|
61
|
+
if (!gitBase && isGitWorktree(context.cwd)) {
|
|
62
|
+
context.skip?.(NO_GIT_BASE);
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
if (gitBase)
|
|
66
|
+
return this.againstGitBase(context, files, ignore, gitBase);
|
|
56
67
|
// Group files by language
|
|
57
68
|
const filesByLang = new Map();
|
|
58
69
|
for (const file of files) {
|
|
@@ -94,6 +105,38 @@ export class StyleDriftGate extends Gate {
|
|
|
94
105
|
return []; // No failures on first scan
|
|
95
106
|
}
|
|
96
107
|
// Subsequent scan: compare each file against its own language's baseline
|
|
108
|
+
return this.compare(context, files, baseline);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The changed and untracked files, each against a fingerprint of its language's files committed at the base.
|
|
112
|
+
* On the base branch with a clean tree nothing differs, so nothing is compared.
|
|
113
|
+
*/
|
|
114
|
+
async againstGitBase(context, files, ignore, gitBase) {
|
|
115
|
+
const changed = new Set([...gitBase.changedFiles, ...untrackedFiles(context.cwd)]);
|
|
116
|
+
const compared = files.filter(file => changed.has(file));
|
|
117
|
+
if (compared.length === 0)
|
|
118
|
+
return [];
|
|
119
|
+
const languages = new Set(compared.map(file => languageAdapters.getAdapter(file)?.id));
|
|
120
|
+
const sampled = new Map();
|
|
121
|
+
for (const file of FileScanner.filterPaths(filesAtCommit(context.cwd, gitBase.base), { patterns: languageAdapters.getScanPatterns(), ignore })) {
|
|
122
|
+
const id = languageAdapters.getAdapter(file)?.id;
|
|
123
|
+
if (!id || !languages.has(id))
|
|
124
|
+
continue;
|
|
125
|
+
const list = sampled.get(id) ?? [];
|
|
126
|
+
if (list.length < this.config.sample_size)
|
|
127
|
+
list.push(file);
|
|
128
|
+
sampled.set(id, list);
|
|
129
|
+
}
|
|
130
|
+
const atBase = readManyAtCommit(context.cwd, gitBase.base, [...sampled.values()].flat());
|
|
131
|
+
const baseline = { languages: {}, createdAt: '', version: 3 };
|
|
132
|
+
for (const [id, list] of sampled) {
|
|
133
|
+
baseline.languages[id] = this.fingerprintOf(new Map(list.flatMap(file => atBase.has(file) ? [[file, atBase.get(file)]] : [])));
|
|
134
|
+
}
|
|
135
|
+
return this.compare(context, compared, baseline);
|
|
136
|
+
}
|
|
137
|
+
/** Each file against its own language's fingerprint. */
|
|
138
|
+
async compare(context, files, baseline) {
|
|
139
|
+
const failures = [];
|
|
97
140
|
const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
|
|
98
141
|
for (const [file, content] of contents) {
|
|
99
142
|
const adapter = languageAdapters.getAdapter(file);
|
|
@@ -132,8 +175,10 @@ export class StyleDriftGate extends Gate {
|
|
|
132
175
|
return baseline;
|
|
133
176
|
}
|
|
134
177
|
async computeFingerprint(context, files) {
|
|
178
|
+
return this.fingerprintOf(await FileScanner.readFiles(context.cwd, files, context.fileCache));
|
|
179
|
+
}
|
|
180
|
+
fingerprintOf(contents) {
|
|
135
181
|
const fingerprint = this.emptyFingerprint();
|
|
136
|
-
const contents = await FileScanner.readFiles(context.cwd, files, context.fileCache);
|
|
137
182
|
for (const [file, content] of contents) {
|
|
138
183
|
const fileAnalysis = this.analyzeFile(content, file);
|
|
139
184
|
this.mergeIntoFingerprint(fingerprint, fileAnalysis);
|
|
@@ -165,10 +210,13 @@ export class StyleDriftGate extends Gate {
|
|
|
165
210
|
? extractComparableJsNames(content, filePath)
|
|
166
211
|
: adapter.extractNamingPatterns(content);
|
|
167
212
|
for (const pattern of namingPatterns) {
|
|
213
|
+
if (pattern.convention === 'ambiguous')
|
|
214
|
+
continue; // a one-word name says nothing about the casing
|
|
168
215
|
if (pattern.kind === 'function' || pattern.kind === 'method') {
|
|
169
216
|
fp.naming.functions[pattern.convention]++;
|
|
170
217
|
}
|
|
171
|
-
else if (pattern.kind === 'variable'
|
|
218
|
+
else if (pattern.kind === 'variable') {
|
|
219
|
+
// A constant follows the constant convention (`MAX_ROWS`), never the variables': it is not compared.
|
|
172
220
|
fp.naming.variables[pattern.convention]++;
|
|
173
221
|
}
|
|
174
222
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js'
|
|
|
11
11
|
export * from './utils/logger.js';
|
|
12
12
|
export { normalizeScopePatterns, isScoped } from './utils/scope.js';
|
|
13
13
|
export { deepAnalysisError } from './utils/deep-status.js';
|
|
14
|
-
export { rigourUserDir, repoStateDir } from './utils/user-state.js';
|
|
14
|
+
export { rigourUserDir, repoStateDir, legacyStateNote } from './utils/user-state.js';
|
|
15
15
|
export { locateTransformers, semanticRuntimeDir, semanticRuntimeInstalled, TRANSFORMERS_SPEC } from './pattern-index/semantic-runtime.js';
|
|
16
16
|
export { reviewChange, toReviewFinding, type ReviewInput, type ReviewResult, type ReviewFinding } from './review/review.js';
|
|
17
17
|
export { splitByChangedLines, type ChangedLineSplit } from './review/changed-lines.js';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ export { FrontendSecretExposureGate } from './gates/frontend-secret-exposure.js'
|
|
|
11
11
|
export * from './utils/logger.js';
|
|
12
12
|
export { normalizeScopePatterns, isScoped } from './utils/scope.js';
|
|
13
13
|
export { deepAnalysisError } from './utils/deep-status.js';
|
|
14
|
-
export { rigourUserDir, repoStateDir } from './utils/user-state.js';
|
|
14
|
+
export { rigourUserDir, repoStateDir, legacyStateNote } from './utils/user-state.js';
|
|
15
15
|
export { locateTransformers, semanticRuntimeDir, semanticRuntimeInstalled, TRANSFORMERS_SPEC } from './pattern-index/semantic-runtime.js';
|
|
16
16
|
export { reviewChange, toReviewFinding } from './review/review.js';
|
|
17
17
|
export { splitByChangedLines } from './review/changed-lines.js';
|
|
@@ -16,6 +16,8 @@ export interface RenderOptions {
|
|
|
16
16
|
export interface GateResult {
|
|
17
17
|
id: string;
|
|
18
18
|
status: Status;
|
|
19
|
+
/** Why a SKIP: the check could not check. */
|
|
20
|
+
reason?: string;
|
|
19
21
|
}
|
|
20
22
|
export declare function renderScoreGauge(score: number, label: string): string;
|
|
21
23
|
export declare function renderSeveritySection(failures: Failure[], severity: Severity, maxItems?: number): string;
|
|
@@ -65,6 +65,9 @@ export function renderGateGrid(gates) {
|
|
|
65
65
|
const row = entries.slice(i, i + 3).map(e => e.padEnd(22)).join('');
|
|
66
66
|
lines.push(` ${row}`);
|
|
67
67
|
}
|
|
68
|
+
for (const g of gates)
|
|
69
|
+
if (g.status === 'SKIP' && g.reason)
|
|
70
|
+
lines.push(chalk.dim(` \u{23ED} ${g.id} skipped: ${g.reason}`));
|
|
68
71
|
return lines.join('\n');
|
|
69
72
|
}
|
|
70
73
|
// ── Brain Status ───────────────────────────────────────────────────
|
|
@@ -155,7 +158,7 @@ export function renderFullReport(report, options = {}) {
|
|
|
155
158
|
}
|
|
156
159
|
// Gate grid from report summary
|
|
157
160
|
if (report.summary) {
|
|
158
|
-
const gates = Object.entries(report.summary).map(([id, status]) => ({ id, status }));
|
|
161
|
+
const gates = Object.entries(report.summary).map(([id, status]) => ({ id, status, reason: report.skips?.[id] }));
|
|
159
162
|
if (gates.length > 0) {
|
|
160
163
|
boxContent.push(renderGateGrid(gates));
|
|
161
164
|
boxContent.push('');
|
package/dist/types/index.d.ts
CHANGED
|
@@ -3435,6 +3435,8 @@ export type Failure = z.infer<typeof FailureSchema>;
|
|
|
3435
3435
|
export declare const ReportSchema: z.ZodObject<{
|
|
3436
3436
|
status: z.ZodEnum<["PASS", "FAIL", "SKIP", "ERROR"]>;
|
|
3437
3437
|
summary: z.ZodRecord<z.ZodString, z.ZodEnum<["PASS", "FAIL", "SKIP", "ERROR"]>>;
|
|
3438
|
+
/** Why a check reported SKIP: it could not check (for example, no main branch to compare with). */
|
|
3439
|
+
skips: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
3438
3440
|
failures: z.ZodArray<z.ZodObject<{
|
|
3439
3441
|
id: z.ZodString;
|
|
3440
3442
|
title: z.ZodString;
|
|
@@ -3764,6 +3766,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
3764
3766
|
category?: string | undefined;
|
|
3765
3767
|
advisory?: boolean | undefined;
|
|
3766
3768
|
}[];
|
|
3769
|
+
skips?: Record<string, string> | undefined;
|
|
3767
3770
|
}, {
|
|
3768
3771
|
stats: {
|
|
3769
3772
|
duration_ms: number;
|
|
@@ -3828,6 +3831,7 @@ export declare const ReportSchema: z.ZodObject<{
|
|
|
3828
3831
|
category?: string | undefined;
|
|
3829
3832
|
advisory?: boolean | undefined;
|
|
3830
3833
|
}[];
|
|
3834
|
+
skips?: Record<string, string> | undefined;
|
|
3831
3835
|
}>;
|
|
3832
3836
|
export type Report = z.infer<typeof ReportSchema>;
|
|
3833
3837
|
/** Options passed from CLI --deep / --pro / --max / -k flags */
|
package/dist/types/index.js
CHANGED
|
@@ -521,6 +521,8 @@ export const FailureSchema = z.object({
|
|
|
521
521
|
export const ReportSchema = z.object({
|
|
522
522
|
status: StatusSchema,
|
|
523
523
|
summary: z.record(StatusSchema),
|
|
524
|
+
/** Why a check reported SKIP: it could not check (for example, no main branch to compare with). */
|
|
525
|
+
skips: z.record(z.string()).optional(),
|
|
524
526
|
failures: z.array(FailureSchema),
|
|
525
527
|
stats: z.object({
|
|
526
528
|
duration_ms: z.number(),
|
package/dist/utils/scanner.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare class FileScanner {
|
|
|
8
8
|
private static DEFAULT_PATTERNS;
|
|
9
9
|
private static DEFAULT_IGNORE;
|
|
10
10
|
static findFiles(options: ScannerOptions): Promise<string[]>;
|
|
11
|
+
/** The selection findFiles makes, over paths listed elsewhere (a commit's files) instead of the disk. */
|
|
12
|
+
static filterPaths(paths: string[], options: Omit<ScannerOptions, 'cwd'>): string[];
|
|
11
13
|
/**
|
|
12
14
|
* Read file contents. When a FileSystemCache is provided, uses it for
|
|
13
15
|
* shared caching across gates (avoids re-loading per gate).
|
package/dist/utils/scanner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { globby } from 'globby';
|
|
2
|
+
import micromatch from 'micromatch';
|
|
2
3
|
import fs from 'fs-extra';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import { directoriesAsGlobs, withDirectoryForms } from './glob-paths.js';
|
|
@@ -42,6 +43,12 @@ export class FileScanner {
|
|
|
42
43
|
});
|
|
43
44
|
return files.sort();
|
|
44
45
|
}
|
|
46
|
+
/** The selection findFiles makes, over paths listed elsewhere (a commit's files) instead of the disk. */
|
|
47
|
+
static filterPaths(paths, options) {
|
|
48
|
+
const patterns = (options.patterns || this.DEFAULT_PATTERNS).map(toGlobSeparators);
|
|
49
|
+
const ignore = withDirectoryForms([...new Set([...this.DEFAULT_IGNORE, ...(options.ignore || [])])].map(toGlobSeparators));
|
|
50
|
+
return paths.filter(file => micromatch.isMatch(file, patterns, { ignore })).sort();
|
|
51
|
+
}
|
|
45
52
|
/**
|
|
46
53
|
* Read file contents. When a FileSystemCache is provided, uses it for
|
|
47
54
|
* shared caching across gates (avoids re-loading per gate).
|
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The home Rigour keeps its state under (`<home>/.rigour`): RIGOUR_HOME when set (a profile, a sandbox, tests),
|
|
3
|
+
* else the OS home. A RIGOUR_HOME ending in `.rigour` names the state directory itself, so its parent is the home.
|
|
4
|
+
* Earlier versions kept state one level deeper (`<RIGOUR_HOME>/.rigour`): where that holds state, it stays in use
|
|
5
|
+
* and nothing is moved (legacyStateNote says how).
|
|
6
|
+
*/
|
|
2
7
|
export declare function rigourHome(): string;
|
|
8
|
+
/**
|
|
9
|
+
* For `rigour doctor`: state an earlier version wrote under `<RIGOUR_HOME>/.rigour`, still in use, and how to move
|
|
10
|
+
* it; or, when the named directory holds state of its own too, both places. Nothing is moved for anyone.
|
|
11
|
+
*/
|
|
12
|
+
export declare function legacyStateNote(): string | undefined;
|
|
3
13
|
/** The user's Rigour directory: `<rigourHome()>/.rigour`. Every piece of Rigour's own state lives under it. */
|
|
4
14
|
export declare function rigourUserDir(): string;
|
|
5
15
|
/**
|
package/dist/utils/user-state.js
CHANGED
|
@@ -2,9 +2,49 @@ import crypto from 'crypto';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The home Rigour keeps its state under (`<home>/.rigour`): RIGOUR_HOME when set (a profile, a sandbox, tests),
|
|
7
|
+
* else the OS home. A RIGOUR_HOME ending in `.rigour` names the state directory itself, so its parent is the home.
|
|
8
|
+
* Earlier versions kept state one level deeper (`<RIGOUR_HOME>/.rigour`): where that holds state, it stays in use
|
|
9
|
+
* and nothing is moved (legacyStateNote says how).
|
|
10
|
+
*/
|
|
6
11
|
export function rigourHome() {
|
|
7
|
-
|
|
12
|
+
const set = process.env.RIGOUR_HOME;
|
|
13
|
+
if (!set)
|
|
14
|
+
return os.homedir();
|
|
15
|
+
if (path.basename(path.resolve(set)) !== '.rigour')
|
|
16
|
+
return set;
|
|
17
|
+
return hasState(path.join(set, '.rigour')) ? set : path.dirname(path.resolve(set));
|
|
18
|
+
}
|
|
19
|
+
function hasState(dir) {
|
|
20
|
+
try {
|
|
21
|
+
return fs.readdirSync(dir).length > 0;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* For `rigour doctor`: state an earlier version wrote under `<RIGOUR_HOME>/.rigour`, still in use, and how to move
|
|
29
|
+
* it; or, when the named directory holds state of its own too, both places. Nothing is moved for anyone.
|
|
30
|
+
*/
|
|
31
|
+
export function legacyStateNote() {
|
|
32
|
+
const set = process.env.RIGOUR_HOME;
|
|
33
|
+
if (!set || path.basename(path.resolve(set)) !== '.rigour')
|
|
34
|
+
return undefined;
|
|
35
|
+
const named = path.resolve(set);
|
|
36
|
+
const doubled = path.join(named, '.rigour');
|
|
37
|
+
if (!hasState(doubled))
|
|
38
|
+
return undefined;
|
|
39
|
+
const own = (() => { try {
|
|
40
|
+
return fs.readdirSync(named).filter(entry => entry !== '.rigour').length > 0;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
} })();
|
|
45
|
+
return own
|
|
46
|
+
? `Rigour state is in both ${named} and ${doubled}. This run uses ${doubled}, where your tools have been writing; merge the two by hand if you need both.`
|
|
47
|
+
: `Rigour state is in ${doubled}, written by an earlier version. RIGOUR_HOME=${set} now names the state directory itself: to use ${named}, move the contents of ${doubled} up one level.`;
|
|
8
48
|
}
|
|
9
49
|
/** The user's Rigour directory: `<rigourHome()>/.rigour`. Every piece of Rigour's own state lives under it. */
|
|
10
50
|
export function rigourUserDir() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigour-labs/core",
|
|
3
|
-
"version": "6.11.1-rc.
|
|
3
|
+
"version": "6.11.1-rc.3",
|
|
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.11.1-rc.
|
|
79
|
-
"@rigour-labs/brain-
|
|
80
|
-
"@rigour-labs/brain-linux-x64": "6.11.1-rc.
|
|
81
|
-
"@rigour-labs/brain-
|
|
82
|
-
"@rigour-labs/brain-win-x64": "6.11.1-rc.
|
|
78
|
+
"@rigour-labs/brain-darwin-arm64": "6.11.1-rc.3",
|
|
79
|
+
"@rigour-labs/brain-linux-arm64": "6.11.1-rc.3",
|
|
80
|
+
"@rigour-labs/brain-linux-x64": "6.11.1-rc.3",
|
|
81
|
+
"@rigour-labs/brain-darwin-x64": "6.11.1-rc.3",
|
|
82
|
+
"@rigour-labs/brain-win-x64": "6.11.1-rc.3"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/fs-extra": "^11.0.4",
|