@rigour-labs/core 2.21.1 → 2.22.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/dist/gates/base.d.ts +2 -2
- package/dist/gates/base.js +2 -1
- package/dist/gates/content.js +1 -1
- package/dist/gates/context-window-artifacts.d.ts +33 -0
- package/dist/gates/context-window-artifacts.js +211 -0
- package/dist/gates/context.js +3 -3
- package/dist/gates/duplication-drift.d.ts +32 -0
- package/dist/gates/duplication-drift.js +187 -0
- package/dist/gates/file.js +1 -1
- package/dist/gates/hallucinated-imports.d.ts +44 -0
- package/dist/gates/hallucinated-imports.js +292 -0
- package/dist/gates/inconsistent-error-handling.d.ts +38 -0
- package/dist/gates/inconsistent-error-handling.js +222 -0
- package/dist/gates/runner.js +29 -1
- package/dist/gates/security-patterns.js +1 -1
- package/dist/pattern-index/indexer.js +2 -1
- package/dist/templates/index.js +26 -0
- package/dist/types/fix-packet.d.ts +4 -4
- package/dist/types/index.d.ts +277 -0
- package/dist/types/index.js +38 -0
- package/package.json +1 -1
- package/src/gates/base.ts +3 -2
- package/src/gates/content.ts +2 -1
- package/src/gates/context-window-artifacts.ts +277 -0
- package/src/gates/context.ts +6 -3
- package/src/gates/duplication-drift.ts +231 -0
- package/src/gates/file.ts +5 -1
- package/src/gates/hallucinated-imports.ts +361 -0
- package/src/gates/inconsistent-error-handling.ts +254 -0
- package/src/gates/runner.ts +34 -2
- package/src/gates/security-patterns.ts +2 -1
- package/src/pattern-index/indexer.ts +2 -1
- package/src/templates/index.ts +26 -0
- package/src/types/index.ts +41 -0
package/dist/gates/base.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GoldenRecord } from '../services/context-engine.js';
|
|
2
|
-
import { Failure } from '../types/index.js';
|
|
2
|
+
import { Failure, Severity } from '../types/index.js';
|
|
3
3
|
export interface GateContext {
|
|
4
4
|
cwd: string;
|
|
5
5
|
record?: GoldenRecord;
|
|
@@ -11,5 +11,5 @@ export declare abstract class Gate {
|
|
|
11
11
|
readonly title: string;
|
|
12
12
|
constructor(id: string, title: string);
|
|
13
13
|
abstract run(context: GateContext): Promise<Failure[]>;
|
|
14
|
-
protected createFailure(details: string, files?: string[], hint?: string, title?: string, line?: number, endLine?: number): Failure;
|
|
14
|
+
protected createFailure(details: string, files?: string[], hint?: string, title?: string, line?: number, endLine?: number, severity?: Severity): Failure;
|
|
15
15
|
}
|
package/dist/gates/base.js
CHANGED
|
@@ -5,11 +5,12 @@ export class Gate {
|
|
|
5
5
|
this.id = id;
|
|
6
6
|
this.title = title;
|
|
7
7
|
}
|
|
8
|
-
createFailure(details, files, hint, title, line, endLine) {
|
|
8
|
+
createFailure(details, files, hint, title, line, endLine, severity) {
|
|
9
9
|
return {
|
|
10
10
|
id: this.id,
|
|
11
11
|
title: title || this.title,
|
|
12
12
|
details,
|
|
13
|
+
severity: severity || 'medium',
|
|
13
14
|
files,
|
|
14
15
|
line,
|
|
15
16
|
endLine,
|
package/dist/gates/content.js
CHANGED
|
@@ -26,7 +26,7 @@ export class ContentGate extends Gate {
|
|
|
26
26
|
lines.forEach((line, index) => {
|
|
27
27
|
for (const pattern of patterns) {
|
|
28
28
|
if (pattern.test(line)) {
|
|
29
|
-
failures.push(this.createFailure(`Forbidden placeholder '${pattern.source}' found`, [file], 'Remove forbidden comments. address the root cause or create a tracked issue.', undefined, index + 1, index + 1));
|
|
29
|
+
failures.push(this.createFailure(`Forbidden placeholder '${pattern.source}' found`, [file], 'Remove forbidden comments. address the root cause or create a tracked issue.', undefined, index + 1, index + 1, 'info'));
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Window Artifacts Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects quality degradation patterns within a single file that emerge
|
|
5
|
+
* when AI loses context mid-generation. The telltale sign: clean,
|
|
6
|
+
* well-structured code at the top of a file that gradually degrades
|
|
7
|
+
* toward the bottom.
|
|
8
|
+
*
|
|
9
|
+
* Detection signals:
|
|
10
|
+
* 1. Comment density drops sharply (top half vs bottom half)
|
|
11
|
+
* 2. Function complexity increases toward end of file
|
|
12
|
+
* 3. Variable naming quality degrades (shorter names, more single-letter vars)
|
|
13
|
+
* 4. Error handling becomes sparser toward the bottom
|
|
14
|
+
* 5. Code style inconsistencies emerge (indentation, spacing)
|
|
15
|
+
*
|
|
16
|
+
* @since v2.16.0
|
|
17
|
+
*/
|
|
18
|
+
import { Gate, GateContext } from './base.js';
|
|
19
|
+
import { Failure } from '../types/index.js';
|
|
20
|
+
export interface ContextWindowArtifactsConfig {
|
|
21
|
+
enabled?: boolean;
|
|
22
|
+
min_file_lines?: number;
|
|
23
|
+
degradation_threshold?: number;
|
|
24
|
+
signals_required?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class ContextWindowArtifactsGate extends Gate {
|
|
27
|
+
private config;
|
|
28
|
+
constructor(config?: ContextWindowArtifactsConfig);
|
|
29
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
30
|
+
private analyzeFile;
|
|
31
|
+
private measureHalf;
|
|
32
|
+
private measureFunctionLengths;
|
|
33
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Window Artifacts Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects quality degradation patterns within a single file that emerge
|
|
5
|
+
* when AI loses context mid-generation. The telltale sign: clean,
|
|
6
|
+
* well-structured code at the top of a file that gradually degrades
|
|
7
|
+
* toward the bottom.
|
|
8
|
+
*
|
|
9
|
+
* Detection signals:
|
|
10
|
+
* 1. Comment density drops sharply (top half vs bottom half)
|
|
11
|
+
* 2. Function complexity increases toward end of file
|
|
12
|
+
* 3. Variable naming quality degrades (shorter names, more single-letter vars)
|
|
13
|
+
* 4. Error handling becomes sparser toward the bottom
|
|
14
|
+
* 5. Code style inconsistencies emerge (indentation, spacing)
|
|
15
|
+
*
|
|
16
|
+
* @since v2.16.0
|
|
17
|
+
*/
|
|
18
|
+
import { Gate } from './base.js';
|
|
19
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
20
|
+
import { Logger } from '../utils/logger.js';
|
|
21
|
+
import fs from 'fs-extra';
|
|
22
|
+
import path from 'path';
|
|
23
|
+
export class ContextWindowArtifactsGate extends Gate {
|
|
24
|
+
config;
|
|
25
|
+
constructor(config = {}) {
|
|
26
|
+
super('context-window-artifacts', 'Context Window Artifact Detection');
|
|
27
|
+
this.config = {
|
|
28
|
+
enabled: config.enabled ?? true,
|
|
29
|
+
min_file_lines: config.min_file_lines ?? 100,
|
|
30
|
+
degradation_threshold: config.degradation_threshold ?? 0.4,
|
|
31
|
+
signals_required: config.signals_required ?? 2,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async run(context) {
|
|
35
|
+
if (!this.config.enabled)
|
|
36
|
+
return [];
|
|
37
|
+
const failures = [];
|
|
38
|
+
const files = await FileScanner.findFiles({
|
|
39
|
+
cwd: context.cwd,
|
|
40
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py}'],
|
|
41
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*', '**/*.min.*'],
|
|
42
|
+
});
|
|
43
|
+
Logger.info(`Context Window Artifacts: Scanning ${files.length} files`);
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
try {
|
|
46
|
+
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
47
|
+
const lines = content.split('\n');
|
|
48
|
+
if (lines.length < this.config.min_file_lines)
|
|
49
|
+
continue;
|
|
50
|
+
const metrics = this.analyzeFile(content, file);
|
|
51
|
+
if (metrics && metrics.signals.length >= this.config.signals_required &&
|
|
52
|
+
metrics.degradationScore >= this.config.degradation_threshold) {
|
|
53
|
+
const signalList = metrics.signals.map(s => ` • ${s}`).join('\n');
|
|
54
|
+
const midpoint = Math.floor(metrics.totalLines / 2);
|
|
55
|
+
failures.push(this.createFailure(`Context window artifact detected in ${file} (${metrics.totalLines} lines, degradation: ${(metrics.degradationScore * 100).toFixed(0)}%):\n${signalList}`, [file], `This file shows quality degradation from top to bottom, a pattern typical of AI context window exhaustion. Consider refactoring the bottom half or splitting the file. The quality drop begins around line ${midpoint}.`, 'Context Window Artifacts', midpoint, undefined, 'high'));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) { }
|
|
59
|
+
}
|
|
60
|
+
return failures;
|
|
61
|
+
}
|
|
62
|
+
analyzeFile(content, file) {
|
|
63
|
+
const lines = content.split('\n');
|
|
64
|
+
const midpoint = Math.floor(lines.length / 2);
|
|
65
|
+
const topContent = lines.slice(0, midpoint).join('\n');
|
|
66
|
+
const bottomContent = lines.slice(midpoint).join('\n');
|
|
67
|
+
const topMetrics = this.measureHalf(topContent);
|
|
68
|
+
const bottomMetrics = this.measureHalf(bottomContent);
|
|
69
|
+
const signals = [];
|
|
70
|
+
let degradationScore = 0;
|
|
71
|
+
// Signal 1: Comment density drops
|
|
72
|
+
if (topMetrics.commentDensity > 0) {
|
|
73
|
+
const commentRatio = bottomMetrics.commentDensity / topMetrics.commentDensity;
|
|
74
|
+
if (commentRatio < 0.5) {
|
|
75
|
+
signals.push(`Comment density drops ${((1 - commentRatio) * 100).toFixed(0)}% in bottom half`);
|
|
76
|
+
degradationScore += 0.25;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Signal 2: Function length increases
|
|
80
|
+
if (topMetrics.avgFunctionLength > 0 && bottomMetrics.avgFunctionLength > 0) {
|
|
81
|
+
const lengthRatio = bottomMetrics.avgFunctionLength / topMetrics.avgFunctionLength;
|
|
82
|
+
if (lengthRatio > 1.5) {
|
|
83
|
+
signals.push(`Average function length ${lengthRatio.toFixed(1)}x longer in bottom half`);
|
|
84
|
+
degradationScore += 0.2;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Signal 3: Variable naming quality degrades
|
|
88
|
+
if (bottomMetrics.singleCharVarCount > topMetrics.singleCharVarCount * 2 &&
|
|
89
|
+
bottomMetrics.singleCharVarCount >= 3) {
|
|
90
|
+
signals.push(`${bottomMetrics.singleCharVarCount} single-char variables in bottom half vs ${topMetrics.singleCharVarCount} in top`);
|
|
91
|
+
degradationScore += 0.2;
|
|
92
|
+
}
|
|
93
|
+
// Signal 3b: Average identifier length shrinks
|
|
94
|
+
if (topMetrics.avgIdentifierLength > 0 && bottomMetrics.avgIdentifierLength > 0) {
|
|
95
|
+
const nameRatio = bottomMetrics.avgIdentifierLength / topMetrics.avgIdentifierLength;
|
|
96
|
+
if (nameRatio < 0.7) {
|
|
97
|
+
signals.push(`Identifier names ${((1 - nameRatio) * 100).toFixed(0)}% shorter in bottom half`);
|
|
98
|
+
degradationScore += 0.15;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Signal 4: Error handling becomes sparser
|
|
102
|
+
if (topMetrics.errorHandlingDensity > 0) {
|
|
103
|
+
const errorRatio = bottomMetrics.errorHandlingDensity / topMetrics.errorHandlingDensity;
|
|
104
|
+
if (errorRatio < 0.3) {
|
|
105
|
+
signals.push(`Error handling ${((1 - errorRatio) * 100).toFixed(0)}% less frequent in bottom half`);
|
|
106
|
+
degradationScore += 0.2;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Signal 5: Empty blocks increase
|
|
110
|
+
if (bottomMetrics.emptyBlockCount > topMetrics.emptyBlockCount + 2) {
|
|
111
|
+
signals.push(`${bottomMetrics.emptyBlockCount} empty blocks in bottom half vs ${topMetrics.emptyBlockCount} in top`);
|
|
112
|
+
degradationScore += 0.15;
|
|
113
|
+
}
|
|
114
|
+
// Signal 6: TODO/FIXME/HACK density increases at bottom
|
|
115
|
+
if (bottomMetrics.todoCount > topMetrics.todoCount + 1) {
|
|
116
|
+
signals.push(`${bottomMetrics.todoCount} TODO/FIXME/HACK in bottom half vs ${topMetrics.todoCount} in top`);
|
|
117
|
+
degradationScore += 0.1;
|
|
118
|
+
}
|
|
119
|
+
// Cap at 1.0
|
|
120
|
+
degradationScore = Math.min(1.0, degradationScore);
|
|
121
|
+
return {
|
|
122
|
+
file,
|
|
123
|
+
totalLines: lines.length,
|
|
124
|
+
topHalf: topMetrics,
|
|
125
|
+
bottomHalf: bottomMetrics,
|
|
126
|
+
degradationScore,
|
|
127
|
+
signals,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
measureHalf(content) {
|
|
131
|
+
const lines = content.split('\n');
|
|
132
|
+
const codeLines = lines.filter(l => l.trim() && !l.trim().startsWith('//') && !l.trim().startsWith('#') && !l.trim().startsWith('*'));
|
|
133
|
+
const commentLines = lines.filter(l => {
|
|
134
|
+
const trimmed = l.trim();
|
|
135
|
+
return trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*') || trimmed.startsWith('/*');
|
|
136
|
+
});
|
|
137
|
+
// Comment density
|
|
138
|
+
const commentDensity = codeLines.length > 0 ? commentLines.length / codeLines.length : 0;
|
|
139
|
+
// Function lengths
|
|
140
|
+
const funcLengths = this.measureFunctionLengths(content);
|
|
141
|
+
const avgFunctionLength = funcLengths.length > 0
|
|
142
|
+
? funcLengths.reduce((a, b) => a + b, 0) / funcLengths.length
|
|
143
|
+
: 0;
|
|
144
|
+
// Single-char variables (excluding common loop vars i, j, k in for loops)
|
|
145
|
+
const singleCharMatches = content.match(/\b(?:const|let|var)\s+([a-z])\b/g) || [];
|
|
146
|
+
const singleCharVarCount = singleCharMatches.length;
|
|
147
|
+
// Error handling density
|
|
148
|
+
const tryCount = (content.match(/\btry\s*\{/g) || []).length;
|
|
149
|
+
const funcCount = Math.max(1, funcLengths.length);
|
|
150
|
+
const errorHandlingDensity = tryCount / funcCount;
|
|
151
|
+
// Empty blocks
|
|
152
|
+
const emptyBlockCount = (content.match(/\{\s*\}/g) || []).length;
|
|
153
|
+
// TODO/FIXME/HACK count
|
|
154
|
+
const todoCount = (content.match(/\b(TODO|FIXME|HACK|XXX)\b/gi) || []).length;
|
|
155
|
+
// Average identifier length
|
|
156
|
+
const identifiers = content.match(/\b(?:const|let|var|function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g) || [];
|
|
157
|
+
const identNames = identifiers.map(m => {
|
|
158
|
+
const parts = m.split(/\s+/);
|
|
159
|
+
return parts[parts.length - 1];
|
|
160
|
+
});
|
|
161
|
+
const avgIdentifierLength = identNames.length > 0
|
|
162
|
+
? identNames.reduce((sum, n) => sum + n.length, 0) / identNames.length
|
|
163
|
+
: 0;
|
|
164
|
+
return {
|
|
165
|
+
commentDensity,
|
|
166
|
+
avgFunctionLength,
|
|
167
|
+
singleCharVarCount,
|
|
168
|
+
errorHandlingDensity,
|
|
169
|
+
emptyBlockCount,
|
|
170
|
+
todoCount,
|
|
171
|
+
avgIdentifierLength,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
measureFunctionLengths(content) {
|
|
175
|
+
const lines = content.split('\n');
|
|
176
|
+
const lengths = [];
|
|
177
|
+
const funcStarts = [
|
|
178
|
+
/^(?:export\s+)?(?:async\s+)?function\s+\w+/,
|
|
179
|
+
/^(?:export\s+)?(?:const|let|var)\s+\w+\s*=\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/,
|
|
180
|
+
/^\s+(?:async\s+)?\w+\s*\([^)]*\)\s*\{/,
|
|
181
|
+
];
|
|
182
|
+
for (let i = 0; i < lines.length; i++) {
|
|
183
|
+
for (const pattern of funcStarts) {
|
|
184
|
+
if (pattern.test(lines[i])) {
|
|
185
|
+
// Count function body length
|
|
186
|
+
let braceDepth = 0;
|
|
187
|
+
let started = false;
|
|
188
|
+
let bodyLines = 0;
|
|
189
|
+
for (let j = i; j < lines.length; j++) {
|
|
190
|
+
for (const ch of lines[j]) {
|
|
191
|
+
if (ch === '{') {
|
|
192
|
+
braceDepth++;
|
|
193
|
+
started = true;
|
|
194
|
+
}
|
|
195
|
+
if (ch === '}')
|
|
196
|
+
braceDepth--;
|
|
197
|
+
}
|
|
198
|
+
if (started)
|
|
199
|
+
bodyLines++;
|
|
200
|
+
if (started && braceDepth === 0)
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
if (bodyLines > 0)
|
|
204
|
+
lengths.push(bodyLines);
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return lengths;
|
|
210
|
+
}
|
|
211
|
+
}
|
package/dist/gates/context.js
CHANGED
|
@@ -61,7 +61,7 @@ export class ContextGate extends Gate {
|
|
|
61
61
|
// it's a potential "invented" redundancy (e.g. CORE_URL vs CORE_URL_PROD)
|
|
62
62
|
if (accessedVar !== anchor.id && accessedVar.includes(anchor.id)) {
|
|
63
63
|
const deviation = accessedVar.replace(anchor.id, '').replace(/^_|_$/, '');
|
|
64
|
-
failures.push(this.createFailure(`Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`, [file], `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
|
|
64
|
+
failures.push(this.createFailure(`Context Drift: Redundant variation '${accessedVar}' detected in ${file}.`, [file], `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.`, undefined, undefined, undefined, 'high'));
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -140,7 +140,7 @@ export class ContextGate extends Gate {
|
|
|
140
140
|
if (casing !== dominant && count > threshold) {
|
|
141
141
|
const violatingFiles = entries.filter(e => e.casing === casing).map(e => e.file);
|
|
142
142
|
const uniqueFiles = [...new Set(violatingFiles)].slice(0, 5);
|
|
143
|
-
failures.push(this.createFailure(`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`, uniqueFiles, `Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`, 'Naming Convention Drift'));
|
|
143
|
+
failures.push(this.createFailure(`Cross-file naming inconsistency: ${type} names use ${casing} in ${count} places (dominant is ${dominant})`, uniqueFiles, `Standardize ${type} naming to ${dominant}. Found ${casing} in: ${uniqueFiles.join(', ')}`, 'Naming Convention Drift', undefined, undefined, 'high'));
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
}
|
|
@@ -175,7 +175,7 @@ export class ContextGate extends Gate {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
if (mixedFiles.length > 3) {
|
|
178
|
-
failures.push(this.createFailure(`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`, mixedFiles.slice(0, 5), 'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.', 'Import Pattern Drift'));
|
|
178
|
+
failures.push(this.createFailure(`Cross-file import inconsistency: ${mixedFiles.length} files mix relative and absolute imports`, mixedFiles.slice(0, 5), 'Standardize import style across the codebase. Use either relative (./foo) or path aliases (@/foo) consistently.', 'Import Pattern Drift', undefined, undefined, 'high'));
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
/**
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
import { Gate, GateContext } from './base.js';
|
|
16
|
+
import { Failure } from '../types/index.js';
|
|
17
|
+
export interface DuplicationDriftConfig {
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
similarity_threshold?: number;
|
|
20
|
+
min_body_lines?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare class DuplicationDriftGate extends Gate {
|
|
23
|
+
private config;
|
|
24
|
+
constructor(config?: DuplicationDriftConfig);
|
|
25
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
26
|
+
private extractJSFunctions;
|
|
27
|
+
private extractPyFunctions;
|
|
28
|
+
private extractFunctionBody;
|
|
29
|
+
private normalizeBody;
|
|
30
|
+
private hash;
|
|
31
|
+
private findDuplicateGroups;
|
|
32
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
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
|
+
import { Gate } from './base.js';
|
|
16
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
17
|
+
import { Logger } from '../utils/logger.js';
|
|
18
|
+
import crypto from 'crypto';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
export class DuplicationDriftGate extends Gate {
|
|
21
|
+
config;
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
super('duplication-drift', 'AI Duplication Drift Detection');
|
|
24
|
+
this.config = {
|
|
25
|
+
enabled: config.enabled ?? true,
|
|
26
|
+
similarity_threshold: config.similarity_threshold ?? 0.8,
|
|
27
|
+
min_body_lines: config.min_body_lines ?? 5,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async run(context) {
|
|
31
|
+
if (!this.config.enabled)
|
|
32
|
+
return [];
|
|
33
|
+
const failures = [];
|
|
34
|
+
const functions = [];
|
|
35
|
+
const files = await FileScanner.findFiles({
|
|
36
|
+
cwd: context.cwd,
|
|
37
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py}'],
|
|
38
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
39
|
+
});
|
|
40
|
+
Logger.info(`Duplication Drift: Scanning ${files.length} files`);
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
try {
|
|
43
|
+
const { readFile } = await import('fs-extra');
|
|
44
|
+
const content = await readFile(path.join(context.cwd, file), 'utf-8');
|
|
45
|
+
const ext = path.extname(file);
|
|
46
|
+
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
47
|
+
this.extractJSFunctions(content, file, functions);
|
|
48
|
+
}
|
|
49
|
+
else if (ext === '.py') {
|
|
50
|
+
this.extractPyFunctions(content, file, functions);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (e) { }
|
|
54
|
+
}
|
|
55
|
+
// Compare all function pairs across different files
|
|
56
|
+
const duplicateGroups = this.findDuplicateGroups(functions);
|
|
57
|
+
for (const group of duplicateGroups) {
|
|
58
|
+
const files = group.map(f => f.file);
|
|
59
|
+
const locations = group.map(f => `${f.file}:${f.line} (${f.name})`).join(', ');
|
|
60
|
+
failures.push(this.createFailure(`AI Duplication Drift: Function '${group[0].name}' has ${group.length} near-identical copies across files`, [...new Set(files)], `Found duplicate implementations at: ${locations}. Extract to a shared module and import.`, 'Duplication Drift', group[0].line, undefined, 'high'));
|
|
61
|
+
}
|
|
62
|
+
return failures;
|
|
63
|
+
}
|
|
64
|
+
extractJSFunctions(content, file, functions) {
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
// Match function declarations, arrow functions, and method definitions
|
|
67
|
+
const patterns = [
|
|
68
|
+
// function name(...) {
|
|
69
|
+
/^(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
70
|
+
// const name = (...) => {
|
|
71
|
+
/^(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|(\w+))\s*=>/,
|
|
72
|
+
// name(...) { — class method
|
|
73
|
+
/^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/,
|
|
74
|
+
];
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
for (const pattern of patterns) {
|
|
78
|
+
const match = line.match(pattern);
|
|
79
|
+
if (match) {
|
|
80
|
+
const name = match[1];
|
|
81
|
+
const params = match[2] || '';
|
|
82
|
+
const body = this.extractFunctionBody(lines, i);
|
|
83
|
+
if (body.length >= this.config.min_body_lines) {
|
|
84
|
+
const normalized = this.normalizeBody(body.join('\n'));
|
|
85
|
+
functions.push({
|
|
86
|
+
name,
|
|
87
|
+
file,
|
|
88
|
+
line: i + 1,
|
|
89
|
+
paramCount: params ? params.split(',').length : 0,
|
|
90
|
+
bodyHash: this.hash(normalized),
|
|
91
|
+
bodyLength: body.length,
|
|
92
|
+
normalized,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
extractPyFunctions(content, file, functions) {
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
for (let i = 0; i < lines.length; i++) {
|
|
103
|
+
const match = lines[i].match(/^(?:\s*)(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/);
|
|
104
|
+
if (match) {
|
|
105
|
+
const name = match[1];
|
|
106
|
+
const params = match[2] || '';
|
|
107
|
+
const indent = lines[i].match(/^(\s*)/)?.[1]?.length || 0;
|
|
108
|
+
// Extract body by indentation
|
|
109
|
+
const body = [];
|
|
110
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
111
|
+
const lineIndent = lines[j].match(/^(\s*)/)?.[1]?.length || 0;
|
|
112
|
+
if (lines[j].trim() === '' || lineIndent > indent) {
|
|
113
|
+
body.push(lines[j]);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (body.length >= this.config.min_body_lines) {
|
|
120
|
+
const normalized = this.normalizeBody(body.join('\n'));
|
|
121
|
+
functions.push({
|
|
122
|
+
name,
|
|
123
|
+
file,
|
|
124
|
+
line: i + 1,
|
|
125
|
+
paramCount: params ? params.split(',').length : 0,
|
|
126
|
+
bodyHash: this.hash(normalized),
|
|
127
|
+
bodyLength: body.length,
|
|
128
|
+
normalized,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
extractFunctionBody(lines, startIndex) {
|
|
135
|
+
let braceDepth = 0;
|
|
136
|
+
let started = false;
|
|
137
|
+
const body = [];
|
|
138
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i];
|
|
140
|
+
for (const ch of line) {
|
|
141
|
+
if (ch === '{') {
|
|
142
|
+
braceDepth++;
|
|
143
|
+
started = true;
|
|
144
|
+
}
|
|
145
|
+
if (ch === '}')
|
|
146
|
+
braceDepth--;
|
|
147
|
+
}
|
|
148
|
+
if (started)
|
|
149
|
+
body.push(line);
|
|
150
|
+
if (started && braceDepth === 0)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
return body;
|
|
154
|
+
}
|
|
155
|
+
normalizeBody(body) {
|
|
156
|
+
return body
|
|
157
|
+
.replace(/\/\/.*/g, '') // strip single-line comments
|
|
158
|
+
.replace(/\/\*[\s\S]*?\*\//g, '') // strip multi-line comments
|
|
159
|
+
.replace(/#.*/g, '') // strip Python comments
|
|
160
|
+
.replace(/\s+/g, ' ') // collapse whitespace
|
|
161
|
+
.replace(/['"`]/g, '"') // normalize quotes
|
|
162
|
+
.trim();
|
|
163
|
+
}
|
|
164
|
+
hash(text) {
|
|
165
|
+
return crypto.createHash('md5').update(text).digest('hex');
|
|
166
|
+
}
|
|
167
|
+
findDuplicateGroups(functions) {
|
|
168
|
+
const groups = new Map();
|
|
169
|
+
// Group by body hash (exact duplicates across files)
|
|
170
|
+
for (const fn of functions) {
|
|
171
|
+
const existing = groups.get(fn.bodyHash) || [];
|
|
172
|
+
existing.push(fn);
|
|
173
|
+
groups.set(fn.bodyHash, existing);
|
|
174
|
+
}
|
|
175
|
+
// Filter: only groups with functions from DIFFERENT files, 2+ members
|
|
176
|
+
const duplicates = [];
|
|
177
|
+
for (const group of groups.values()) {
|
|
178
|
+
if (group.length < 2)
|
|
179
|
+
continue;
|
|
180
|
+
const uniqueFiles = new Set(group.map(f => f.file));
|
|
181
|
+
if (uniqueFiles.size >= 2) {
|
|
182
|
+
duplicates.push(group);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return duplicates;
|
|
186
|
+
}
|
|
187
|
+
}
|
package/dist/gates/file.js
CHANGED
|
@@ -22,7 +22,7 @@ export class FileGate extends Gate {
|
|
|
22
22
|
}
|
|
23
23
|
if (violations.length > 0) {
|
|
24
24
|
return [
|
|
25
|
-
this.createFailure(`The following files exceed the maximum limit of ${this.config.maxLines} lines:`, violations, 'Break these files into smaller, more modular components to improve maintainability (SOLID - Single Responsibility Principle).'),
|
|
25
|
+
this.createFailure(`The following files exceed the maximum limit of ${this.config.maxLines} lines:`, violations, 'Break these files into smaller, more modular components to improve maintainability (SOLID - Single Responsibility Principle).', undefined, undefined, undefined, 'low'),
|
|
26
26
|
];
|
|
27
27
|
}
|
|
28
28
|
return [];
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hallucinated Imports Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects imports that reference modules which don't exist in the project.
|
|
5
|
+
* This is an AI-specific failure mode — LLMs confidently generate import
|
|
6
|
+
* statements for packages, files, or modules that were never installed
|
|
7
|
+
* or created.
|
|
8
|
+
*
|
|
9
|
+
* Detection strategy:
|
|
10
|
+
* 1. Parse all import/require statements
|
|
11
|
+
* 2. For relative imports: verify the target file exists
|
|
12
|
+
* 3. For package imports: verify the package exists in node_modules or package.json
|
|
13
|
+
* 4. For Python imports: verify the module exists in the project or site-packages
|
|
14
|
+
*
|
|
15
|
+
* @since v2.16.0
|
|
16
|
+
*/
|
|
17
|
+
import { Gate, GateContext } from './base.js';
|
|
18
|
+
import { Failure } from '../types/index.js';
|
|
19
|
+
export interface HallucinatedImport {
|
|
20
|
+
file: string;
|
|
21
|
+
line: number;
|
|
22
|
+
importPath: string;
|
|
23
|
+
type: 'relative' | 'package' | 'python';
|
|
24
|
+
reason: string;
|
|
25
|
+
}
|
|
26
|
+
export interface HallucinatedImportsConfig {
|
|
27
|
+
enabled?: boolean;
|
|
28
|
+
check_relative?: boolean;
|
|
29
|
+
check_packages?: boolean;
|
|
30
|
+
ignore_patterns?: string[];
|
|
31
|
+
}
|
|
32
|
+
export declare class HallucinatedImportsGate extends Gate {
|
|
33
|
+
private config;
|
|
34
|
+
constructor(config?: HallucinatedImportsConfig);
|
|
35
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
36
|
+
private checkJSImports;
|
|
37
|
+
private checkPyImports;
|
|
38
|
+
private resolveRelativeImport;
|
|
39
|
+
private extractPackageName;
|
|
40
|
+
private shouldIgnore;
|
|
41
|
+
private isNodeBuiltin;
|
|
42
|
+
private isPythonStdlib;
|
|
43
|
+
private loadPackageJson;
|
|
44
|
+
}
|