@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.
@@ -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
+ }
@@ -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
- const score = Math.max(0, 100 - (failures.length * 5)); // Basic SME scoring logic
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) {
@@ -89,7 +89,8 @@ export class PatternIndexer {
89
89
  }
90
90
  // Generate embeddings in parallel batches if enabled
91
91
  if (this.config.useEmbeddings && patterns.length > 0) {
92
- console.log(`Generating embeddings for ${patterns.length} patterns...`);
92
+ // Use stderr to avoid contaminating JSON output on stdout
93
+ process.stderr.write(`Generating embeddings for ${patterns.length} patterns...\n`);
93
94
  for (let i = 0; i < patterns.length; i += BATCH_SIZE) {
94
95
  const batch = patterns.slice(i, i + BATCH_SIZE);
95
96
  await Promise.all(batch.map(async (pattern) => {
@@ -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',