thuban 0.3.2 → 0.3.4

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 (59) hide show
  1. package/README.md +137 -102
  2. package/dist/LICENSE +21 -0
  3. package/dist/README.md +185 -0
  4. package/dist/cli.js +2 -0
  5. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  6. package/dist/packages/scanner/alert-manager.js +1 -0
  7. package/dist/packages/scanner/baseline-manager.js +1 -0
  8. package/dist/packages/scanner/code-scanner.js +1 -0
  9. package/dist/packages/scanner/codebase-passport.js +1 -0
  10. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  11. package/dist/packages/scanner/dependency-graph.js +1 -0
  12. package/dist/packages/scanner/drift-detector.js +1 -0
  13. package/dist/packages/scanner/executive-report.js +1 -0
  14. package/dist/packages/scanner/file-collector.js +1 -0
  15. package/dist/packages/scanner/file-watcher.js +1 -0
  16. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  17. package/dist/packages/scanner/hallucination-detector.js +1 -0
  18. package/dist/packages/scanner/health-checker.js +1 -0
  19. package/dist/packages/scanner/html-report.js +1 -0
  20. package/dist/packages/scanner/index.js +1 -0
  21. package/dist/packages/scanner/license-manager.js +1 -0
  22. package/dist/packages/scanner/master-health-checker.js +1 -0
  23. package/dist/packages/scanner/monitor-notifier.js +1 -0
  24. package/dist/packages/scanner/monitor-service.js +1 -0
  25. package/dist/packages/scanner/monitor-store.js +1 -0
  26. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  27. package/dist/packages/scanner/scan-diff.js +1 -0
  28. package/dist/packages/scanner/scan-runner.js +1 -0
  29. package/dist/packages/scanner/secret-scanner.js +1 -0
  30. package/dist/packages/scanner/sentinel-core.js +1 -0
  31. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  32. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  33. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  34. package/dist/packages/scanner/widget-generator.js +1 -0
  35. package/package.json +16 -8
  36. package/cli.js +0 -2412
  37. package/packages/scanner/ai-confidence-scorer.js +0 -260
  38. package/packages/scanner/alert-manager.js +0 -398
  39. package/packages/scanner/baseline-manager.js +0 -109
  40. package/packages/scanner/code-scanner.js +0 -713
  41. package/packages/scanner/codebase-passport.js +0 -299
  42. package/packages/scanner/copy-paste-detector.js +0 -250
  43. package/packages/scanner/dependency-graph.js +0 -536
  44. package/packages/scanner/drift-detector.js +0 -423
  45. package/packages/scanner/executive-report.js +0 -774
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -226
  48. package/packages/scanner/hallucination-detector.js +0 -652
  49. package/packages/scanner/health-checker.js +0 -586
  50. package/packages/scanner/html-report.js +0 -634
  51. package/packages/scanner/index.js +0 -92
  52. package/packages/scanner/license-manager.js +0 -318
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/pre-commit-gate.js +0 -216
  55. package/packages/scanner/sentinel-core.js +0 -608
  56. package/packages/scanner/sentinel-knowledge.js +0 -322
  57. package/packages/scanner/tech-debt-analyzer.js +0 -565
  58. package/packages/scanner/tech-debt-cost.js +0 -194
  59. package/packages/scanner/widget-generator.js +0 -415
