@rigour-labs/core 6.11.1-rc.1 → 6.11.1-rc.2
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.
|
@@ -1,4 +1,7 @@
|
|
|
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
|
}
|
|
@@ -14,3 +17,9 @@ export interface BranchBase {
|
|
|
14
17
|
export declare function branchBase(cwd: string): BranchBase | null;
|
|
15
18
|
/** Use a fixed commit for the entire scan; never move the baseline on read. */
|
|
16
19
|
export declare function resolveGitLogicBase(cwd: string): GitLogicBase | null;
|
|
20
|
+
/** Files git does not track and does not ignore: what an agent just created. */
|
|
21
|
+
export declare function untrackedFiles(cwd: string): string[];
|
|
22
|
+
/** Every file committed at `commit`, repository-relative with `/`. */
|
|
23
|
+
export declare function filesAtCommit(cwd: string, commit: string): string[];
|
|
24
|
+
/** The contents of `files` at `commit`, read by one git process; a file missing there is left out. */
|
|
25
|
+
export declare function readManyAtCommit(cwd: string, commit: string, files: string[]): Map<string, string>;
|
|
@@ -40,7 +40,43 @@ export function resolveGitLogicBase(cwd) {
|
|
|
40
40
|
if (diff === null)
|
|
41
41
|
return null;
|
|
42
42
|
return {
|
|
43
|
+
base,
|
|
43
44
|
changedFiles: new Set(diff.split('\0').filter(Boolean)),
|
|
44
45
|
readAtBase: file => git(cwd, ['show', `${base}:${file}`]),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
/** Files git does not track and does not ignore: what an agent just created. */
|
|
49
|
+
export function untrackedFiles(cwd) {
|
|
50
|
+
return (git(cwd, ['ls-files', '--others', '--exclude-standard', '-z']) ?? '').split('\0').filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
/** Every file committed at `commit`, repository-relative with `/`. */
|
|
53
|
+
export function filesAtCommit(cwd, commit) {
|
|
54
|
+
return (git(cwd, ['ls-tree', '-r', '--name-only', '-z', commit]) ?? '').split('\0').filter(Boolean);
|
|
55
|
+
}
|
|
56
|
+
/** The contents of `files` at `commit`, read by one git process; a file missing there is left out. */
|
|
57
|
+
export function readManyAtCommit(cwd, commit, files) {
|
|
58
|
+
const contents = new Map();
|
|
59
|
+
if (files.length === 0)
|
|
60
|
+
return contents;
|
|
61
|
+
let out;
|
|
62
|
+
try {
|
|
63
|
+
out = execFileSync('git', ['cat-file', '--batch'], { cwd, input: files.map(file => `${commit}:${file}`).join('\n') + '\n', stdio: ['pipe', 'pipe', 'ignore'], maxBuffer: 256 * 1024 * 1024 });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return contents;
|
|
67
|
+
}
|
|
68
|
+
let at = 0;
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const end = out.indexOf(0x0a, at);
|
|
71
|
+
if (end < 0)
|
|
72
|
+
break;
|
|
73
|
+
const header = out.subarray(at, end).toString('utf8').split(' ');
|
|
74
|
+
at = end + 1;
|
|
75
|
+
if (header[1] !== 'blob')
|
|
76
|
+
continue; // "<name> missing", or not a file
|
|
77
|
+
const size = Number(header[2]);
|
|
78
|
+
contents.set(file, out.subarray(at, at + size).toString('utf8'));
|
|
79
|
+
at += size + 1;
|
|
80
|
+
}
|
|
81
|
+
return contents;
|
|
82
|
+
}
|
|
@@ -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, 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
|
+
Logger.info('Style Drift: no main reference available; comparison unavailable');
|
|
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);
|
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).
|
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.2",
|
|
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-
|
|
79
|
-
"@rigour-labs/brain-darwin-x64": "6.11.1-rc.
|
|
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-linux-arm64": "6.11.1-rc.2",
|
|
79
|
+
"@rigour-labs/brain-darwin-x64": "6.11.1-rc.2",
|
|
80
|
+
"@rigour-labs/brain-linux-x64": "6.11.1-rc.2",
|
|
81
|
+
"@rigour-labs/brain-darwin-arm64": "6.11.1-rc.2",
|
|
82
|
+
"@rigour-labs/brain-win-x64": "6.11.1-rc.2"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/fs-extra": "^11.0.4",
|