@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,71 @@
1
+ /**
2
+ * Staleness Detector
3
+ *
4
+ * Detects when AI is suggesting deprecated or outdated patterns.
5
+ * Uses package.json analysis and a deprecation database.
6
+ */
7
+ import type { StalenessResult, StalenessIssue, DeprecationEntry } from './types.js';
8
+ /**
9
+ * Project context extracted from package.json and other config files.
10
+ */
11
+ interface ProjectContext {
12
+ dependencies: Record<string, string>;
13
+ devDependencies: Record<string, string>;
14
+ nodeVersion?: string;
15
+ typescriptVersion?: string;
16
+ }
17
+ /**
18
+ * Staleness Detector class.
19
+ */
20
+ export declare class StalenessDetector {
21
+ private deprecations;
22
+ private projectContext;
23
+ private rootDir;
24
+ private remoteRulesUrl;
25
+ constructor(rootDir: string, customDeprecations?: DeprecationEntry[]);
26
+ /**
27
+ * Fetch latest deprecation rules from Rigour's remote registry.
28
+ * This ensures the tool stays up-to-date even without a package update.
29
+ */
30
+ syncRemoteRules(): Promise<number>;
31
+ /**
32
+ * Check NPM registry for live deprecation status of project dependencies.
33
+ * This is the ultimate "up-to-date" check.
34
+ */
35
+ checkLiveRegistry(context: ProjectContext): Promise<StalenessIssue[]>;
36
+ /**
37
+ * Load project context from package.json.
38
+ */
39
+ loadProjectContext(): Promise<ProjectContext>;
40
+ /**
41
+ * Check code for staleness issues.
42
+ */
43
+ checkStaleness(code: string, filePath?: string, options?: {
44
+ live?: boolean;
45
+ }): Promise<StalenessResult>;
46
+ /**
47
+ * Check if project has a library.
48
+ */
49
+ private hasLibrary;
50
+ /**
51
+ * Get installed version of a library.
52
+ */
53
+ private getInstalledVersion;
54
+ /**
55
+ * Add custom deprecation rules.
56
+ */
57
+ addDeprecation(entry: DeprecationEntry): void;
58
+ /**
59
+ * Load deprecations from a YAML file.
60
+ */
61
+ loadDeprecationsFromFile(filePath: string): Promise<void>;
62
+ /**
63
+ * Get all deprecations for display.
64
+ */
65
+ getAllDeprecations(): DeprecationEntry[];
66
+ }
67
+ /**
68
+ * Quick helper to check code for staleness.
69
+ */
70
+ export declare function checkCodeStaleness(rootDir: string, code: string): Promise<StalenessResult>;
71
+ export {};
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Staleness Detector
3
+ *
4
+ * Detects when AI is suggesting deprecated or outdated patterns.
5
+ * Uses package.json analysis and a deprecation database.
6
+ */
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import semver from 'semver';
10
+ /**
11
+ * Built-in deprecation database.
12
+ * This is bundled with Rigour and updated with releases.
13
+ */
14
+ const BUILT_IN_DEPRECATIONS = [
15
+ // React deprecations
16
+ {
17
+ pattern: 'componentWillMount',
18
+ library: 'react',
19
+ deprecatedIn: '16.3.0',
20
+ replacement: 'useEffect(() => { ... }, [])',
21
+ severity: 'error',
22
+ reason: 'Unsafe lifecycle method removed in React 18',
23
+ docs: 'https://react.dev/reference/react/Component#unsafe_componentwillmount'
24
+ },
25
+ {
26
+ pattern: 'componentWillReceiveProps',
27
+ library: 'react',
28
+ deprecatedIn: '16.3.0',
29
+ replacement: 'getDerivedStateFromProps or useEffect',
30
+ severity: 'error',
31
+ reason: 'Unsafe lifecycle method removed in React 18'
32
+ },
33
+ {
34
+ pattern: 'componentWillUpdate',
35
+ library: 'react',
36
+ deprecatedIn: '16.3.0',
37
+ replacement: 'getSnapshotBeforeUpdate or useEffect',
38
+ severity: 'error',
39
+ reason: 'Unsafe lifecycle method removed in React 18'
40
+ },
41
+ {
42
+ pattern: 'UNSAFE_componentWillMount',
43
+ library: 'react',
44
+ deprecatedIn: '18.0.0',
45
+ replacement: 'useEffect(() => { ... }, [])',
46
+ severity: 'warning',
47
+ reason: 'Prepare for React 19 removal'
48
+ },
49
+ {
50
+ pattern: 'ReactDOM.render',
51
+ library: 'react-dom',
52
+ deprecatedIn: '18.0.0',
53
+ replacement: 'createRoot(container).render(<App />)',
54
+ severity: 'error',
55
+ reason: 'Legacy root API deprecated in React 18'
56
+ },
57
+ {
58
+ pattern: 'ReactDOM.hydrate',
59
+ library: 'react-dom',
60
+ deprecatedIn: '18.0.0',
61
+ replacement: 'hydrateRoot(container, <App />)',
62
+ severity: 'error',
63
+ reason: 'Legacy hydration API deprecated in React 18'
64
+ },
65
+ // Package deprecations
66
+ {
67
+ pattern: "import.*from ['\"]moment['\"]",
68
+ deprecatedIn: 'ecosystem',
69
+ replacement: "import { format } from 'date-fns'",
70
+ severity: 'warning',
71
+ reason: 'moment.js is in maintenance mode since September 2020',
72
+ docs: 'https://momentjs.com/docs/#/-project-status/'
73
+ },
74
+ {
75
+ pattern: "require\\(['\"]request['\"]\\)",
76
+ deprecatedIn: 'ecosystem',
77
+ replacement: 'Use native fetch or axios',
78
+ severity: 'error',
79
+ reason: 'request package deprecated in February 2020'
80
+ },
81
+ {
82
+ pattern: "import.*from ['\"]request['\"]",
83
+ deprecatedIn: 'ecosystem',
84
+ replacement: 'Use native fetch or axios',
85
+ severity: 'error',
86
+ reason: 'request package deprecated in February 2020'
87
+ },
88
+ // JavaScript/TypeScript deprecations
89
+ {
90
+ pattern: '\\bvar\\s+\\w+\\s*=',
91
+ deprecatedIn: 'es6',
92
+ replacement: 'Use const or let',
93
+ severity: 'warning',
94
+ reason: 'var has function scope which leads to bugs. Use block-scoped const/let'
95
+ },
96
+ // Redux deprecations
97
+ {
98
+ pattern: 'createStore\\(',
99
+ library: 'redux',
100
+ deprecatedIn: '4.2.0',
101
+ replacement: "configureStore from '@reduxjs/toolkit'",
102
+ severity: 'warning',
103
+ reason: 'Redux Toolkit is now the recommended way',
104
+ docs: 'https://redux.js.org/introduction/why-rtk-is-redux-today'
105
+ },
106
+ // Node.js deprecations
107
+ {
108
+ pattern: 'new Buffer\\(',
109
+ deprecatedIn: 'node@6.0.0',
110
+ replacement: 'Buffer.alloc() or Buffer.from()',
111
+ severity: 'error',
112
+ reason: 'Buffer constructor is a security hazard'
113
+ },
114
+ // Express deprecations
115
+ {
116
+ pattern: 'app\\.del\\(',
117
+ library: 'express',
118
+ deprecatedIn: '4.0.0',
119
+ replacement: 'app.delete()',
120
+ severity: 'warning',
121
+ reason: 'app.del() was renamed to app.delete()'
122
+ },
123
+ // TypeScript patterns to avoid
124
+ {
125
+ pattern: '\\benum\\s+\\w+',
126
+ deprecatedIn: 'best-practice',
127
+ replacement: 'const object with as const assertion',
128
+ severity: 'info',
129
+ reason: 'Enums have quirks. Consider using const objects for better tree-shaking',
130
+ docs: 'https://www.typescriptlang.org/docs/handbook/enums.html#const-enums'
131
+ },
132
+ // Next.js deprecations
133
+ {
134
+ pattern: 'getInitialProps',
135
+ library: 'next',
136
+ deprecatedIn: '13.0.0',
137
+ replacement: 'getServerSideProps or App Router with async components',
138
+ severity: 'warning',
139
+ reason: 'getInitialProps prevents static optimization'
140
+ },
141
+ {
142
+ pattern: "from ['\"]next/router['\"]",
143
+ library: 'next',
144
+ deprecatedIn: '13.0.0',
145
+ replacement: "useRouter from 'next/navigation' in App Router",
146
+ severity: 'info',
147
+ reason: 'Use next/navigation for App Router projects'
148
+ }
149
+ ];
150
+ /**
151
+ * Staleness Detector class.
152
+ */
153
+ export class StalenessDetector {
154
+ deprecations;
155
+ projectContext = null;
156
+ rootDir;
157
+ remoteRulesUrl = 'https://raw.githubusercontent.com/rigour-labs/rules/main/deprecations.json';
158
+ constructor(rootDir, customDeprecations = []) {
159
+ this.rootDir = rootDir;
160
+ this.deprecations = [...BUILT_IN_DEPRECATIONS, ...customDeprecations];
161
+ }
162
+ /**
163
+ * Fetch latest deprecation rules from Rigour's remote registry.
164
+ * This ensures the tool stays up-to-date even without a package update.
165
+ */
166
+ async syncRemoteRules() {
167
+ try {
168
+ // Using dynamic import for fetch to avoid Node < 18 issues
169
+ const response = await fetch(this.remoteRulesUrl);
170
+ if (!response.ok)
171
+ throw new Error(`HTTP error! status: ${response.status}`);
172
+ const data = await response.json();
173
+ if (data.deprecations && Array.isArray(data.deprecations)) {
174
+ // Merge remote rules, avoiding duplicates
175
+ const existingPatterns = new Set(this.deprecations.map(d => d.pattern));
176
+ const newRules = data.deprecations.filter((d) => !existingPatterns.has(d.pattern));
177
+ this.deprecations.push(...newRules);
178
+ return newRules.length;
179
+ }
180
+ return 0;
181
+ }
182
+ catch (error) {
183
+ console.warn('Failed to sync remote rules, using built-in database:', error);
184
+ return 0;
185
+ }
186
+ }
187
+ /**
188
+ * Check NPM registry for live deprecation status of project dependencies.
189
+ * This is the ultimate "up-to-date" check.
190
+ */
191
+ async checkLiveRegistry(context) {
192
+ const issues = [];
193
+ const { execa } = await import('execa');
194
+ // We only check top-level dependencies to avoid noise/performance hits
195
+ const deps = Object.keys(context.dependencies);
196
+ for (const dep of deps) {
197
+ try {
198
+ // Run 'npm info <package> --json' to get metadata
199
+ const { stdout } = await execa('npm', ['info', dep, '--json']);
200
+ const info = JSON.parse(stdout);
201
+ // 1. Check if package is deprecated
202
+ if (info.deprecated) {
203
+ issues.push({
204
+ line: 0, // Package-level
205
+ pattern: dep,
206
+ severity: 'error',
207
+ reason: `Package "${dep}" is marked as DEPRECATED in NPM registry: ${info.deprecated}`,
208
+ replacement: 'Check package README for suggested alternatives',
209
+ docs: `https://www.npmjs.com/package/${dep}`
210
+ });
211
+ }
212
+ // 2. Check for latest version staleness
213
+ const current = context.dependencies[dep].replace(/^[\^~>=<]+/, '');
214
+ const latest = info['dist-tags']?.latest;
215
+ if (latest && semver.major(latest) > semver.major(current)) {
216
+ issues.push({
217
+ line: 0,
218
+ pattern: dep,
219
+ severity: 'info',
220
+ reason: `Package "${dep}" has a new major version available (${latest}). Your version: ${current}`,
221
+ replacement: `npm install ${dep}@latest`,
222
+ docs: `https://www.npmjs.com/package/${dep}`
223
+ });
224
+ }
225
+ }
226
+ catch (error) {
227
+ // Silently skip if npm check fails
228
+ continue;
229
+ }
230
+ }
231
+ return issues;
232
+ }
233
+ /**
234
+ * Load project context from package.json.
235
+ */
236
+ async loadProjectContext() {
237
+ if (this.projectContext) {
238
+ return this.projectContext;
239
+ }
240
+ const pkgPath = path.join(this.rootDir, 'package.json');
241
+ try {
242
+ const content = await fs.readFile(pkgPath, 'utf-8');
243
+ const pkg = JSON.parse(content);
244
+ this.projectContext = {
245
+ dependencies: pkg.dependencies || {},
246
+ devDependencies: pkg.devDependencies || {},
247
+ nodeVersion: pkg.engines?.node,
248
+ typescriptVersion: (pkg.devDependencies?.typescript || pkg.dependencies?.typescript)
249
+ };
250
+ return this.projectContext;
251
+ }
252
+ catch {
253
+ this.projectContext = {
254
+ dependencies: {},
255
+ devDependencies: {}
256
+ };
257
+ return this.projectContext;
258
+ }
259
+ }
260
+ /**
261
+ * Check code for staleness issues.
262
+ */
263
+ async checkStaleness(code, filePath, options = {}) {
264
+ const context = await this.loadProjectContext();
265
+ const issues = [];
266
+ // 1. Check built-in/remote rules
267
+ const lines = code.split('\n');
268
+ for (const deprecation of this.deprecations) {
269
+ // Check if this deprecation applies to the project
270
+ if (deprecation.library && !this.hasLibrary(deprecation.library, context)) {
271
+ continue;
272
+ }
273
+ // Check version constraints
274
+ if (deprecation.library && deprecation.deprecatedIn !== 'ecosystem' &&
275
+ deprecation.deprecatedIn !== 'best-practice' &&
276
+ deprecation.deprecatedIn !== 'es6') {
277
+ const installed = this.getInstalledVersion(deprecation.library, context);
278
+ if (installed && !semver.gte(semver.coerce(installed) || '0.0.0', semver.coerce(deprecation.deprecatedIn) || '0.0.0')) {
279
+ // Project is on older version where this isn't deprecated yet
280
+ continue;
281
+ }
282
+ }
283
+ // Check for pattern match
284
+ const regex = new RegExp(deprecation.pattern, 'g');
285
+ for (let i = 0; i < lines.length; i++) {
286
+ if (regex.test(lines[i])) {
287
+ issues.push({
288
+ line: i + 1,
289
+ pattern: deprecation.pattern,
290
+ severity: deprecation.severity,
291
+ reason: deprecation.reason || `Deprecated in ${deprecation.deprecatedIn}`,
292
+ replacement: deprecation.replacement,
293
+ docs: deprecation.docs
294
+ });
295
+ }
296
+ regex.lastIndex = 0;
297
+ }
298
+ }
299
+ // 2. Perform live registry check if requested (only once per run/file usually)
300
+ if (options.live) {
301
+ const liveIssues = await this.checkLiveRegistry(context);
302
+ issues.push(...liveIssues);
303
+ }
304
+ // Determine overall status
305
+ let status = 'FRESH';
306
+ if (issues.some(i => i.severity === 'error')) {
307
+ status = 'DEPRECATED';
308
+ }
309
+ else if (issues.some(i => i.severity === 'warning')) {
310
+ status = 'STALE';
311
+ }
312
+ // Build project context for response
313
+ const projectContextOutput = {};
314
+ for (const [name, version] of Object.entries(context.dependencies)) {
315
+ if (['react', 'react-dom', 'next', 'typescript', 'redux', 'express'].includes(name)) {
316
+ projectContextOutput[name] = version;
317
+ }
318
+ }
319
+ for (const [name, version] of Object.entries(context.devDependencies)) {
320
+ if (['typescript'].includes(name)) {
321
+ projectContextOutput[name] = version;
322
+ }
323
+ }
324
+ return {
325
+ status,
326
+ issues,
327
+ projectContext: projectContextOutput
328
+ };
329
+ }
330
+ /**
331
+ * Check if project has a library.
332
+ */
333
+ hasLibrary(library, context) {
334
+ return library in context.dependencies || library in context.devDependencies;
335
+ }
336
+ /**
337
+ * Get installed version of a library.
338
+ */
339
+ getInstalledVersion(library, context) {
340
+ const version = context.dependencies[library] || context.devDependencies[library];
341
+ if (!version)
342
+ return null;
343
+ // Remove version prefix (^, ~, >=, etc.)
344
+ return version.replace(/^[\^~>=<]+/, '');
345
+ }
346
+ /**
347
+ * Add custom deprecation rules.
348
+ */
349
+ addDeprecation(entry) {
350
+ this.deprecations.push(entry);
351
+ }
352
+ /**
353
+ * Load deprecations from a YAML file.
354
+ */
355
+ async loadDeprecationsFromFile(filePath) {
356
+ try {
357
+ const content = await fs.readFile(filePath, 'utf-8');
358
+ const { parse } = await import('yaml');
359
+ const data = parse(content);
360
+ if (data.deprecations && Array.isArray(data.deprecations)) {
361
+ this.deprecations.push(...data.deprecations);
362
+ }
363
+ }
364
+ catch (error) {
365
+ console.error(`Failed to load deprecations from ${filePath}:`, error);
366
+ }
367
+ }
368
+ /**
369
+ * Get all deprecations for display.
370
+ */
371
+ getAllDeprecations() {
372
+ return [...this.deprecations];
373
+ }
374
+ }
375
+ /**
376
+ * Quick helper to check code for staleness.
377
+ */
378
+ export async function checkCodeStaleness(rootDir, code) {
379
+ const detector = new StalenessDetector(rootDir);
380
+ return detector.checkStaleness(code);
381
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Staleness Detector Tests
3
+ *
4
+ * Tests for the staleness detection system.
5
+ */
6
+ export {};
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Staleness Detector Tests
3
+ *
4
+ * Tests for the staleness detection system.
5
+ */
6
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { StalenessDetector, checkCodeStaleness } from './staleness.js';
11
+ describe('StalenessDetector', () => {
12
+ let testDir;
13
+ beforeEach(async () => {
14
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-staleness-'));
15
+ });
16
+ afterEach(async () => {
17
+ await fs.rm(testDir, { recursive: true, force: true });
18
+ });
19
+ describe('React deprecations', () => {
20
+ it('should detect componentWillMount', async () => {
21
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { react: '^18.0.0' } }));
22
+ const detector = new StalenessDetector(testDir);
23
+ const result = await detector.checkStaleness(`
24
+ class MyComponent extends React.Component {
25
+ componentWillMount() {
26
+ console.log('mounting');
27
+ }
28
+ }
29
+ `);
30
+ expect(result.status).toBe('DEPRECATED');
31
+ expect(result.issues.some(i => i.pattern.includes('componentWillMount'))).toBe(true);
32
+ expect(result.issues[0].replacement).toContain('useEffect');
33
+ });
34
+ it('should detect ReactDOM.render', async () => {
35
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' } }));
36
+ const detector = new StalenessDetector(testDir);
37
+ const result = await detector.checkStaleness(`
38
+ ReactDOM.render(<App />, document.getElementById('root'));
39
+ `);
40
+ expect(result.status).toBe('DEPRECATED');
41
+ expect(result.issues.some(i => i.pattern.includes('ReactDOM.render'))).toBe(true);
42
+ expect(result.issues[0].replacement).toContain('createRoot');
43
+ });
44
+ it('should not flag React deprecations if React is not installed', async () => {
45
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { express: '^4.0.0' } }));
46
+ const detector = new StalenessDetector(testDir);
47
+ const result = await detector.checkStaleness(`
48
+ componentWillMount() {}
49
+ `);
50
+ // Should not flag React-specific patterns if React isn't a dependency
51
+ expect(result.issues.filter(i => i.pattern.includes('componentWillMount'))).toHaveLength(0);
52
+ });
53
+ });
54
+ describe('Package deprecations', () => {
55
+ it('should detect moment.js usage', async () => {
56
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
57
+ const detector = new StalenessDetector(testDir);
58
+ const result = await detector.checkStaleness(`
59
+ import moment from 'moment';
60
+ const date = moment().format('YYYY-MM-DD');
61
+ `);
62
+ expect(result.status).toBe('STALE');
63
+ expect(result.issues.some(i => i.replacement.includes('date-fns'))).toBe(true);
64
+ });
65
+ it('should detect request library usage', async () => {
66
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
67
+ const detector = new StalenessDetector(testDir);
68
+ const result = await detector.checkStaleness(`
69
+ const request = require('request');
70
+ request.get('https://example.com');
71
+ `);
72
+ expect(result.status).toBe('DEPRECATED');
73
+ expect(result.issues.some(i => i.replacement.includes('fetch'))).toBe(true);
74
+ });
75
+ });
76
+ describe('JavaScript deprecations', () => {
77
+ it('should detect var keyword usage', async () => {
78
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
79
+ const detector = new StalenessDetector(testDir);
80
+ const result = await detector.checkStaleness(`
81
+ var name = 'test';
82
+ var count = 0;
83
+ `);
84
+ expect(result.issues.some(i => i.replacement.includes('const') || i.replacement.includes('let'))).toBe(true);
85
+ });
86
+ });
87
+ describe('Redux deprecations', () => {
88
+ it('should detect createStore usage in modern Redux projects', async () => {
89
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { redux: '^4.2.0' } }));
90
+ const detector = new StalenessDetector(testDir);
91
+ const result = await detector.checkStaleness(`
92
+ import { createStore } from 'redux';
93
+ const store = createStore(reducer);
94
+ `);
95
+ expect(result.status).toBe('STALE');
96
+ expect(result.issues.some(i => i.replacement.includes('configureStore'))).toBe(true);
97
+ });
98
+ });
99
+ describe('Node.js deprecations', () => {
100
+ it('should detect Buffer constructor', async () => {
101
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
102
+ const detector = new StalenessDetector(testDir);
103
+ const result = await detector.checkStaleness(`
104
+ const buf = new Buffer('hello');
105
+ `);
106
+ expect(result.status).toBe('DEPRECATED');
107
+ expect(result.issues.some(i => i.replacement.includes('Buffer.from'))).toBe(true);
108
+ });
109
+ });
110
+ describe('Next.js deprecations', () => {
111
+ it('should detect getInitialProps in Next 13+ projects', async () => {
112
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { next: '^13.0.0' } }));
113
+ const detector = new StalenessDetector(testDir);
114
+ const result = await detector.checkStaleness(`
115
+ MyPage.getInitialProps = async (ctx) => {
116
+ return { data: {} };
117
+ };
118
+ `);
119
+ expect(result.issues.some(i => i.pattern.includes('getInitialProps'))).toBe(true);
120
+ });
121
+ });
122
+ describe('TypeScript best practices', () => {
123
+ it('should warn about enum usage', async () => {
124
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
125
+ const detector = new StalenessDetector(testDir);
126
+ const result = await detector.checkStaleness(`
127
+ enum Status {
128
+ Active = 'active',
129
+ Inactive = 'inactive'
130
+ }
131
+ `);
132
+ expect(result.issues.some(i => i.severity === 'info' && i.pattern.includes('enum'))).toBe(true);
133
+ });
134
+ });
135
+ describe('Project context', () => {
136
+ it('should return project versions in context', async () => {
137
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({
138
+ dependencies: {
139
+ react: '^18.2.0',
140
+ next: '^14.0.0'
141
+ },
142
+ devDependencies: {
143
+ typescript: '^5.0.0'
144
+ }
145
+ }));
146
+ const detector = new StalenessDetector(testDir);
147
+ const result = await detector.checkStaleness('const x = 1;');
148
+ expect(result.projectContext.react).toBeDefined();
149
+ expect(result.projectContext.next).toBeDefined();
150
+ expect(result.projectContext.typescript).toBeDefined();
151
+ });
152
+ });
153
+ describe('Custom deprecations', () => {
154
+ it('should allow adding custom deprecation rules', async () => {
155
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
156
+ const detector = new StalenessDetector(testDir);
157
+ detector.addDeprecation({
158
+ pattern: 'legacyFunction',
159
+ deprecatedIn: '1.0.0',
160
+ replacement: 'newFunction()',
161
+ severity: 'error',
162
+ reason: 'Custom deprecation'
163
+ });
164
+ const result = await detector.checkStaleness(`
165
+ legacyFunction();
166
+ `);
167
+ expect(result.issues.some(i => i.replacement === 'newFunction()')).toBe(true);
168
+ });
169
+ });
170
+ describe('Overall status', () => {
171
+ it('should return FRESH when no issues found', async () => {
172
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
173
+ const detector = new StalenessDetector(testDir);
174
+ const result = await detector.checkStaleness(`
175
+ const name = 'test';
176
+ const greet = () => console.log('Hello');
177
+ `);
178
+ expect(result.status).toBe('FRESH');
179
+ });
180
+ it('should return STALE for warnings', async () => {
181
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: { redux: '^4.2.0' } }));
182
+ const detector = new StalenessDetector(testDir);
183
+ const result = await detector.checkStaleness(`
184
+ createStore(reducer);
185
+ `);
186
+ expect(result.status).toBe('STALE');
187
+ });
188
+ it('should return DEPRECATED for errors', async () => {
189
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
190
+ const detector = new StalenessDetector(testDir);
191
+ const result = await detector.checkStaleness(`
192
+ new Buffer('hello');
193
+ `);
194
+ expect(result.status).toBe('DEPRECATED');
195
+ });
196
+ });
197
+ });
198
+ describe('checkCodeStaleness', () => {
199
+ let testDir;
200
+ beforeEach(async () => {
201
+ testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-staleness-'));
202
+ await fs.writeFile(path.join(testDir, 'package.json'), JSON.stringify({ dependencies: {} }));
203
+ });
204
+ afterEach(async () => {
205
+ await fs.rm(testDir, { recursive: true, force: true });
206
+ });
207
+ it('should be a quick helper for staleness checking', async () => {
208
+ const result = await checkCodeStaleness(testDir, 'const x = 1;');
209
+ expect(result.status).toBe('FRESH');
210
+ });
211
+ });