@rigour-labs/core 2.9.4 → 2.11.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.
Files changed (38) hide show
  1. package/dist/index.js +4 -0
  2. package/dist/pattern-index/embeddings.d.ts +19 -0
  3. package/dist/pattern-index/embeddings.js +78 -0
  4. package/dist/pattern-index/index.d.ts +12 -0
  5. package/dist/pattern-index/index.js +17 -0
  6. package/dist/pattern-index/indexer.d.ts +127 -0
  7. package/dist/pattern-index/indexer.js +891 -0
  8. package/dist/pattern-index/indexer.test.d.ts +6 -0
  9. package/dist/pattern-index/indexer.test.js +188 -0
  10. package/dist/pattern-index/matcher.d.ts +106 -0
  11. package/dist/pattern-index/matcher.js +376 -0
  12. package/dist/pattern-index/matcher.test.d.ts +6 -0
  13. package/dist/pattern-index/matcher.test.js +238 -0
  14. package/dist/pattern-index/overrides.d.ts +64 -0
  15. package/dist/pattern-index/overrides.js +196 -0
  16. package/dist/pattern-index/security.d.ts +25 -0
  17. package/dist/pattern-index/security.js +127 -0
  18. package/dist/pattern-index/staleness.d.ts +71 -0
  19. package/dist/pattern-index/staleness.js +381 -0
  20. package/dist/pattern-index/staleness.test.d.ts +6 -0
  21. package/dist/pattern-index/staleness.test.js +211 -0
  22. package/dist/pattern-index/types.d.ts +221 -0
  23. package/dist/pattern-index/types.js +7 -0
  24. package/package.json +14 -1
  25. package/src/index.ts +4 -0
  26. package/src/pattern-index/embeddings.ts +84 -0
  27. package/src/pattern-index/index.ts +59 -0
  28. package/src/pattern-index/indexer.test.ts +276 -0
  29. package/src/pattern-index/indexer.ts +1022 -0
  30. package/src/pattern-index/matcher.test.ts +293 -0
  31. package/src/pattern-index/matcher.ts +493 -0
  32. package/src/pattern-index/overrides.ts +235 -0
  33. package/src/pattern-index/security.ts +151 -0
  34. package/src/pattern-index/staleness.test.ts +313 -0
  35. package/src/pattern-index/staleness.ts +438 -0
  36. package/src/pattern-index/types.ts +339 -0
  37. package/vitest.config.ts +7 -0
  38. package/vitest.setup.ts +30 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Pattern Index - Types
