@rigour-labs/core 2.22.0 → 3.0.1
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-owasp.test.d.ts +1 -0
- package/dist/gates/security-patterns-owasp.test.js +171 -0
- package/dist/gates/security-patterns.d.ts +6 -1
- package/dist/gates/security-patterns.js +101 -0
- package/dist/gates/structure.js +1 -1
- package/dist/hooks/checker.d.ts +23 -0
- package/dist/hooks/checker.js +222 -0
- package/dist/hooks/checker.test.d.ts +1 -0
- package/dist/hooks/checker.test.js +132 -0
- package/dist/hooks/index.d.ts +9 -0
- package/dist/hooks/index.js +8 -0
- package/dist/hooks/standalone-checker.d.ts +15 -0
- package/dist/hooks/standalone-checker.js +106 -0
- package/dist/hooks/templates.d.ts +22 -0
- package/dist/hooks/templates.js +232 -0
- package/dist/hooks/types.d.ts +34 -0
- package/dist/hooks/types.js +21 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -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 +176 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +207 -0
- package/dist/types/index.js +32 -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
package/src/gates/context.ts
DELETED
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
import { Gate, GateContext } from './base.js';
|
|
2
|
-
import { Failure, Gates } from '../types/index.js';
|
|
3
|
-
import { FileScanner } from '../utils/scanner.js';
|
|
4
|
-
import { Logger } from '../utils/logger.js';
|
|
5
|
-
import fs from 'fs-extra';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Extended Context Configuration (v2.14+)
|
|
10
|
-
* For 1M token frontier models like Opus 4.6
|
|
11
|
-
*/
|
|
12
|
-
export interface ExtendedContextConfig {
|
|
13
|
-
enabled?: boolean;
|
|
14
|
-
sensitivity?: number;
|
|
15
|
-
mining_depth?: number;
|
|
16
|
-
cross_file_patterns?: boolean; // NEW: Enable cross-file pattern analysis
|
|
17
|
-
naming_consistency?: boolean; // NEW: Check naming convention drift
|
|
18
|
-
import_relationships?: boolean; // NEW: Validate import patterns
|
|
19
|
-
max_cross_file_depth?: number; // NEW: How many related files to analyze
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class ContextGate extends Gate {
|
|
23
|
-
private extendedConfig: ExtendedContextConfig;
|
|
24
|
-
|
|
25
|
-
constructor(private config: Gates) {
|
|
26
|
-
super('context-drift', 'Context Awareness & Drift Detection');
|
|
27
|
-
this.extendedConfig = {
|
|
28
|
-
enabled: config.context?.enabled ?? false,
|
|
29
|
-
sensitivity: config.context?.sensitivity ?? 0.8,
|
|
30
|
-
mining_depth: config.context?.mining_depth ?? 100,
|
|
31
|
-
cross_file_patterns: true, // Default ON for frontier model support
|
|
32
|
-
naming_consistency: true,
|
|
33
|
-
import_relationships: true,
|
|
34
|
-
max_cross_file_depth: 50,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
39
|
-
const failures: Failure[] = [];
|
|
40
|
-
const record = context.record;
|
|
41
|
-
if (!record || !this.extendedConfig.enabled) return [];
|
|
42
|
-
|
|
43
|
-
const files = await FileScanner.findFiles({ cwd: context.cwd });
|
|
44
|
-
const envAnchors = record.anchors.filter(a => a.type === 'env' && a.confidence >= 1);
|
|
45
|
-
|
|
46
|
-
// Collect all patterns across files for cross-file analysis
|
|
47
|
-
const namingPatterns: Map<string, { casing: string; file: string; count: number }[]> = new Map();
|
|
48
|
-
const importPatterns: Map<string, string[]> = new Map();
|
|
49
|
-
|
|
50
|
-
for (const file of files) {
|
|
51
|
-
try {
|
|
52
|
-
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
53
|
-
|
|
54
|
-
// 1. Original: Detect Redundant Suffixes (The Golden Example)
|
|
55
|
-
this.checkEnvDrift(content, file, envAnchors, failures);
|
|
56
|
-
|
|
57
|
-
// 2. NEW: Cross-file pattern collection
|
|
58
|
-
if (this.extendedConfig.cross_file_patterns) {
|
|
59
|
-
this.collectNamingPatterns(content, file, namingPatterns);
|
|
60
|
-
this.collectImportPatterns(content, file, importPatterns);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
} catch (e) { }
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// 3. NEW: Analyze naming consistency across files
|
|
67
|
-
if (this.extendedConfig.naming_consistency) {
|
|
68
|
-
this.analyzeNamingConsistency(namingPatterns, failures);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// 4. NEW: Analyze import relationship patterns
|
|
72
|
-
if (this.extendedConfig.import_relationships) {
|
|
73
|
-
this.analyzeImportPatterns(importPatterns, failures);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return failures;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private checkEnvDrift(content: string, file: string, anchors: any[], failures: Failure[]) {
|
|
80
|
-
// Find all environment variable accesses in the content
|
|
81
|
-
const matches = content.matchAll(/process\.env(?:\.([A-Z0-9_]+)|\[['"]([A-Z0-9_]+)['"]\])/g);
|
|
82
|
-
|
|
83
|
-
for (const match of matches) {
|
|
84
|
-
const accessedVar = match[1] || match[2];
|
|
85
|
-
|
|
86
|
-
for (const anchor of anchors) {
|
|
87
|
-
// If the accessed variable contains the anchor but is not equal to it,
|
|
88
|
-
// it's a potential "invented" redundancy (e.g. CORE_URL vs CORE_URL_PROD)
|
|
89
|
-
if (accessedVar !== anchor.id && accessedVar.includes(anchor.id)) {
|
|
90
|
-
const deviation = accessedVar.replace(anchor.id, '').replace(/^_|_$/, '');
|
|
91
|
-
|
|
92
|
-
failures.push(this.createFailure(
|
|
93
|
-
`Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`,
|
|
94
|
-
[file],
|
|
95
|
-
`The project already uses '${anchor.id}' as a standard anchor. Avoid inventing variations like '${deviation}'. Reuse the existing anchor or align with established project patterns.`,
|
|
96
|
-
undefined, undefined, undefined, 'high'
|
|
97
|
-
));
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Collect naming patterns (function names, class names, variable names)
|
|
105
|
-
*/
|
|
106
|
-
private collectNamingPatterns(
|
|
107
|
-
content: string,
|
|
108
|
-
file: string,
|
|
109
|
-
patterns: Map<string, { casing: string; file: string; count: number }[]>
|
|
110
|
-
) {
|
|
111
|
-
// Named function declarations: function fetchData() { ... }
|
|
112
|
-
const namedFuncMatches = content.matchAll(/function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g);
|
|
113
|
-
for (const match of namedFuncMatches) {
|
|
114
|
-
const casing = this.detectCasing(match[1]);
|
|
115
|
-
this.addPattern(patterns, 'function', { casing, file, count: 1 });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Arrow function expressions: (export) const fetchData = (async) (...) => { ... }
|
|
119
|
-
const arrowFuncMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[a-zA-Z_$][a-zA-Z0-9_$]*)\s*=>/g);
|
|
120
|
-
for (const match of arrowFuncMatches) {
|
|
121
|
-
const casing = this.detectCasing(match[1]);
|
|
122
|
-
this.addPattern(patterns, 'function', { casing, file, count: 1 });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Function expressions: (export) const fetchData = (async) function(...) { ... }
|
|
126
|
-
const funcExprMatches = content.matchAll(/(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function\s*\(/g);
|
|
127
|
-
for (const match of funcExprMatches) {
|
|
128
|
-
const casing = this.detectCasing(match[1]);
|
|
129
|
-
this.addPattern(patterns, 'function', { casing, file, count: 1 });
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Class declarations
|
|
133
|
-
const classMatches = content.matchAll(/class\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
|
|
134
|
-
for (const match of classMatches) {
|
|
135
|
-
const casing = this.detectCasing(match[1]);
|
|
136
|
-
this.addPattern(patterns, 'class', { casing, file, count: 1 });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Interface declarations (TypeScript)
|
|
140
|
-
const interfaceMatches = content.matchAll(/interface\s+([A-Za-z_$][A-Za-z0-9_$]*)/g);
|
|
141
|
-
for (const match of interfaceMatches) {
|
|
142
|
-
const casing = this.detectCasing(match[1]);
|
|
143
|
-
this.addPattern(patterns, 'interface', { casing, file, count: 1 });
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Collect import patterns
|
|
149
|
-
*/
|
|
150
|
-
private collectImportPatterns(content: string, file: string, patterns: Map<string, string[]>) {
|
|
151
|
-
// ES6 imports
|
|
152
|
-
const importMatches = content.matchAll(/import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^'"]+)['"]/g);
|
|
153
|
-
for (const match of importMatches) {
|
|
154
|
-
const importPath = match[1];
|
|
155
|
-
if (!patterns.has(file)) {
|
|
156
|
-
patterns.set(file, []);
|
|
157
|
-
}
|
|
158
|
-
patterns.get(file)!.push(importPath);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Analyze naming consistency across files
|
|
164
|
-
*/
|
|
165
|
-
private analyzeNamingConsistency(
|
|
166
|
-
patterns: Map<string, { casing: string; file: string; count: number }[]>,
|
|
167
|
-
failures: Failure[]
|
|
168
|
-
) {
|
|
169
|
-
for (const [type, entries] of patterns) {
|
|
170
|
-
const casingCounts = new Map<string, number>();
|
|
171
|
-
for (const entry of entries) {
|
|
172
|
-
casingCounts.set(entry.casing, (casingCounts.get(entry.casing) || 0) + entry.count);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Find dominant casing
|
|
176
|
-
let dominant = '';
|
|
177
|
-
let maxCount = 0;
|
|
178
|
-
for (const [casing, count] of casingCounts) {
|
|
179
|
-
if (count > maxCount) {
|
|
180
|
-
dominant = casing;
|
|
181
|
-
maxCount = count;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Report violations (non-dominant casing with significant usage)
|
|
186
|
-
const total = entries.reduce((sum, e) => sum + e.count, 0);
|
|
187
|
-
const threshold = total * (1 - (this.extendedConfig.sensitivity ?? 0.8));
|
|
188
|
-
|
|
189
|
-
for (const [casing, count] of casingCounts) {
|
|
190
|
-
if (casing !== dominant && count > threshold) {
|
|
191
|
-
const violatingFiles = entries.filter(e => e.casing === casing).map(e => e.file);
|
|
192
|
-
const uniqueFiles = [...new Set(violatingFiles)].slice(0, 5);
|
|
193
|
-
|
|
194
|
-
failures.push(this.createFailure(
|
|
195
|
-
`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`,
|
|
196
|
-
uniqueFiles,
|
|
197
|
-
`Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`,
|
|
198
|
-
'Naming Convention Drift',
|
|
199
|
-
undefined, undefined, 'high'
|
|
200
|
-
));
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Analyze import patterns for consistency
|
|
208
|
-
*/
|
|
209
|
-
private analyzeImportPatterns(patterns: Map<string, string[]>, failures: Failure[]) {
|
|
210
|
-
// Check for mixed import styles (relative vs absolute)
|
|
211
|
-
const relativeCount = new Map<string, number>();
|
|
212
|
-
const absoluteCount = new Map<string, number>();
|
|
213
|
-
|
|
214
|
-
for (const [file, imports] of patterns) {
|
|
215
|
-
for (const imp of imports) {
|
|
216
|
-
if (imp.startsWith('.') || imp.startsWith('..')) {
|
|
217
|
-
relativeCount.set(file, (relativeCount.get(file) || 0) + 1);
|
|
218
|
-
} else if (!imp.startsWith('@') && !imp.includes('/')) {
|
|
219
|
-
// Skip external packages
|
|
220
|
-
} else {
|
|
221
|
-
absoluteCount.set(file, (absoluteCount.get(file) || 0) + 1);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Detect files with both relative AND absolute local imports
|
|
227
|
-
const mixedFiles: string[] = [];
|
|
228
|
-
for (const file of patterns.keys()) {
|
|
229
|
-
const hasRelative = (relativeCount.get(file) || 0) > 0;
|
|
230
|
-
const hasAbsolute = (absoluteCount.get(file) || 0) > 0;
|
|
231
|
-
if (hasRelative && hasAbsolute) {
|
|
232
|
-
mixedFiles.push(file);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (mixedFiles.length > 3) {
|
|
237
|
-
failures.push(this.createFailure(
|
|
238
|
-
`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`,
|
|
239
|
-
mixedFiles.slice(0, 5),
|
|
240
|
-
'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.',
|
|
241
|
-
'Import Pattern Drift',
|
|
242
|
-
undefined, undefined, 'high'
|
|
243
|
-
));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Detect casing convention of an identifier
|
|
249
|
-
*/
|
|
250
|
-
private detectCasing(name: string): string {
|
|
251
|
-
if (/^[A-Z][a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'PascalCase';
|
|
252
|
-
if (/^[a-z]/.test(name) && /[a-z][A-Z]/.test(name)) return 'camelCase';
|
|
253
|
-
if (/^[a-z][a-zA-Z0-9]*$/.test(name)) return 'camelCase'; // single-word lowercase (e.g. fetch, use, get)
|
|
254
|
-
if (/^[a-z]+(_[a-z]+)+$/.test(name)) return 'snake_case';
|
|
255
|
-
if (/^[A-Z]+(_[A-Z]+)*$/.test(name)) return 'SCREAMING_SNAKE';
|
|
256
|
-
if (/^[A-Z][a-zA-Z]*$/.test(name)) return 'PascalCase';
|
|
257
|
-
return 'unknown';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
private addPattern(
|
|
261
|
-
patterns: Map<string, { casing: string; file: string; count: number }[]>,
|
|
262
|
-
type: string,
|
|
263
|
-
entry: { casing: string; file: string; count: number }
|
|
264
|
-
) {
|
|
265
|
-
if (!patterns.has(type)) {
|
|
266
|
-
patterns.set(type, []);
|
|
267
|
-
}
|
|
268
|
-
patterns.get(type)!.push(entry);
|
|
269
|
-
}
|
|
270
|
-
}
|
package/src/gates/coverage.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { Gate, GateContext } from './base.js';
|
|
4
|
-
import { Failure, Gates } from '../types/index.js';
|
|
5
|
-
import { globby } from 'globby';
|
|
6
|
-
|
|
7
|
-
export class CoverageGate extends Gate {
|
|
8
|
-
constructor(private config: Gates) {
|
|
9
|
-
super('coverage-guard', 'Dynamic Coverage Guard');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
13
|
-
const failures: Failure[] = [];
|
|
14
|
-
|
|
15
|
-
// 1. Locate coverage report (lcov.info is standard)
|
|
16
|
-
const reports = await globby(['**/lcov.info', '**/coverage-final.json'], {
|
|
17
|
-
cwd: context.cwd,
|
|
18
|
-
ignore: ['node_modules/**']
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
if (reports.length === 0) {
|
|
22
|
-
// If no reports found, and coverage is required, we could flag it.
|
|
23
|
-
// But for now, we'll just skip silently if not configured.
|
|
24
|
-
return [];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// 2. Parse coverage (Simplified LCOV parser for demonstration)
|
|
28
|
-
const coverageData = await this.parseLcov(path.join(context.cwd, reports[0]));
|
|
29
|
-
|
|
30
|
-
// 3. Quality Handshake: SME SME LOGIC
|
|
31
|
-
// We look for files that have high complexity but low coverage.
|
|
32
|
-
// In a real implementation, we would share data between ASTGate and CoverageGate.
|
|
33
|
-
// For this demo, we'll implement a standalone check.
|
|
34
|
-
|
|
35
|
-
for (const [file, stats] of Object.entries(coverageData)) {
|
|
36
|
-
const coverage = (stats.hit / stats.found) * 100;
|
|
37
|
-
const threshold = stats.isComplex ? 80 : 50; // SME logic: Complex files need higher coverage
|
|
38
|
-
|
|
39
|
-
if (coverage < threshold) {
|
|
40
|
-
failures.push({
|
|
41
|
-
id: 'DYNAMIC_COVERAGE_LOW',
|
|
42
|
-
title: `Low coverage for high-risk file: ${file}`,
|
|
43
|
-
details: `Current coverage: ${coverage.toFixed(2)}%. Required: ${threshold}% due to structural risk.`,
|
|
44
|
-
files: [file],
|
|
45
|
-
hint: `Add dynamic tests to cover complex logical branches in this file.`
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return failures;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private async parseLcov(reportPath: string): Promise<Record<string, { found: number, hit: number, isComplex: boolean }>> {
|
|
54
|
-
const content = await fs.readFile(reportPath, 'utf-8');
|
|
55
|
-
const results: Record<string, { found: number, hit: number, isComplex: boolean }> = {};
|
|
56
|
-
let currentFile = '';
|
|
57
|
-
|
|
58
|
-
for (const line of content.split('\n')) {
|
|
59
|
-
if (line.startsWith('SF:')) {
|
|
60
|
-
currentFile = line.substring(3);
|
|
61
|
-
results[currentFile] = { found: 0, hit: 0, isComplex: false };
|
|
62
|
-
} else if (line.startsWith('LF:')) {
|
|
63
|
-
const found = parseInt(line.substring(3));
|
|
64
|
-
results[currentFile].found = found;
|
|
65
|
-
// SME Logic: If a file has > 100 logical lines, it's considered "Complex"
|
|
66
|
-
// and triggers the higher (80%) coverage requirement.
|
|
67
|
-
if (found > 100) results[currentFile].isComplex = true;
|
|
68
|
-
} else if (line.startsWith('LH:')) {
|
|
69
|
-
results[currentFile].hit = parseInt(line.substring(3));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return results;
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/gates/dependency.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { Failure, Config } from '../types/index.js';
|
|
4
|
-
import { Gate, GateContext } from './base.js';
|
|
5
|
-
|
|
6
|
-
export class DependencyGate extends Gate {
|
|
7
|
-
constructor(private config: Config) {
|
|
8
|
-
super('dependency-guardian', 'Dependency Guardian');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
12
|
-
const failures: Failure[] = [];
|
|
13
|
-
const forbidden = this.config.gates.dependencies?.forbid || [];
|
|
14
|
-
|
|
15
|
-
if (forbidden.length === 0) return [];
|
|
16
|
-
|
|
17
|
-
const { cwd } = context;
|
|
18
|
-
|
|
19
|
-
// 1. Scan Node.js (package.json)
|
|
20
|
-
const pkgPath = path.join(cwd, 'package.json');
|
|
21
|
-
if (await fs.pathExists(pkgPath)) {
|
|
22
|
-
try {
|
|
23
|
-
const pkg = await fs.readJson(pkgPath);
|
|
24
|
-
const allDeps = {
|
|
25
|
-
...(pkg.dependencies || {}),
|
|
26
|
-
...(pkg.devDependencies || {}),
|
|
27
|
-
...(pkg.peerDependencies || {}),
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
for (const dep of forbidden) {
|
|
31
|
-
if (allDeps[dep]) {
|
|
32
|
-
failures.push(this.createFailure(
|
|
33
|
-
`The package '${dep}' is forbidden by project standards.`,
|
|
34
|
-
['package.json'],
|
|
35
|
-
`Remove '${dep}' from package.json and use approved alternatives.`,
|
|
36
|
-
'Forbidden Dependency'
|
|
37
|
-
));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
} catch (e) { }
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// 2. Scan Python (requirements.txt, pyproject.toml)
|
|
44
|
-
const reqPath = path.join(cwd, 'requirements.txt');
|
|
45
|
-
if (await fs.pathExists(reqPath)) {
|
|
46
|
-
const content = await fs.readFile(reqPath, 'utf-8');
|
|
47
|
-
for (const dep of forbidden) {
|
|
48
|
-
if (new RegExp(`^${dep}([=<>! ]|$)`, 'm').test(content)) {
|
|
49
|
-
failures.push(this.createFailure(
|
|
50
|
-
`The Python package '${dep}' is forbidden.`,
|
|
51
|
-
['requirements.txt'],
|
|
52
|
-
`Remove '${dep}' from requirements.txt.`,
|
|
53
|
-
'Forbidden Dependency'
|
|
54
|
-
));
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const pyprojPath = path.join(cwd, 'pyproject.toml');
|
|
60
|
-
if (await fs.pathExists(pyprojPath)) {
|
|
61
|
-
const content = await fs.readFile(pyprojPath, 'utf-8');
|
|
62
|
-
for (const dep of forbidden) {
|
|
63
|
-
if (new RegExp(`^${dep}\\s*=`, 'm').test(content)) {
|
|
64
|
-
failures.push(this.createFailure(
|
|
65
|
-
`The Python package '${dep}' is forbidden in pyproject.toml.`,
|
|
66
|
-
['pyproject.toml'],
|
|
67
|
-
`Remove '${dep}' from pyproject.toml dependencies.`,
|
|
68
|
-
'Forbidden Dependency'
|
|
69
|
-
));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// 3. Scan Go (go.mod)
|
|
75
|
-
const goModPath = path.join(cwd, 'go.mod');
|
|
76
|
-
if (await fs.pathExists(goModPath)) {
|
|
77
|
-
const content = await fs.readFile(goModPath, 'utf-8');
|
|
78
|
-
for (const dep of forbidden) {
|
|
79
|
-
if (content.includes(dep)) {
|
|
80
|
-
failures.push(this.createFailure(
|
|
81
|
-
`The Go module '${dep}' is forbidden.`,
|
|
82
|
-
['go.mod'],
|
|
83
|
-
`Remove '${dep}' from go.mod.`,
|
|
84
|
-
'Forbidden Dependency'
|
|
85
|
-
));
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// 4. Scan Java (pom.xml)
|
|
91
|
-
const pomPath = path.join(cwd, 'pom.xml');
|
|
92
|
-
if (await fs.pathExists(pomPath)) {
|
|
93
|
-
const content = await fs.readFile(pomPath, 'utf-8');
|
|
94
|
-
for (const dep of forbidden) {
|
|
95
|
-
if (content.includes(`<artifactId>${dep}</artifactId>`)) {
|
|
96
|
-
failures.push(this.createFailure(
|
|
97
|
-
`The Java artifact '${dep}' is forbidden.`,
|
|
98
|
-
['pom.xml'],
|
|
99
|
-
`Remove '${dep}' from pom.xml.`,
|
|
100
|
-
'Forbidden Dependency'
|
|
101
|
-
));
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return failures;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Duplication Drift Gate
|
|
3
|
-
*
|
|
4
|
-
* Detects when AI generates near-identical functions across files because
|
|
5
|
-
* it doesn't remember what it already wrote. This is an AI-specific failure
|
|
6
|
-
* mode — humans reuse via copy-paste (same file), AI re-invents (cross-file).
|
|
7
|
-
*
|
|
8
|
-
* Detection strategy:
|
|
9
|
-
* 1. Extract all function bodies (normalized: strip whitespace, comments)
|
|
10
|
-
* 2. Compare function signatures + body hashes across files
|
|
11
|
-
* 3. Flag functions with >80% similarity in different files
|
|
12
|
-
*
|
|
13
|
-
* @since v2.16.0
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { Gate, GateContext } from './base.js';
|
|
17
|
-
import { Failure } from '../types/index.js';
|
|
18
|
-
import { FileScanner } from '../utils/scanner.js';
|
|
19
|
-
import { Logger } from '../utils/logger.js';
|
|
20
|
-
import crypto from 'crypto';
|
|
21
|
-
import path from 'path';
|
|
22
|
-
|
|
23
|
-
interface FunctionSignature {
|
|
24
|
-
name: string;
|
|
25
|
-
file: string;
|
|
26
|
-
line: number;
|
|
27
|
-
paramCount: number;
|
|
28
|
-
bodyHash: string;
|
|
29
|
-
bodyLength: number;
|
|
30
|
-
normalized: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface DuplicationDriftConfig {
|
|
34
|
-
enabled?: boolean;
|
|
35
|
-
similarity_threshold?: number; // 0-1, default 0.8
|
|
36
|
-
min_body_lines?: number; // Ignore trivial functions, default 5
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export class DuplicationDriftGate extends Gate {
|
|
40
|
-
private config: Required<DuplicationDriftConfig>;
|
|
41
|
-
|
|
42
|
-
constructor(config: DuplicationDriftConfig = {}) {
|
|
43
|
-
super('duplication-drift', 'AI Duplication Drift Detection');
|
|
44
|
-
this.config = {
|
|
45
|
-
enabled: config.enabled ?? true,
|
|
46
|
-
similarity_threshold: config.similarity_threshold ?? 0.8,
|
|
47
|
-
min_body_lines: config.min_body_lines ?? 5,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async run(context: GateContext): Promise<Failure[]> {
|
|
52
|
-
if (!this.config.enabled) return [];
|
|
53
|
-
|
|
54
|
-
const failures: Failure[] = [];
|
|
55
|
-
const functions: FunctionSignature[] = [];
|
|
56
|
-
|
|
57
|
-
const files = await FileScanner.findFiles({
|
|
58
|
-
cwd: context.cwd,
|
|
59
|
-
patterns: ['**/*.{ts,js,tsx,jsx,py}'],
|
|
60
|
-
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
Logger.info(`Duplication Drift: Scanning ${files.length} files`);
|
|
64
|
-
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
try {
|
|
67
|
-
const { readFile } = await import('fs-extra');
|
|
68
|
-
const content = await readFile(path.join(context.cwd, file), 'utf-8');
|
|
69
|
-
const ext = path.extname(file);
|
|
70
|
-
|
|
71
|
-
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
72
|
-
this.extractJSFunctions(content, file, functions);
|
|
73
|
-
} else if (ext === '.py') {
|
|
74
|
-
this.extractPyFunctions(content, file, functions);
|
|
75
|
-
}
|
|
76
|
-
} catch (e) { }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Compare all function pairs across different files
|
|
80
|
-
const duplicateGroups = this.findDuplicateGroups(functions);
|
|
81
|
-
|
|
82
|
-
for (const group of duplicateGroups) {
|
|
83
|
-
const files = group.map(f => f.file);
|
|
84
|
-
const locations = group.map(f => `${f.file}:${f.line} (${f.name})`).join(', ');
|
|
85
|
-
|
|
86
|
-
failures.push(this.createFailure(
|
|
87
|
-
`AI Duplication Drift: Function '${group[0].name}' has ${group.length} near-identical copies across files`,
|
|
88
|
-
[...new Set(files)],
|
|
89
|
-
`Found duplicate implementations at: ${locations}. Extract to a shared module and import.`,
|
|
90
|
-
'Duplication Drift',
|
|
91
|
-
group[0].line,
|
|
92
|
-
undefined,
|
|
93
|
-
'high'
|
|
94
|
-
));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return failures;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
private extractJSFunctions(content: string, file: string, functions: FunctionSignature[]) {
|
|
101
|
-
const lines = content.split('\n');
|
|
102
|
-
|
|
103
|
-
// Match function declarations, arrow functions, and method definitions
|
|
104
|
-
const patterns = [
|
|
105
|
-
// function name(...) {
|
|
106
|
-
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
107
|
-
// const name = (...) => {
|
|
108
|
-
/^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/,
|
|
109
|
-
// name(...) { — class method
|
|
110
|
-
/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/,
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
for (let i = 0; i < lines.length; i++) {
|
|
114
|
-
const line = lines[i];
|
|
115
|
-
for (const pattern of patterns) {
|
|
116
|
-
const match = line.match(pattern);
|
|
117
|
-
if (match) {
|
|
118
|
-
const name = match[1];
|
|
119
|
-
const params = match[2] || '';
|
|
120
|
-
const body = this.extractFunctionBody(lines, i);
|
|
121
|
-
|
|
122
|
-
if (body.length >= this.config.min_body_lines) {
|
|
123
|
-
const normalized = this.normalizeBody(body.join('\n'));
|
|
124
|
-
functions.push({
|
|
125
|
-
name,
|
|
126
|
-
file,
|
|
127
|
-
line: i + 1,
|
|
128
|
-
paramCount: params ? params.split(',').length : 0,
|
|
129
|
-
bodyHash: this.hash(normalized),
|
|
130
|
-
bodyLength: body.length,
|
|
131
|
-
normalized,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
private extractPyFunctions(content: string, file: string, functions: FunctionSignature[]) {
|
|
141
|
-
const lines = content.split('\n');
|
|
142
|
-
|
|
143
|
-
for (let i = 0; i < lines.length; i++) {
|
|
144
|
-
const match = lines[i].match(/^(?:\s*)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/);
|
|
145
|
-
if (match) {
|
|
146
|
-
const name = match[1];
|
|
147
|
-
const params = match[2] || '';
|
|
148
|
-
const indent = lines[i].match(/^(\s*)/)?.[1]?.length || 0;
|
|
149
|
-
|
|
150
|
-
// Extract body by indentation
|
|
151
|
-
const body: string[] = [];
|
|
152
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
153
|
-
const lineIndent = lines[j].match(/^(\s*)/)?.[1]?.length || 0;
|
|
154
|
-
if (lines[j].trim() === '' || lineIndent > indent) {
|
|
155
|
-
body.push(lines[j]);
|
|
156
|
-
} else {
|
|
157
|
-
break;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (body.length >= this.config.min_body_lines) {
|
|
162
|
-
const normalized = this.normalizeBody(body.join('\n'));
|
|
163
|
-
functions.push({
|
|
164
|
-
name,
|
|
165
|
-
file,
|
|
166
|
-
line: i + 1,
|
|
167
|
-
paramCount: params ? params.split(',').length : 0,
|
|
168
|
-
bodyHash: this.hash(normalized),
|
|
169
|
-
bodyLength: body.length,
|
|
170
|
-
normalized,
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
private extractFunctionBody(lines: string[], startIndex: number): string[] {
|
|
178
|
-
let braceDepth = 0;
|
|
179
|
-
let started = false;
|
|
180
|
-
const body: string[] = [];
|
|
181
|
-
|
|
182
|
-
for (let i = startIndex; i < lines.length; i++) {
|
|
183
|
-
const line = lines[i];
|
|
184
|
-
for (const ch of line) {
|
|
185
|
-
if (ch === '{') { braceDepth++; started = true; }
|
|
186
|
-
if (ch === '}') braceDepth--;
|
|
187
|
-
}
|
|
188
|
-
if (started) body.push(line);
|
|
189
|
-
if (started && braceDepth === 0) break;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return body;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private normalizeBody(body: string): string {
|
|
196
|
-
return body
|
|
197
|
-
.replace(/\/\/.*/g, '') // strip single-line comments
|
|
198
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // strip multi-line comments
|
|
199
|
-
.replace(/#.*/g, '') // strip Python comments
|
|
200
|
-
.replace(/\s+/g, ' ') // collapse whitespace
|
|
201
|
-
.replace(/['"`]/g, '"') // normalize quotes
|
|
202
|
-
.trim();
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
private hash(text: string): string {
|
|
206
|
-
return crypto.createHash('md5').update(text).digest('hex');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private findDuplicateGroups(functions: FunctionSignature[]): FunctionSignature[][] {
|
|
210
|
-
const groups = new Map<string, FunctionSignature[]>();
|
|
211
|
-
|
|
212
|
-
// Group by body hash (exact duplicates across files)
|
|
213
|
-
for (const fn of functions) {
|
|
214
|
-
const existing = groups.get(fn.bodyHash) || [];
|
|
215
|
-
existing.push(fn);
|
|
216
|
-
groups.set(fn.bodyHash, existing);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Filter: only groups with functions from DIFFERENT files, 2+ members
|
|
220
|
-
const duplicates: FunctionSignature[][] = [];
|
|
221
|
-
for (const group of groups.values()) {
|
|
222
|
-
if (group.length < 2) continue;
|
|
223
|
-
const uniqueFiles = new Set(group.map(f => f.file));
|
|
224
|
-
if (uniqueFiles.size >= 2) {
|
|
225
|
-
duplicates.push(group);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
return duplicates;
|
|
230
|
-
}
|
|
231
|
-
}
|