@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.
@@ -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
- const kind = /^[A-Z][A-Z0-9_]*$/.test(name) ? 'constant' : 'variable';
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(name: string): NamingConvention;
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(name) {
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
- Logger.info('Logic Drift: no main reference available; comparison unavailable');
63
+ context.skip?.(NO_GIT_BASE);
64
64
  return [];
65
65
  }
66
66
  const scanFiles = gitBase ? files.filter(file => gitBase.changedFiles.has(file)) : files;
@@ -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
- const gateFailures = await gate.run({ cwd, record, ignore, patterns, fileCache });
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: [...(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
+ 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' || pattern.kind === 'constant') {
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('');
@@ -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 */
@@ -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(),
@@ -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).
@@ -1,5 +1,15 @@
1
- /** The home Rigour keeps its state under: RIGOUR_HOME when set (a profile, a sandbox, tests), else the OS home. */
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
  /**
@@ -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
- /** The home Rigour keeps its state under: RIGOUR_HOME when set (a profile, a sandbox, tests), else the OS home. */
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
- return process.env.RIGOUR_HOME || os.homedir();
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.1",
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.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-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",