@@ -1,652 +0,0 @@
1
- /**
2
- * @thuban-dna
3
- * @purpose Detect AI hallucinations, phantom imports, invented APIs, and code drift
4
- * @module thuban
5
- * @layer application
6
- * @imports-from fs, path
7
- * @exports-to cli, tech-debt-analyzer, code-scanner
8
- * @side-effects File system reads, Console output
9
- * @critical-for Core differentiator — no other tool detects AI-generated code issues
10
- */
11
-
12
- const fs = require('fs');
13
- const path = require('path');
14
-
15
- class HallucinationDetector {
16
- constructor(config = {}) {
17
- this.config = {
18
- rootPath: config.rootPath || process.cwd(),
19
- ...config
20
- };
21
-
22
- // Known Node.js built-in modules
23
- this.builtins = new Set([
24
- 'assert', 'async_hooks', 'buffer', 'child_process', 'cluster',
25
- 'console', 'constants', 'crypto', 'dgram', 'diagnostics_channel',
26
- 'dns', 'domain', 'events', 'fs', 'http', 'http2', 'https',
27
- 'inspector', 'module', 'net', 'os', 'path', 'perf_hooks',
28
- 'process', 'punycode', 'querystring', 'readline', 'repl',
29
- 'stream', 'string_decoder', 'sys', 'timers', 'tls', 'trace_events',
30
- 'tty', 'url', 'util', 'v8', 'vm', 'wasi', 'worker_threads', 'zlib',
31
- 'node:fs', 'node:path', 'node:os', 'node:crypto', 'node:http',
32
- 'node:https', 'node:events', 'node:util', 'node:stream',
33
- 'node:child_process', 'node:url', 'node:querystring', 'node:buffer',
34
- 'node:net', 'node:tls', 'node:dns', 'node:readline', 'node:zlib',
35
- 'node:worker_threads', 'node:cluster', 'node:vm', 'node:assert',
36
- 'node:timers', 'node:timers/promises', 'node:test',
37
- 'fs/promises', 'stream/promises', 'timers/promises', 'dns/promises',
38
- 'readline/promises',
39
- ]);
40
-
41
- // Common phantom packages that LLMs hallucinate
42
- this.phantomPackages = new Set([
43
- 'node-fetch-extra', 'express-async', 'mongoose-v2',
44
- 'react-native-web-view', 'axios-retry-enhanced',
45
- 'lodash-extended', 'moment-timezone-v2', 'socket.io-extra',
46
- 'graphql-tools-v2', 'next-auth-v5', 'prisma-client-v2',
47
- 'tailwind-utils', 'react-query-v4', 'jest-extended-v2',
48
- ]);
49
-
50
- // Common hallucinated API patterns (methods that don't exist)
51
- this.phantomAPIs = [
52
- { pattern: /fs\.readFileAsync\s*\(/, suggestion: 'Use fs.promises.readFile() or fs.readFileSync()', name: 'fs.readFileAsync', id: 'HALL_API001' },
53
- { pattern: /fs\.writeFileAsync\s*\(/, suggestion: 'Use fs.promises.writeFile() or fs.writeFileSync()', name: 'fs.writeFileAsync', id: 'HALL_API002' },
54
- { pattern: /fs\.existsAsync\s*\(/, suggestion: 'Use fs.promises.access() or fs.existsSync()', name: 'fs.existsAsync', id: 'HALL_API003' },
55
- { pattern: /path\.exists\s*\(/, suggestion: 'Use fs.existsSync() — path module has no exists()', name: 'path.exists', id: 'HALL_API004' },
56
- { pattern: /path\.isFile\s*\(/, suggestion: 'Use fs.statSync().isFile() — path module has no isFile()', name: 'path.isFile', id: 'HALL_API005' },
57
- { pattern: /path\.isDirectory\s*\(/, suggestion: 'Use fs.statSync().isDirectory()', name: 'path.isDirectory', id: 'HALL_API006' },
58
- { pattern: /console\.success\s*\(/, suggestion: 'Use console.log() — console.success() does not exist', name: 'console.success', id: 'HALL_API007' },
59
- { pattern: /console\.verbose\s*\(/, suggestion: 'Use console.log() or console.debug()', name: 'console.verbose', id: 'HALL_API008' },
60
- { pattern: /JSON\.tryParse\s*\(/, suggestion: 'Use try/catch with JSON.parse()', name: 'JSON.tryParse', id: 'HALL_API009' },
61
- { pattern: /JSON\.safeStringify\s*\(/, suggestion: 'Use JSON.stringify() with a replacer', name: 'JSON.safeStringify', id: 'HALL_API010' },
62
- { pattern: /Array\.flatten\s*\(/, suggestion: 'Use Array.prototype.flat()', name: 'Array.flatten', id: 'HALL_API011' },
63
- { pattern: /Object\.deepClone\s*\(/, suggestion: 'Use structuredClone() or JSON.parse(JSON.stringify())', name: 'Object.deepClone', id: 'HALL_API012' },
64
- { pattern: /Object\.deepMerge\s*\(/, suggestion: 'Use a library like lodash.merge or write a custom function', name: 'Object.deepMerge', id: 'HALL_API013' },
65
- { pattern: /String\.prototype\.replaceAll\s*&&.*polyfill/i, suggestion: 'replaceAll() is native since ES2021 — no polyfill needed', name: 'replaceAll polyfill', id: 'HALL_API014' },
66
- { pattern: /require\s*\(\s*['"]fs\/sync['"]\s*\)/, suggestion: 'fs/sync is not a real module — use fs directly', name: 'fs/sync', id: 'HALL_API015' },
67
- { pattern: /require\s*\(\s*['"]http\/server['"]\s*\)/, suggestion: 'http/server is not a real module — use http.createServer()', name: 'http/server', id: 'HALL_API016' },
68
- { pattern: /process\.env\.get\s*\(/, suggestion: 'Use process.env.VAR_NAME — process.env is a plain object, not a Map', name: 'process.env.get()', id: 'HALL_API017' },
69
- { pattern: /process\.exit\s*\(\s*['"]/, suggestion: 'process.exit() takes a number, not a string', name: 'process.exit(string)', id: 'HALL_API018' },
70
- ];
71
-
72
- // ─── Python phantom APIs ────────────────────────────────
73
- this.pythonPhantomAPIs = [
74
- { pattern: /os\.path\.exists_sync\s*\(/, suggestion: 'Use os.path.exists() — exists_sync does not exist in Python', name: 'os.path.exists_sync', id: 'PY_HALL001' },
75
- { pattern: /json\.tryParse\s*\(/, suggestion: 'Use json.loads() with try/except', name: 'json.tryParse', id: 'PY_HALL002' },
76
- { pattern: /list\.flatMap\s*\(/, suggestion: 'Python lists have no flatMap — use list comprehension', name: 'list.flatMap', id: 'PY_HALL003' },
77
- { pattern: /dict\.merge\s*\(/, suggestion: 'Use {**dict1, **dict2} or dict1 | dict2 (3.9+)', name: 'dict.merge', id: 'PY_HALL004' },
78
- { pattern: /string\.format_map\s*\((?!.*\bself\b)/, suggestion: 'str.format_map exists but is rarely correct — did you mean .format()?', name: 'string.format_map misuse', id: 'PY_HALL005' },
79
- { pattern: /from\s+collections\s+import\s+OrderedDict.*#.*maintain\s+order/i, suggestion: 'Regular dict maintains order since Python 3.7 — OrderedDict is unnecessary', name: 'Unnecessary OrderedDict', id: 'PY_HALL006' },
80
- { pattern: /async\s+def\s+\w+.*asyncio\.sleep\s*\(\s*0\s*\)\s*#.*yield/i, suggestion: 'asyncio.sleep(0) to yield is a code smell — review async design', name: 'asyncio.sleep(0) hack', id: 'PY_HALL007' },
81
- { pattern: /import\s+tensorflow\.v2/, suggestion: 'tensorflow.v2 is not a real module — use import tensorflow', name: 'tensorflow.v2', id: 'PY_HALL008' },
82
- { pattern: /from\s+sklearn\.model_selection\s+import\s+train_test_split_v2/, suggestion: 'train_test_split_v2 does not exist — use train_test_split', name: 'sklearn v2 hallucination', id: 'PY_HALL009' },
83
- { pattern: /requests\.async_get\s*\(/, suggestion: 'requests has no async_get — use aiohttp or httpx', name: 'requests.async_get', id: 'PY_HALL010' },
84
- ];
85
-
86
- this.pythonDeprecated = [
87
- { pattern: /from\s+distutils/, suggestion: 'distutils removed in Python 3.12 — use setuptools', since: 'Python 3.12', name: 'distutils', id: 'PY_DEPR001' },
88
- { pattern: /from\s+imp\s+import/, suggestion: 'imp removed in Python 3.12 — use importlib', since: 'Python 3.12', name: 'imp module', id: 'PY_DEPR002' },
89
- { pattern: /asyncio\.get_event_loop\(\)/, suggestion: 'Deprecated in 3.10+ — use asyncio.run() or get_running_loop()', since: 'Python 3.10', name: 'asyncio.get_event_loop()', id: 'PY_DEPR003' },
90
- { pattern: /collections\.MutableMapping/, suggestion: 'Moved to collections.abc.MutableMapping in Python 3.3+', since: 'Python 3.9', name: 'collections.MutableMapping', id: 'PY_DEPR004' },
91
- { pattern: /optparse\.OptionParser/, suggestion: 'optparse deprecated since Python 3.2 — use argparse', since: 'Python 3.2', name: 'optparse', id: 'PY_DEPR005' },
92
- { pattern: /cgi\.parse_header\s*\(/, suggestion: 'cgi module deprecated in Python 3.11, removed in 3.13', since: 'Python 3.11', name: 'cgi module', id: 'PY_DEPR006' },
93
- { pattern: /from\s+typing\s+import\s+(List|Dict|Tuple|Set)\b/, suggestion: 'Use built-in list, dict, tuple, set for type hints (Python 3.9+)', since: 'Python 3.9', name: 'typing.List/Dict/Tuple', id: 'PY_DEPR007' },
94
- { pattern: /unittest\.makeSuite\s*\(/, suggestion: 'makeSuite deprecated — use TestLoader.loadTestsFromTestCase', since: 'Python 3.11', name: 'unittest.makeSuite', id: 'PY_DEPR008' },
95
- ];
96
-
97
- this.pythonSmells = [
98
- { pattern: /print\s*\(\s*f?['"]\s*debug/i, id: 'PY_SMELL001', name: 'Debug Print', message: 'Debug print statement left in code' },
99
- { pattern: /except\s*:\s*$|except\s+Exception\s*:\s*\n\s*pass/m, id: 'PY_SMELL002', name: 'Bare Except', message: 'Bare except or swallowed exception — hides real errors' },
100
- { pattern: /#\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'PY_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
101
- { pattern: /raise\s+NotImplementedError\s*\(?\s*\)?/, id: 'PY_SMELL004', name: 'Not Implemented', message: 'Stub implementation — function body was not generated' },
102
- { pattern: /['"]your.api.key.here['"]|['"]sk-[.]{3,}['"]/i, id: 'PY_SMELL005', name: 'Dummy Credential', message: 'Placeholder credential — will fail at runtime' },
103
- { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'PY_SMELL006', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
104
- { pattern: /import\s+\*/, id: 'PY_SMELL007', name: 'Wildcard Import', message: 'Wildcard import — pollutes namespace, hides dependencies' },
105
- ];
106
-
107
- // ─── Go phantom APIs ────────────────────────────────────
108
- this.goPhantomAPIs = [
109
- { pattern: /ioutil\.ReadFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.ReadFile()', name: 'ioutil.ReadFile', id: 'GO_HALL001' },
110
- { pattern: /ioutil\.WriteFile\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.WriteFile()', name: 'ioutil.WriteFile', id: 'GO_HALL002' },
111
- { pattern: /ioutil\.TempDir\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use os.MkdirTemp()', name: 'ioutil.TempDir', id: 'GO_HALL003' },
112
- { pattern: /ioutil\.ReadAll\s*\(/, suggestion: 'ioutil removed in Go 1.16 — use io.ReadAll()', name: 'ioutil.ReadAll', id: 'GO_HALL004' },
113
- { pattern: /strings\.Title\s*\(/, suggestion: 'strings.Title deprecated in Go 1.18 — use cases.Title from golang.org/x/text', name: 'strings.Title', id: 'GO_HALL005' },
114
- ];
115
-
116
- this.goSmells = [
117
- { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'GO_SMELL001', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
118
- { pattern: /panic\s*\(\s*["']not implemented["']\s*\)/, id: 'GO_SMELL002', name: 'Panic Not Implemented', message: 'Stub implementation — will panic at runtime' },
119
- { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'GO_SMELL003', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
120
- ];
121
-
122
- // ─── Rust phantom APIs ──────────────────────────────────
123
- this.rustSmells = [
124
- { pattern: /todo!\s*\(\s*\)/, id: 'RS_SMELL001', name: 'todo! macro', message: 'todo!() macro — will panic at runtime' },
125
- { pattern: /unimplemented!\s*\(\s*\)/, id: 'RS_SMELL002', name: 'unimplemented! macro', message: 'unimplemented!() macro — will panic at runtime' },
126
- { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'RS_SMELL003', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
127
- { pattern: /unwrap\(\).*unwrap\(\)/, id: 'RS_SMELL004', name: 'Double Unwrap', message: 'Chained unwrap() — will panic on None/Err' },
128
- { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'RS_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
129
- ];
130
-
131
- // ─── Java/C# common issues ──────────────────────────────
132
- this.javaSmells = [
133
- { pattern: /System\.out\.println\s*\(.*debug/i, id: 'JAVA_SMELL001', name: 'Debug Println', message: 'Debug System.out.println — use a logger' },
134
- { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)/i, id: 'JAVA_SMELL002', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished' },
135
- { pattern: /throw\s+new\s+(?:Runtime)?Exception\s*\(\s*["']Not\s+implemented["']/i, id: 'JAVA_SMELL003', name: 'Not Implemented', message: 'Stub implementation — will throw at runtime' },
136
- { pattern: /catch\s*\(\s*Exception\s+\w+\s*\)\s*\{\s*\}/, id: 'JAVA_SMELL004', name: 'Empty Catch', message: 'Empty catch block — swallows all exceptions' },
137
- { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'JAVA_SMELL005', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
138
- ];
139
-
140
- // Language extensions mapping
141
- this.jsExtensions = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs']);
142
- this.pyExtensions = new Set(['.py', '.pyw']);
143
- this.goExtensions = new Set(['.go']);
144
- this.rustExtensions = new Set(['.rs']);
145
- this.javaExtensions = new Set(['.java', '.kt', '.cs']);
146
-
147
- // Patterns suggesting AI-generated code with common issues
148
- this.aiCodeSmells = [
149
- { pattern: /\/\/\s*TODO:?\s*(implement|add|fix|complete|finish)\s*(this|here|later)/i, id: 'AI_SMELL001', name: 'Placeholder TODO', message: 'AI-generated placeholder — likely unfinished implementation' },
150
- { pattern: /throw new Error\(['"]Not implemented['"]\)/, id: 'AI_SMELL002', name: 'Not Implemented', message: 'Stub implementation — function body was not generated' },
151
- { pattern: /\/\/\s*\.\.\.\s*(rest|remaining|more|other|additional)/i, id: 'AI_SMELL003', name: 'Truncated Code', message: 'AI output was truncated — code is incomplete' },
152
- { pattern: /\/\/\s*(your|replace|insert|put)\s+(code|logic|implementation|api[_\s]?key)/i, id: 'AI_SMELL004', name: 'Template Placeholder', message: 'Template placeholder left in code — needs real implementation' },
153
- { pattern: /['"]your-api-key-here['"]|['"]sk-[.]{3,}['"]|['"]xxx+['"]/i, id: 'AI_SMELL005', name: 'Dummy Credential', message: 'Placeholder credential — will fail at runtime' },
154
- { pattern: /example\.com|test@test\.com|john@doe\.com|foo@bar\.com/i, id: 'AI_SMELL006', name: 'Example Domain', message: 'Example domain/email in production code' },
155
- { pattern: /localhost:\d{4}(?!.*(?:dev|test|local|development))/i, id: 'AI_SMELL007', name: 'Hardcoded Localhost', message: 'Hardcoded localhost URL — will fail in production' },
156
- ];
157
-
158
- // Deprecated/removed Node.js APIs that LLMs still suggest
159
- this.deprecatedAPIs = [
160
- { pattern: /new Buffer\s*\(/, suggestion: 'Use Buffer.from(), Buffer.alloc(), or Buffer.allocUnsafe()', since: 'Node 6', name: 'new Buffer()', id: 'DEPR_API001' },
161
- { pattern: /require\s*\(\s*['"]sys['"]\s*\)/, suggestion: 'Use require("util") — sys was removed in Node 1.0', since: 'Node 1.0', name: 'require("sys")', id: 'DEPR_API002' },
162
- { pattern: /fs\.exists\s*\((?!Sync)/, suggestion: 'Use fs.access() or fs.stat() — fs.exists() is deprecated', since: 'Node 1.0', name: 'fs.exists()', id: 'DEPR_API003' },
163
- { pattern: /domain\.create\s*\(/, suggestion: 'Use async_hooks or try/catch — domain module is deprecated', since: 'Node 4', name: 'domain.create()', id: 'DEPR_API004' },
164
- { pattern: /require\s*\(\s*['"]punycode['"]\s*\)/, suggestion: 'Use userland punycode package — built-in is deprecated', since: 'Node 7', name: 'require("punycode")', id: 'DEPR_API005' },
165
- { pattern: /url\.parse\s*\(/, suggestion: 'Use new URL() — url.parse() is legacy', since: 'Node 11', name: 'url.parse()', id: 'DEPR_API006' },
166
- { pattern: /querystring\.(parse|stringify)\s*\(/, suggestion: 'Use URLSearchParams — querystring is legacy', since: 'Node 14', name: 'querystring', id: 'DEPR_API007' },
167
- { pattern: /util\.isArray\s*\(/, suggestion: 'Use Array.isArray() — util type checks are deprecated', since: 'Node 4', name: 'util.isArray()', id: 'DEPR_API008' },
168
- { pattern: /util\.isFunction\s*\(/, suggestion: 'Use typeof fn === "function"', since: 'Node 4', name: 'util.isFunction()', id: 'DEPR_API009' },
169
- { pattern: /util\.pump\s*\(/, suggestion: 'Use stream.pipeline() — util.pump() was removed', since: 'Node 1.0', name: 'util.pump()', id: 'DEPR_API010' },
170
- ];
171
- }
172
-
173
- /**
174
- * Full hallucination scan across all files
175
- */
176
- async scan(files) {
177
- const startTime = Date.now();
178
- const results = {
179
- phantomImports: [],
180
- phantomAPIs: [],
181
- aiSmells: [],
182
- deprecatedAPIs: [],
183
- unresolvedDeps: [],
184
- stats: {
185
- filesScanned: 0,
186
- totalIssues: 0,
187
- scanTime: 0,
188
- },
189
- };
190
-
191
- // Load package.json dependencies
192
- const declaredDeps = this._loadDeclaredDeps();
193
-
194
- // Load .thubanrc.json ignore rules
195
- const ignoreFiles = this._loadIgnoreRules();
196
-
197
- for (const file of files) {
198
- try {
199
- const ext = path.extname(file).toLowerCase();
200
- const isJS = this.jsExtensions.has(ext);
201
- const isPython = this.pyExtensions.has(ext);
202
- const isGo = this.goExtensions.has(ext);
203
- const isRust = this.rustExtensions.has(ext);
204
- const isJava = this.javaExtensions.has(ext);
205
-
206
- if (!isJS && !isPython && !isGo && !isRust && !isJava) continue;
207
-
208
- const relPath = path.relative(this.config.rootPath, file);
209
-
210
- // Skip files in the ignore list
211
- if (ignoreFiles.some(pattern => relPath.includes(pattern) || file.includes(pattern))) continue;
212
-
213
- const content = fs.readFileSync(file, 'utf-8');
214
- const lines = content.split('\n');
215
-
216
- // Build set of lines with thuban-ignore comments
217
- const ignoredLines = new Set();
218
- for (let i = 0; i < lines.length; i++) {
219
- if (lines[i].includes('thuban-ignore')) {
220
- ignoredLines.add(i + 1); // 1-based
221
- ignoredLines.add(i + 2); // Also ignore next line
222
- }
223
- }
224
-
225
- // Skip files that are pattern definition files (contain regex pattern arrays)
226
- // These files define what to look for, not actual violations
227
- const isPatternFile = this._isPatternDefinitionFile(content);
228
-
229
- results.stats.filesScanned++;
230
-
231
- // ─── Language-specific checks ──────────────────────
232
- if (isJS) {
233
- // 1. Phantom imports — packages that don't exist
234
- this._checkPhantomImports(relPath, content, lines, declaredDeps, results, ignoredLines);
235
- // 2. Phantom APIs — methods that don't exist on known objects
236
- if (!isPatternFile) {
237
- this._checkPhantomAPIs(relPath, content, lines, results, ignoredLines);
238
- }
239
- // 3. AI code smells
240
- this._checkAISmells(relPath, content, lines, results, ignoredLines);
241
- // 4. Deprecated APIs
242
- if (!isPatternFile) {
243
- this._checkDeprecatedAPIs(relPath, content, lines, results, ignoredLines);
244
- }
245
- } else if (isPython) {
246
- this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonPhantomAPIs, 'phantomAPIs');
247
- this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.pythonDeprecated, 'deprecatedAPIs');
248
- this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.pythonSmells);
249
- } else if (isGo) {
250
- this._checkLanguagePatterns(relPath, content, lines, results, ignoredLines, this.goPhantomAPIs, 'phantomAPIs');
251
- this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.goSmells);
252
- } else if (isRust) {
253
- this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.rustSmells);
254
- } else if (isJava) {
255
- this._checkLanguageSmells(relPath, content, lines, results, ignoredLines, this.javaSmells);
256
- }
257
-
258
- } catch (e) { /* skip unreadable */ }
259
- }
260
-
261
- results.stats.totalIssues =
262
- results.phantomImports.length +
263
- results.phantomAPIs.length +
264
- results.aiSmells.length +
265
- results.deprecatedAPIs.length +
266
- results.unresolvedDeps.length;
267
-
268
- results.stats.scanTime = Date.now() - startTime;
269
-
270
- return results;
271
- }
272
-
273
- /**
274
- * Generic language pattern checker (for phantom APIs and deprecated APIs)
275
- */
276
- _checkLanguagePatterns(relPath, content, lines, results, ignoredLines, patterns, resultKey) {
277
- for (let i = 0; i < lines.length; i++) {
278
- if (ignoredLines.has(i + 1)) continue;
279
- const line = lines[i];
280
- for (const check of patterns) {
281
- if (check.pattern.test(line)) {
282
- const entry = {
283
- file: relPath,
284
- line: i + 1,
285
- name: check.name,
286
- id: check.id,
287
- };
288
- if (check.suggestion) entry.suggestion = check.suggestion;
289
- if (check.since) {
290
- entry.since = check.since;
291
- entry.fix = check.suggestion;
292
- }
293
- if (resultKey === 'deprecatedAPIs') {
294
- results.deprecatedAPIs.push(entry);
295
- } else {
296
- results.phantomAPIs.push(entry);
297
- }
298
- }
299
- }
300
- }
301
- }
302
-
303
- /**
304
- * Generic language smell checker
305
- */
306
- _checkLanguageSmells(relPath, content, lines, results, ignoredLines, smells) {
307
- for (let i = 0; i < lines.length; i++) {
308
- if (ignoredLines.has(i + 1)) continue;
309
- const line = lines[i];
310
- for (const smell of smells) {
311
- if (smell.pattern.test(line)) {
312
- results.aiSmells.push({
313
- file: relPath,
314
- line: i + 1,
315
- id: smell.id,
316
- name: smell.name,
317
- message: smell.message,
318
- type: 'ai_smell',
319
- severity: 'medium',
320
- });
321
- }
322
- }
323
- }
324
- }
325
-
326
- /**
327
- * Format results for terminal output
328
- */
329
- formatReport(results) {
330
- const C = {
331
- reset: '\x1b[0m', bold: '\x1b[1m',
332
- red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m',
333
- cyan: '\x1b[36m', gray: '\x1b[90m', magenta: '\x1b[35m',
334
- };
335
-
336
- let output = '';
337
- output += `\n${C.magenta} ╔══════════════════════════════════════════════╗${C.reset}\n`;
338
- output += `${C.magenta} ║${C.bold} THUBAN HALLUCINATION REPORT ${C.reset}${C.magenta}║${C.reset}\n`;
339
- output += `${C.magenta} ╚══════════════════════════════════════════════╝${C.reset}\n\n`;
340
-
341
- if (results.stats.totalIssues === 0) {
342
- output += ` ${C.green}${C.bold}No hallucinations detected.${C.reset} ${C.gray}Clean codebase.${C.reset}\n\n`;
343
- return output;
344
- }
345
-
346
- // Phantom imports
347
- if (results.phantomImports.length > 0) {
348
- output += ` ${C.red}${C.bold}Phantom Imports (${results.phantomImports.length})${C.reset}\n`;
349
- output += ` ${C.gray}Packages that don't exist in package.json or node_modules${C.reset}\n\n`;
350
- for (const item of results.phantomImports.slice(0, 10)) {
351
- output += ` ${C.red}✗${C.reset} ${C.cyan}${item.file}${C.reset}:${item.line}\n`;
352
- output += ` ${C.yellow}${item.module}${C.reset} ${C.gray}— ${item.reason}${C.reset}\n`;
353
- }
354
- if (results.phantomImports.length > 10) {
355
- output += ` ${C.gray}... and ${results.phantomImports.length - 10} more${C.reset}\n`;
356
- }
357
- output += '\n';
358
- }
359
-
360
- // Phantom APIs
361
- if (results.phantomAPIs.length > 0) {
362
- output += ` ${C.red}${C.bold}Phantom APIs (${results.phantomAPIs.length})${C.reset}\n`;
363
- output += ` ${C.gray}Methods/properties that don't exist on their objects${C.reset}\n\n`;
364
- for (const item of results.phantomAPIs.slice(0, 10)) {
365
- const ruleId = item.id ? `${C.gray}[${item.id}]${C.reset} ` : '';
366
- output += ` ${C.red}✗${C.reset} ${ruleId}${C.cyan}${item.file}${C.reset}:${item.line}\n`;
367
- output += ` ${C.yellow}${item.name}${C.reset} ${C.gray}— ${item.suggestion}${C.reset}\n`;
368
- output += ` ${C.gray}Suppress: // thuban-ignore ${item.id || 'HALL_API'}${C.reset}\n`;
369
- }
370
- if (results.phantomAPIs.length > 10) {
371
- output += ` ${C.gray}... and ${results.phantomAPIs.length - 10} more${C.reset}\n`;
372
- }
373
- output += '\n';
374
- }
375
-
376
- // AI code smells
377
- if (results.aiSmells.length > 0) {
378
- output += ` ${C.yellow}${C.bold}AI Code Smells (${results.aiSmells.length})${C.reset}\n`;
379
- output += ` ${C.gray}Patterns typical of AI-generated code that needs review${C.reset}\n\n`;
380
- for (const item of results.aiSmells.slice(0, 10)) {
381
- output += ` ${C.yellow}!${C.reset} ${C.cyan}${item.file}${C.reset}:${item.line}\n`;
382
- output += ` ${C.gray}${item.message}${C.reset}\n`;
383
- }
384
- if (results.aiSmells.length > 10) {
385
- output += ` ${C.gray}... and ${results.aiSmells.length - 10} more${C.reset}\n`;
386
- }
387
- output += '\n';
388
- }
389
-
390
- // Deprecated APIs
391
- if (results.deprecatedAPIs.length > 0) {
392
- output += ` ${C.yellow}${C.bold}Deprecated APIs (${results.deprecatedAPIs.length})${C.reset}\n`;
393
- output += ` ${C.gray}APIs that LLMs still suggest but are deprecated or removed${C.reset}\n\n`;
394
- for (const item of results.deprecatedAPIs.slice(0, 10)) {
395
- output += ` ${C.yellow}⚠${C.reset} ${C.cyan}${item.file}${C.reset}:${item.line}\n`;
396
- output += ` ${C.yellow}${item.name}${C.reset} ${C.gray}(deprecated since ${item.since})${C.reset}\n`;
397
- output += ` ${C.green}→${C.reset} ${C.gray}${item.suggestion}${C.reset}\n`;
398
- }
399
- if (results.deprecatedAPIs.length > 10) {
400
- output += ` ${C.gray}... and ${results.deprecatedAPIs.length - 10} more${C.reset}\n`;
401
- }
402
- output += '\n';
403
- }
404
-
405
- // Summary
406
- output += ` ${C.bold}Summary:${C.reset} ${C.cyan}${results.stats.filesScanned}${C.reset} files scanned, `;
407
- output += `${results.stats.totalIssues > 0 ? C.red : C.green}${results.stats.totalIssues}${C.reset} hallucination issues found `;
408
- output += `${C.gray}(${results.stats.scanTime}ms)${C.reset}\n\n`;
409
-
410
- return output;
411
- }
412
-
413
- // ─── Private Methods ─────────────────────────────────────
414
-
415
- _loadDeclaredDeps() {
416
- const deps = new Set();
417
- // Collect all package.json files — root + subdirectories (monorepo support)
418
- this._packageJsonPaths = new Map(); // dir -> Set<dep names>
419
- this._collectPackageJsonDeps(this.config.rootPath, deps, 0);
420
- return deps;
421
- }
422
-
423
- /**
424
- * Recursively collect deps from all package.json files in the project.
425
- * Handles monorepos: workspaces, packages/*, apps/*, etc.
426
- * Max depth prevents scanning into node_modules.
427
- */
428
- _collectPackageJsonDeps(dir, globalDeps, depth) {
429
- if (depth > 5) return; // Don't go too deep
430
- try {
431
- const pkgPath = path.join(dir, 'package.json');
432
- if (fs.existsSync(pkgPath)) {
433
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
434
- const localDeps = new Set();
435
- for (const name of Object.keys(pkg.dependencies || {})) { globalDeps.add(name); localDeps.add(name); }
436
- for (const name of Object.keys(pkg.devDependencies || {})) { globalDeps.add(name); localDeps.add(name); }
437
- for (const name of Object.keys(pkg.peerDependencies || {})) { globalDeps.add(name); localDeps.add(name); }
438
- this._packageJsonPaths.set(dir, localDeps);
439
- }
440
-
441
- // Scan subdirectories for more package.json files
442
- // Skip node_modules, .git, dist, build, coverage
443
- const skipDirs = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.cache', 'vendor']);
444
- const entries = fs.readdirSync(dir, { withFileTypes: true });
445
- for (const entry of entries) {
446
- if (entry.isDirectory() && !skipDirs.has(entry.name) && !entry.name.startsWith('.')) {
447
- this._collectPackageJsonDeps(path.join(dir, entry.name), globalDeps, depth + 1);
448
- }
449
- }
450
- } catch (e) { /* skip unreadable dirs */ }
451
- }
452
-
453
- _checkPhantomImports(relPath, content, lines, declaredDeps, results, ignoredLines = new Set()) {
454
- // Match require() and import statements
455
- const requireRegex = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
456
- const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
457
- const dynamicImportRegex = /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
458
-
459
- const allImportRegexes = [requireRegex, importRegex, dynamicImportRegex];
460
-
461
- for (const regex of allImportRegexes) {
462
- let match;
463
- while ((match = regex.exec(content)) !== null) {
464
- const moduleName = match[1];
465
-
466
- // Skip relative imports
467
- if (moduleName.startsWith('.') || moduleName.startsWith('/')) continue;
468
-
469
- // Skip builtins
470
- const baseModule = moduleName.split('/')[0];
471
- if (this.builtins.has(moduleName) || this.builtins.has(baseModule)) continue;
472
-
473
- // Skip scoped packages base
474
- const scopedBase = moduleName.startsWith('@')
475
- ? moduleName.split('/').slice(0, 2).join('/')
476
- : baseModule;
477
-
478
- // Check if declared in any package.json (root or workspace)
479
- if (!declaredDeps.has(scopedBase)) {
480
- // Check if it physically exists in any node_modules (root or nearest parent)
481
- const nmExists = this._findInNodeModules(file, scopedBase);
482
- if (!nmExists) {
483
- // Find line number
484
- const lineIdx = this._findLineNumber(content, match.index);
485
-
486
- // Skip if on an ignored line
487
- if (ignoredLines.has(lineIdx)) continue;
488
-
489
- // Check if it's a known phantom
490
- const isKnownPhantom = this.phantomPackages.has(moduleName);
491
-
492
- results.phantomImports.push({
493
- file: relPath,
494
- line: lineIdx,
495
- module: moduleName,
496
- reason: isKnownPhantom
497
- ? 'Known AI-hallucinated package — does not exist on npm'
498
- : 'Not in package.json and not in node_modules',
499
- severity: isKnownPhantom ? 'critical' : 'high',
500
- fixable: false,
501
- });
502
- }
503
- }
504
- }
505
- }
506
- }
507
-
508
- _checkPhantomAPIs(relPath, content, lines, results, ignoredLines = new Set()) {
509
- for (const api of this.phantomAPIs) {
510
- let match;
511
- const regex = new RegExp(api.pattern.source, api.pattern.flags + (api.pattern.flags.includes('g') ? '' : 'g'));
512
- while ((match = regex.exec(content)) !== null) {
513
- const lineIdx = this._findLineNumber(content, match.index);
514
- if (ignoredLines.has(lineIdx)) continue;
515
- // Skip if inside a string literal or regex pattern (common in pattern definition files)
516
- const line = lines[lineIdx - 1] || '';
517
- if (this._isInsidePattern(line, match[0])) continue;
518
- results.phantomAPIs.push({
519
- file: relPath,
520
- line: lineIdx,
521
- name: api.name,
522
- id: api.id,
523
- suggestion: api.suggestion,
524
- severity: 'high',
525
- fixable: true,
526
- fixAction: 'replace_phantom_api',
527
- });
528
- }
529
- }
530
- }
531
-
532
- _checkAISmells(relPath, content, lines, results, ignoredLines = new Set()) {
533
- for (const smell of this.aiCodeSmells) {
534
- let match;
535
- const regex = new RegExp(smell.pattern.source, smell.pattern.flags + (smell.pattern.flags.includes('g') ? '' : 'g'));
536
- while ((match = regex.exec(content)) !== null) {
537
- const lineIdx = this._findLineNumber(content, match.index);
538
- if (ignoredLines.has(lineIdx)) continue;
539
- results.aiSmells.push({
540
- file: relPath,
541
- line: lineIdx,
542
- id: smell.id,
543
- name: smell.name,
544
- message: smell.message,
545
- severity: 'warning',
546
- code: lines[lineIdx - 1]?.trim().substring(0, 100),
547
- });
548
- }
549
- }
550
- }
551
-
552
- _checkDeprecatedAPIs(relPath, content, lines, results, ignoredLines = new Set()) {
553
- for (const api of this.deprecatedAPIs) {
554
- let match;
555
- const regex = new RegExp(api.pattern.source, api.pattern.flags + (api.pattern.flags.includes('g') ? '' : 'g'));
556
- while ((match = regex.exec(content)) !== null) {
557
- const lineIdx = this._findLineNumber(content, match.index);
558
- if (ignoredLines.has(lineIdx)) continue;
559
- const line = lines[lineIdx - 1] || '';
560
- if (this._isInsidePattern(line, match[0])) continue;
561
- results.deprecatedAPIs.push({
562
- file: relPath,
563
- line: lineIdx,
564
- name: api.name,
565
- id: api.id,
566
- suggestion: api.suggestion,
567
- since: api.since,
568
- severity: 'warning',
569
- fixable: true,
570
- fixAction: 'update_deprecated_api',
571
- });
572
- }
573
- }
574
- }
575
-
576
- _findLineNumber(content, charIndex) {
577
- let line = 1;
578
- for (let i = 0; i < charIndex && i < content.length; i++) {
579
- if (content[i] === '\n') line++;
580
- }
581
- return line;
582
- }
583
-
584
- /**
585
- * Detect if a file is a pattern definition file (like this detector itself)
586
- * These files contain regex patterns that reference the APIs they detect
587
- */
588
- _isPatternDefinitionFile(content) {
589
- // Count regex pattern definitions — if file has many, it's likely a detector/rule file
590
- const regexPatterns = (content.match(/pattern:\s*\//g) || []).length;
591
- const phantomRefs = (content.match(/phantom|hallucin/gi) || []).length;
592
- return regexPatterns > 5 && phantomRefs > 2;
593
- }
594
-
595
- /**
596
- * Check if a match is inside a regex pattern, string literal, or comment
597
- */
598
- _isInsidePattern(line, matchText) {
599
- const trimmed = line.trim();
600
- // Inside a regex pattern definition
601
- if (/pattern:\s*\//.test(trimmed)) return true;
602
- // Inside a comment
603
- if (trimmed.startsWith('//') || trimmed.startsWith('*')) return true;
604
- // Inside a string that's defining a name/suggestion/message
605
- if (/(?:name|suggestion|message|description):\s*['"]/.test(trimmed)) return true;
606
- return false;
607
- }
608
-
609
- /**
610
- * Load ignore rules from .thubanrc.json
611
- */
612
- _loadIgnoreRules() {
613
- const ignoreFiles = [];
614
- try {
615
- const rcPath = path.join(this.config.rootPath, '.thubanrc.json');
616
- const rc = JSON.parse(fs.readFileSync(rcPath, 'utf-8'));
617
- if (rc.hallucination?.ignore) {
618
- ignoreFiles.push(...rc.hallucination.ignore);
619
- }
620
- if (rc.ignore) {
621
- ignoreFiles.push(...rc.ignore);
622
- }
623
- } catch (e) { /* no config file */ }
624
- return ignoreFiles;
625
- }
626
-
627
- /**
628
- * Walk up directory tree from file location to project root,
629
- * checking each node_modules directory for the package.
630
- * This handles monorepos where deps are installed at workspace level.
631
- */
632
- _findInNodeModules(filePath, packageName) {
633
- let dir = path.dirname(filePath);
634
- const root = this.config.rootPath;
635
-
636
- // Walk up from file to root, checking each node_modules
637
- while (dir.length >= root.length) {
638
- const nmPath = path.join(dir, 'node_modules', packageName);
639
- try {
640
- if (fs.existsSync(nmPath)) return true;
641
- } catch (e) { /* skip */ }
642
-
643
- const parent = path.dirname(dir);
644
- if (parent === dir) break; // reached filesystem root
645
- dir = parent;
646
- }
647
-
648
- return false;
649
- }
650
- }
651
-
652
- module.exports = HallucinationDetector;