thuban 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Code Scanner
3
+ *
4
+ * @purpose Event-driven CodeScanner component
5
+ * @module sentinel
6
+ * @layer application
7
+ * @imports-from fs, path, events
8
+ * @exports-to sentinel
9
+ * @side-effects File system reads, Spawns child processes, Console output, Event emission
10
+ * @critical-for sentinel
11
+ */
12
+
13
+ /**
14
+ * Sentinel Code Scanner
15
+ *
16
+ * Scans codebase for security, quality, performance, and AI-specific issues.
17
+ * Based on CODEX/Thuban FORGE layer specifications.
18
+ *
19
+ * Categories:
20
+ * - Security: Secrets, injection, auth gaps
21
+ * - Quality: Complexity, duplication, dead code
22
+ * - Performance: N+1, memory leaks, blocking
23
+ * - Maintainability: Docs, nesting, coupling
24
+ * - AI-Specific: Hallucinated imports, inconsistent patterns
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const EventEmitter = require('events');
30
+
31
+ class CodeScanner extends EventEmitter {
32
+ constructor(config = {}) {
33
+ super();
34
+
35
+ this.config = {
36
+ rootPath: config.rootPath || process.cwd(),
37
+ ignorePatterns: config.ignorePatterns || [
38
+ 'node_modules/**',
39
+ '.git/**',
40
+ 'dist/**',
41
+ '.orion/snapshots/**'
42
+ ],
43
+ maxFileSize: config.maxFileSize || 1024 * 1024, // 1MB
44
+ complexityThreshold: config.complexityThreshold || 15,
45
+ nestingThreshold: config.nestingThreshold || 4,
46
+ ...config
47
+ };
48
+
49
+ // Safe patterns that should NOT trigger security warnings
50
+ this.safePatterns = [
51
+ // Environment variable names (not actual secrets)
52
+ /keyEnv\s*[:=]/i,
53
+ /envVar\s*[:=]/i,
54
+ /process\.env\[/,
55
+ /process\.env\./,
56
+ // Model version strings and hashes
57
+ /model\s*[:=]\s*['"][^'"]+['"]/i,
58
+ /version\s*[:=]\s*['"][^'"]+['"]/i, // version: "hash"
59
+ /claude-|gpt-|gemini-|llama-|mistral-|deepseek-|grok-|sonar-/i,
60
+ // API endpoints (URLs, not keys)
61
+ /endpoint\s*[:=]/i,
62
+ /https?:\/\//,
63
+ // Common safe strings
64
+ /_API_KEY['"]/, // env var names end with _API_KEY
65
+ /API_KEY['"]\s*\)/, // process.env["API_KEY"]
66
+ // Error messages and validation
67
+ /errors?\.\w+\s*=/i, // errors.password = "message"
68
+ /Error\s*[:=]/i,
69
+ /required|invalid|must be|cannot be/i, // validation messages
70
+ ];
71
+
72
+ // Test file patterns (allow mock data)
73
+ this.testFilePatterns = [
74
+ /\.test\.(js|ts|jsx|tsx)$/,
75
+ /\.spec\.(js|ts|jsx|tsx)$/,
76
+ /__tests__\//,
77
+ /\.mock\.(js|ts)$/,
78
+ /test-.*\.js$/,
79
+ ];
80
+
81
+ // Issue patterns - security focused
82
+ this.securityPatterns = [
83
+ {
84
+ id: 'SEC001',
85
+ name: 'Hardcoded API Key',
86
+ // Match strings that look like actual API keys:
87
+ // - sk-xxx, pk-xxx (Stripe-style)
88
+ // - xox-xxx (Slack-style)
89
+ // - AKIA... (AWS-style)
90
+ // - Mix of numbers/letters that looks random (not readable words)
91
+ pattern: /['"](?:sk-|pk-|xox[pboa]-|AKIA|ghp_|gho_|github_pat_)[a-zA-Z0-9_-]{10,}['"]|['"][a-zA-Z0-9]{32,}['"]/,
92
+ context: /(api[_-]?key|apikey|secret[_\s]*key|token|credential|bearer)/i,
93
+ severity: 'critical',
94
+ message: 'Possible hardcoded API key or secret detected',
95
+ skipIfSafe: true // Skip if line matches safe patterns
96
+ },
97
+ {
98
+ id: 'SEC002',
99
+ name: 'Hardcoded Password',
100
+ pattern: /password\s*[:=]\s*['"][^'"]+['"]/i,
101
+ severity: 'critical',
102
+ message: 'Hardcoded password detected',
103
+ skipInTests: true, // Skip in test files
104
+ skipIfSafe: true // Skip error messages and validation
105
+ },
106
+ {
107
+ id: 'SEC003',
108
+ name: 'SQL Injection Risk',
109
+ // Only match actual SQL queries with string interpolation
110
+ // Requires SQL keyword as whole word (not path.join etc)
111
+ pattern: /\b(?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WHERE)\b.*(\$\{|\+\s*['"]|\+\s*\w+)/i,
112
+ severity: 'error',
113
+ message: 'Potential SQL injection - use parameterized queries'
114
+ },
115
+ {
116
+ id: 'SEC004',
117
+ name: 'eval() Usage',
118
+ pattern: /\beval\s*\(/,
119
+ severity: 'error',
120
+ message: 'eval() is dangerous and should be avoided'
121
+ },
122
+ {
123
+ id: 'SEC005',
124
+ name: 'innerHTML Assignment',
125
+ pattern: /\.innerHTML\s*=/,
126
+ severity: 'warning',
127
+ message: 'innerHTML can cause XSS - consider using textContent or sanitization'
128
+ },
129
+ {
130
+ id: 'SEC006',
131
+ name: 'Disabled Security',
132
+ pattern: /rejectUnauthorized\s*:\s*false/,
133
+ severity: 'error',
134
+ message: 'SSL certificate validation disabled'
135
+ },
136
+ {
137
+ id: 'SEC007',
138
+ name: 'Exposed .env Reference',
139
+ pattern: /\.env['"]/,
140
+ context: /fs\.(read|write)|path\.join/i,
141
+ severity: 'warning',
142
+ message: 'Direct .env file manipulation - ensure not exposed'
143
+ }
144
+ ];
145
+
146
+ // Quality patterns
147
+ this.qualityPatterns = [
148
+ {
149
+ id: 'QUAL001',
150
+ name: 'Console.log in Production',
151
+ pattern: /console\.(log|debug|info)\(/,
152
+ severity: 'info',
153
+ message: 'console.log found - consider using proper logging',
154
+ // Only report in non-dev files - skip test files and dev utilities
155
+ skipInTests: true,
156
+ skipInDirs: ['test', 'scripts', 'tools', 'dev']
157
+ },
158
+ {
159
+ id: 'QUAL002',
160
+ name: 'TODO Comment',
161
+ pattern: /\/\/\s*TODO|\/\*\s*TODO|\*\s*TODO/i,
162
+ severity: 'info',
163
+ message: 'TODO comment found - track in issue system'
164
+ },
165
+ {
166
+ id: 'QUAL003',
167
+ name: 'FIXME Comment',
168
+ pattern: /\/\/\s*FIXME|\/\*\s*FIXME|\*\s*FIXME/i,
169
+ severity: 'warning',
170
+ message: 'FIXME comment found - needs attention'
171
+ },
172
+ {
173
+ id: 'QUAL004',
174
+ name: 'Empty Catch Block',
175
+ pattern: /catch\s*\([^)]*\)\s*\{\s*\}/,
176
+ severity: 'warning',
177
+ message: 'Empty catch block swallows errors'
178
+ },
179
+ {
180
+ id: 'QUAL005',
181
+ name: 'Magic Number',
182
+ // Only flag suspicious numbers, not common ones like ports, timeouts, dates
183
+ pattern: /(?<![0-9a-zA-Z_])(?!(?:1000|2000|3000|3001|4000|5000|8000|8080|9000|80|443|24|60|1024|2048|4096)\b)\d{5,}(?![0-9])/,
184
+ severity: 'info',
185
+ message: 'Magic number detected - consider using named constant',
186
+ skipInTests: true
187
+ },
188
+ {
189
+ id: 'QUAL006',
190
+ name: 'Callback Hell',
191
+ pattern: /\)\s*=>\s*\{[^}]*\)\s*=>\s*\{[^}]*\)\s*=>\s*\{/,
192
+ severity: 'warning',
193
+ message: 'Deeply nested callbacks - consider async/await'
194
+ }
195
+ ];
196
+
197
+ // Performance patterns
198
+ this.performancePatterns = [
199
+ {
200
+ id: 'PERF001',
201
+ name: 'Sync FS in Async',
202
+ pattern: /fs\.(readFileSync|writeFileSync|readdirSync|statSync)/,
203
+ severity: 'warning',
204
+ message: 'Synchronous file operation - may block event loop'
205
+ },
206
+ {
207
+ id: 'PERF002',
208
+ name: 'Missing Await',
209
+ pattern: /async\s+function[^{]*\{[^}]*(?<!await\s)new\s+Promise/,
210
+ severity: 'warning',
211
+ message: 'Promise created in async function without await'
212
+ },
213
+ {
214
+ id: 'PERF003',
215
+ name: 'Loop Database Query',
216
+ pattern: /for\s*\([^)]*\)\s*\{[^}]*(query|find|select|update)/i,
217
+ severity: 'error',
218
+ message: 'Possible N+1 query pattern - consider batch operation'
219
+ },
220
+ {
221
+ id: 'PERF004',
222
+ name: 'Large Array in Memory',
223
+ pattern: /new\s+Array\s*\(\s*\d{6,}\s*\)/,
224
+ severity: 'warning',
225
+ message: 'Large array allocation - consider streaming'
226
+ }
227
+ ];
228
+
229
+ // AI-specific patterns
230
+ this.aiPatterns = [
231
+ {
232
+ id: 'AI001',
233
+ name: 'Potentially Hallucinated Import',
234
+ pattern: /require\(['"](?!\.)[^'"]+['"]\)/,
235
+ severity: 'info',
236
+ message: 'External dependency - verify it exists in package.json',
237
+ check: (match, content, filePath) => this._checkDependencyExists(match, filePath)
238
+ },
239
+ {
240
+ id: 'AI002',
241
+ name: 'Unused Import',
242
+ pattern: /const\s+(\w+)\s*=\s*require\(['"][^'"]+['"]\)/,
243
+ severity: 'info',
244
+ message: 'Import may be unused - verify usage',
245
+ check: (match, content) => this._checkImportUsage(match, content)
246
+ },
247
+ {
248
+ id: 'AI003',
249
+ name: 'Over-Engineered Pattern',
250
+ pattern: /class\s+\w+Factory\s*\{|AbstractFactory|Singleton\.getInstance/,
251
+ severity: 'info',
252
+ message: 'Complex pattern detected - ensure justified by requirements'
253
+ }
254
+ ];
255
+ }
256
+
257
+ /**
258
+ * Scan all files in the codebase
259
+ */
260
+ async scanAll() {
261
+ console.log('[CODE-SCANNER] Starting full codebase scan...');
262
+ const startTime = Date.now();
263
+
264
+ const result = {
265
+ filesScanned: 0,
266
+ issues: [],
267
+ summary: {},
268
+ duration: 0
269
+ };
270
+
271
+ try {
272
+ const files = await this._discoverFiles(this.config.rootPath);
273
+ console.log(`[CODE-SCANNER] Found ${files.length} files to scan`);
274
+
275
+ for (const file of files) {
276
+ const fileIssues = await this.scanFile(file);
277
+ result.filesScanned++;
278
+ result.issues.push(...fileIssues);
279
+ }
280
+
281
+ result.duration = Date.now() - startTime;
282
+ result.summary = this._summarizeIssues(result.issues);
283
+
284
+ console.log(`[CODE-SCANNER] Scan complete: ${result.filesScanned} files, ${result.issues.length} issues`);
285
+ return result;
286
+ } catch (error) {
287
+ console.error('[CODE-SCANNER] Scan failed:', error.message);
288
+ result.error = error.message;
289
+ return result;
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Scan specific files
295
+ */
296
+ async scanFiles(filePaths) {
297
+ const result = {
298
+ filesScanned: 0,
299
+ issues: [],
300
+ summary: {}
301
+ };
302
+
303
+ for (const filePath of filePaths) {
304
+ const fileIssues = await this.scanFile(filePath);
305
+ result.filesScanned++;
306
+ result.issues.push(...fileIssues);
307
+ }
308
+
309
+ result.summary = this._summarizeIssues(result.issues);
310
+ return result;
311
+ }
312
+
313
+ /**
314
+ * Scan a single file
315
+ */
316
+ async scanFile(filePath) {
317
+ const issues = [];
318
+
319
+ try {
320
+ // Check file size
321
+ const stats = fs.statSync(filePath);
322
+ if (stats.size > this.config.maxFileSize) {
323
+ return [{
324
+ file: filePath,
325
+ category: 'quality',
326
+ severity: 'info',
327
+ id: 'FILE001',
328
+ message: `File too large (${Math.round(stats.size / 1024)}KB) - skipped detailed scan`
329
+ }];
330
+ }
331
+
332
+ // Read file content
333
+ const content = fs.readFileSync(filePath, 'utf8');
334
+ const lines = content.split('\n');
335
+ const ext = path.extname(filePath).toLowerCase();
336
+
337
+ // Only scan JS/TS files with pattern matching
338
+ if (['.js', '.ts', '.jsx', '.tsx'].includes(ext)) {
339
+ // Security scan
340
+ issues.push(...this._runPatternScan(filePath, content, lines, this.securityPatterns, 'security'));
341
+
342
+ // Quality scan
343
+ issues.push(...this._runPatternScan(filePath, content, lines, this.qualityPatterns, 'quality'));
344
+
345
+ // Performance scan
346
+ issues.push(...this._runPatternScan(filePath, content, lines, this.performancePatterns, 'performance'));
347
+
348
+ // AI-specific scan
349
+ issues.push(...this._runPatternScan(filePath, content, lines, this.aiPatterns, 'ai'));
350
+
351
+ // Complexity analysis
352
+ issues.push(...this._analyzeComplexity(filePath, content, lines));
353
+
354
+ // Nesting analysis
355
+ issues.push(...this._analyzeNesting(filePath, content, lines));
356
+ }
357
+
358
+ // JSON-specific checks
359
+ if (ext === '.json') {
360
+ issues.push(...this._scanJson(filePath, content));
361
+ }
362
+
363
+ // Emit event for each issue found
364
+ for (const issue of issues) {
365
+ this.emit('issueFound', issue);
366
+ }
367
+
368
+ return issues;
369
+ } catch (error) {
370
+ return [{
371
+ file: filePath,
372
+ category: 'error',
373
+ severity: 'error',
374
+ id: 'SCAN001',
375
+ message: `Failed to scan file: ${error.message}`
376
+ }];
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Get current scan rules
382
+ */
383
+ getRules() {
384
+ return {
385
+ security: this.securityPatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity })),
386
+ quality: this.qualityPatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity })),
387
+ performance: this.performancePatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity })),
388
+ ai: this.aiPatterns.map(p => ({ id: p.id, name: p.name, severity: p.severity }))
389
+ };
390
+ }
391
+
392
+ // Private methods
393
+
394
+ async _discoverFiles(dir, files = []) {
395
+ try {
396
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
397
+
398
+ for (const entry of entries) {
399
+ const fullPath = path.join(dir, entry.name);
400
+ const relativePath = path.relative(this.config.rootPath, fullPath);
401
+
402
+ // Check ignore patterns
403
+ if (this._shouldIgnore(relativePath)) {
404
+ continue;
405
+ }
406
+
407
+ if (entry.isDirectory()) {
408
+ await this._discoverFiles(fullPath, files);
409
+ } else if (entry.isFile()) {
410
+ const ext = path.extname(entry.name).toLowerCase();
411
+ if (['.js', '.ts', '.jsx', '.tsx', '.json'].includes(ext)) {
412
+ files.push(fullPath);
413
+ }
414
+ }
415
+ }
416
+ } catch (error) {
417
+ // Skip inaccessible directories
418
+ }
419
+
420
+ return files;
421
+ }
422
+
423
+ _shouldIgnore(relativePath) {
424
+ const normalized = relativePath.replace(/\\/g, '/');
425
+
426
+ // Always ignore any path containing these directories (anywhere in path)
427
+ const alwaysIgnore = ['node_modules', '.git', 'dist', '.vite'];
428
+ for (const dir of alwaysIgnore) {
429
+ if (normalized.includes('/' + dir + '/') ||
430
+ normalized.startsWith(dir + '/') ||
431
+ normalized === dir ||
432
+ normalized.endsWith('/' + dir)) {
433
+ return true;
434
+ }
435
+ }
436
+
437
+ for (const pattern of this.config.ignorePatterns) {
438
+ const normalizedPattern = pattern.replace(/\\/g, '/');
439
+
440
+ // Extract base directory from pattern (e.g., 'node_modules' from 'node_modules/**')
441
+ const baseDir = normalizedPattern.split('/')[0].replace(/\*/g, '');
442
+
443
+ // Check if path IS the ignored directory or starts with it
444
+ if (baseDir && (normalized === baseDir || normalized.startsWith(baseDir + '/'))) {
445
+ return true;
446
+ }
447
+
448
+ // Simple glob matching for more complex patterns
449
+ let regexPattern = normalizedPattern
450
+ .replace(/\./g, '\\.')
451
+ .replace(/\*\*/g, '.*')
452
+ .replace(/\*/g, '[^/]*');
453
+
454
+ if (new RegExp('^' + regexPattern).test(normalized)) {
455
+ return true;
456
+ }
457
+ }
458
+
459
+ return false;
460
+ }
461
+
462
+ _runPatternScan(filePath, content, lines, patterns, category) {
463
+ const issues = [];
464
+
465
+ // Check if this is a test file
466
+ const isTestFile = this.testFilePatterns.some(p => p.test(filePath));
467
+ const relativePath = path.relative(this.config.rootPath, filePath).replace(/\\/g, '/');
468
+
469
+ for (const pattern of patterns) {
470
+ // Skip this pattern entirely in test files if flagged
471
+ if (pattern.skipInTests && isTestFile) {
472
+ continue;
473
+ }
474
+
475
+ // Skip if file is in excluded directories
476
+ if (pattern.skipInDirs) {
477
+ const inSkippedDir = pattern.skipInDirs.some(dir =>
478
+ relativePath.startsWith(dir + '/') || relativePath.includes('/' + dir + '/')
479
+ );
480
+ if (inSkippedDir) continue;
481
+ }
482
+
483
+ // Check context pattern first if it exists (file-level)
484
+ if (pattern.context && !pattern.lineContext && !pattern.context.test(content)) {
485
+ continue;
486
+ }
487
+
488
+ // Find all matches
489
+ let lineNum = 0;
490
+ for (const line of lines) {
491
+ lineNum++;
492
+ const match = line.match(pattern.pattern);
493
+
494
+ if (match) {
495
+ // Skip if line matches any safe pattern (for SEC001 type checks)
496
+ if (pattern.skipIfSafe) {
497
+ const isSafe = this.safePatterns.some(sp => sp.test(line));
498
+ if (isSafe) continue;
499
+ }
500
+
501
+ // Run additional check if defined
502
+ if (pattern.check) {
503
+ const shouldReport = pattern.check(match, content, filePath);
504
+ if (!shouldReport) continue;
505
+ }
506
+
507
+ issues.push({
508
+ file: filePath,
509
+ line: lineNum,
510
+ column: match.index,
511
+ category,
512
+ severity: pattern.severity,
513
+ id: pattern.id,
514
+ name: pattern.name,
515
+ message: pattern.message,
516
+ code: line.trim().substring(0, 100)
517
+ });
518
+ }
519
+ }
520
+ }
521
+
522
+ return issues;
523
+ }
524
+
525
+ _analyzeComplexity(filePath, content, lines) {
526
+ const issues = [];
527
+
528
+ // Count decision points (simplified cyclomatic complexity)
529
+ const decisionKeywords = /\b(if|else|for|while|switch|case|catch|&&|\|\||\?)\b/g;
530
+ const functionMatches = content.match(/function\s+\w+|=>\s*\{|async\s+\w+/g) || [];
531
+ const decisionMatches = content.match(decisionKeywords) || [];
532
+
533
+ const avgComplexity = functionMatches.length > 0
534
+ ? decisionMatches.length / functionMatches.length
535
+ : 0;
536
+
537
+ if (avgComplexity > this.config.complexityThreshold) {
538
+ issues.push({
539
+ file: filePath,
540
+ category: 'quality',
541
+ severity: 'warning',
542
+ id: 'CMPLX001',
543
+ name: 'High Complexity',
544
+ message: `Average cyclomatic complexity is ${avgComplexity.toFixed(1)} (threshold: ${this.config.complexityThreshold})`
545
+ });
546
+ }
547
+
548
+ // Check file length
549
+ if (lines.length > 500) {
550
+ issues.push({
551
+ file: filePath,
552
+ category: 'quality',
553
+ severity: 'info',
554
+ id: 'CMPLX002',
555
+ name: 'Long File',
556
+ message: `File has ${lines.length} lines - consider splitting`
557
+ });
558
+ }
559
+
560
+ return issues;
561
+ }
562
+
563
+ _analyzeNesting(filePath, content, lines) {
564
+ const issues = [];
565
+ let maxNesting = 0;
566
+ let currentNesting = 0;
567
+ let maxNestingLine = 0;
568
+
569
+ let lineNum = 0;
570
+ for (const line of lines) {
571
+ lineNum++;
572
+
573
+ // Count braces (simplified)
574
+ const opens = (line.match(/\{/g) || []).length;
575
+ const closes = (line.match(/\}/g) || []).length;
576
+
577
+ currentNesting += opens - closes;
578
+
579
+ if (currentNesting > maxNesting) {
580
+ maxNesting = currentNesting;
581
+ maxNestingLine = lineNum;
582
+ }
583
+ }
584
+
585
+ if (maxNesting > this.config.nestingThreshold) {
586
+ issues.push({
587
+ file: filePath,
588
+ line: maxNestingLine,
589
+ category: 'quality',
590
+ severity: 'warning',
591
+ id: 'NEST001',
592
+ name: 'Deep Nesting',
593
+ message: `Max nesting depth is ${maxNesting} at line ${maxNestingLine} (threshold: ${this.config.nestingThreshold})`
594
+ });
595
+ }
596
+
597
+ return issues;
598
+ }
599
+
600
+ _scanJson(filePath, content) {
601
+ const issues = [];
602
+
603
+ try {
604
+ JSON.parse(content);
605
+ } catch (error) {
606
+ issues.push({
607
+ file: filePath,
608
+ category: 'quality',
609
+ severity: 'error',
610
+ id: 'JSON001',
611
+ name: 'Invalid JSON',
612
+ message: `JSON parse error: ${error.message}`
613
+ });
614
+ }
615
+
616
+ // Check for sensitive data in JSON
617
+ if (/(password|secret|api[_-]?key|token)/i.test(content)) {
618
+ issues.push({
619
+ file: filePath,
620
+ category: 'security',
621
+ severity: 'warning',
622
+ id: 'JSON002',
623
+ name: 'Sensitive Data in JSON',
624
+ message: 'Possible sensitive data in JSON file - verify not committed'
625
+ });
626
+ }
627
+
628
+ return issues;
629
+ }
630
+
631
+ _checkDependencyExists(match, filePath) {
632
+ // Extract module name
633
+ const moduleMatch = match[0].match(/require\(['"]([^'"]+)['"]\)/);
634
+ if (!moduleMatch) return true;
635
+
636
+ const moduleName = moduleMatch[1];
637
+
638
+ // Skip relative imports
639
+ if (moduleName.startsWith('.')) return false;
640
+
641
+ // Skip Node built-ins
642
+ const builtins = ['fs', 'path', 'os', 'crypto', 'http', 'https', 'events', 'util', 'stream', 'child_process', 'url', 'querystring', 'buffer'];
643
+ if (builtins.includes(moduleName.split('/')[0])) return false;
644
+
645
+ // Check package.json
646
+ try {
647
+ let pkgPath = path.join(this.config.rootPath, 'package.json');
648
+
649
+ // Also check server/package.json for server files
650
+ if (filePath.includes('server')) {
651
+ const serverPkgPath = path.join(this.config.rootPath, 'server', 'package.json');
652
+ if (fs.existsSync(serverPkgPath)) {
653
+ pkgPath = serverPkgPath;
654
+ }
655
+ }
656
+
657
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
658
+ const allDeps = {
659
+ ...(pkg.dependencies || {}),
660
+ ...(pkg.devDependencies || {})
661
+ };
662
+
663
+ const baseModule = moduleName.split('/')[0];
664
+ return !allDeps[baseModule] && !allDeps[moduleName];
665
+ } catch (e) {
666
+ return false;
667
+ }
668
+ }
669
+
670
+ _checkImportUsage(match, content) {
671
+ const varName = match[1];
672
+ if (!varName) return false;
673
+
674
+ // Count occurrences (beyond the require line)
675
+ const regex = new RegExp('\\b' + varName + '\\b', 'g');
676
+ const occurrences = (content.match(regex) || []).length;
677
+
678
+ // If only appears once (the require), it's unused
679
+ return occurrences <= 1;
680
+ }
681
+
682
+ _summarizeIssues(issues) {
683
+ return {
684
+ total: issues.length,
685
+ bySeverity: {
686
+ critical: issues.filter(i => i.severity === 'critical').length,
687
+ error: issues.filter(i => i.severity === 'error').length,
688
+ warning: issues.filter(i => i.severity === 'warning').length,
689
+ info: issues.filter(i => i.severity === 'info').length
690
+ },
691
+ byCategory: issues.reduce((acc, i) => {
692
+ acc[i.category] = (acc[i.category] || 0) + 1;
693
+ return acc;
694
+ }, {}),
695
+ topFiles: this._getTopFiles(issues, 10)
696
+ };
697
+ }
698
+
699
+ _getTopFiles(issues, limit) {
700
+ const fileCounts = issues.reduce((acc, i) => {
701
+ acc[i.file] = (acc[i.file] || 0) + 1;
702
+ return acc;
703
+ }, {});
704
+
705
+ return Object.entries(fileCounts)
706
+ .sort((a, b) => b[1] - a[1])
707
+ .slice(0, limit)
708
+ .map(([file, count]) => ({ file: path.relative(this.config.rootPath, file), count }));
709
+ }
710
+ }
711
+
712
+ module.exports = CodeScanner;