thuban 0.3.3 → 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 (66) hide show
  1. package/dist/LICENSE +21 -0
  2. package/dist/README.md +185 -0
  3. package/dist/cli.js +2 -0
  4. package/dist/packages/scanner/ai-confidence-scorer.js +1 -0
  5. package/dist/packages/scanner/alert-manager.js +1 -0
  6. package/dist/packages/scanner/baseline-manager.js +1 -0
  7. package/dist/packages/scanner/code-scanner.js +1 -0
  8. package/dist/packages/scanner/codebase-passport.js +1 -0
  9. package/dist/packages/scanner/copy-paste-detector.js +1 -0
  10. package/dist/packages/scanner/dependency-graph.js +1 -0
  11. package/dist/packages/scanner/drift-detector.js +1 -0
  12. package/dist/packages/scanner/executive-report.js +1 -0
  13. package/dist/packages/scanner/file-collector.js +1 -0
  14. package/dist/packages/scanner/file-watcher.js +1 -0
  15. package/dist/packages/scanner/ghost-code-detector.js +1 -0
  16. package/dist/packages/scanner/hallucination-detector.js +1 -0
  17. package/dist/packages/scanner/health-checker.js +1 -0
  18. package/dist/packages/scanner/html-report.js +1 -0
  19. package/dist/packages/scanner/index.js +1 -0
  20. package/dist/packages/scanner/license-manager.js +1 -0
  21. package/dist/packages/scanner/master-health-checker.js +1 -0
  22. package/dist/packages/scanner/monitor-notifier.js +1 -0
  23. package/dist/packages/scanner/monitor-service.js +1 -0
  24. package/dist/packages/scanner/monitor-store.js +1 -0
  25. package/dist/packages/scanner/pre-commit-gate.js +1 -0
  26. package/dist/packages/scanner/scan-diff.js +1 -0
  27. package/dist/packages/scanner/scan-runner.js +1 -0
  28. package/dist/packages/scanner/secret-scanner.js +1 -0
  29. package/dist/packages/scanner/sentinel-core.js +1 -0
  30. package/dist/packages/scanner/sentinel-knowledge.js +1 -0
  31. package/dist/packages/scanner/tech-debt-analyzer.js +1 -0
  32. package/dist/packages/scanner/tech-debt-cost.js +1 -0
  33. package/dist/packages/scanner/widget-generator.js +1 -0
  34. package/package.json +12 -7
  35. package/cli.js +0 -2627
  36. package/packages/scanner/ai-confidence-scorer.js +0 -260
  37. package/packages/scanner/alert-manager.js +0 -398
  38. package/packages/scanner/baseline-manager.js +0 -109
  39. package/packages/scanner/code-scanner.js +0 -758
  40. package/packages/scanner/codebase-passport.js +0 -299
  41. package/packages/scanner/copy-paste-detector.js +0 -276
  42. package/packages/scanner/dependency-graph.js +0 -541
  43. package/packages/scanner/drift-detector.js +0 -423
  44. package/packages/scanner/executive-report.js +0 -774
  45. package/packages/scanner/file-collector.js +0 -64
  46. package/packages/scanner/file-watcher.js +0 -323
  47. package/packages/scanner/ghost-code-detector.js +0 -301
  48. package/packages/scanner/hallucination-detector.js +0 -822
  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 -100
  52. package/packages/scanner/license-manager.js +0 -331
  53. package/packages/scanner/master-health-checker.js +0 -513
  54. package/packages/scanner/monitor-notifier.js +0 -64
  55. package/packages/scanner/monitor-service.js +0 -103
  56. package/packages/scanner/monitor-store.js +0 -117
  57. package/packages/scanner/pre-commit-gate.js +0 -216
  58. package/packages/scanner/scan-diff.js +0 -50
  59. package/packages/scanner/scan-runner.js +0 -172
  60. package/packages/scanner/secret-scanner.js +0 -612
  61. package/packages/scanner/secret-scanner.test.js +0 -103
  62. package/packages/scanner/sentinel-core.js +0 -616
  63. package/packages/scanner/sentinel-knowledge.js +0 -322
  64. package/packages/scanner/tech-debt-analyzer.js +0 -583
  65. package/packages/scanner/tech-debt-cost.js +0 -194
  66. package/packages/scanner/widget-generator.js +0 -415
