@rigour-labs/core 2.22.0 → 3.0.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.
- package/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +3 -1
- package/dist/gates/base.js +3 -0
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/context-window-artifacts.d.ts +2 -1
- package/dist/gates/context-window-artifacts.js +6 -3
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +1 -0
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +2 -1
- package/dist/gates/duplication-drift.js +4 -1
- package/dist/gates/environment.js +4 -4
- package/dist/gates/hallucinated-imports.d.ts +21 -2
- package/dist/gates/hallucinated-imports.js +116 -2
- package/dist/gates/inconsistent-error-handling.d.ts +2 -1
- package/dist/gates/inconsistent-error-handling.js +21 -7
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +34 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns.d.ts +2 -1
- package/dist/gates/security-patterns.js +1 -0
- package/dist/gates/structure.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +169 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +153 -0
- package/dist/types/index.js +19 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -28
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -51
- package/src/gates/context-window-artifacts.ts +0 -277
- package/src/gates/context.ts +0 -270
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/duplication-drift.ts +0 -231
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -46
- package/src/gates/hallucinated-imports.ts +0 -361
- package/src/gates/inconsistent-error-handling.ts +0 -254
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -188
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -306
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -338
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -200
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Human Override Manager
|
|
3
|
-
*
|
|
4
|
-
* Manages human overrides for pattern matching.
|
|
5
|
-
* Supports inline comments, config files, and MCP approvals.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from 'fs/promises';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import type { PatternOverride } from './types.js';
|
|
11
|
-
|
|
12
|
-
/** Default override expiration in days */
|
|
13
|
-
const DEFAULT_EXPIRATION_DAYS = 30;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Override Manager class.
|
|
17
|
-
*/
|
|
18
|
-
export class OverrideManager {
|
|
19
|
-
private overrides: PatternOverride[] = [];
|
|
20
|
-
private rootDir: string;
|
|
21
|
-
private overridesPath: string;
|
|
22
|
-
|
|
23
|
-
constructor(rootDir: string) {
|
|
24
|
-
this.rootDir = rootDir;
|
|
25
|
-
this.overridesPath = path.join(rootDir, '.rigour', 'allow.json');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Load overrides from disk.
|
|
30
|
-
*/
|
|
31
|
-
async load(): Promise<PatternOverride[]> {
|
|
32
|
-
try {
|
|
33
|
-
const content = await fs.readFile(this.overridesPath, 'utf-8');
|
|
34
|
-
const data = JSON.parse(content);
|
|
35
|
-
this.overrides = data.overrides || [];
|
|
36
|
-
|
|
37
|
-
// Filter out expired overrides
|
|
38
|
-
this.overrides = this.overrides.filter(o => !this.isExpired(o));
|
|
39
|
-
|
|
40
|
-
return this.overrides;
|
|
41
|
-
} catch {
|
|
42
|
-
this.overrides = [];
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Save overrides to disk.
|
|
49
|
-
*/
|
|
50
|
-
async save(): Promise<void> {
|
|
51
|
-
const dir = path.dirname(this.overridesPath);
|
|
52
|
-
await fs.mkdir(dir, { recursive: true });
|
|
53
|
-
|
|
54
|
-
await fs.writeFile(
|
|
55
|
-
this.overridesPath,
|
|
56
|
-
JSON.stringify({ overrides: this.overrides }, null, 2),
|
|
57
|
-
'utf-8'
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Add a new override.
|
|
63
|
-
*/
|
|
64
|
-
async addOverride(override: Omit<PatternOverride, 'createdAt'>): Promise<PatternOverride> {
|
|
65
|
-
const fullOverride: PatternOverride = {
|
|
66
|
-
...override,
|
|
67
|
-
createdAt: new Date().toISOString(),
|
|
68
|
-
expiresAt: override.expiresAt || this.getDefaultExpiration()
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Remove any existing override for the same pattern
|
|
72
|
-
this.overrides = this.overrides.filter(o => o.pattern !== fullOverride.pattern);
|
|
73
|
-
|
|
74
|
-
this.overrides.push(fullOverride);
|
|
75
|
-
await this.save();
|
|
76
|
-
|
|
77
|
-
return fullOverride;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Remove an override.
|
|
82
|
-
*/
|
|
83
|
-
async removeOverride(pattern: string): Promise<boolean> {
|
|
84
|
-
const before = this.overrides.length;
|
|
85
|
-
this.overrides = this.overrides.filter(o => o.pattern !== pattern);
|
|
86
|
-
|
|
87
|
-
if (this.overrides.length !== before) {
|
|
88
|
-
await this.save();
|
|
89
|
-
return true;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Check if a pattern is overridden.
|
|
97
|
-
*/
|
|
98
|
-
isOverridden(name: string): PatternOverride | null {
|
|
99
|
-
for (const override of this.overrides) {
|
|
100
|
-
if (this.isExpired(override)) continue;
|
|
101
|
-
|
|
102
|
-
// Exact match
|
|
103
|
-
if (override.pattern === name) {
|
|
104
|
-
return override;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Glob match
|
|
108
|
-
if (override.pattern.includes('*')) {
|
|
109
|
-
const regex = new RegExp(
|
|
110
|
-
'^' + override.pattern.replace(/\*/g, '.*') + '$'
|
|
111
|
-
);
|
|
112
|
-
if (regex.test(name)) {
|
|
113
|
-
return override;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Get all active overrides.
|
|
123
|
-
*/
|
|
124
|
-
getActiveOverrides(): PatternOverride[] {
|
|
125
|
-
return this.overrides.filter(o => !this.isExpired(o));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Get all expired overrides.
|
|
130
|
-
*/
|
|
131
|
-
getExpiredOverrides(): PatternOverride[] {
|
|
132
|
-
return this.overrides.filter(o => this.isExpired(o));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Clean up expired overrides.
|
|
137
|
-
*/
|
|
138
|
-
async cleanupExpired(): Promise<number> {
|
|
139
|
-
const before = this.overrides.length;
|
|
140
|
-
this.overrides = this.overrides.filter(o => !this.isExpired(o));
|
|
141
|
-
|
|
142
|
-
if (this.overrides.length !== before) {
|
|
143
|
-
await this.save();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return before - this.overrides.length;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Parse inline override comments from code.
|
|
151
|
-
*/
|
|
152
|
-
parseInlineOverrides(code: string, filePath: string): PatternOverride[] {
|
|
153
|
-
const overrides: PatternOverride[] = [];
|
|
154
|
-
const lines = code.split('\n');
|
|
155
|
-
|
|
156
|
-
for (let i = 0; i < lines.length; i++) {
|
|
157
|
-
const line = lines[i];
|
|
158
|
-
|
|
159
|
-
// Match: // rigour-allow: pattern-name
|
|
160
|
-
// Or: // rigour-allow: pattern-name (reason)
|
|
161
|
-
const match = line.match(/\/\/\s*rigour-allow:\s*(\S+)(?:\s*\(([^)]+)\))?/i);
|
|
162
|
-
|
|
163
|
-
if (match) {
|
|
164
|
-
overrides.push({
|
|
165
|
-
pattern: match[1],
|
|
166
|
-
reason: match[2] || `Inline override in ${filePath}:${i + 1}`,
|
|
167
|
-
createdAt: new Date().toISOString(),
|
|
168
|
-
approvedBy: `inline:${filePath}:${i + 1}`
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return overrides;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Check if an override is expired.
|
|
178
|
-
*/
|
|
179
|
-
private isExpired(override: PatternOverride): boolean {
|
|
180
|
-
if (!override.expiresAt) return false;
|
|
181
|
-
return new Date(override.expiresAt) < new Date();
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Get default expiration date.
|
|
186
|
-
*/
|
|
187
|
-
private getDefaultExpiration(): string {
|
|
188
|
-
const date = new Date();
|
|
189
|
-
date.setDate(date.getDate() + DEFAULT_EXPIRATION_DAYS);
|
|
190
|
-
return date.toISOString();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Load overrides from rigour.config.yaml.
|
|
196
|
-
*/
|
|
197
|
-
export async function loadConfigOverrides(rootDir: string): Promise<PatternOverride[]> {
|
|
198
|
-
const configPaths = [
|
|
199
|
-
path.join(rootDir, 'rigour.config.yaml'),
|
|
200
|
-
path.join(rootDir, 'rigour.config.yml'),
|
|
201
|
-
path.join(rootDir, '.rigour', 'config.yaml')
|
|
202
|
-
];
|
|
203
|
-
|
|
204
|
-
for (const configPath of configPaths) {
|
|
205
|
-
try {
|
|
206
|
-
const content = await fs.readFile(configPath, 'utf-8');
|
|
207
|
-
const { parse } = await import('yaml');
|
|
208
|
-
const config = parse(content);
|
|
209
|
-
|
|
210
|
-
if (config.patterns?.allow && Array.isArray(config.patterns.allow)) {
|
|
211
|
-
return config.patterns.allow.map((item: any) => {
|
|
212
|
-
if (typeof item === 'string') {
|
|
213
|
-
return {
|
|
214
|
-
pattern: item,
|
|
215
|
-
reason: 'Configured in rigour.config.yaml',
|
|
216
|
-
createdAt: new Date().toISOString()
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
pattern: item.pattern || item.path || item.name,
|
|
221
|
-
reason: item.reason || 'Configured in rigour.config.yaml',
|
|
222
|
-
expiresAt: item.expires,
|
|
223
|
-
approvedBy: item.approved_by,
|
|
224
|
-
createdAt: new Date().toISOString()
|
|
225
|
-
};
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
} catch {
|
|
229
|
-
// Config file doesn't exist or is invalid
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return [];
|
|
235
|
-
}
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security Detector
|
|
3
|
-
*
|
|
4
|
-
* Detects CVEs and security vulnerabilities in the project's dependencies
|
|
5
|
-
* and alerts the AI/Editor before code is written.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { execa } from 'execa';
|
|
9
|
-
import * as fs from 'fs/promises';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import { createHash } from 'crypto';
|
|
12
|
-
import type { SecurityEntry, SecurityResult } from './types.js';
|
|
13
|
-
|
|
14
|
-
interface SecurityCache {
|
|
15
|
-
lockfileHash: string;
|
|
16
|
-
timestamp: string;
|
|
17
|
-
result: SecurityResult;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class SecurityDetector {
|
|
21
|
-
private rootDir: string;
|
|
22
|
-
private cachePath: string;
|
|
23
|
-
private CACHE_TTL = 3600000; // 1 hour in milliseconds
|
|
24
|
-
|
|
25
|
-
constructor(rootDir: string) {
|
|
26
|
-
this.rootDir = rootDir;
|
|
27
|
-
this.cachePath = path.join(rootDir, '.rigour', 'security-cache.json');
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Run a live security audit using NPM.
|
|
32
|
-
* This provides the latest CVE info from the NPM registry.
|
|
33
|
-
*/
|
|
34
|
-
async runAudit(): Promise<SecurityResult> {
|
|
35
|
-
try {
|
|
36
|
-
const lockfileHash = await this.getLockfileHash();
|
|
37
|
-
const cached = await this.getCachedResult(lockfileHash);
|
|
38
|
-
|
|
39
|
-
if (cached) {
|
|
40
|
-
return cached;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Run npm audit --json for machine-readable CVE data
|
|
44
|
-
const { stdout } = await execa('npm', ['audit', '--json'], {
|
|
45
|
-
cwd: this.rootDir,
|
|
46
|
-
reject: false // npm audit returns non-zero for found vulnerabilities
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const auditData = JSON.parse(stdout);
|
|
50
|
-
const vulnerabilities: SecurityEntry[] = [];
|
|
51
|
-
|
|
52
|
-
if (auditData.vulnerabilities) {
|
|
53
|
-
for (const [name, vuln] of Object.entries(auditData.vulnerabilities as any)) {
|
|
54
|
-
const v = vuln as any;
|
|
55
|
-
|
|
56
|
-
// Dig into the advisory data
|
|
57
|
-
const via = v.via && Array.isArray(v.via) ? v.via[0] : null;
|
|
58
|
-
|
|
59
|
-
vulnerabilities.push({
|
|
60
|
-
cveId: via?.name || 'N/A',
|
|
61
|
-
packageName: name,
|
|
62
|
-
vulnerableRange: v.range,
|
|
63
|
-
severity: v.severity,
|
|
64
|
-
title: via?.title || `Vulnerability in ${name}`,
|
|
65
|
-
url: via?.url || `https://www.npmjs.com/package/${name}/vulnerability`,
|
|
66
|
-
currentVersion: v.nodes && v.nodes[0] ? v.version : undefined
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const result: SecurityResult = {
|
|
72
|
-
status: vulnerabilities.length > 0 ? 'VULNERABLE' : 'SECURE',
|
|
73
|
-
vulnerabilities: vulnerabilities.sort((a, b) => {
|
|
74
|
-
const severityOrder = { critical: 0, high: 1, moderate: 2, low: 3 };
|
|
75
|
-
return (severityOrder as any)[a.severity] - (severityOrder as any)[b.severity];
|
|
76
|
-
})
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
// Save to cache
|
|
80
|
-
await this.saveCache(lockfileHash, result);
|
|
81
|
-
|
|
82
|
-
return result;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
console.error('Security audit failed:', error);
|
|
85
|
-
return { status: 'SECURE', vulnerabilities: [] };
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get a quick summary for the AI context.
|
|
91
|
-
*/
|
|
92
|
-
async getSecuritySummary(): Promise<string> {
|
|
93
|
-
const result = await this.runAudit();
|
|
94
|
-
if (result.status === 'SECURE') return '✅ No known vulnerabilities found in dependencies.';
|
|
95
|
-
|
|
96
|
-
const topVulns = result.vulnerabilities.slice(0, 3);
|
|
97
|
-
let summary = `⚠️ FOUND ${result.vulnerabilities.length} VULNERABILITIES:\n`;
|
|
98
|
-
|
|
99
|
-
for (const v of topVulns) {
|
|
100
|
-
summary += `- [${v.severity.toUpperCase()}] ${v.packageName}: ${v.title} (${v.url})\n`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (result.vulnerabilities.length > 3) {
|
|
104
|
-
summary += `- ...and ${result.vulnerabilities.length - 3} more. Run 'rigour check' for full report.`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return summary;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private async getLockfileHash(): Promise<string> {
|
|
111
|
-
const lockfiles = ['pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'];
|
|
112
|
-
for (const file of lockfiles) {
|
|
113
|
-
try {
|
|
114
|
-
const content = await fs.readFile(path.join(this.rootDir, file), 'utf-8');
|
|
115
|
-
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
116
|
-
} catch {
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
return 'no-lockfile';
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private async getCachedResult(currentHash: string): Promise<SecurityResult | null> {
|
|
124
|
-
try {
|
|
125
|
-
const content = await fs.readFile(this.cachePath, 'utf-8');
|
|
126
|
-
const cache: SecurityCache = JSON.parse(content);
|
|
127
|
-
|
|
128
|
-
const isExpired = Date.now() - new Date(cache.timestamp).getTime() > this.CACHE_TTL;
|
|
129
|
-
if (!isExpired && cache.lockfileHash === currentHash) {
|
|
130
|
-
return cache.result;
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
// No cache or invalid cache
|
|
134
|
-
}
|
|
135
|
-
return null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private async saveCache(hash: string, result: SecurityResult): Promise<void> {
|
|
139
|
-
try {
|
|
140
|
-
await fs.mkdir(path.dirname(this.cachePath), { recursive: true });
|
|
141
|
-
const cache: SecurityCache = {
|
|
142
|
-
lockfileHash: hash,
|
|
143
|
-
timestamp: new Date().toISOString(),
|
|
144
|
-
result
|
|
145
|
-
};
|
|
146
|
-
await fs.writeFile(this.cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
147
|
-
} catch (error) {
|
|
148
|
-
console.warn('Failed to save security cache:', error);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Staleness Detector Tests
|
|
3
|
-
*
|
|
4
|
-
* Tests for the staleness detection system.
|
|
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 { StalenessDetector, checkCodeStaleness } from './staleness.js';
|
|
12
|
-
|
|
13
|
-
describe('StalenessDetector', () => {
|
|
14
|
-
let testDir: string;
|
|
15
|
-
|
|
16
|
-
beforeEach(async () => {
|
|
17
|
-
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-staleness-'));
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(async () => {
|
|
21
|
-
await fs.rm(testDir, { recursive: true, force: true });
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('React deprecations', () => {
|
|
25
|
-
it('should detect componentWillMount', async () => {
|
|
26
|
-
await fs.writeFile(
|
|
27
|
-
path.join(testDir, 'package.json'),
|
|
28
|
-
JSON.stringify({ dependencies: { react: '^18.0.0' } })
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const detector = new StalenessDetector(testDir);
|
|
32
|
-
const result = await detector.checkStaleness(`
|
|
33
|
-
class MyComponent extends React.Component {
|
|
34
|
-
componentWillMount() {
|
|
35
|
-
console.log('mounting');
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
`);
|
|
39
|
-
|
|
40
|
-
expect(result.status).toBe('DEPRECATED');
|
|
41
|
-
expect(result.issues.some(i => i.pattern.includes('componentWillMount'))).toBe(true);
|
|
42
|
-
expect(result.issues[0].replacement).toContain('useEffect');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('should detect ReactDOM.render', async () => {
|
|
46
|
-
await fs.writeFile(
|
|
47
|
-
path.join(testDir, 'package.json'),
|
|
48
|
-
JSON.stringify({ dependencies: { react: '^18.0.0', 'react-dom': '^18.0.0' } })
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
const detector = new StalenessDetector(testDir);
|
|
52
|
-
const result = await detector.checkStaleness(`
|
|
53
|
-
ReactDOM.render(<App />, document.getElementById('root'));
|
|
54
|
-
`);
|
|
55
|
-
|
|
56
|
-
expect(result.status).toBe('DEPRECATED');
|
|
57
|
-
expect(result.issues.some(i => i.pattern.includes('ReactDOM.render'))).toBe(true);
|
|
58
|
-
expect(result.issues[0].replacement).toContain('createRoot');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('should not flag React deprecations if React is not installed', async () => {
|
|
62
|
-
await fs.writeFile(
|
|
63
|
-
path.join(testDir, 'package.json'),
|
|
64
|
-
JSON.stringify({ dependencies: { express: '^4.0.0' } })
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
const detector = new StalenessDetector(testDir);
|
|
68
|
-
const result = await detector.checkStaleness(`
|
|
69
|
-
componentWillMount() {}
|
|
70
|
-
`);
|
|
71
|
-
|
|
72
|
-
// Should not flag React-specific patterns if React isn't a dependency
|
|
73
|
-
expect(result.issues.filter(i => i.pattern.includes('componentWillMount'))).toHaveLength(0);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('Package deprecations', () => {
|
|
78
|
-
it('should detect moment.js usage', async () => {
|
|
79
|
-
await fs.writeFile(
|
|
80
|
-
path.join(testDir, 'package.json'),
|
|
81
|
-
JSON.stringify({ dependencies: {} })
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
const detector = new StalenessDetector(testDir);
|
|
85
|
-
const result = await detector.checkStaleness(`
|
|
86
|
-
import moment from 'moment';
|
|
87
|
-
const date = moment().format('YYYY-MM-DD');
|
|
88
|
-
`);
|
|
89
|
-
|
|
90
|
-
expect(result.status).toBe('STALE');
|
|
91
|
-
expect(result.issues.some(i => i.replacement.includes('date-fns'))).toBe(true);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should detect request library usage', async () => {
|
|
95
|
-
await fs.writeFile(
|
|
96
|
-
path.join(testDir, 'package.json'),
|
|
97
|
-
JSON.stringify({ dependencies: {} })
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const detector = new StalenessDetector(testDir);
|
|
101
|
-
const result = await detector.checkStaleness(`
|
|
102
|
-
const request = require('request');
|
|
103
|
-
request.get('https://example.com');
|
|
104
|
-
`);
|
|
105
|
-
|
|
106
|
-
expect(result.status).toBe('DEPRECATED');
|
|
107
|
-
expect(result.issues.some(i => i.replacement.includes('fetch'))).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
describe('JavaScript deprecations', () => {
|
|
112
|
-
it('should detect var keyword usage', async () => {
|
|
113
|
-
await fs.writeFile(
|
|
114
|
-
path.join(testDir, 'package.json'),
|
|
115
|
-
JSON.stringify({ dependencies: {} })
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const detector = new StalenessDetector(testDir);
|
|
119
|
-
const result = await detector.checkStaleness(`
|
|
120
|
-
var name = 'test';
|
|
121
|
-
var count = 0;
|
|
122
|
-
`);
|
|
123
|
-
|
|
124
|
-
expect(result.issues.some(i => i.replacement.includes('const') || i.replacement.includes('let'))).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
describe('Redux deprecations', () => {
|
|
129
|
-
it('should detect createStore usage in modern Redux projects', async () => {
|
|
130
|
-
await fs.writeFile(
|
|
131
|
-
path.join(testDir, 'package.json'),
|
|
132
|
-
JSON.stringify({ dependencies: { redux: '^4.2.0' } })
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
const detector = new StalenessDetector(testDir);
|
|
136
|
-
const result = await detector.checkStaleness(`
|
|
137
|
-
import { createStore } from 'redux';
|
|
138
|
-
const store = createStore(reducer);
|
|
139
|
-
`);
|
|
140
|
-
|
|
141
|
-
expect(result.status).toBe('STALE');
|
|
142
|
-
expect(result.issues.some(i => i.replacement.includes('configureStore'))).toBe(true);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe('Node.js deprecations', () => {
|
|
147
|
-
it('should detect Buffer constructor', async () => {
|
|
148
|
-
await fs.writeFile(
|
|
149
|
-
path.join(testDir, 'package.json'),
|
|
150
|
-
JSON.stringify({ dependencies: {} })
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const detector = new StalenessDetector(testDir);
|
|
154
|
-
const result = await detector.checkStaleness(`
|
|
155
|
-
const buf = new Buffer('hello');
|
|
156
|
-
`);
|
|
157
|
-
|
|
158
|
-
expect(result.status).toBe('DEPRECATED');
|
|
159
|
-
expect(result.issues.some(i => i.replacement.includes('Buffer.from'))).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
describe('Next.js deprecations', () => {
|
|
164
|
-
it('should detect getInitialProps in Next 13+ projects', async () => {
|
|
165
|
-
await fs.writeFile(
|
|
166
|
-
path.join(testDir, 'package.json'),
|
|
167
|
-
JSON.stringify({ dependencies: { next: '^13.0.0' } })
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
const detector = new StalenessDetector(testDir);
|
|
171
|
-
const result = await detector.checkStaleness(`
|
|
172
|
-
MyPage.getInitialProps = async (ctx) => {
|
|
173
|
-
return { data: {} };
|
|
174
|
-
};
|
|
175
|
-
`);
|
|
176
|
-
|
|
177
|
-
expect(result.issues.some(i => i.pattern.includes('getInitialProps'))).toBe(true);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('TypeScript best practices', () => {
|
|
182
|
-
it('should warn about enum usage', async () => {
|
|
183
|
-
await fs.writeFile(
|
|
184
|
-
path.join(testDir, 'package.json'),
|
|
185
|
-
JSON.stringify({ dependencies: {} })
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
const detector = new StalenessDetector(testDir);
|
|
189
|
-
const result = await detector.checkStaleness(`
|
|
190
|
-
enum Status {
|
|
191
|
-
Active = 'active',
|
|
192
|
-
Inactive = 'inactive'
|
|
193
|
-
}
|
|
194
|
-
`);
|
|
195
|
-
|
|
196
|
-
expect(result.issues.some(i => i.severity === 'info' && i.pattern.includes('enum'))).toBe(true);
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('Project context', () => {
|
|
201
|
-
it('should return project versions in context', async () => {
|
|
202
|
-
await fs.writeFile(
|
|
203
|
-
path.join(testDir, 'package.json'),
|
|
204
|
-
JSON.stringify({
|
|
205
|
-
dependencies: {
|
|
206
|
-
react: '^18.2.0',
|
|
207
|
-
next: '^14.0.0'
|
|
208
|
-
},
|
|
209
|
-
devDependencies: {
|
|
210
|
-
typescript: '^5.0.0'
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const detector = new StalenessDetector(testDir);
|
|
216
|
-
const result = await detector.checkStaleness('const x = 1;');
|
|
217
|
-
|
|
218
|
-
expect(result.projectContext.react).toBeDefined();
|
|
219
|
-
expect(result.projectContext.next).toBeDefined();
|
|
220
|
-
expect(result.projectContext.typescript).toBeDefined();
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe('Custom deprecations', () => {
|
|
225
|
-
it('should allow adding custom deprecation rules', async () => {
|
|
226
|
-
await fs.writeFile(
|
|
227
|
-
path.join(testDir, 'package.json'),
|
|
228
|
-
JSON.stringify({ dependencies: {} })
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
const detector = new StalenessDetector(testDir);
|
|
232
|
-
detector.addDeprecation({
|
|
233
|
-
pattern: 'legacyFunction',
|
|
234
|
-
deprecatedIn: '1.0.0',
|
|
235
|
-
replacement: 'newFunction()',
|
|
236
|
-
severity: 'error',
|
|
237
|
-
reason: 'Custom deprecation'
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
const result = await detector.checkStaleness(`
|
|
241
|
-
legacyFunction();
|
|
242
|
-
`);
|
|
243
|
-
|
|
244
|
-
expect(result.issues.some(i => i.replacement === 'newFunction()')).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
describe('Overall status', () => {
|
|
249
|
-
it('should return FRESH when no issues found', async () => {
|
|
250
|
-
await fs.writeFile(
|
|
251
|
-
path.join(testDir, 'package.json'),
|
|
252
|
-
JSON.stringify({ dependencies: {} })
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
const detector = new StalenessDetector(testDir);
|
|
256
|
-
const result = await detector.checkStaleness(`
|
|
257
|
-
const name = 'test';
|
|
258
|
-
const greet = () => console.log('Hello');
|
|
259
|
-
`);
|
|
260
|
-
|
|
261
|
-
expect(result.status).toBe('FRESH');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should return STALE for warnings', async () => {
|
|
265
|
-
await fs.writeFile(
|
|
266
|
-
path.join(testDir, 'package.json'),
|
|
267
|
-
JSON.stringify({ dependencies: { redux: '^4.2.0' } })
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const detector = new StalenessDetector(testDir);
|
|
271
|
-
const result = await detector.checkStaleness(`
|
|
272
|
-
createStore(reducer);
|
|
273
|
-
`);
|
|
274
|
-
|
|
275
|
-
expect(result.status).toBe('STALE');
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('should return DEPRECATED for errors', async () => {
|
|
279
|
-
await fs.writeFile(
|
|
280
|
-
path.join(testDir, 'package.json'),
|
|
281
|
-
JSON.stringify({ dependencies: {} })
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
const detector = new StalenessDetector(testDir);
|
|
285
|
-
const result = await detector.checkStaleness(`
|
|
286
|
-
new Buffer('hello');
|
|
287
|
-
`);
|
|
288
|
-
|
|
289
|
-
expect(result.status).toBe('DEPRECATED');
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
describe('checkCodeStaleness', () => {
|
|
295
|
-
let testDir: string;
|
|
296
|
-
|
|
297
|
-
beforeEach(async () => {
|
|
298
|
-
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'rigour-staleness-'));
|
|
299
|
-
await fs.writeFile(
|
|
300
|
-
path.join(testDir, 'package.json'),
|
|
301
|
-
JSON.stringify({ dependencies: {} })
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
afterEach(async () => {
|
|
306
|
-
await fs.rm(testDir, { recursive: true, force: true });
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it('should be a quick helper for staleness checking', async () => {
|
|
310
|
-
const result = await checkCodeStaleness(testDir, 'const x = 1;');
|
|
311
|
-
expect(result.status).toBe('FRESH');
|
|
312
|
-
});
|
|
313
|
-
});
|