@rigour-labs/core 2.21.2 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/context.test.js +2 -3
- package/dist/environment.test.js +2 -1
- package/dist/gates/agent-team.d.ts +2 -1
- package/dist/gates/agent-team.js +1 -0
- package/dist/gates/base.d.ts +4 -2
- package/dist/gates/base.js +5 -1
- package/dist/gates/checkpoint.d.ts +2 -1
- package/dist/gates/checkpoint.js +3 -2
- package/dist/gates/content.js +1 -1
- package/dist/gates/context-window-artifacts.d.ts +34 -0
- package/dist/gates/context-window-artifacts.js +214 -0
- package/dist/gates/context.d.ts +2 -1
- package/dist/gates/context.js +4 -3
- package/dist/gates/coverage.js +3 -1
- package/dist/gates/dependency.js +5 -5
- package/dist/gates/duplication-drift.d.ts +33 -0
- package/dist/gates/duplication-drift.js +190 -0
- package/dist/gates/environment.js +4 -4
- package/dist/gates/file.js +1 -1
- package/dist/gates/hallucinated-imports.d.ts +63 -0
- package/dist/gates/hallucinated-imports.js +406 -0
- package/dist/gates/inconsistent-error-handling.d.ts +39 -0
- package/dist/gates/inconsistent-error-handling.js +236 -0
- package/dist/gates/promise-safety.d.ts +68 -0
- package/dist/gates/promise-safety.js +509 -0
- package/dist/gates/retry-loop-breaker.d.ts +2 -1
- package/dist/gates/retry-loop-breaker.js +2 -1
- package/dist/gates/runner.js +62 -1
- package/dist/gates/safety.d.ts +2 -1
- package/dist/gates/safety.js +2 -1
- package/dist/gates/security-patterns.d.ts +2 -1
- package/dist/gates/security-patterns.js +2 -1
- package/dist/gates/structure.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/fix-packet-service.d.ts +0 -1
- package/dist/services/fix-packet-service.js +9 -14
- package/dist/services/score-history.d.ts +54 -0
- package/dist/services/score-history.js +122 -0
- package/dist/templates/index.js +195 -0
- package/dist/types/fix-packet.d.ts +5 -5
- package/dist/types/fix-packet.js +1 -1
- package/dist/types/index.d.ts +430 -0
- package/dist/types/index.js +57 -0
- package/package.json +21 -1
- package/src/context.test.ts +0 -256
- package/src/discovery.test.ts +0 -88
- package/src/discovery.ts +0 -112
- package/src/environment.test.ts +0 -115
- package/src/gates/agent-team.test.ts +0 -134
- package/src/gates/agent-team.ts +0 -210
- package/src/gates/ast-handlers/base.ts +0 -13
- package/src/gates/ast-handlers/python.ts +0 -145
- package/src/gates/ast-handlers/python_parser.py +0 -181
- package/src/gates/ast-handlers/typescript.ts +0 -264
- package/src/gates/ast-handlers/universal.ts +0 -184
- package/src/gates/ast.ts +0 -54
- package/src/gates/base.ts +0 -27
- package/src/gates/checkpoint.test.ts +0 -135
- package/src/gates/checkpoint.ts +0 -311
- package/src/gates/content.ts +0 -50
- package/src/gates/context.ts +0 -267
- package/src/gates/coverage.ts +0 -74
- package/src/gates/dependency.ts +0 -108
- package/src/gates/environment.ts +0 -94
- package/src/gates/file.ts +0 -42
- package/src/gates/retry-loop-breaker.ts +0 -151
- package/src/gates/runner.ts +0 -156
- package/src/gates/safety.ts +0 -56
- package/src/gates/security-patterns.test.ts +0 -162
- package/src/gates/security-patterns.ts +0 -305
- package/src/gates/structure.ts +0 -36
- package/src/index.ts +0 -13
- package/src/pattern-index/embeddings.ts +0 -84
- package/src/pattern-index/index.ts +0 -59
- package/src/pattern-index/indexer.test.ts +0 -276
- package/src/pattern-index/indexer.ts +0 -1023
- package/src/pattern-index/matcher.test.ts +0 -293
- package/src/pattern-index/matcher.ts +0 -493
- package/src/pattern-index/overrides.ts +0 -235
- package/src/pattern-index/security.ts +0 -151
- package/src/pattern-index/staleness.test.ts +0 -313
- package/src/pattern-index/staleness.ts +0 -568
- package/src/pattern-index/types.ts +0 -339
- package/src/safety.test.ts +0 -53
- package/src/services/adaptive-thresholds.test.ts +0 -189
- package/src/services/adaptive-thresholds.ts +0 -275
- package/src/services/context-engine.ts +0 -104
- package/src/services/fix-packet-service.ts +0 -42
- package/src/services/state-service.ts +0 -138
- package/src/smoke.test.ts +0 -18
- package/src/templates/index.ts +0 -312
- package/src/types/fix-packet.ts +0 -32
- package/src/types/index.ts +0 -159
- package/src/utils/logger.ts +0 -43
- package/src/utils/scanner.test.ts +0 -37
- package/src/utils/scanner.ts +0 -43
- package/tsconfig.json +0 -10
- package/vitest.config.ts +0 -7
- package/vitest.setup.ts +0 -30
|
@@ -0,0 +1,406 @@
|
|
|
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
|
+
* 5. For Go imports: verify relative package paths exist in the project
|
|
15
|
+
* 6. For Ruby/C#: verify relative require/using paths exist
|
|
16
|
+
*
|
|
17
|
+
* Supported languages: JS/TS, Python, Go, Ruby, C#
|
|
18
|
+
*
|
|
19
|
+
* @since v2.16.0
|
|
20
|
+
*/
|
|
21
|
+
import { Gate } from './base.js';
|
|
22
|
+
import { FileScanner } from '../utils/scanner.js';
|
|
23
|
+
import { Logger } from '../utils/logger.js';
|
|
24
|
+
import fs from 'fs-extra';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
export class HallucinatedImportsGate extends Gate {
|
|
27
|
+
config;
|
|
28
|
+
constructor(config = {}) {
|
|
29
|
+
super('hallucinated-imports', 'Hallucinated Import Detection');
|
|
30
|
+
this.config = {
|
|
31
|
+
enabled: config.enabled ?? true,
|
|
32
|
+
check_relative: config.check_relative ?? true,
|
|
33
|
+
check_packages: config.check_packages ?? true,
|
|
34
|
+
ignore_patterns: config.ignore_patterns ?? [
|
|
35
|
+
'\\.css$', '\\.scss$', '\\.less$', '\\.svg$', '\\.png$', '\\.jpg$',
|
|
36
|
+
'\\.json$', '\\.wasm$', '\\.graphql$', '\\.gql$',
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
get provenance() { return 'ai-drift'; }
|
|
41
|
+
async run(context) {
|
|
42
|
+
if (!this.config.enabled)
|
|
43
|
+
return [];
|
|
44
|
+
const failures = [];
|
|
45
|
+
const hallucinated = [];
|
|
46
|
+
const files = await FileScanner.findFiles({
|
|
47
|
+
cwd: context.cwd,
|
|
48
|
+
patterns: ['**/*.{ts,js,tsx,jsx,py,go,rb,cs}'],
|
|
49
|
+
ignore: [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
50
|
+
'**/.venv/**', '**/venv/**', '**/vendor/**', '**/bin/Debug/**', '**/bin/Release/**', '**/obj/**'],
|
|
51
|
+
});
|
|
52
|
+
Logger.info(`Hallucinated Imports: Scanning ${files.length} files`);
|
|
53
|
+
// Build lookup sets for fast resolution
|
|
54
|
+
const projectFiles = new Set(files.map(f => f.replace(/\\/g, '/')));
|
|
55
|
+
const packageJson = await this.loadPackageJson(context.cwd);
|
|
56
|
+
const allDeps = new Set([
|
|
57
|
+
...Object.keys(packageJson?.dependencies || {}),
|
|
58
|
+
...Object.keys(packageJson?.devDependencies || {}),
|
|
59
|
+
...Object.keys(packageJson?.peerDependencies || {}),
|
|
60
|
+
]);
|
|
61
|
+
// Check if node_modules exists (for package verification)
|
|
62
|
+
const hasNodeModules = await fs.pathExists(path.join(context.cwd, 'node_modules'));
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
try {
|
|
65
|
+
const fullPath = path.join(context.cwd, file);
|
|
66
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
67
|
+
const ext = path.extname(file);
|
|
68
|
+
if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) {
|
|
69
|
+
await this.checkJSImports(content, file, context.cwd, projectFiles, allDeps, hasNodeModules, hallucinated);
|
|
70
|
+
}
|
|
71
|
+
else if (ext === '.py') {
|
|
72
|
+
await this.checkPyImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
73
|
+
}
|
|
74
|
+
else if (ext === '.go') {
|
|
75
|
+
this.checkGoImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
76
|
+
}
|
|
77
|
+
else if (ext === '.rb') {
|
|
78
|
+
this.checkRubyImports(content, file, projectFiles, hallucinated);
|
|
79
|
+
}
|
|
80
|
+
else if (ext === '.cs') {
|
|
81
|
+
this.checkCSharpImports(content, file, projectFiles, hallucinated);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (e) { }
|
|
85
|
+
}
|
|
86
|
+
// Group hallucinated imports by file for cleaner output
|
|
87
|
+
const byFile = new Map();
|
|
88
|
+
for (const h of hallucinated) {
|
|
89
|
+
const existing = byFile.get(h.file) || [];
|
|
90
|
+
existing.push(h);
|
|
91
|
+
byFile.set(h.file, existing);
|
|
92
|
+
}
|
|
93
|
+
for (const [file, imports] of byFile) {
|
|
94
|
+
const details = imports.map(i => ` L${i.line}: import '${i.importPath}' — ${i.reason}`).join('\n');
|
|
95
|
+
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'));
|
|
96
|
+
}
|
|
97
|
+
return failures;
|
|
98
|
+
}
|
|
99
|
+
async checkJSImports(content, file, cwd, projectFiles, allDeps, hasNodeModules, hallucinated) {
|
|
100
|
+
const lines = content.split('\n');
|
|
101
|
+
// Match: import ... from '...', require('...'), import('...')
|
|
102
|
+
const importPatterns = [
|
|
103
|
+
/import\s+(?:{[^}]*}|\*\s+as\s+\w+|\w+(?:\s*,\s*{[^}]*})?)\s+from\s+['"]([^'"]+)['"]/g,
|
|
104
|
+
/require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
105
|
+
/import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
106
|
+
/export\s+(?:{[^}]*}|\*)\s+from\s+['"]([^'"]+)['"]/g,
|
|
107
|
+
];
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
for (const pattern of importPatterns) {
|
|
111
|
+
pattern.lastIndex = 0;
|
|
112
|
+
let match;
|
|
113
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
114
|
+
const importPath = match[1];
|
|
115
|
+
// Skip ignored patterns (assets, etc.)
|
|
116
|
+
if (this.shouldIgnore(importPath))
|
|
117
|
+
continue;
|
|
118
|
+
if (importPath.startsWith('.')) {
|
|
119
|
+
// Relative import — check file exists
|
|
120
|
+
if (this.config.check_relative) {
|
|
121
|
+
const resolved = this.resolveRelativeImport(file, importPath, projectFiles);
|
|
122
|
+
if (!resolved) {
|
|
123
|
+
hallucinated.push({
|
|
124
|
+
file, line: i + 1, importPath, type: 'relative',
|
|
125
|
+
reason: `File not found: ${importPath}`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
// Package import — check it exists
|
|
132
|
+
if (this.config.check_packages) {
|
|
133
|
+
const pkgName = this.extractPackageName(importPath);
|
|
134
|
+
// Skip Node.js built-ins
|
|
135
|
+
if (this.isNodeBuiltin(pkgName))
|
|
136
|
+
continue;
|
|
137
|
+
if (!allDeps.has(pkgName)) {
|
|
138
|
+
// Double-check node_modules if available
|
|
139
|
+
if (hasNodeModules) {
|
|
140
|
+
const pkgPath = path.join(cwd, 'node_modules', pkgName);
|
|
141
|
+
if (await fs.pathExists(pkgPath))
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
hallucinated.push({
|
|
145
|
+
file, line: i + 1, importPath, type: 'package',
|
|
146
|
+
reason: `Package '${pkgName}' not in package.json dependencies`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async checkPyImports(content, file, cwd, projectFiles, hallucinated) {
|
|
156
|
+
const lines = content.split('\n');
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
const line = lines[i].trim();
|
|
159
|
+
// Match: from X import Y, import X
|
|
160
|
+
const fromMatch = line.match(/^from\s+([\w.]+)\s+import/);
|
|
161
|
+
const importMatch = line.match(/^import\s+([\w.]+)/);
|
|
162
|
+
const modulePath = fromMatch?.[1] || importMatch?.[1];
|
|
163
|
+
if (!modulePath)
|
|
164
|
+
continue;
|
|
165
|
+
// Skip standard library modules
|
|
166
|
+
if (this.isPythonStdlib(modulePath))
|
|
167
|
+
continue;
|
|
168
|
+
// Check if it's a relative project import
|
|
169
|
+
if (modulePath.startsWith('.')) {
|
|
170
|
+
// Relative Python import
|
|
171
|
+
const pyFile = modulePath.replace(/\./g, '/') + '.py';
|
|
172
|
+
const pyInit = modulePath.replace(/\./g, '/') + '/__init__.py';
|
|
173
|
+
const fileDir = path.dirname(file);
|
|
174
|
+
const resolved1 = path.join(fileDir, pyFile).replace(/\\/g, '/');
|
|
175
|
+
const resolved2 = path.join(fileDir, pyInit).replace(/\\/g, '/');
|
|
176
|
+
if (!projectFiles.has(resolved1) && !projectFiles.has(resolved2)) {
|
|
177
|
+
hallucinated.push({
|
|
178
|
+
file, line: i + 1, importPath: modulePath, type: 'python',
|
|
179
|
+
reason: `Relative module '${modulePath}' not found in project`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// Absolute import — check if it's a project module
|
|
185
|
+
const topLevel = modulePath.split('.')[0];
|
|
186
|
+
const pyFile = topLevel + '.py';
|
|
187
|
+
const pyInit = topLevel + '/__init__.py';
|
|
188
|
+
// If it matches a project file, it's a local import — verify it exists
|
|
189
|
+
const isLocalModule = projectFiles.has(pyFile) || projectFiles.has(pyInit) ||
|
|
190
|
+
[...projectFiles].some(f => f.startsWith(topLevel + '/'));
|
|
191
|
+
// If not local and not stdlib, we can't easily verify pip packages
|
|
192
|
+
// without a requirements.txt or pyproject.toml check
|
|
193
|
+
if (isLocalModule) {
|
|
194
|
+
// It's referencing a local module — verify the full path
|
|
195
|
+
const fullModulePath = modulePath.replace(/\./g, '/');
|
|
196
|
+
const candidates = [
|
|
197
|
+
fullModulePath + '.py',
|
|
198
|
+
fullModulePath + '/__init__.py',
|
|
199
|
+
];
|
|
200
|
+
const exists = candidates.some(c => projectFiles.has(c));
|
|
201
|
+
if (!exists && modulePath.includes('.')) {
|
|
202
|
+
// Only flag deep module paths that partially resolve
|
|
203
|
+
hallucinated.push({
|
|
204
|
+
file, line: i + 1, importPath: modulePath, type: 'python',
|
|
205
|
+
reason: `Module '${modulePath}' partially resolves but target not found`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
resolveRelativeImport(fromFile, importPath, projectFiles) {
|
|
213
|
+
const dir = path.dirname(fromFile);
|
|
214
|
+
const resolved = path.join(dir, importPath).replace(/\\/g, '/');
|
|
215
|
+
// Try exact match, then common extensions
|
|
216
|
+
const extensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
217
|
+
const indexFiles = extensions.map(ext => `${resolved}/index${ext}`);
|
|
218
|
+
const candidates = [
|
|
219
|
+
...extensions.map(ext => resolved + ext),
|
|
220
|
+
...indexFiles,
|
|
221
|
+
];
|
|
222
|
+
return candidates.some(c => projectFiles.has(c));
|
|
223
|
+
}
|
|
224
|
+
extractPackageName(importPath) {
|
|
225
|
+
// Scoped packages: @scope/package/... → @scope/package
|
|
226
|
+
if (importPath.startsWith('@')) {
|
|
227
|
+
const parts = importPath.split('/');
|
|
228
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : importPath;
|
|
229
|
+
}
|
|
230
|
+
// Regular packages: package/... → package
|
|
231
|
+
return importPath.split('/')[0];
|
|
232
|
+
}
|
|
233
|
+
shouldIgnore(importPath) {
|
|
234
|
+
return this.config.ignore_patterns.some(pattern => new RegExp(pattern).test(importPath));
|
|
235
|
+
}
|
|
236
|
+
isNodeBuiltin(name) {
|
|
237
|
+
const builtins = new Set([
|
|
238
|
+
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
239
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'http2',
|
|
240
|
+
'https', 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',
|
|
241
|
+
'process', 'punycode', 'querystring', 'readline', 'repl', 'stream',
|
|
242
|
+
'string_decoder', 'sys', 'timers', 'tls', 'trace_events', 'tty',
|
|
243
|
+
'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',
|
|
244
|
+
'node:assert', 'node:buffer', 'node:child_process', 'node:cluster',
|
|
245
|
+
'node:console', 'node:constants', 'node:crypto', 'node:dgram',
|
|
246
|
+
'node:dns', 'node:domain', 'node:events', 'node:fs', 'node:http',
|
|
247
|
+
'node:http2', 'node:https', 'node:inspector', 'node:module', 'node:net',
|
|
248
|
+
'node:os', 'node:path', 'node:perf_hooks', 'node:process',
|
|
249
|
+
'node:punycode', 'node:querystring', 'node:readline', 'node:repl',
|
|
250
|
+
'node:stream', 'node:string_decoder', 'node:sys', 'node:timers',
|
|
251
|
+
'node:tls', 'node:trace_events', 'node:tty', 'node:url', 'node:util',
|
|
252
|
+
'node:v8', 'node:vm', 'node:wasi', 'node:worker_threads', 'node:zlib',
|
|
253
|
+
'fs-extra', // common enough to skip
|
|
254
|
+
]);
|
|
255
|
+
return builtins.has(name) || name.startsWith('node:');
|
|
256
|
+
}
|
|
257
|
+
isPythonStdlib(modulePath) {
|
|
258
|
+
const topLevel = modulePath.split('.')[0];
|
|
259
|
+
const stdlibs = new Set([
|
|
260
|
+
'abc', 'aifc', 'argparse', 'array', 'ast', 'asyncio', 'atexit',
|
|
261
|
+
'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins',
|
|
262
|
+
'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code',
|
|
263
|
+
'codecs', 'codeop', 'collections', 'colorsys', 'compileall',
|
|
264
|
+
'concurrent', 'configparser', 'contextlib', 'contextvars', 'copy',
|
|
265
|
+
'copyreg', 'cProfile', 'csv', 'ctypes', 'curses', 'dataclasses',
|
|
266
|
+
'datetime', 'dbm', 'decimal', 'difflib', 'dis', 'distutils',
|
|
267
|
+
'doctest', 'email', 'encodings', 'enum', 'errno', 'faulthandler',
|
|
268
|
+
'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'fractions', 'ftplib',
|
|
269
|
+
'functools', 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'grp',
|
|
270
|
+
'gzip', 'hashlib', 'heapq', 'hmac', 'html', 'http', 'idlelib',
|
|
271
|
+
'imaplib', 'imghdr', 'importlib', 'inspect', 'io', 'ipaddress',
|
|
272
|
+
'itertools', 'json', 'keyword', 'lib2to3', 'linecache', 'locale',
|
|
273
|
+
'logging', 'lzma', 'mailbox', 'mailcap', 'marshal', 'math',
|
|
274
|
+
'mimetypes', 'mmap', 'modulefinder', 'multiprocessing', 'netrc',
|
|
275
|
+
'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os',
|
|
276
|
+
'ossaudiodev', 'parser', 'pathlib', 'pdb', 'pickle', 'pickletools',
|
|
277
|
+
'pipes', 'pkgutil', 'platform', 'plistlib', 'poplib', 'posix',
|
|
278
|
+
'posixpath', 'pprint', 'profile', 'pstats', 'pty', 'pwd', 'py_compile',
|
|
279
|
+
'pyclbr', 'pydoc', 'queue', 'quopri', 'random', 're', 'readline',
|
|
280
|
+
'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched', 'secrets',
|
|
281
|
+
'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal',
|
|
282
|
+
'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver',
|
|
283
|
+
'spwd', 'sqlite3', 'sre_compile', 'sre_constants', 'sre_parse',
|
|
284
|
+
'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct',
|
|
285
|
+
'subprocess', 'sunau', 'symtable', 'sys', 'sysconfig', 'syslog',
|
|
286
|
+
'tabnanny', 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test',
|
|
287
|
+
'textwrap', 'threading', 'time', 'timeit', 'tkinter', 'token',
|
|
288
|
+
'tokenize', 'tomllib', 'trace', 'traceback', 'tracemalloc', 'tty',
|
|
289
|
+
'turtle', 'turtledemo', 'types', 'typing', 'unicodedata', 'unittest',
|
|
290
|
+
'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref',
|
|
291
|
+
'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml',
|
|
292
|
+
'xmlrpc', 'zipapp', 'zipfile', 'zipimport', 'zlib',
|
|
293
|
+
'_thread', '__future__', '__main__',
|
|
294
|
+
]);
|
|
295
|
+
return stdlibs.has(topLevel);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Check Go imports — verify relative/project package paths exist
|
|
299
|
+
* Go stdlib packages are skipped; only project-relative imports are checked
|
|
300
|
+
*/
|
|
301
|
+
checkGoImports(content, file, cwd, projectFiles, hallucinated) {
|
|
302
|
+
const lines = content.split('\n');
|
|
303
|
+
let inImportBlock = false;
|
|
304
|
+
for (let i = 0; i < lines.length; i++) {
|
|
305
|
+
const line = lines[i].trim();
|
|
306
|
+
// Detect import block: import ( ... )
|
|
307
|
+
if (/^import\s*\(/.test(line)) {
|
|
308
|
+
inImportBlock = true;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (inImportBlock && line === ')') {
|
|
312
|
+
inImportBlock = false;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Single import: import "path"
|
|
316
|
+
const singleMatch = line.match(/^import\s+"([^"]+)"/);
|
|
317
|
+
const blockMatch = inImportBlock ? line.match(/^\s*"([^"]+)"/) : null;
|
|
318
|
+
const importPath = singleMatch?.[1] || blockMatch?.[1];
|
|
319
|
+
if (!importPath)
|
|
320
|
+
continue;
|
|
321
|
+
// Skip Go stdlib (no dots in path = stdlib or well-known)
|
|
322
|
+
if (!importPath.includes('.') && !importPath.includes('/'))
|
|
323
|
+
continue;
|
|
324
|
+
// Project-relative imports (contain module path from go.mod)
|
|
325
|
+
// We only flag imports that look like project paths but don't resolve
|
|
326
|
+
if (importPath.includes('/') && !importPath.startsWith('github.com') && !importPath.startsWith('golang.org')) {
|
|
327
|
+
// Check if the path maps to a directory in the project
|
|
328
|
+
const dirPath = importPath.split('/').slice(-2).join('/');
|
|
329
|
+
const hasMatchingFile = [...projectFiles].some(f => f.includes(dirPath));
|
|
330
|
+
if (!hasMatchingFile) {
|
|
331
|
+
hallucinated.push({
|
|
332
|
+
file, line: i + 1, importPath, type: 'go',
|
|
333
|
+
reason: `Go import '${importPath}' — package path not found in project`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Check Ruby imports — verify require_relative paths exist
|
|
341
|
+
*/
|
|
342
|
+
checkRubyImports(content, file, projectFiles, hallucinated) {
|
|
343
|
+
const lines = content.split('\n');
|
|
344
|
+
for (let i = 0; i < lines.length; i++) {
|
|
345
|
+
const line = lines[i].trim();
|
|
346
|
+
// require_relative 'path' — should resolve to a real file
|
|
347
|
+
const relMatch = line.match(/require_relative\s+['"]([^'"]+)['"]/);
|
|
348
|
+
if (relMatch) {
|
|
349
|
+
const reqPath = relMatch[1];
|
|
350
|
+
const dir = path.dirname(file);
|
|
351
|
+
const resolved = path.join(dir, reqPath).replace(/\\/g, '/');
|
|
352
|
+
const candidates = [resolved + '.rb', resolved];
|
|
353
|
+
if (!candidates.some(c => projectFiles.has(c))) {
|
|
354
|
+
hallucinated.push({
|
|
355
|
+
file, line: i + 1, importPath: reqPath, type: 'ruby',
|
|
356
|
+
reason: `require_relative '${reqPath}' — file not found in project`,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Check C# imports — verify relative using paths match project namespaces
|
|
364
|
+
* (C# uses namespaces, not file paths — we check for obviously wrong namespaces)
|
|
365
|
+
*/
|
|
366
|
+
checkCSharpImports(content, file, projectFiles, hallucinated) {
|
|
367
|
+
const lines = content.split('\n');
|
|
368
|
+
for (let i = 0; i < lines.length; i++) {
|
|
369
|
+
const line = lines[i].trim();
|
|
370
|
+
// using ProjectName.Something — check if namespace maps to project files
|
|
371
|
+
const usingMatch = line.match(/^using\s+([\w.]+)\s*;/);
|
|
372
|
+
if (!usingMatch)
|
|
373
|
+
continue;
|
|
374
|
+
const namespace = usingMatch[1];
|
|
375
|
+
// Skip System.* and Microsoft.* and common framework namespaces
|
|
376
|
+
if (/^(?:System|Microsoft|Newtonsoft|NUnit|Xunit|Moq|AutoMapper)\b/.test(namespace))
|
|
377
|
+
continue;
|
|
378
|
+
// Check if the namespace maps to any .cs file path in the project
|
|
379
|
+
const nsPath = namespace.replace(/\./g, '/');
|
|
380
|
+
const hasMatch = [...projectFiles].some(f => f.endsWith('.cs') && (f.includes(nsPath) || f.includes(namespace.split('.')[0])));
|
|
381
|
+
// Only flag if the project has NO files that could match this namespace
|
|
382
|
+
if (!hasMatch && namespace.includes('.')) {
|
|
383
|
+
// Could be a NuGet package — we can't verify without .csproj parsing
|
|
384
|
+
// Only flag obvious project-relative namespaces
|
|
385
|
+
const topLevel = namespace.split('.')[0];
|
|
386
|
+
const hasProjectFiles = [...projectFiles].some(f => f.endsWith('.cs') && f.includes(topLevel));
|
|
387
|
+
if (hasProjectFiles) {
|
|
388
|
+
hallucinated.push({
|
|
389
|
+
file, line: i + 1, importPath: namespace, type: 'csharp',
|
|
390
|
+
reason: `Namespace '${namespace}' — no matching files found in project`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
async loadPackageJson(cwd) {
|
|
397
|
+
try {
|
|
398
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
399
|
+
if (await fs.pathExists(pkgPath)) {
|
|
400
|
+
return await fs.readJson(pkgPath);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (e) { }
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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, Provenance } 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
|
+
protected get provenance(): Provenance;
|
|
34
|
+
run(context: GateContext): Promise<Failure[]>;
|
|
35
|
+
private extractErrorHandlers;
|
|
36
|
+
private classifyStrategy;
|
|
37
|
+
private extractCatchBody;
|
|
38
|
+
private extractCatchCallbackBody;
|
|
39
|
+
}
|