@@ -1,64 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- function collectFiles(dir, ignorePatterns = []) {
5
- const defaultIgnore = ['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', '.venv', 'venv', 'coverage', '.cache', '.turbo'];
6
- const allIgnore = [...defaultIgnore, ...ignorePatterns];
7
- const extensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs', '.py', '.pyw', '.go', '.rs', '.java', '.kt', '.cs', '.php', '.rb', '.json'];
8
- const maxFileSize = 1024 * 1024;
9
- const maxFiles = 50000;
10
- const files = [];
11
- const visitedPaths = new Set(); // Track real paths to handle symlinks
12
-
13
- function walk(currentDir) {
14
- if (files.length >= maxFiles) return;
15
- let entries;
16
- try {
17
- entries = fs.readdirSync(currentDir, { withFileTypes: true });
18
- } catch (error) {
19
- return;
20
- }
21
-
22
- for (const entry of entries) {
23
- if (files.length >= maxFiles) return;
24
- if (allIgnore.some(pattern => entry.name === pattern || entry.name.match(pattern))) continue;
25
- const fullPath = path.join(currentDir, entry.name);
26
-
27
- // Handle symlinks — resolve and skip if already visited or points outside
28
- if (entry.isSymbolicLink()) {
29
- try {
30
- const realPath = fs.realpathSync(fullPath);
31
- if (visitedPaths.has(realPath)) continue;
32
- visitedPaths.add(realPath);
33
- const stat = fs.statSync(fullPath); // follow symlink
34
- if (stat.isDirectory()) {
35
- walk(fullPath);
36
- } else if (stat.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
37
- if (stat.size > 0 && stat.size <= maxFileSize) files.push(fullPath);
38
- }
39
- } catch {
40
- continue; // Broken symlink — skip
41
- }
42
- continue;
43
- }
44
-
45
- if (entry.isDirectory()) {
46
- walk(fullPath);
47
- } else if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
48
- try {
49
- const stat = fs.statSync(fullPath);
50
- if (stat.size > 0 && stat.size <= maxFileSize) files.push(fullPath);
51
- } catch (error) {
52
- // Skip files we can't stat
53
- }
54
- }
55
- }
56
- }
57
-
58
- walk(dir);
59
- return files;
60
- }
61
-
62
- module.exports = {
63
- collectFiles,
64
- };
@@ -1,323 +0,0 @@
1
- /**
2
- * File Watcher
3
- *
4
- * @purpose Event-driven FileWatcher component
5
- * @module sentinel
6
- * @layer application
7
- * @imports-from fs, path, events
8
- * @exports-to sentinel
9
- * @side-effects File system reads, Console output, Event emission
10
- * @critical-for sentinel
11
- */
12
-
13
- /**
14
- * Sentinel File Watcher
15
- *
16
- * Real-time file system monitoring for the Orion codebase.
17
- * Detects file changes, additions, and deletions to trigger scans.
18
- */
19
-
20
- const fs = require('fs');
21
- const path = require('path');
22
- const EventEmitter = require('events');
23
-
24
- class FileWatcher extends EventEmitter {
25
- constructor(config = {}) {
26
- super();
27
-
28
- this.config = {
29
- rootPath: config.rootPath || process.cwd(),
30
- watchPatterns: config.watchPatterns || ['**/*.js', '**/*.ts', '**/*.json'],
31
- ignorePatterns: config.ignorePatterns || [
32
- 'node_modules/**',
33
- '.git/**',
34
- 'dist/**',
35
- 'build/**',
36
- '.orion/snapshots/**',
37
- 'coverage/**',
38
- '*.log'
39
- ],
40
- debounceMs: config.debounceMs || 300,
41
- ...config
42
- };
43
-
44
- this.watchers = [];
45
- this.watchedFiles = new Set();
46
- this.debounceTimers = new Map();
47
- this.isRunning = false;
48
- }
49
-
50
- /**
51
- * Start watching files
52
- */
53
- async start() {
54
- if (this.isRunning) {
55
- return { success: true, message: 'Already watching' };
56
- }
57
-
58
- console.log('[FILE-WATCHER] Starting file watcher...');
59
- console.log(`[FILE-WATCHER] Root path: ${this.config.rootPath}`);
60
-
61
- try {
62
- // Discover files to watch
63
- await this._discoverFiles(this.config.rootPath);
64
-
65
- // Set up watchers
66
- this._setupWatchers();
67
-
68
- this.isRunning = true;
69
- console.log(`[FILE-WATCHER] Watching ${this.watchedFiles.size} files`);
70
-
71
- return {
72
- success: true,
73
- filesWatched: this.watchedFiles.size
74
- };
75
- } catch (error) {
76
- console.error('[FILE-WATCHER] Start failed:', error.message);
77
- return { success: false, error: error.message };
78
- }
79
- }
80
-
81
- /**
82
- * Stop watching files
83
- */
84
- async stop() {
85
- console.log('[FILE-WATCHER] Stopping file watcher...');
86
-
87
- // Close all watchers
88
- for (const watcher of this.watchers) {
89
- try {
90
- watcher.close();
91
- } catch (e) {
92
- // Ignore close errors
93
- }
94
- }
95
-
96
- // Clear debounce timers
97
- for (const timer of this.debounceTimers.values()) {
98
- clearTimeout(timer);
99
- }
100
-
101
- this.watchers = [];
102
- this.debounceTimers.clear();
103
- this.isRunning = false;
104
-
105
- console.log('[FILE-WATCHER] Stopped');
106
- return { success: true };
107
- }
108
-
109
- /**
110
- * Get count of watched files
111
- */
112
- getWatchedCount() {
113
- return this.watchedFiles.size;
114
- }
115
-
116
- /**
117
- * Get list of watched files
118
- */
119
- getWatchedFiles() {
120
- return Array.from(this.watchedFiles);
121
- }
122
-
123
- /**
124
- * Check if a file path should be ignored
125
- */
126
- shouldIgnore(filePath) {
127
- const relativePath = path.relative(this.config.rootPath, filePath);
128
-
129
- for (const pattern of this.config.ignorePatterns) {
130
- if (this._matchPattern(relativePath, pattern)) {
131
- return true;
132
- }
133
- }
134
-
135
- return false;
136
- }
137
-
138
- /**
139
- * Check if a file path matches watch patterns
140
- */
141
- shouldWatch(filePath) {
142
- const relativePath = path.relative(this.config.rootPath, filePath);
143
-
144
- // First check if ignored
145
- if (this.shouldIgnore(filePath)) {
146
- return false;
147
- }
148
-
149
- // Then check if matches watch patterns
150
- for (const pattern of this.config.watchPatterns) {
151
- if (this._matchPattern(relativePath, pattern)) {
152
- return true;
153
- }
154
- }
155
-
156
- return false;
157
- }
158
-
159
- // Private methods
160
-
161
- async _discoverFiles(dir) {
162
- try {
163
- const entries = fs.readdirSync(dir, { withFileTypes: true });
164
-
165
- for (const entry of entries) {
166
- const fullPath = path.join(dir, entry.name);
167
-
168
- // Skip ignored directories early
169
- if (entry.isDirectory()) {
170
- if (!this.shouldIgnore(fullPath)) {
171
- await this._discoverFiles(fullPath);
172
- }
173
- } else if (entry.isFile()) {
174
- if (this.shouldWatch(fullPath)) {
175
- this.watchedFiles.add(fullPath);
176
- }
177
- }
178
- }
179
- } catch (error) {
180
- // Directory might not exist or be inaccessible
181
- if (error.code !== 'ENOENT' && error.code !== 'EACCES') {
182
- console.warn(`[FILE-WATCHER] Error scanning ${dir}:`, error.message);
183
- }
184
- }
185
- }
186
-
187
- _setupWatchers() {
188
- // Group files by directory for efficient watching
189
- const directories = new Set();
190
- for (const file of this.watchedFiles) {
191
- directories.add(path.dirname(file));
192
- }
193
-
194
- // Watch each directory
195
- for (const dir of directories) {
196
- try {
197
- const watcher = fs.watch(dir, { persistent: true }, (eventType, filename) => {
198
- if (filename) {
199
- const fullPath = path.join(dir, filename);
200
- this._handleFileEvent(eventType, fullPath);
201
- }
202
- });
203
-
204
- watcher.on('error', (error) => {
205
- console.warn(`[FILE-WATCHER] Watcher error for ${dir}:`, error.message);
206
- });
207
-
208
- this.watchers.push(watcher);
209
- } catch (error) {
210
- console.warn(`[FILE-WATCHER] Could not watch ${dir}:`, error.message);
211
- }
212
- }
213
-
214
- // Also set up a recursive watcher for the root (if supported)
215
- try {
216
- const rootWatcher = fs.watch(this.config.rootPath, { recursive: true }, (eventType, filename) => {
217
- if (filename) {
218
- const fullPath = path.join(this.config.rootPath, filename);
219
- this._handleFileEvent(eventType, fullPath);
220
- }
221
- });
222
-
223
- rootWatcher.on('error', (error) => {
224
- // Recursive watch might not be supported on all platforms
225
- if (error.code !== 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM') {
226
- console.warn('[FILE-WATCHER] Root watcher error:', error.message);
227
- }
228
- });
229
-
230
- this.watchers.push(rootWatcher);
231
- } catch (error) {
232
- // Recursive watching might not be available
233
- }
234
- }
235
-
236
- _handleFileEvent(eventType, filePath) {
237
- // Skip if ignored
238
- if (this.shouldIgnore(filePath)) {
239
- return;
240
- }
241
-
242
- // Debounce rapid events
243
- if (this.debounceTimers.has(filePath)) {
244
- clearTimeout(this.debounceTimers.get(filePath));
245
- }
246
-
247
- this.debounceTimers.set(filePath, setTimeout(() => {
248
- this.debounceTimers.delete(filePath);
249
- this._processFileEvent(eventType, filePath);
250
- }, this.config.debounceMs));
251
- }
252
-
253
- _processFileEvent(eventType, filePath) {
254
- // Check if file exists
255
- const exists = fs.existsSync(filePath);
256
- const isFile = exists && fs.statSync(filePath).isFile();
257
-
258
- let event;
259
-
260
- if (!exists) {
261
- // File was deleted
262
- if (this.watchedFiles.has(filePath)) {
263
- this.watchedFiles.delete(filePath);
264
- event = {
265
- type: 'delete',
266
- filePath,
267
- timestamp: new Date().toISOString()
268
- };
269
- }
270
- } else if (isFile && this.shouldWatch(filePath)) {
271
- if (this.watchedFiles.has(filePath)) {
272
- // File was modified
273
- event = {
274
- type: 'change',
275
- filePath,
276
- timestamp: new Date().toISOString()
277
- };
278
- } else {
279
- // New file added
280
- this.watchedFiles.add(filePath);
281
- event = {
282
- type: 'add',
283
- filePath,
284
- timestamp: new Date().toISOString()
285
- };
286
- }
287
- }
288
-
289
- if (event) {
290
- this.emit(event.type, event);
291
- this.emit('any', event);
292
- }
293
- }
294
-
295
- _matchPattern(filePath, pattern) {
296
- // Simple glob-like pattern matching
297
- // Supports: *, **, ?
298
-
299
- // Normalize separators
300
- const normalizedPath = filePath.replace(/\\/g, '/');
301
- const normalizedPattern = pattern.replace(/\\/g, '/');
302
-
303
- // Convert glob pattern to regex
304
- let regexPattern = normalizedPattern
305
- .replace(/\./g, '\\.') // Escape dots
306
- .replace(/\*\*/g, '<<<GLOBSTAR>>>') // Temporarily replace **
307
- .replace(/\*/g, '[^/]*') // * matches anything except /
308
- .replace(/<<<GLOBSTAR>>>/g, '.*') // ** matches anything including /
309
- .replace(/\?/g, '.'); // ? matches single char
310
-
311
- // Anchor the pattern
312
- regexPattern = '^' + regexPattern + '$';
313
-
314
- try {
315
- const regex = new RegExp(regexPattern);
316
- return regex.test(normalizedPath);
317
- } catch (e) {
318
- return false;
319
- }
320
- }
321
- }
322
-
323
- module.exports = FileWatcher;
@@ -1,301 +0,0 @@
1
- /**
2
- * Thuban Ghost Code Detector
3
- *
4
- * @purpose Find AI-generated functions that were pasted but never called
5
- * @module thuban
6
- * @layer scanner
7
- * @exports-to cli
8
- * @critical-for Unique feature — no other tool targets "pasted but unwired" code
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
-
14
- class GhostCodeDetector {
15
- constructor(opts = {}) {
16
- this.rootPath = opts.rootPath || process.cwd();
17
- }
18
-
19
- /**
20
- * Scan files for ghost (uncalled) functions
21
- */
22
- scan(files) {
23
- const functions = []; // All function definitions
24
- const references = {}; // Function name → reference count
25
-
26
- // Pass 1: Find all function definitions
27
- for (const file of files) {
28
- let content;
29
- try {
30
- const stat = fs.statSync(file);
31
- if (stat.size > 512 * 1024) continue;
32
- content = fs.readFileSync(file, 'utf-8');
33
- } catch (e) {
34
- continue;
35
- }
36
-
37
- const relPath = path.relative(this.rootPath, file);
38
-
39
- // Skip test files, config files, and entry points
40
- if (this._isSkippable(relPath)) continue;
41
-
42
- const lines = content.split('\n');
43
-
44
- for (let i = 0; i < lines.length; i++) {
45
- const line = lines[i];
46
- const fn = this._extractFunction(line, i, lines);
47
- if (fn) {
48
- fn.file = relPath;
49
- fn.line = i + 1;
50
- fn.lineCount = this._countFunctionLines(lines, i);
51
- functions.push(fn);
52
-
53
- // Initialize reference counter
54
- if (!references[fn.name]) references[fn.name] = 0;
55
- }
56
- }
57
- }
58
-
59
- // Pass 2: Count references to each function
60
- for (const file of files) {
61
- let content;
62
- try {
63
- content = fs.readFileSync(file, 'utf-8');
64
- } catch (e) {
65
- continue;
66
- }
67
-
68
- // Strip comments and strings before counting references to avoid false negatives
69
- // from functions that appear only in commented-out code
70
- const strippedContent = this._stripCommentsAndStrings(content);
71
-
72
- for (const fn of functions) {
73
- if (fn.name.length < 3) continue; // Skip very short names (likely false positives)
74
-
75
- // Count how many times this function name appears (excluding its definition)
76
- const regex = new RegExp(`\\b${this._escapeRegex(fn.name)}\\b`, 'g');
77
- const matches = strippedContent.match(regex);
78
- if (matches) {
79
- const relPath = path.relative(this.rootPath, file);
80
- // Subtract 1 if this is the file where it's defined (the definition itself)
81
- const isDefFile = relPath === fn.file;
82
- references[fn.name] += matches.length - (isDefFile ? 1 : 0);
83
- }
84
- }
85
- }
86
-
87
- // Pass 3: Find ghosts — defined but never referenced elsewhere
88
- const ghosts = functions
89
- .filter(fn => {
90
- const refs = references[fn.name] || 0;
91
- // It's a ghost if:
92
- // - Referenced 0 times outside its definition
93
- // - Not an export (exports are meant to be used externally)
94
- // - Not a constructor, lifecycle method, or callback pattern
95
- return refs <= 0 && !fn.isExport && !fn.isLifecycle && fn.lineCount >= 5;
96
- })
97
- .map(fn => ({
98
- ...fn,
99
- references: references[fn.name] || 0,
100
- wastedLines: fn.lineCount,
101
- category: 'ghost',
102
- severity: fn.lineCount > 30 ? 'high' : 'medium',
103
- message: `${fn.name}() — ${fn.lineCount} lines, never called`,
104
- }))
105
- .sort((a, b) => b.wastedLines - a.wastedLines);
106
-
107
- const totalWastedLines = ghosts.reduce((sum, g) => sum + g.wastedLines, 0);
108
- const totalLines = files.reduce((sum, f) => {
109
- try { return sum + fs.readFileSync(f, 'utf-8').split('\n').length; } catch (e) { return sum; }
110
- }, 0);
111
-
112
- return {
113
- ghosts,
114
- totalGhosts: ghosts.length,
115
- totalWastedLines,
116
- wastedPercent: totalLines > 0 ? Math.round((totalWastedLines / totalLines) * 1000) / 10 : 0,
117
- totalLines,
118
- };
119
- }
120
-
121
- // ─── Private ─────────────────────────────────────────────
122
-
123
- _extractFunction(line, lineIndex, lines) {
124
- const trimmed = line.trim();
125
-
126
- // JavaScript/TypeScript: function name(
127
- let match = trimmed.match(/^(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/);
128
- if (match) {
129
- return {
130
- name: match[1],
131
- type: 'function',
132
- isExport: this._isExported(line, lineIndex, lines),
133
- isLifecycle: this._isLifecycle(match[1]),
134
- };
135
- }
136
-
137
- // JavaScript/TypeScript: const name = (async) function
138
- match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?function/);
139
- if (match) {
140
- return {
141
- name: match[1],
142
- type: 'const-function',
143
- isExport: this._isExported(line, lineIndex, lines),
144
- isLifecycle: this._isLifecycle(match[1]),
145
- };
146
- }
147
-
148
- // JavaScript/TypeScript: const name = (...) =>
149
- match = trimmed.match(/^(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\(?/);
150
- if (match && (trimmed.includes('=>') || (lineIndex + 1 < lines.length && lines[lineIndex + 1].includes('=>')))) {
151
- return {
152
- name: match[1],
153
- type: 'arrow',
154
- isExport: this._isExported(line, lineIndex, lines),
155
- isLifecycle: this._isLifecycle(match[1]),
156
- };
157
- }
158
-
159
- // Kotlin: fun functionName(
160
- match = trimmed.match(/^(?:private\s+|public\s+|internal\s+|protected\s+)?(?:suspend\s+)?fun\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
161
- if (match) {
162
- return {
163
- name: match[1],
164
- type: 'kotlin-fun',
165
- isExport: !trimmed.startsWith('private'),
166
- isLifecycle: this._isLifecycle(match[1]),
167
- };
168
- }
169
-
170
- // Rust: fn function_name(
171
- match = trimmed.match(/^(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*[(<]/);
172
- if (match) {
173
- return {
174
- name: match[1],
175
- type: 'rust-fn',
176
- isExport: /^pub/.test(trimmed),
177
- isLifecycle: this._isLifecycle(match[1]),
178
- };
179
- }
180
-
181
- // PHP: function functionName( or public/private/protected function functionName(
182
- match = trimmed.match(/^(?:(?:public|private|protected|static)\s+)*function\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(/);
183
- if (match) {
184
- return {
185
- name: match[1],
186
- type: 'php-function',
187
- isExport: !/private/.test(trimmed),
188
- isLifecycle: this._isLifecycle(match[1]),
189
- };
190
- }
191
-
192
- // Ruby: def method_name or def self.method_name
193
- match = trimmed.match(/^def\s+(?:self\.)?([a-zA-Z_][a-zA-Z0-9_?!]*)\s*[(\s]?/);
194
- if (match) {
195
- return {
196
- name: match[1],
197
- type: 'ruby-def',
198
- isExport: !match[1].startsWith('_'),
199
- isLifecycle: this._isLifecycle(match[1]),
200
- };
201
- }
202
-
203
- // Class method: name(
204
- match = trimmed.match(/^(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*{/);
205
- if (match && !['if', 'for', 'while', 'switch', 'catch', 'constructor', 'render', 'toString'].includes(match[1])) {
206
- return {
207
- name: match[1],
208
- type: 'method',
209
- isExport: false,
210
- isLifecycle: this._isLifecycle(match[1]),
211
- };
212
- }
213
-
214
- return null;
215
- }
216
-
217
- _isExported(line, lineIndex, lines) {
218
- if (/^export\s/.test(line.trim())) return true;
219
- if (/module\.exports/.test(line)) return true;
220
-
221
- // Check if it's exported later in the file
222
- // (simplified — check last 20 lines for module.exports containing the function name)
223
- return false;
224
- }
225
-
226
- _isLifecycle(name) {
227
- const lifecycle = [
228
- // JS/TS lifecycle
229
- 'constructor', 'render', 'componentDidMount', 'componentWillUnmount',
230
- 'componentDidUpdate', 'shouldComponentUpdate', 'getSnapshotBeforeUpdate',
231
- 'getDerivedStateFromProps', 'useEffect', 'useState', 'useMemo',
232
- 'toString', 'valueOf', 'toJSON', 'inspect',
233
- 'setup', 'teardown', 'beforeEach', 'afterEach', 'beforeAll', 'afterAll',
234
- 'init', 'destroy', 'configure', 'bootstrap', 'main',
235
- 'get', 'set', 'post', 'put', 'delete', 'patch', 'handle',
236
- // Kotlin/Java lifecycle
237
- 'onCreate', 'onStart', 'onResume', 'onPause', 'onStop', 'onDestroy',
238
- 'onCreateView', 'onViewCreated', 'onDestroyView', 'hashCode', 'equals',
239
- // Rust lifecycle / trait methods
240
- 'new', 'default', 'drop', 'fmt', 'from', 'into', 'clone', 'copy',
241
- // Ruby lifecycle
242
- 'initialize', 'to_s', 'to_str', 'inspect', 'freeze', 'dup',
243
- // PHP lifecycle
244
- '__construct', '__destruct', '__toString', '__get', '__set', '__call',
245
- ];
246
- return lifecycle.includes(name) || name.startsWith('_') || name.startsWith('on');
247
- }
248
-
249
- _countFunctionLines(lines, startLine) {
250
- let depth = 0;
251
- let started = false;
252
- let count = 0;
253
-
254
- for (let i = startLine; i < lines.length && i < startLine + 200; i++) {
255
- const line = lines[i];
256
- count++;
257
-
258
- for (const ch of line) {
259
- if (ch === '{') { depth++; started = true; }
260
- if (ch === '}') depth--;
261
- }
262
-
263
- if (started && depth <= 0) break;
264
- }
265
-
266
- return count;
267
- }
268
-
269
- _isSkippable(relPath) {
270
- const skip = [
271
- /\.test\./i, /\.spec\./i, /test[\/\\]/i, /spec[\/\\]/i,
272
- /__test__/i, /__mocks__/i, /\.config\./i, /\.d\.ts$/,
273
- /index\.(js|ts)$/, /server\.(js|ts)$/, /app\.(js|ts)$/,
274
- /routes?\.(js|ts)$/, /middleware/i,
275
- ];
276
- return skip.some(r => r.test(relPath));
277
- }
278
-
279
- _escapeRegex(str) {
280
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
281
- }
282
-
283
- _stripCommentsAndStrings(content) {
284
- // Replace string literals with placeholder (preserve length approximately)
285
- let result = content
286
- // Template literals
287
- .replace(/`[^`\\]*(?:\\[\s\S][^`\\]*)*`/g, '""')
288
- // Double-quoted strings
289
- .replace(/"[^"\\\n]*(?:\\.[^"\\\n]*)*"/g, '""')
290
- // Single-quoted strings
291
- .replace(/'[^'\\\n]*(?:\\.[^'\\\n]*)*'/g, "''")
292
- // Block comments (JS/Java/Go/Rust/PHP/Kotlin)
293
- .replace(/\/\*[\s\S]*?\*\//g, ' ')
294
- // Line comments: //, #
295
- .replace(/\/\/[^\n]*/g, ' ')
296
- .replace(/#[^\n]*/g, ' ');
297
- return result;
298
- }
299
- }
300
-
301
- module.exports = GhostCodeDetector;