3
+ *
4
+ * Core type definitions for the Pattern Index system.
5
+ * Rigour's Pattern Index prevents AI from reinventing existing code.
6
+ */
7
+ /**
8
+ * All supported pattern types that can be indexed.
9
+ * Organized by category for clarity.
10
+ */
11
+ export type PatternType = 'function' | 'class' | 'method' | 'component' | 'hook' | 'decorator' | 'middleware' | 'type' | 'interface' | 'schema' | 'model' | 'enum' | 'constant' | 'config' | 'env' | 'route' | 'handler' | 'resolver' | 'rpc' | 'store' | 'reducer' | 'action' | 'selector' | 'error' | 'exception' | 'mock' | 'fixture' | 'factory' | 'command' | 'task' | 'event' | 'protocol';
12
+ /**
13
+ * A single indexed pattern entry.
14
+ */
15
+ export interface PatternEntry {
16
+ /** Unique identifier (hash of file + name) */
17
+ id: string;
18
+ /** The type of pattern */
19
+ type: PatternType;
20
+ /** Name of the pattern (e.g., "formatDate") */
21
+ name: string;
22
+ /** Relative path to the file */
23
+ file: string;
24
+ /** Line number where the pattern is defined */
25
+ line: number;
26
+ /** End line number */
27
+ endLine: number;
28
+ /** Function/method signature if applicable */
29
+ signature: string;
30
+ /** Description from JSDoc/docstring */
31
+ description: string;
32
+ /** Extracted semantic keywords for matching */
33
+ keywords: string[];
34
+ /** Content hash for change detection */
35
+ hash: string;
36
+ /** Is this pattern exported? */
37
+ exported: boolean;
38
+ /** How many files import this pattern */
39
+ usageCount: number;
40
+ /** User-defined category/grouping */
41
+ category?: string;
42
+ embedding?: number[];
43
+ /** Last indexed timestamp */
44
+ indexedAt: string;
45
+ }
46
+ /**
47
+ * The complete pattern index structure.
48
+ */
49
+ export interface PatternIndex {
50
+ /** Index format version */
51
+ version: string;
52
+ /** When the index was last updated */
53
+ lastUpdated: string;
54
+ /** Root directory that was indexed */
55
+ rootDir: string;
56
+ /** All indexed patterns */
57
+ patterns: PatternEntry[];
58
+ /** Index statistics */
59
+ stats: PatternIndexStats;
60
+ /** Files that were indexed */
61
+ files: IndexedFile[];
62
+ }
63
+ /**
64
+ * Statistics about the pattern index.
65
+ */
66
+ export interface PatternIndexStats {
67
+ totalPatterns: number;
68
+ totalFiles: number;
69
+ byType: Record<PatternType, number>;
70
+ indexDurationMs: number;
71
+ }
72
+ /**
73
+ * Information about an indexed file.
74
+ */
75
+ export interface IndexedFile {
76
+ path: string;
77
+ hash: string;
78
+ patternCount: number;
79
+ indexedAt: string;
80
+ }
81
+ /**
82
+ * Configuration for the pattern indexer.
83
+ */
84
+ export interface PatternIndexConfig {
85
+ /** Directories to index (defaults to src/) */
86
+ include: string[];
87
+ /** Directories to exclude */
88
+ exclude: string[];
89
+ /** File extensions to index */
90
+ extensions: string[];
91
+ /** Whether to index test files */
92
+ indexTests: boolean;
93
+ /** Whether to index node_modules */
94
+ indexNodeModules: boolean;
95
+ /** Minimum pattern name length to index */
96
+ minNameLength: number;
97
+ /** Custom categories for patterns */
98
+ categories: Record<string, string[]>;
99
+ /** Whether to generate semantic embeddings for patterns */
100
+ useEmbeddings?: boolean;
101
+ }
102
+ /**
103
+ * Result from matching against the pattern index.
104
+ */
105
+ export interface PatternMatchResult {
106
+ /** The query that was matched */
107
+ query: string;
108
+ /** All matches found */
109
+ matches: PatternMatch[];
110
+ /** Suggestion for what to do */
111
+ suggestion: string;
112
+ /** Whether human override is available */
113
+ canOverride: boolean;
114
+ /** Overall status */
115
+ status: 'FOUND_SIMILAR' | 'NO_MATCH' | 'OVERRIDE_ALLOWED';
116
+ /** Recommended action */
117
+ action: 'BLOCK' | 'WARN' | 'ALLOW';
118
+ }
119
+ /**
120
+ * A single pattern match.
121
+ */
122
+ export interface PatternMatch {
123
+ /** The matched pattern */
124
+ pattern: PatternEntry;
125
+ /** How the match was determined */
126
+ matchType: 'exact' | 'fuzzy' | 'signature' | 'semantic';
127
+ /** Confidence score 0-100 */
128
+ confidence: number;
129
+ /** Human-readable reason for the match */
130
+ reason: string;
131
+ }
132
+ /**
133
+ * Human override entry.
134
+ */
135
+ export interface PatternOverride {
136
+ /** Pattern name or glob */
137
+ pattern: string;
138
+ /** Why the override was granted */
139
+ reason: string;
140
+ /** When the override expires */
141
+ expiresAt?: string;
142
+ /** Who approved the override */
143
+ approvedBy?: string;
144
+ /** When the override was created */
145
+ createdAt: string;
146
+ }
147
+ /**
148
+ * Staleness detection result.
149
+ */
150
+ export interface StalenessResult {
151
+ /** Overall status */
152
+ status: 'FRESH' | 'STALE' | 'DEPRECATED';
153
+ /** All staleness issues found */
154
+ issues: StalenessIssue[];
155
+ /** Project context (versions) */
156
+ projectContext: Record<string, string>;
157
+ }
158
+ /**
159
+ * A single staleness issue.
160
+ */
161
+ export interface StalenessIssue {
162
+ /** Line number */
163
+ line: number;
164
+ /** The stale pattern */
165
+ pattern: string;
166
+ /** Severity */
167
+ severity: 'error' | 'warning' | 'info';
168
+ /** Why it's stale */
169
+ reason: string;
170
+ /** What to use instead */
171
+ replacement: string;
172
+ /** Link to documentation */
173
+ docs?: string;
174
+ }
175
+ /**
176
+ * Security/CVE entry for a package.
177
+ */
178
+ export interface SecurityEntry {
179
+ /** CVE Identifier (e.g., CVE-2021-1234) */
180
+ cveId: string;
181
+ /** Package name */
182
+ packageName: string;
183
+ /** Vulnerable version range (semver) */
184
+ vulnerableRange: string;
185
+ /** Severity level */
186
+ severity: 'critical' | 'high' | 'moderate' | 'low';
187
+ /** Brief description of the vulnerability */
188
+ title: string;
189
+ /** Link to advisory */
190
+ url: string;
191
+ /** The version of the package in the current project */
192
+ currentVersion?: string;
193
+ }
194
+ /**
195
+ * Result from security check.
196
+ */
197
+ export interface SecurityResult {
198
+ /** Overall security status */
199
+ status: 'SECURE' | 'VULNERABLE';
200
+ /** All CVEs/vulnerabilities found */
201
+ vulnerabilities: SecurityEntry[];
202
+ }
203
+ /**
204
+ * Deprecation entry in the deprecation database.
205
+ */
206
+ export interface DeprecationEntry {
207
+ /** Pattern to match (can be regex) */
208
+ pattern: string;
209
+ /** Library this belongs to */
210
+ library?: string;
211
+ /** Version when deprecated */
212
+ deprecatedIn: string;
213
+ /** Suggested replacement */
214
+ replacement: string;
215
+ /** Severity level */
216
+ severity: 'error' | 'warning' | 'info';
217
+ /** Additional context */
218
+ reason?: string;
219
+ /** Documentation link */
220
+ docs?: string;
221
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Pattern Index - Types
3
+ *
4
+ * Core type definitions for the Pattern Index system.
5
+ * Rigour's Pattern Index prevents AI from reinventing existing code.
6
+ */
7
+ export {};
package/package.json CHANGED
@@ -1,9 +1,19 @@
1
1
  {
2
2
  "name": "@rigour-labs/core",
3
- "version": "2.9.4",
3
+ "version": "2.11.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./pattern-index": {
13
+ "types": "./dist/pattern-index/index.d.ts",
14
+ "import": "./dist/pattern-index/index.js"
15
+ }
16
+ },
7
17
  "repository": {
8
18
  "type": "git",
9
19
  "url": "https://github.com/rigour-labs/rigour"
@@ -24,6 +34,9 @@
24
34
  "yaml": "^2.3.4",
25
35
  "zod": "^3.22.4"
26
36
  },
37
+ "optionalDependencies": {
38
+ "@xenova/transformers": "^2.17.2"
39
+ },
27
40
  "devDependencies": {
28
41
  "@types/fs-extra": "^11.0.4",
29
42
  "@types/micromatch": "^4.0.10",
package/src/index.ts CHANGED
@@ -7,3 +7,7 @@ export * from './types/fix-packet.js';
7
7
  export { Gate, GateContext } from './gates/base.js';
8
8
  export { RetryLoopBreakerGate } from './gates/retry-loop-breaker.js';
9
9
  export * from './utils/logger.js';
10
+ // Pattern Index is intentionally NOT exported here to prevent
11
+ // native dependency issues (sharp/transformers) from leaking into
12
+ // non-AI parts of the system.
13
+ // Import from @rigour-labs/core/pattern-index instead.
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Semantic Embedding Service
3
+ *
4
+ * Uses Transformers.js for local vector embeddings.
5
+ */
6
+
7
+ /**
8
+ * Singleton for the embedding pipeline to avoid re-loading the model.
9
+ */
10
+ let embeddingPipeline: any = null;
11
+
12
+ /**
13
+ * Get or initialize the embedding pipeline.
14
+ */
15
+ async function getPipeline() {
16
+ // Definitive bypass for tests to avoid native 'sharp' dependency issues
17
+ if (process.env.VITEST) {
18
+ return async (text: string) => {
19
+ const vector = new Array(384).fill(0);
20
+ for (let i = 0; i < Math.min(text.length, 384); i++) {
21
+ vector[i] = text.charCodeAt(i) / 255;
22
+ }
23
+ return { data: new Float32Array(vector) };
24
+ };
25
+ }
26
+
27
+ if (!embeddingPipeline) {
28
+ try {
29
+ // Dynamic import to isolate native dependency issues (like sharp)
30
+ const { pipeline } = await import('@xenova/transformers');
31
+
32
+ // Using a compact but high-quality model for local embeddings
33
+ embeddingPipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
34
+ } catch (error) {
35
+ console.error('Failed to initialize embedding pipeline:', error);
36
+ throw error;
37
+ }
38
+ }
39
+ return embeddingPipeline;
40
+ }
41
+
42
+ /**
43
+ * Generate an embedding for a piece of text.
44
+ */
45
+ export async function generateEmbedding(text: string): Promise<number[]> {
46
+ try {
47
+ const extractor = await getPipeline();
48
+ const output = await extractor(text, { pooling: 'mean', normalize: true });
49
+ return Array.from(output.data);
50
+ } catch (error) {
51
+ console.warn('Semantic reasoning disabled: Embedding generation failed.', error);
52
+ return [];
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Calculate cosine similarity between two vectors.
58
+ */
59
+ export function cosineSimilarity(v1: number[], v2: number[]): number {
60
+ if (!v1 || !v2 || v1.length !== v2.length || v1.length === 0) return 0;
61
+
62
+ let dotProduct = 0;
63
+ let norm1 = 0;
64
+ let norm2 = 0;
65
+
66
+ for (let i = 0; i < v1.length; i++) {
67
+ dotProduct += v1[i] * v2[i];
68
+ norm1 += v1[i] * v1[i];
69
+ norm2 += v2[i] * v2[i];
70
+ }
71
+
72
+ const denominator = Math.sqrt(norm1) * Math.sqrt(norm2);
73
+ return denominator === 0 ? 0 : dotProduct / denominator;
74
+ }
75
+
76
+ /**
77
+ * Perform semantic search against a list of embeddings.
78
+ */
79
+ export function semanticSearch(queryVector: number[], entries: { embedding?: number[] }[]): number[] {
80
+ return entries.map(entry => {
81
+ if (!entry.embedding) return 0;
82
+ return cosineSimilarity(queryVector, entry.embedding);
83
+ });
84
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Pattern Index - Main Export
3
+ *
4
+ * This is the public API for the Pattern Index system.
5
+ */
6
+
7
+ // Types
8
+ export type {
9
+ PatternType,
10
+ PatternEntry,
11
+ PatternIndex,
12
+ PatternIndexConfig,
13
+ PatternIndexStats,
14
+ IndexedFile,
15
+ PatternMatchResult,
16
+ PatternMatch,
17
+ PatternOverride,
18
+ StalenessResult,
19
+ StalenessIssue,
20
+ DeprecationEntry
21
+ } from './types.js';
22
+
23
+ // Indexer
24
+ export {
25
+ PatternIndexer,
26
+ savePatternIndex,
27
+ loadPatternIndex,
28
+ getDefaultIndexPath
29
+ } from './indexer.js';
30
+
31
+ // Matcher
32
+ export {
33
+ PatternMatcher,
34
+ checkPatternDuplicate,
35
+ type MatcherConfig
36
+ } from './matcher.js';
37
+
38
+ // Staleness Detection
39
+ export {
40
+ StalenessDetector,
41
+ checkCodeStaleness
42
+ } from './staleness.js';
43
+
44
+ // Security Detection
45
+ export {
46
+ SecurityDetector
47
+ } from './security.js';
48
+
49
+ // Override Management
50
+ export {
51
+ OverrideManager,
52
+ loadConfigOverrides
53
+ } from './overrides.js';
54
+ // Embeddings
55
+ export {
56
+ generateEmbedding,
57
+ semanticSearch,
58
+ cosineSimilarity
59
+ } from './embeddings.js';
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Pattern Indexer Tests
3
+ *
4
+ * Comprehensive tests for the pattern indexer.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8
+ import * as fs from 'fs/promises';
9
+ import * as path from 'path';
10
+ import * as os from 'os';
11
+ import { PatternIndexer, savePatternIndex, loadPatternIndex } from './indexer.js';
12
+
13
+ describe('PatternIndexer', () => {
14
+ let testDir: string;
15
+
16
+ beforeEach(async () => {
17
+ // Create a temporary test directory
18
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-test-'));
19
+ await fs.mkdir(path.join(testDir, 'src'), { recursive: true });
20
+ });
21
+
22
+ afterEach(async () => {
23
+ // Clean up
24
+ await fs.rm(testDir, { recursive: true, force: true });
25
+ });
26
+
27
+ describe('buildIndex', () => {
28
+ it('should index function declarations', async () => {
29
+ await fs.writeFile(
30
+ path.join(testDir, 'src', 'utils.ts'),
31
+ `
32
+ /**
33
+ * Format a date to a readable string.
34
+ */
35
+ export function formatDate(date: Date): string {
36
+ return date.toISOString();
37
+ }
38
+ `
39
+ );
40
+
41
+ const indexer = new PatternIndexer(testDir);
42
+ const index = await indexer.buildIndex();
43
+
44
+ expect(index.patterns).toHaveLength(1);
45
+ expect(index.patterns[0].name).toBe('formatDate');
46
+ expect(index.patterns[0].type).toBe('function');
47
+ expect(index.patterns[0].exported).toBe(true);
48
+ expect(index.patterns[0].signature).toContain('date: Date');
49
+ expect(index.patterns[0].description).toContain('Format a date');
50
+ });
51
+
52
+ it('should index arrow functions', async () => {
53
+ await fs.writeFile(
54
+ path.join(testDir, 'src', 'helpers.ts'),
55
+ `
56
+ export const slugify = (text: string): string => {
57
+ return text.toLowerCase().replace(/\\s+/g, '-');
58
+ };
59
+ `
60
+ );
61
+
62
+ const indexer = new PatternIndexer(testDir);
63
+ const index = await indexer.buildIndex();
64
+
65
+ expect(index.patterns).toHaveLength(1);
66
+ expect(index.patterns[0].name).toBe('slugify');
67
+ expect(index.patterns[0].type).toBe('function');
68
+ });
69
+
70
+ it('should detect React hooks', async () => {
71
+ await fs.writeFile(
72
+ path.join(testDir, 'src', 'hooks.ts'),
73
+ `
74
+ export const useAuth = () => {
75
+ return { user: null, login: () => {} };
76
+ };
77
+ `
78
+ );
79
+
80
+ const indexer = new PatternIndexer(testDir);
81
+ const index = await indexer.buildIndex();
82
+
83
+ expect(index.patterns).toHaveLength(1);
84
+ expect(index.patterns[0].name).toBe('useAuth');
85
+ expect(index.patterns[0].type).toBe('hook');
86
+ });
87
+
88
+ it('should index classes', async () => {
89
+ await fs.writeFile(
90
+ path.join(testDir, 'src', 'services.ts'),
91
+ `
92
+ export class UserService {
93
+ getUser(id: string) {
94
+ return { id };
95
+ }
96
+ }
97
+ `
98
+ );
99
+
100
+ const indexer = new PatternIndexer(testDir);
101
+ const index = await indexer.buildIndex();
102
+
103
+ expect(index.patterns.some(p => p.name === 'UserService')).toBe(true);
104
+ expect(index.patterns.find(p => p.name === 'UserService')?.type).toBe('class');
105
+ });
106
+
107
+ it('should detect error classes', async () => {
108
+ await fs.writeFile(
109
+ path.join(testDir, 'src', 'errors.ts'),
110
+ `
111
+ export class ValidationError extends Error {
112
+ constructor(message: string) {
113
+ super(message);
114
+ }
115
+ }
116
+ `
117
+ );
118
+
119
+ const indexer = new PatternIndexer(testDir);
120
+ const index = await indexer.buildIndex();
121
+
122
+ expect(index.patterns[0].type).toBe('error');
123
+ });
124
+
125
+ it('should index interfaces', async () => {
126
+ await fs.writeFile(
127
+ path.join(testDir, 'src', 'types.ts'),
128
+ `
129
+ export interface User {
130
+ id: string;
131
+ name: string;
132
+ email: string;
133
+ }
134
+ `
135
+ );
136
+
137
+ const indexer = new PatternIndexer(testDir);
138
+ const index = await indexer.buildIndex();
139
+
140
+ expect(index.patterns).toHaveLength(1);
141
+ expect(index.patterns[0].name).toBe('User');
142
+ expect(index.patterns[0].type).toBe('interface');
143
+ });
144
+
145
+ it('should index type aliases', async () => {
146
+ await fs.writeFile(
147
+ path.join(testDir, 'src', 'types.ts'),
148
+ `
149
+ export type UserId = string;
150
+ `
151
+ );
152
+
153
+ const indexer = new PatternIndexer(testDir);
154
+ const index = await indexer.buildIndex();
155
+
156
+ expect(index.patterns).toHaveLength(1);
157
+ expect(index.patterns[0].type).toBe('type');
158
+ });
159
+
160
+ it('should index enums', async () => {
161
+ await fs.writeFile(
162
+ path.join(testDir, 'src', 'constants.ts'),
163
+ `
164
+ export enum Status {
165
+ Active = 'active',
166
+ Inactive = 'inactive'
167
+ }
168
+ `
169
+ );
170
+
171
+ const indexer = new PatternIndexer(testDir);
172
+ const index = await indexer.buildIndex();
173
+
174
+ expect(index.patterns).toHaveLength(1);
175
+ expect(index.patterns[0].name).toBe('Status');
176
+ expect(index.patterns[0].type).toBe('enum');
177
+ });
178
+
179
+ it('should index constants (all caps)', async () => {
180
+ await fs.writeFile(
181
+ path.join(testDir, 'src', 'config.ts'),
182
+ `
183
+ export const API_URL = 'https://api.example.com';
184
+ export const MAX_RETRIES = 3;
185
+ `
186
+ );
187
+
188
+ const indexer = new PatternIndexer(testDir);
189
+ const index = await indexer.buildIndex();
190
+
191
+ const constants = index.patterns.filter(p => p.type === 'constant');
192
+ expect(constants.length).toBeGreaterThanOrEqual(1);
193
+ expect(constants.some(c => c.name === 'API_URL')).toBe(true);
194
+ });
195
+
196
+ it('should track index statistics', async () => {
197
+ await fs.writeFile(
198
+ path.join(testDir, 'src', 'utils.ts'),
199
+ `
200
+ export function foo() {}
201
+ export function bar() {}
202
+ export interface Baz {}
203
+ `
204
+ );
205
+
206
+ const indexer = new PatternIndexer(testDir);
207
+ const index = await indexer.buildIndex();
208
+
209
+ expect(index.stats.totalPatterns).toBe(3);
210
+ expect(index.stats.totalFiles).toBe(1);
211
+ expect(index.stats.indexDurationMs).toBeGreaterThan(0);
212
+ });
213
+
214
+ it('should exclude test files by default', async () => {
215
+ await fs.writeFile(path.join(testDir, 'src', 'utils.ts'), 'export function main() {}');
216
+ await fs.writeFile(path.join(testDir, 'src', 'utils.test.ts'), 'export function testMain() {}');
217
+
218
+ const indexer = new PatternIndexer(testDir);
219
+ const index = await indexer.buildIndex();
220
+
221
+ expect(index.patterns.every(p => !p.file.includes('.test.'))).toBe(true);
222
+ });
223
+ });
224
+
225
+ describe('updateIndex (incremental)', () => {
226
+ it('should only reindex changed files', async () => {
227
+ // Initial index
228
+ await fs.writeFile(
229
+ path.join(testDir, 'src', 'a.ts'),
230
+ 'export function funcA() {}'
231
+ );
232
+ await fs.writeFile(
233
+ path.join(testDir, 'src', 'b.ts'),
234
+ 'export function funcB() {}'
235
+ );
236
+
237
+ const indexer = new PatternIndexer(testDir);
238
+ const initialIndex = await indexer.buildIndex();
239
+ expect(initialIndex.patterns).toHaveLength(2);
240
+
241
+ // Modify only one file
242
+ await fs.writeFile(
243
+ path.join(testDir, 'src', 'a.ts'),
244
+ 'export function funcA() {} export function funcA2() {}'
245
+ );
246
+
247
+ const updatedIndex = await indexer.updateIndex(initialIndex);
248
+ expect(updatedIndex.patterns).toHaveLength(3);
249
+ });
250
+ });
251
+
252
+ describe('savePatternIndex / loadPatternIndex', () => {
253
+ it('should save and load index correctly', async () => {
254
+ await fs.writeFile(
255
+ path.join(testDir, 'src', 'utils.ts'),
256
+ 'export function myFunc() {}'
257
+ );
258
+
259
+ const indexer = new PatternIndexer(testDir);
260
+ const index = await indexer.buildIndex();
261
+
262
+ const indexPath = path.join(testDir, '.rigour', 'patterns.json');
263
+ await savePatternIndex(index, indexPath);
264
+
265
+ const loaded = await loadPatternIndex(indexPath);
266
+ expect(loaded).not.toBeNull();
267
+ expect(loaded!.patterns).toHaveLength(index.patterns.length);
268
+ expect(loaded!.version).toBe(index.version);
269
+ });
270
+
271
+ it('should return null for non-existent index', async () => {
272
+ const loaded = await loadPatternIndex('/non/existent/path.json');
273
+ expect(loaded).toBeNull();
274
+ });
275
+ });
276
+ });