@rigour-labs/core 2.21.2 → 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/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/templates/index.ts +26 -0
- package/src/types/index.ts +41 -0
|
@@ -0,0 +1,292 @@
|
|
|
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 } from './base.js';
|
|
18
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
19
|
+
import { Logger } from '../utils/logger.js';
|
|
20
|
+
import fs from 'fs-extra';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
export class HallucinatedImportsGate extends Gate {
|
|
23
|
+
config;
|
|
24
|
+
constructor(config = {}) {
|
|
25
|
+
super('hallucinated-imports', 'Hallucinated Import Detection');
|
|
26
|
+
this.config = {
|
|
27
|
+
enabled: config.enabled ?? true,
|
|
28
|
+
check_relative: config.check_relative ?? true,
|
|
29
|
+
check_packages: config.check_packages ?? true,
|
|
30
|
+
ignore_patterns: config.ignore_patterns ?? [
|
|
31
|
+
'\\.css$', '\\.scss$', '\\.less$', '\\.svg$', '\\.png$', '\\.jpg$',
|
|
32
|
+
'\\.json$', '\\.wasm$', '\\.graphql$', '\\.gql$',
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async run(context) {
|
|
37
|
+
if (!this.config.enabled)
|
|
38
|
+
return [];
|
|
39
|
+
const failures = [];
|
|
40
|
+
const hallucinated = [];
|
|
41
|
+
const files = await FileScanner.findFiles({
|
|
42
|
+
cwd: context.cwd,
|
|
43
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py}'],
|
|
44
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**', '**/.venv/**'],
|
|
45
|
+
});
|
|
46
|
+
Logger.info(`Hallucinated Imports: Scanning ${files.length} files`);
|
|
47
|
+
// Build lookup sets for fast resolution
|
|
48
|
+
const projectFiles = new Set(files.map(f => f.replace(/\\/g, '/')));
|
|
49
|
+
const packageJson = await this.loadPackageJson(context.cwd);
|
|
50
|
+
const allDeps = new Set([
|
|
51
|
+
...Object.keys(packageJson?.dependencies || {}),
|
|
52
|
+
...Object.keys(packageJson?.devDependencies || {}),
|
|
53
|
+
...Object.keys(packageJson?.peerDependencies || {}),
|
|
54
|
+
]);
|
|
55
|
+
// Check if node_modules exists (for package verification)
|
|
56
|
+
const hasNodeModules = await fs.pathExists(path.join(context.cwd, 'node_modules'));
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
try {
|
|
59
|
+
const fullPath = path.join(context.cwd, file);
|
|
60
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
61
|
+
const ext = path.extname(file);
|
|
62
|
+
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
63
|
+
await this.checkJSImports(content, file, context.cwd, projectFiles, allDeps, hasNodeModules, hallucinated);
|
|
64
|
+
}
|
|
65
|
+
else if (ext === '.py') {
|
|
66
|
+
await this.checkPyImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) { }
|
|
70
|
+
}
|
|
71
|
+
// Group hallucinated imports by file for cleaner output
|
|
72
|
+
const byFile = new Map();
|
|
73
|
+
for (const h of hallucinated) {
|
|
74
|
+
const existing = byFile.get(h.file) || [];
|
|
75
|
+
existing.push(h);
|
|
76
|
+
byFile.set(h.file, existing);
|
|
77
|
+
}
|
|
78
|
+
for (const [file, imports] of byFile) {
|
|
79
|
+
const details = imports.map(i => ` L${i.line}: import '${i.importPath}' — ${i.reason}`).join('\n');
|
|
80
|
+
failures.push(this.createFailure(`Hallucinated imports in ${file}:\n${details}`, [file], `These imports reference modules that don't exist. Remove or replace with real modules. AI models often "hallucinate" package names or file paths.`, 'Hallucinated Imports', imports[0].line, undefined, 'critical'));
|
|
81
|
+
}
|
|
82
|
+
return failures;
|
|
83
|
+
}
|
|
84
|
+
async checkJSImports(content, file, cwd, projectFiles, allDeps, hasNodeModules, hallucinated) {
|
|
85
|
+
const lines = content.split('\n');
|
|
86
|
+
// Match: import ... from '...', require('...'), import('...')
|
|
87
|
+
const importPatterns = [
|
|
88
|
+
/import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+(?:\s*,\s*{[^}]*})?)\s+from\s+['"]([^'"]+)['"]/g,
|
|
89
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
90
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
91
|
+
/export\s+(?:{[^}]*}|\*)\s+from\s+['"]([^'"]+)['"]/g,
|
|
92
|
+
];
|
|
93
|
+
for (let i = 0; i < lines.length; i++) {
|
|
94
|
+
const line = lines[i];
|
|
95
|
+
for (const pattern of importPatterns) {
|
|
96
|
+
pattern.lastIndex = 0;
|
|
97
|
+
let match;
|
|
98
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
99
|
+
const importPath = match[1];
|
|
100
|
+
// Skip ignored patterns (assets, etc.)
|
|
101
|
+
if (this.shouldIgnore(importPath))
|
|
102
|
+
continue;
|
|
103
|
+
if (importPath.startsWith('.')) {
|
|
104
|
+
// Relative import — check file exists
|
|
105
|
+
if (this.config.check_relative) {
|
|
106
|
+
const resolved = this.resolveRelativeImport(file, importPath, projectFiles);
|
|
107
|
+
if (!resolved) {
|
|
108
|
+
hallucinated.push({
|
|
109
|
+
file, line: i + 1, importPath, type: 'relative',
|
|
110
|
+
reason: `File not found: ${importPath}`,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
// Package import — check it exists
|
|
117
|
+
if (this.config.check_packages) {
|
|
118
|
+
const pkgName = this.extractPackageName(importPath);
|
|
119
|
+
// Skip Node.js built-ins
|
|
120
|
+
if (this.isNodeBuiltin(pkgName))
|
|
121
|
+
continue;
|
|
122
|
+
if (!allDeps.has(pkgName)) {
|
|
123
|
+
// Double-check node_modules if available
|
|
124
|
+
if (hasNodeModules) {
|
|
125
|
+
const pkgPath = path.join(cwd, 'node_modules', pkgName);
|
|
126
|
+
if (await fs.pathExists(pkgPath))
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
hallucinated.push({
|
|
130
|
+
file, line: i + 1, importPath, type: 'package',
|
|
131
|
+
reason: `Package '${pkgName}' not in package.json dependencies`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async checkPyImports(content, file, cwd, projectFiles, hallucinated) {
|
|
141
|
+
const lines = content.split('\n');
|
|
142
|
+
for (let i = 0; i < lines.length; i++) {
|
|
143
|
+
const line = lines[i].trim();
|
|
144
|
+
// Match: from X import Y, import X
|
|
145
|
+
const fromMatch = line.match(/^from\s+([\w.]+)\s+import/);
|
|
146
|
+
const importMatch = line.match(/^import\s+([\w.]+)/);
|
|
147
|
+
const modulePath = fromMatch?.[1] || importMatch?.[1];
|
|
148
|
+
if (!modulePath)
|
|
149
|
+
continue;
|
|
150
|
+
// Skip standard library modules
|
|
151
|
+
if (this.isPythonStdlib(modulePath))
|
|
152
|
+
continue;
|
|
153
|
+
// Check if it's a relative project import
|
|
154
|
+
if (modulePath.startsWith('.')) {
|
|
155
|
+
// Relative Python import
|
|
156
|
+
const pyFile = modulePath.replace(/\./g, '/') + '.py';
|
|
157
|
+
const pyInit = modulePath.replace(/\./g, '/') + '/__init__.py';
|
|
158
|
+
const fileDir = path.dirname(file);
|
|
159
|
+
const resolved1 = path.join(fileDir, pyFile).replace(/\\/g, '/');
|
|
160
|
+
const resolved2 = path.join(fileDir, pyInit).replace(/\\/g, '/');
|
|
161
|
+
if (!projectFiles.has(resolved1) && !projectFiles.has(resolved2)) {
|
|
162
|
+
hallucinated.push({
|
|
163
|
+
file, line: i + 1, importPath: modulePath, type: 'python',
|
|
164
|
+
reason: `Relative module '${modulePath}' not found in project`,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Absolute import — check if it's a project module
|
|
170
|
+
const topLevel = modulePath.split('.')[0];
|
|
171
|
+
const pyFile = topLevel + '.py';
|
|
172
|
+
const pyInit = topLevel + '/__init__.py';
|
|
173
|
+
// If it matches a project file, it's a local import — verify it exists
|
|
174
|
+
const isLocalModule = projectFiles.has(pyFile) || projectFiles.has(pyInit) ||
|
|
175
|
+
[...projectFiles].some(f => f.startsWith(topLevel + '/'));
|
|
176
|
+
// If not local and not stdlib, we can't easily verify pip packages
|
|
177
|
+
// without a requirements.txt or pyproject.toml check
|
|
178
|
+
if (isLocalModule) {
|
|
179
|
+
// It's referencing a local module — verify the full path
|
|
180
|
+
const fullModulePath = modulePath.replace(/\./g, '/');
|
|
181
|
+
const candidates = [
|
|
182
|
+
fullModulePath + '.py',
|
|
183
|
+
fullModulePath + '/__init__.py',
|
|
184
|
+
];
|
|
185
|
+
const exists = candidates.some(c => projectFiles.has(c));
|
|
186
|
+
if (!exists && modulePath.includes('.')) {
|
|
187
|
+
// Only flag deep module paths that partially resolve
|
|
188
|
+
hallucinated.push({
|
|
189
|
+
file, line: i + 1, importPath: modulePath, type: 'python',
|
|
190
|
+
reason: `Module '${modulePath}' partially resolves but target not found`,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
resolveRelativeImport(fromFile, importPath, projectFiles) {
|
|
198
|
+
const dir = path.dirname(fromFile);
|
|
199
|
+
const resolved = path.join(dir, importPath).replace(/\\/g, '/');
|
|
200
|
+
// Try exact match, then common extensions
|
|
201
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
202
|
+
const indexFiles = extensions.map(ext => `${resolved}/index${ext}`);
|
|
203
|
+
const candidates = [
|
|
204
|
+
...extensions.map(ext => resolved + ext),
|
|
205
|
+
...indexFiles,
|
|
206
|
+
];
|
|
207
|
+
return candidates.some(c => projectFiles.has(c));
|
|
208
|
+
}
|
|
209
|
+
extractPackageName(importPath) {
|
|
210
|
+
// Scoped packages: @scope/package/... → @scope/package
|
|
211
|
+
if (importPath.startsWith('@')) {
|
|
212
|
+
const parts = importPath.split('/');
|
|
213
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
|
|
214
|
+
}
|
|
215
|
+
// Regular packages: package/... → package
|
|
216
|
+
return importPath.split('/')[0];
|
|
217
|
+
}
|
|
218
|
+
shouldIgnore(importPath) {
|
|
219
|
+
return this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(importPath));
|
|
220
|
+
}
|
|
221
|
+
isNodeBuiltin(name) {
|
|
222
|
+
const builtins = new Set([
|
|
223
|
+
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
224
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2',
|
|
225
|
+
'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',
|
|
226
|
+
'process', 'punycode', 'querystring', 'readline', 'repl', 'stream',
|
|
227
|
+
'string_decoder', 'sys', 'timers', 'tls', 'trace_events', 'tty',
|
|
228
|
+
'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',
|
|
229
|
+
'node:assert', 'node:buffer', 'node:child_process', 'node:cluster',
|
|
230
|
+
'node:console', 'node:constants', 'node:crypto', 'node:dgram',
|
|
231
|
+
'node:dns', 'node:domain', 'node:events', 'node:fs', 'node:http',
|
|
232
|
+
'node:http2', 'node:https', 'node:inspector', 'node:module', 'node:net',
|
|
233
|
+
'node:os', 'node:path', 'node:perf_hooks', 'node:process',
|
|
234
|
+
'node:punycode', 'node:querystring', 'node:readline', 'node:repl',
|
|
235
|
+
'node:stream', 'node:string_decoder', 'node:sys', 'node:timers',
|
|
236
|
+
'node:tls', 'node:trace_events', 'node:tty', 'node:url', 'node:util',
|
|
237
|
+
'node:v8', 'node:vm', 'node:wasi', 'node:worker_threads', 'node:zlib',
|
|
238
|
+
'fs-extra', // common enough to skip
|
|
239
|
+
]);
|
|
240
|
+
return builtins.has(name) || name.startsWith('node:');
|
|
241
|
+
}
|
|
242
|
+
isPythonStdlib(modulePath) {
|
|
243
|
+
const topLevel = modulePath.split('.')[0];
|
|
244
|
+
const stdlibs = new Set([
|
|
245
|
+
'abc', 'aifc', 'argparse', 'array', 'ast', 'asyncio', 'atexit',
|
|
246
|
+
'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins',
|
|
247
|
+
'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code',
|
|
248
|
+
'codecs', 'codeop', 'collections', 'colorsys', 'compileall',
|
|
249
|
+
'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy',
|
|
250
|
+
'copyreg', 'cProfile', 'csv', 'ctypes', 'curses', 'dataclasses',
|
|
251
|
+
'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils',
|
|
252
|
+
'doctest', 'email', 'encodings', 'enum', 'errno', 'faulthandler',
|
|
253
|
+
'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'fractions', 'ftplib',
|
|
254
|
+
'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp',
|
|
255
|
+
'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'idlelib',
|
|
256
|
+
'imaplib', 'imghdr', 'importlib', 'inspect', 'io', 'ipaddress',
|
|
257
|
+
'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale',
|
|
258
|
+
'logging', 'lzma', 'mailbox', 'mailcap', 'marshal', 'math',
|
|
259
|
+
'mimetypes', 'mmap', 'modulefinder', 'multiprocessing', 'netrc',
|
|
260
|
+
'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os',
|
|
261
|
+
'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools',
|
|
262
|
+
'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix',
|
|
263
|
+
'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile',
|
|
264
|
+
'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline',
|
|
265
|
+
'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets',
|
|
266
|
+
'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal',
|
|
267
|
+
'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver',
|
|
268
|
+
'spwd', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse',
|
|
269
|
+
'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct',
|
|
270
|
+
'subprocess', 'sunau', 'symtable', 'sys', 'sysconfig', 'syslog',
|
|
271
|
+
'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test',
|
|
272
|
+
'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token',
|
|
273
|
+
'tokenize', 'tomllib', 'trace', 'traceback', 'tracemalloc', 'tty',
|
|
274
|
+
'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest',
|
|
275
|
+
'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref',
|
|
276
|
+
'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml',
|
|
277
|
+
'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib',
|
|
278
|
+
'_thread', '__future__', '__main__',
|
|
279
|
+
]);
|
|
280
|
+
return stdlibs.has(topLevel);
|
|
281
|
+
}
|
|
282
|
+
async loadPackageJson(cwd) {
|
|
283
|
+
try {
|
|
284
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
285
|
+
if (await fs.pathExists(pkgPath)) {
|
|
286
|
+
return await fs.readJson(pkgPath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch (e) { }
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inconsistent Error Handling Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects when the same error type is handled differently across the codebase.
|
|
5
|
+
* This is an AI-specific failure mode — each agent session writes error handling
|
|
6
|
+
* from scratch, leading to 4 different patterns for the same error type.
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Extract all try-catch blocks and error handling patterns
|
|
10
|
+
* 2. Cluster by caught error type (e.g. "Error", "TypeError", custom errors)
|
|
11
|
+
* 3. Compare handling strategies within each cluster
|
|
12
|
+
* 4. Flag types with >2 distinct handling patterns across files
|
|
13
|
+
*
|
|
14
|
+
* Examples of inconsistency:
|
|
15
|
+
* - File A: catch(e) { console.log(e) }
|
|
16
|
+
* - File B: catch(e) { throw new AppError(e) }
|
|
17
|
+
* - File C: catch(e) { return null }
|
|
18
|
+
* - File D: catch(e) { [empty] }
|
|
19
|
+
*
|
|
20
|
+
* @since v2.16.0
|
|
21
|
+
*/
|
|
22
|
+
import { Gate, GateContext } from './base.js';
|
|
23
|
+
import { Failure } from '../types/index.js';
|
|
24
|
+
export interface InconsistentErrorHandlingConfig {
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
max_strategies_per_type?: number;
|
|
27
|
+
min_occurrences?: number;
|
|
28
|
+
ignore_empty_catches?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare class InconsistentErrorHandlingGate extends Gate {
|
|
31
|
+
private config;
|
|
32
|
+
constructor(config?: InconsistentErrorHandlingConfig);
|
|
33
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
34
|
+
private extractErrorHandlers;
|
|
35
|
+
private classifyStrategy;
|
|
36
|
+
private extractCatchBody;
|
|
37
|
+
private extractCatchCallbackBody;
|
|
38
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inconsistent Error Handling Gate
|
|
3
|
+
*
|
|
4
|
+
* Detects when the same error type is handled differently across the codebase.
|
|
5
|
+
* This is an AI-specific failure mode — each agent session writes error handling
|
|
6
|
+
* from scratch, leading to 4 different patterns for the same error type.
|
|
7
|
+
*
|
|
8
|
+
* Detection strategy:
|
|
9
|
+
* 1. Extract all try-catch blocks and error handling patterns
|
|
10
|
+
* 2. Cluster by caught error type (e.g. "Error", "TypeError", custom errors)
|
|
11
|
+
* 3. Compare handling strategies within each cluster
|
|
12
|
+
* 4. Flag types with >2 distinct handling patterns across files
|
|
13
|
+
*
|
|
14
|
+
* Examples of inconsistency:
|
|
15
|
+
* - File A: catch(e) { console.log(e) }
|
|
16
|
+
* - File B: catch(e) { throw new AppError(e) }
|
|
17
|
+
* - File C: catch(e) { return null }
|
|
18
|
+
* - File D: catch(e) { [empty] }
|
|
19
|
+
*
|
|
20
|
+
* @since v2.16.0
|
|
21
|
+
*/
|
|
22
|
+
import { Gate } from './base.js';
|
|
23
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
24
|
+
import { Logger } from '../utils/logger.js';
|
|
25
|
+
import fs from 'fs-extra';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
export class InconsistentErrorHandlingGate extends Gate {
|
|
28
|
+
config;
|
|
29
|
+
constructor(config = {}) {
|
|
30
|
+
super('inconsistent-error-handling', 'Inconsistent Error Handling Detection');
|
|
31
|
+
this.config = {
|
|
32
|
+
enabled: config.enabled ?? true,
|
|
33
|
+
max_strategies_per_type: config.max_strategies_per_type ?? 2,
|
|
34
|
+
min_occurrences: config.min_occurrences ?? 3,
|
|
35
|
+
ignore_empty_catches: config.ignore_empty_catches ?? false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
async run(context) {
|
|
39
|
+
if (!this.config.enabled)
|
|
40
|
+
return [];
|
|
41
|
+
const failures = [];
|
|
42
|
+
const handlers = [];
|
|
43
|
+
const files = await FileScanner.findFiles({
|
|
44
|
+
cwd: context.cwd,
|
|
45
|
+
patterns: ['**/*.{ts,js,tsx,jsx}'],
|
|
46
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/*.test.*', '**/*.spec.*'],
|
|
47
|
+
});
|
|
48
|
+
Logger.info(`Inconsistent Error Handling: Scanning ${files.length} files`);
|
|
49
|
+
for (const file of files) {
|
|
50
|
+
try {
|
|
51
|
+
const content = await fs.readFile(path.join(context.cwd, file), 'utf-8');
|
|
52
|
+
this.extractErrorHandlers(content, file, handlers);
|
|
53
|
+
}
|
|
54
|
+
catch (e) { }
|
|
55
|
+
}
|
|
56
|
+
// Group by error type
|
|
57
|
+
const byType = new Map();
|
|
58
|
+
for (const handler of handlers) {
|
|
59
|
+
const existing = byType.get(handler.errorType) || [];
|
|
60
|
+
existing.push(handler);
|
|
61
|
+
byType.set(handler.errorType, existing);
|
|
62
|
+
}
|
|
63
|
+
// Analyze each error type for inconsistent handling
|
|
64
|
+
for (const [errorType, typeHandlers] of byType) {
|
|
65
|
+
if (typeHandlers.length < this.config.min_occurrences)
|
|
66
|
+
continue;
|
|
67
|
+
// Count unique strategies
|
|
68
|
+
const strategies = new Map();
|
|
69
|
+
for (const handler of typeHandlers) {
|
|
70
|
+
const existing = strategies.get(handler.strategy) || [];
|
|
71
|
+
existing.push(handler);
|
|
72
|
+
strategies.set(handler.strategy, existing);
|
|
73
|
+
}
|
|
74
|
+
// Filter out empty catches if configured
|
|
75
|
+
const activeStrategies = this.config.ignore_empty_catches
|
|
76
|
+
? new Map([...strategies].filter(([k]) => k !== 'swallow'))
|
|
77
|
+
: strategies;
|
|
78
|
+
if (activeStrategies.size > this.config.max_strategies_per_type) {
|
|
79
|
+
// Only flag if handlers span multiple files
|
|
80
|
+
const uniqueFiles = new Set(typeHandlers.map(h => h.file));
|
|
81
|
+
if (uniqueFiles.size < 2)
|
|
82
|
+
continue;
|
|
83
|
+
const strategyBreakdown = [...activeStrategies.entries()]
|
|
84
|
+
.map(([strategy, handlers]) => {
|
|
85
|
+
const files = [...new Set(handlers.map(h => h.file))].slice(0, 3);
|
|
86
|
+
return ` • ${strategy} (${handlers.length}x): ${files.join(', ')}`;
|
|
87
|
+
})
|
|
88
|
+
.join('\n');
|
|
89
|
+
failures.push(this.createFailure(`Inconsistent error handling for '${errorType}': ${activeStrategies.size} different strategies found across ${uniqueFiles.size} files:\n${strategyBreakdown}`, [...uniqueFiles].slice(0, 5), `Standardize error handling for '${errorType}'. Create a shared error handler or establish a project convention. AI agents often write error handling from scratch each session, leading to divergent patterns.`, 'Inconsistent Error Handling', typeHandlers[0].line, undefined, 'high'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return failures;
|
|
93
|
+
}
|
|
94
|
+
extractErrorHandlers(content, file, handlers) {
|
|
95
|
+
const lines = content.split('\n');
|
|
96
|
+
for (let i = 0; i < lines.length; i++) {
|
|
97
|
+
// Match catch clauses: catch (e), catch (error: Error), catch (e: TypeError)
|
|
98
|
+
const catchMatch = lines[i].match(/\bcatch\s*\(\s*(\w+)(?:\s*:\s*(\w+))?\s*\)/);
|
|
99
|
+
if (!catchMatch)
|
|
100
|
+
continue;
|
|
101
|
+
const varName = catchMatch[1];
|
|
102
|
+
const explicitType = catchMatch[2] || 'any';
|
|
103
|
+
// Extract catch body (up to next closing brace at same level)
|
|
104
|
+
const body = this.extractCatchBody(lines, i);
|
|
105
|
+
if (!body)
|
|
106
|
+
continue;
|
|
107
|
+
const strategy = this.classifyStrategy(body, varName);
|
|
108
|
+
const rawPattern = body.split('\n')[0]?.trim() || '';
|
|
109
|
+
handlers.push({
|
|
110
|
+
file,
|
|
111
|
+
line: i + 1,
|
|
112
|
+
errorType: explicitType,
|
|
113
|
+
strategy,
|
|
114
|
+
rawPattern,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// Also detect .catch() promise patterns
|
|
118
|
+
for (let i = 0; i < lines.length; i++) {
|
|
119
|
+
const catchMatch = lines[i].match(/\.catch\s*\(\s*(?:async\s+)?(?:\(\s*)?(\w+)/);
|
|
120
|
+
if (!catchMatch)
|
|
121
|
+
continue;
|
|
122
|
+
const varName = catchMatch[1];
|
|
123
|
+
// Extract the callback body
|
|
124
|
+
const body = this.extractCatchCallbackBody(lines, i);
|
|
125
|
+
if (!body)
|
|
126
|
+
continue;
|
|
127
|
+
const strategy = this.classifyStrategy(body, varName);
|
|
128
|
+
handlers.push({
|
|
129
|
+
file,
|
|
130
|
+
line: i + 1,
|
|
131
|
+
errorType: 'Promise',
|
|
132
|
+
strategy,
|
|
133
|
+
rawPattern: body.split('\n')[0]?.trim() || '',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
classifyStrategy(body, varName) {
|
|
138
|
+
const trimmed = body.trim();
|
|
139
|
+
// Empty catch (swallow)
|
|
140
|
+
if (!trimmed || trimmed === '{}' || trimmed === '') {
|
|
141
|
+
return 'swallow';
|
|
142
|
+
}
|
|
143
|
+
// Re-throw
|
|
144
|
+
if (/\bthrow\b/.test(trimmed)) {
|
|
145
|
+
if (/\bthrow\s+new\b/.test(trimmed))
|
|
146
|
+
return 'wrap-and-throw';
|
|
147
|
+
if (new RegExp(`\\bthrow\\s+${varName}\\b`).test(trimmed))
|
|
148
|
+
return 'rethrow';
|
|
149
|
+
return 'throw-new';
|
|
150
|
+
}
|
|
151
|
+
// Return patterns
|
|
152
|
+
if (/\breturn\s+null\b/.test(trimmed))
|
|
153
|
+
return 'return-null';
|
|
154
|
+
if (/\breturn\s+undefined\b/.test(trimmed) || /\breturn\s*;/.test(trimmed))
|
|
155
|
+
return 'return-undefined';
|
|
156
|
+
if (/\breturn\s+\[\s*\]/.test(trimmed))
|
|
157
|
+
return 'return-empty-array';
|
|
158
|
+
if (/\breturn\s+\{\s*\}/.test(trimmed))
|
|
159
|
+
return 'return-empty-object';
|
|
160
|
+
if (/\breturn\s+false\b/.test(trimmed))
|
|
161
|
+
return 'return-false';
|
|
162
|
+
if (/\breturn\s+/.test(trimmed))
|
|
163
|
+
return 'return-value';
|
|
164
|
+
// Logging patterns
|
|
165
|
+
if (/console\.(error|warn)\b/.test(trimmed))
|
|
166
|
+
return 'log-error';
|
|
167
|
+
if (/console\.log\b/.test(trimmed))
|
|
168
|
+
return 'log-info';
|
|
169
|
+
if (/\blogger\b/i.test(trimmed) || /\blog\b/i.test(trimmed))
|
|
170
|
+
return 'log-custom';
|
|
171
|
+
// Process.exit
|
|
172
|
+
if (/process\.exit\b/.test(trimmed))
|
|
173
|
+
return 'exit';
|
|
174
|
+
// Response patterns (Express/HTTP)
|
|
175
|
+
if (/\bres\s*\.\s*status\b/.test(trimmed) || /\bres\s*\.\s*json\b/.test(trimmed))
|
|
176
|
+
return 'http-response';
|
|
177
|
+
// Notification patterns
|
|
178
|
+
if (/\bnotif|toast|alert|modal\b/i.test(trimmed))
|
|
179
|
+
return 'user-notification';
|
|
180
|
+
return 'other';
|
|
181
|
+
}
|
|
182
|
+
extractCatchBody(lines, catchLine) {
|
|
183
|
+
let braceDepth = 0;
|
|
184
|
+
let started = false;
|
|
185
|
+
const body = [];
|
|
186
|
+
for (let i = catchLine; i < lines.length; i++) {
|
|
187
|
+
for (const ch of lines[i]) {
|
|
188
|
+
if (ch === '{') {
|
|
189
|
+
braceDepth++;
|
|
190
|
+
started = true;
|
|
191
|
+
}
|
|
192
|
+
if (ch === '}')
|
|
193
|
+
braceDepth--;
|
|
194
|
+
}
|
|
195
|
+
if (started && i > catchLine)
|
|
196
|
+
body.push(lines[i]);
|
|
197
|
+
if (started && braceDepth === 0)
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
return body.length > 0 ? body.join('\n') : null;
|
|
201
|
+
}
|
|
202
|
+
extractCatchCallbackBody(lines, startLine) {
|
|
203
|
+
let depth = 0;
|
|
204
|
+
let started = false;
|
|
205
|
+
const body = [];
|
|
206
|
+
for (let i = startLine; i < Math.min(startLine + 20, lines.length); i++) {
|
|
207
|
+
for (const ch of lines[i]) {
|
|
208
|
+
if (ch === '{' || ch === '(') {
|
|
209
|
+
depth++;
|
|
210
|
+
started = true;
|
|
211
|
+
}
|
|
212
|
+
if (ch === '}' || ch === ')')
|
|
213
|
+
depth--;
|
|
214
|
+
}
|
|
215
|
+
if (started && i > startLine)
|
|
216
|
+
body.push(lines[i]);
|
|
217
|
+
if (started && depth <= 0)
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
return body.length > 0 ? body.join('\n') : null;
|
|
221
|
+
}
|
|
222
|
+
}
|
package/dist/gates/runner.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SEVERITY_WEIGHTS } from '../types/index.js';
|
|
1
2
|
import { FileGate } from './file.js';
|
|
2
3
|
import { ContentGate } from './content.js';
|
|
3
4
|
import { StructureGate } from './structure.js';
|
|
@@ -12,6 +13,10 @@ import { RetryLoopBreakerGate } from './retry-loop-breaker.js';
|
|
|
12
13
|
import { AgentTeamGate } from './agent-team.js';
|
|
13
14
|
import { CheckpointGate } from './checkpoint.js';
|
|
14
15
|
import { SecurityPatternsGate } from './security-patterns.js';
|
|
16
|
+
import { DuplicationDriftGate } from './duplication-drift.js';
|
|
17
|
+
import { HallucinatedImportsGate } from './hallucinated-imports.js';
|
|
18
|
+
import { InconsistentErrorHandlingGate } from './inconsistent-error-handling.js';
|
|
19
|
+
import { ContextWindowArtifactsGate } from './context-window-artifacts.js';
|
|
15
20
|
import { execa } from 'execa';
|
|
16
21
|
import { Logger } from '../utils/logger.js';
|
|
17
22
|
export class GateRunner {
|
|
@@ -55,6 +60,19 @@ export class GateRunner {
|
|
|
55
60
|
if (this.config.gates.security?.enabled !== false) {
|
|
56
61
|
this.gates.push(new SecurityPatternsGate(this.config.gates.security));
|
|
57
62
|
}
|
|
63
|
+
// v2.16+ AI-Native Drift Detection Gates (enabled by default)
|
|
64
|
+
if (this.config.gates.duplication_drift?.enabled !== false) {
|
|
65
|
+
this.gates.push(new DuplicationDriftGate(this.config.gates.duplication_drift));
|
|
66
|
+
}
|
|
67
|
+
if (this.config.gates.hallucinated_imports?.enabled !== false) {
|
|
68
|
+
this.gates.push(new HallucinatedImportsGate(this.config.gates.hallucinated_imports));
|
|
69
|
+
}
|
|
70
|
+
if (this.config.gates.inconsistent_error_handling?.enabled !== false) {
|
|
71
|
+
this.gates.push(new InconsistentErrorHandlingGate(this.config.gates.inconsistent_error_handling));
|
|
72
|
+
}
|
|
73
|
+
if (this.config.gates.context_window_artifacts?.enabled !== false) {
|
|
74
|
+
this.gates.push(new ContextWindowArtifactsGate(this.config.gates.context_window_artifacts));
|
|
75
|
+
}
|
|
58
76
|
// Environment Alignment Gate (Should be prioritized)
|
|
59
77
|
if (this.config.gates.environment?.enabled) {
|
|
60
78
|
this.gates.unshift(new EnvironmentGate(this.config.gates));
|
|
@@ -125,7 +143,16 @@ export class GateRunner {
|
|
|
125
143
|
}
|
|
126
144
|
}
|
|
127
145
|
const status = failures.length > 0 ? 'FAIL' : 'PASS';
|
|
128
|
-
|
|
146
|
+
// Severity-weighted scoring: each failure deducts based on its severity
|
|
147
|
+
// critical=20, high=10, medium=5, low=2, info=0
|
|
148
|
+
const severityBreakdown = {};
|
|
149
|
+
let totalDeduction = 0;
|
|
150
|
+
for (const f of failures) {
|
|
151
|
+
const sev = (f.severity || 'medium');
|
|
152
|
+
severityBreakdown[sev] = (severityBreakdown[sev] || 0) + 1;
|
|
153
|
+
totalDeduction += SEVERITY_WEIGHTS[sev] ?? 5;
|
|
154
|
+
}
|
|
155
|
+
const score = Math.max(0, 100 - totalDeduction);
|
|
129
156
|
return {
|
|
130
157
|
status,
|
|
131
158
|
summary,
|
|
@@ -133,6 +160,7 @@ export class GateRunner {
|
|
|
133
160
|
stats: {
|
|
134
161
|
duration_ms: Date.now() - start,
|
|
135
162
|
score,
|
|
163
|
+
severity_breakdown: severityBreakdown,
|
|
136
164
|
},
|
|
137
165
|
};
|
|
138
166
|
}
|
|
@@ -186,7 +186,7 @@ export class SecurityPatternsGate extends Gate {
|
|
|
186
186
|
const blockThreshold = this.severityOrder[this.config.block_on_severity ?? 'high'];
|
|
187
187
|
for (const vuln of filteredVulns) {
|
|
188
188
|
if (this.severityOrder[vuln.severity] <= blockThreshold) {
|
|
189
|
-
failures.push(this.createFailure(`[${vuln.cwe}] ${vuln.description}`, [vuln.file], `Found: "${vuln.match.slice(0, 60)}..." - Use parameterized queries/sanitization.`, `Security: ${vuln.type.replace('_', ' ').toUpperCase()}`, vuln.line, vuln.line));
|
|
189
|
+
failures.push(this.createFailure(`[${vuln.cwe}] ${vuln.description}`, [vuln.file], `Found: "${vuln.match.slice(0, 60)}..." - Use parameterized queries/sanitization.`, `Security: ${vuln.type.replace('_', ' ').toUpperCase()}`, vuln.line, vuln.line, vuln.severity));
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
192
|
if (filteredVulns.length > 0 && failures.length === 0) {
|
package/dist/templates/index.js
CHANGED
|
@@ -286,6 +286,32 @@ export const UNIVERSAL_CONFIG = {
|
|
|
286
286
|
'prefer-const': false,
|
|
287
287
|
},
|
|
288
288
|
},
|
|
289
|
+
duplication_drift: {
|
|
290
|
+
enabled: true,
|
|
291
|
+
similarity_threshold: 0.8,
|
|
292
|
+
min_body_lines: 5,
|
|
293
|
+
},
|
|
294
|
+
hallucinated_imports: {
|
|
295
|
+
enabled: true,
|
|
296
|
+
check_relative: true,
|
|
297
|
+
check_packages: true,
|
|
298
|
+
ignore_patterns: [
|
|
299
|
+
'\\.css$', '\\.scss$', '\\.less$', '\\.svg$', '\\.png$', '\\.jpg$',
|
|
300
|
+
'\\.json$', '\\.wasm$', '\\.graphql$', '\\.gql$',
|
|
301
|
+
],
|
|
302
|
+
},
|
|
303
|
+
inconsistent_error_handling: {
|
|
304
|
+
enabled: true,
|
|
305
|
+
max_strategies_per_type: 2,
|
|
306
|
+
min_occurrences: 3,
|
|
307
|
+
ignore_empty_catches: false,
|
|
308
|
+
},
|
|
309
|
+
context_window_artifacts: {
|
|
310
|
+
enabled: true,
|
|
311
|
+
min_file_lines: 100,
|
|
312
|
+
degradation_threshold: 0.4,
|
|
313
|
+
signals_required: 2,
|
|
314
|
+
},
|
|
289
315
|
},
|
|
290
316
|
output: {
|
|
291
317
|
report_path: 'rigour-report.json',
|