@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.
Files changed (101) hide show
  1. package/README.md +58 -0
  2. package/dist/context.test.js +2 -3
  3. package/dist/environment.test.js +2 -1
  4. package/dist/gates/agent-team.d.ts +2 -1
  5. package/dist/gates/agent-team.js +1 -0
  6. package/dist/gates/base.d.ts +4 -2
  7. package/dist/gates/base.js +5 -1
  8. package/dist/gates/checkpoint.d.ts +2 -1
  9. package/dist/gates/checkpoint.js +3 -2
  10. package/dist/gates/content.js +1 -1
  11. package/dist/gates/context-window-artifacts.d.ts +34 -0
  12. package/dist/gates/context-window-artifacts.js +214 -0
  13. package/dist/gates/context.d.ts +2 -1
  14. package/dist/gates/context.js +4 -3
  15. package/dist/gates/coverage.js +3 -1
  16. package/dist/gates/dependency.js +5 -5
  17. package/dist/gates/duplication-drift.d.ts +33 -0
  18. package/dist/gates/duplication-drift.js +190 -0
  19. package/dist/gates/environment.js +4 -4
  20. package/dist/gates/file.js +1 -1
  21. package/dist/gates/hallucinated-imports.d.ts +63 -0
  22. package/dist/gates/hallucinated-imports.js +406 -0
  23. package/dist/gates/inconsistent-error-handling.d.ts +39 -0
  24. package/dist/gates/inconsistent-error-handling.js +236 -0
  25. package/dist/gates/promise-safety.d.ts +68 -0
  26. package/dist/gates/promise-safety.js +509 -0
  27. package/dist/gates/retry-loop-breaker.d.ts +2 -1
  28. package/dist/gates/retry-loop-breaker.js +2 -1
  29. package/dist/gates/runner.js +62 -1
  30. package/dist/gates/safety.d.ts +2 -1
  31. package/dist/gates/safety.js +2 -1
  32. package/dist/gates/security-patterns.d.ts +2 -1
  33. package/dist/gates/security-patterns.js +2 -1
  34. package/dist/gates/structure.js +1 -1
  35. package/dist/index.d.ts +1 -0
  36. package/dist/index.js +1 -0
  37. package/dist/services/fix-packet-service.d.ts +0 -1
  38. package/dist/services/fix-packet-service.js +9 -14
  39. package/dist/services/score-history.d.ts +54 -0
  40. package/dist/services/score-history.js +122 -0
  41. package/dist/templates/index.js +195 -0
  42. package/dist/types/fix-packet.d.ts +5 -5
  43. package/dist/types/fix-packet.js +1 -1
  44. package/dist/types/index.d.ts +430 -0
  45. package/dist/types/index.js +57 -0
  46. package/package.json +21 -1
  47. package/src/context.test.ts +0 -256
  48. package/src/discovery.test.ts +0 -88
  49. package/src/discovery.ts +0 -112
  50. package/src/environment.test.ts +0 -115
  51. package/src/gates/agent-team.test.ts +0 -134
  52. package/src/gates/agent-team.ts +0 -210
  53. package/src/gates/ast-handlers/base.ts +0 -13
  54. package/src/gates/ast-handlers/python.ts +0 -145
  55. package/src/gates/ast-handlers/python_parser.py +0 -181
  56. package/src/gates/ast-handlers/typescript.ts +0 -264
  57. package/src/gates/ast-handlers/universal.ts +0 -184
  58. package/src/gates/ast.ts +0 -54
  59. package/src/gates/base.ts +0 -27
  60. package/src/gates/checkpoint.test.ts +0 -135
  61. package/src/gates/checkpoint.ts +0 -311
  62. package/src/gates/content.ts +0 -50
  63. package/src/gates/context.ts +0 -267
  64. package/src/gates/coverage.ts +0 -74
  65. package/src/gates/dependency.ts +0 -108
  66. package/src/gates/environment.ts +0 -94
  67. package/src/gates/file.ts +0 -42
  68. package/src/gates/retry-loop-breaker.ts +0 -151
  69. package/src/gates/runner.ts +0 -156
  70. package/src/gates/safety.ts +0 -56
  71. package/src/gates/security-patterns.test.ts +0 -162
  72. package/src/gates/security-patterns.ts +0 -305
  73. package/src/gates/structure.ts +0 -36
  74. package/src/index.ts +0 -13
  75. package/src/pattern-index/embeddings.ts +0 -84
  76. package/src/pattern-index/index.ts +0 -59
  77. package/src/pattern-index/indexer.test.ts +0 -276
  78. package/src/pattern-index/indexer.ts +0 -1023
  79. package/src/pattern-index/matcher.test.ts +0 -293
  80. package/src/pattern-index/matcher.ts +0 -493
  81. package/src/pattern-index/overrides.ts +0 -235
  82. package/src/pattern-index/security.ts +0 -151
  83. package/src/pattern-index/staleness.test.ts +0 -313
  84. package/src/pattern-index/staleness.ts +0 -568
  85. package/src/pattern-index/types.ts +0 -339
  86. package/src/safety.test.ts +0 -53
  87. package/src/services/adaptive-thresholds.test.ts +0 -189
  88. package/src/services/adaptive-thresholds.ts +0 -275
  89. package/src/services/context-engine.ts +0 -104
  90. package/src/services/fix-packet-service.ts +0 -42
  91. package/src/services/state-service.ts +0 -138
  92. package/src/smoke.test.ts +0 -18
  93. package/src/templates/index.ts +0 -312
  94. package/src/types/fix-packet.ts +0 -32
  95. package/src/types/index.ts +0 -159
  96. package/src/utils/logger.ts +0 -43
  97. package/src/utils/scanner.test.ts +0 -37
  98. package/src/utils/scanner.ts +0 -43
  99. package/tsconfig.json +0 -10
  100. package/vitest.config.ts +0 -7
  101. 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
+ }