@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: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.d.ts'],
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);
@@ -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).
@@ -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.1",
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-darwin-arm64": "6.11.1-rc.1",
79
- "@rigour-labs/brain-darwin-x64": "6.11.1-rc.1",
80
- "@rigour-labs/brain-linux-x64": "6.11.1-rc.1",
81
- "@rigour-labs/brain-linux-arm64": "6.11.1-rc.1",
82
- "@rigour-labs/brain-win-x64": "6.11.1-rc.1"
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",