projscan 0.1.13 → 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.
Files changed (54) hide show
  1. package/README.md +164 -19
  2. package/dist/analyzers/architectureCheck.js +1 -0
  3. package/dist/analyzers/architectureCheck.js.map +1 -1
  4. package/dist/analyzers/securityCheck.js +16 -1
  5. package/dist/analyzers/securityCheck.js.map +1 -1
  6. package/dist/cli/index.js +209 -164
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/core/fileInspector.d.ts +13 -0
  9. package/dist/core/fileInspector.js +205 -0
  10. package/dist/core/fileInspector.js.map +1 -0
  11. package/dist/core/hotspotAnalyzer.d.ts +16 -0
  12. package/dist/core/hotspotAnalyzer.js +342 -0
  13. package/dist/core/hotspotAnalyzer.js.map +1 -0
  14. package/dist/core/repositoryScanner.d.ts +4 -1
  15. package/dist/core/repositoryScanner.js +6 -3
  16. package/dist/core/repositoryScanner.js.map +1 -1
  17. package/dist/index.d.ts +10 -1
  18. package/dist/index.js +9 -0
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/prompts.d.ts +14 -0
  21. package/dist/mcp/prompts.js +126 -0
  22. package/dist/mcp/prompts.js.map +1 -0
  23. package/dist/mcp/resources.d.ts +8 -0
  24. package/dist/mcp/resources.js +57 -0
  25. package/dist/mcp/resources.js.map +1 -0
  26. package/dist/mcp/server.d.ts +5 -0
  27. package/dist/mcp/server.js +205 -0
  28. package/dist/mcp/server.js.map +1 -0
  29. package/dist/mcp/tools.d.ts +9 -0
  30. package/dist/mcp/tools.js +176 -0
  31. package/dist/mcp/tools.js.map +1 -0
  32. package/dist/reporters/consoleReporter.d.ts +3 -1
  33. package/dist/reporters/consoleReporter.js +127 -0
  34. package/dist/reporters/consoleReporter.js.map +1 -1
  35. package/dist/reporters/jsonReporter.d.ts +3 -1
  36. package/dist/reporters/jsonReporter.js +6 -0
  37. package/dist/reporters/jsonReporter.js.map +1 -1
  38. package/dist/reporters/markdownReporter.d.ts +3 -1
  39. package/dist/reporters/markdownReporter.js +99 -0
  40. package/dist/reporters/markdownReporter.js.map +1 -1
  41. package/dist/reporters/sarifReporter.d.ts +61 -0
  42. package/dist/reporters/sarifReporter.js +102 -0
  43. package/dist/reporters/sarifReporter.js.map +1 -0
  44. package/dist/types.d.ts +112 -1
  45. package/dist/utils/baseline.d.ts +4 -4
  46. package/dist/utils/baseline.js +71 -5
  47. package/dist/utils/baseline.js.map +1 -1
  48. package/dist/utils/changedFiles.d.ts +14 -0
  49. package/dist/utils/changedFiles.js +113 -0
  50. package/dist/utils/changedFiles.js.map +1 -0
  51. package/dist/utils/config.d.ts +8 -0
  52. package/dist/utils/config.js +117 -0
  53. package/dist/utils/config.js.map +1 -0
  54. package/package.json +2 -2
package/dist/cli/index.js CHANGED
@@ -14,33 +14,74 @@ import { detectLanguages } from '../core/languageDetector.js';
14
14
  import { detectFrameworks } from '../core/frameworkDetector.js';
15
15
  import { analyzeDependencies } from '../core/dependencyAnalyzer.js';
16
16
  import { collectIssues } from '../core/issueEngine.js';
17
+ import { analyzeHotspots } from '../core/hotspotAnalyzer.js';
18
+ import { inspectFile, extractImports, extractExports, inferPurpose, detectFileIssues, } from '../core/fileInspector.js';
17
19
  import { getAllAvailableFixes } from '../fixes/fixRegistry.js';
18
20
  import { setLogLevel } from '../utils/logger.js';
19
21
  import { calculateScore, badgeUrl, badgeMarkdown } from '../utils/scoreCalculator.js';
20
22
  import { showBanner, showCompactBanner, showHelp } from '../utils/banner.js';
21
23
  import { saveBaseline, loadBaseline, computeDiff } from '../utils/baseline.js';
22
- import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, } from '../reporters/consoleReporter.js';
23
- import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, } from '../reporters/jsonReporter.js';
24
- import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, } from '../reporters/markdownReporter.js';
24
+ import { loadConfig, applyConfigToIssues } from '../utils/config.js';
25
+ import { getChangedFiles } from '../utils/changedFiles.js';
26
+ import { runMcpServer } from '../mcp/server.js';
27
+ import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, } from '../reporters/consoleReporter.js';
28
+ import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, } from '../reporters/jsonReporter.js';
29
+ import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, } from '../reporters/markdownReporter.js';
30
+ import { reportAnalysisSarif, reportHealthSarif, reportCiSarif, } from '../reporters/sarifReporter.js';
25
31
  // ── CLI Setup ─────────────────────────────────────────────
26
32
  const program = new Command();
27
33
  program
28
34
  .name('projscan')
29
35
  .description('Instant codebase insights — doctor, x-ray, and architecture map for any repository')
30
36
  .version(pkg.version)
31
- .option('--format <type>', 'output format: console, json, markdown', 'console')
37
+ .option('--format <type>', 'output format: console, json, markdown, sarif', 'console')
38
+ .option('--config <path>', 'path to .projscanrc config file')
32
39
  .option('--verbose', 'enable verbose output')
33
40
  .option('--quiet', 'suppress non-essential output');
34
41
  function getFormat() {
35
42
  const opts = program.opts();
36
43
  const f = opts.format;
37
- if (f === 'json' || f === 'markdown')
44
+ if (f === 'json' || f === 'markdown' || f === 'sarif')
38
45
  return f;
39
46
  return 'console';
40
47
  }
41
48
  function getRootPath() {
42
49
  return process.cwd();
43
50
  }
51
+ async function loadProjectConfig() {
52
+ const opts = program.opts();
53
+ const explicit = typeof opts.config === 'string' ? opts.config : undefined;
54
+ try {
55
+ const { config, source } = await loadConfig(getRootPath(), explicit);
56
+ if (source && !opts.quiet && getFormat() === 'console') {
57
+ console.error(chalk.dim(` [config: ${path.relative(getRootPath(), source) || source}]`));
58
+ }
59
+ return config;
60
+ }
61
+ catch (err) {
62
+ const msg = err instanceof Error ? err.message : String(err);
63
+ console.error(chalk.red(` Config error: ${msg}`));
64
+ process.exit(1);
65
+ }
66
+ }
67
+ async function filterIssuesByChangedFiles(issues, rootPath, baseRef) {
68
+ const result = await getChangedFiles(rootPath, baseRef);
69
+ if (!result.available) {
70
+ if (getFormat() === 'console' && !program.opts().quiet) {
71
+ console.error(chalk.yellow(` [--changed-only: ${result.reason ?? 'unavailable'} — reporting all issues]`));
72
+ }
73
+ return issues;
74
+ }
75
+ if (getFormat() === 'console' && !program.opts().quiet) {
76
+ console.error(chalk.dim(` [--changed-only: base=${result.baseRef}, ${result.files.length} file(s)]`));
77
+ }
78
+ const set = new Set(result.files);
79
+ return issues.filter((issue) => {
80
+ if (!issue.locations || issue.locations.length === 0)
81
+ return false;
82
+ return issue.locations.some((loc) => set.has(loc.file));
83
+ });
84
+ }
44
85
  function setupLogLevel() {
45
86
  const opts = program.opts();
46
87
  if (opts.verbose)
@@ -74,14 +115,17 @@ function maybeCompactBanner() {
74
115
  program
75
116
  .command('analyze', { isDefault: true })
76
117
  .description('Analyze repository and show project report')
77
- .action(async () => {
118
+ .option('--changed-only', 'only report issues on files changed vs base ref')
119
+ .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
120
+ .action(async (cmdOpts) => {
78
121
  setupLogLevel();
79
122
  maybeBanner();
80
123
  const rootPath = getRootPath();
81
124
  const format = getFormat();
125
+ const config = await loadProjectConfig();
82
126
  const spinner = format === 'console' ? ora('Scanning repository...').start() : null;
83
127
  try {
84
- const scan = await scanRepository(rootPath);
128
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
85
129
  if (spinner)
86
130
  spinner.text = 'Detecting languages...';
87
131
  const languages = detectLanguages(scan.files);
@@ -93,7 +137,11 @@ program
93
137
  const dependencies = await analyzeDependencies(rootPath);
94
138
  if (spinner)
95
139
  spinner.text = 'Checking for issues...';
96
- const issues = await collectIssues(rootPath, scan.files);
140
+ let issues = await collectIssues(rootPath, scan.files);
141
+ issues = applyConfigToIssues(issues, config);
142
+ if (cmdOpts.changedOnly) {
143
+ issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
144
+ }
97
145
  if (spinner)
98
146
  spinner.stop();
99
147
  const report = {
@@ -113,6 +161,9 @@ program
113
161
  case 'markdown':
114
162
  reportAnalysisMarkdown(report);
115
163
  break;
164
+ case 'sarif':
165
+ reportAnalysisSarif(issues, pkg.version);
166
+ break;
116
167
  default:
117
168
  reportAnalysis(report);
118
169
  }
@@ -128,15 +179,22 @@ program
128
179
  program
129
180
  .command('doctor')
130
181
  .description('Evaluate project health and detect issues')
131
- .action(async () => {
182
+ .option('--changed-only', 'only report issues on files changed vs base ref')
183
+ .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
184
+ .action(async (cmdOpts) => {
132
185
  setupLogLevel();
133
186
  maybeCompactBanner();
134
187
  const rootPath = getRootPath();
135
188
  const format = getFormat();
189
+ const config = await loadProjectConfig();
136
190
  const spinner = format === 'console' ? ora('Running health checks...').start() : null;
137
191
  try {
138
- const scan = await scanRepository(rootPath);
139
- const issues = await collectIssues(rootPath, scan.files);
192
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
193
+ let issues = await collectIssues(rootPath, scan.files);
194
+ issues = applyConfigToIssues(issues, config);
195
+ if (cmdOpts.changedOnly) {
196
+ issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
197
+ }
140
198
  if (spinner)
141
199
  spinner.stop();
142
200
  switch (format) {
@@ -146,6 +204,9 @@ program
146
204
  case 'markdown':
147
205
  reportHealthMarkdown(issues);
148
206
  break;
207
+ case 'sarif':
208
+ reportHealthSarif(issues, pkg.version);
209
+ break;
149
210
  default:
150
211
  reportHealth(issues, scan.scanDurationMs);
151
212
  }
@@ -161,16 +222,24 @@ program
161
222
  program
162
223
  .command('ci')
163
224
  .description('Run health check for CI pipelines (exits 1 if score below threshold)')
164
- .option('--min-score <score>', 'minimum passing score (0-100)', '70')
225
+ .option('--min-score <score>', 'minimum passing score (0-100)')
226
+ .option('--changed-only', 'gate only on issues in files changed vs base ref')
227
+ .option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
165
228
  .action(async (cmdOpts) => {
166
229
  setupLogLevel();
167
230
  maybeCompactBanner();
168
231
  const rootPath = getRootPath();
169
232
  const format = getFormat();
233
+ const config = await loadProjectConfig();
170
234
  try {
171
- const scan = await scanRepository(rootPath);
172
- const issues = await collectIssues(rootPath, scan.files);
173
- const threshold = Math.max(0, Math.min(100, parseInt(cmdOpts.minScore, 10) || 70));
235
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
236
+ let issues = await collectIssues(rootPath, scan.files);
237
+ issues = applyConfigToIssues(issues, config);
238
+ if (cmdOpts.changedOnly) {
239
+ issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
240
+ }
241
+ const rawThreshold = cmdOpts.minScore ?? config.minScore ?? 70;
242
+ const threshold = Math.max(0, Math.min(100, typeof rawThreshold === 'string' ? parseInt(rawThreshold, 10) || 70 : rawThreshold));
174
243
  const { score } = calculateScore(issues);
175
244
  switch (format) {
176
245
  case 'json':
@@ -179,6 +248,9 @@ program
179
248
  case 'markdown':
180
249
  reportCiMarkdown(issues, threshold);
181
250
  break;
251
+ case 'sarif':
252
+ reportCiSarif(issues, pkg.version);
253
+ break;
182
254
  default:
183
255
  reportCi(issues, threshold);
184
256
  }
@@ -202,15 +274,24 @@ program
202
274
  maybeCompactBanner();
203
275
  const rootPath = getRootPath();
204
276
  const format = getFormat();
277
+ const config = await loadProjectConfig();
205
278
  try {
206
- const scan = await scanRepository(rootPath);
207
- const issues = await collectIssues(rootPath, scan.files);
279
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
280
+ let issues = await collectIssues(rootPath, scan.files);
281
+ issues = applyConfigToIssues(issues, config);
282
+ const hotspotReport = await analyzeHotspots(rootPath, scan.files, issues, { limit: 20 });
208
283
  if (cmdOpts.saveBaseline) {
209
- const filePath = await saveBaseline(rootPath, issues);
284
+ const filePath = await saveBaseline(rootPath, issues, hotspotReport);
210
285
  const { score, grade } = calculateScore(issues);
211
286
  console.log(chalk.green(`\n Baseline saved to ${filePath}`));
212
287
  console.log(` Score: ${chalk.bold(`${grade} (${score}/100)`)}`);
213
- console.log(` Issues: ${issues.length}\n`);
288
+ console.log(` Issues: ${issues.length}`);
289
+ if (hotspotReport.available) {
290
+ console.log(` Hotspots snapshotted: ${hotspotReport.hotspots.length}\n`);
291
+ }
292
+ else {
293
+ console.log('');
294
+ }
214
295
  return;
215
296
  }
216
297
  let baseline;
@@ -222,7 +303,7 @@ program
222
303
  console.error(` Run ${chalk.bold.cyan('projscan diff --save-baseline')} first to create one.\n`);
223
304
  process.exit(1);
224
305
  }
225
- const diff = computeDiff(baseline, issues);
306
+ const diff = computeDiff(baseline, issues, hotspotReport);
226
307
  switch (format) {
227
308
  case 'json':
228
309
  reportDiffJson(diff);
@@ -249,9 +330,11 @@ program
249
330
  maybeCompactBanner();
250
331
  const rootPath = getRootPath();
251
332
  const spinner = ora('Detecting issues...').start();
333
+ const config = await loadProjectConfig();
252
334
  try {
253
- const scan = await scanRepository(rootPath);
254
- const issues = await collectIssues(rootPath, scan.files);
335
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
336
+ let issues = await collectIssues(rootPath, scan.files);
337
+ issues = applyConfigToIssues(issues, config);
255
338
  const fixes = getAllAvailableFixes(issues);
256
339
  spinner.stop();
257
340
  if (fixes.length === 0) {
@@ -299,6 +382,42 @@ program
299
382
  process.exit(1);
300
383
  }
301
384
  });
385
+ // ── Command: file ─────────────────────────────────────────
386
+ program
387
+ .command('file <file>')
388
+ .description('Drill into a file — purpose, risk, ownership, related issues')
389
+ .action(async (filePath) => {
390
+ setupLogLevel();
391
+ maybeCompactBanner();
392
+ const rootPath = getRootPath();
393
+ const format = getFormat();
394
+ const spinner = format === 'console' ? ora('Inspecting file...').start() : null;
395
+ try {
396
+ const inspection = await inspectFile(rootPath, filePath);
397
+ if (spinner)
398
+ spinner.stop();
399
+ if (!inspection.exists) {
400
+ console.error(chalk.red(`\n ${inspection.reason ?? 'File unavailable'}: ${filePath}\n`));
401
+ process.exit(1);
402
+ }
403
+ switch (format) {
404
+ case 'json':
405
+ reportFileJson(inspection);
406
+ break;
407
+ case 'markdown':
408
+ reportFileMarkdown(inspection);
409
+ break;
410
+ default:
411
+ reportFileInspection(inspection);
412
+ }
413
+ }
414
+ catch (error) {
415
+ if (spinner)
416
+ spinner.fail('File inspection failed');
417
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
418
+ process.exit(1);
419
+ }
420
+ });
302
421
  // ── Command: explain ──────────────────────────────────────
303
422
  program
304
423
  .command('explain <file>')
@@ -341,9 +460,10 @@ program
341
460
  maybeCompactBanner();
342
461
  const rootPath = getRootPath();
343
462
  const format = getFormat();
463
+ const config = await loadProjectConfig();
344
464
  const spinner = format === 'console' ? ora('Analyzing architecture...').start() : null;
345
465
  try {
346
- const scan = await scanRepository(rootPath);
466
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
347
467
  const frameworks = await detectFrameworks(rootPath, scan.files);
348
468
  const layers = buildArchitectureLayers(scan.files, frameworks.frameworks.map((f) => f.name));
349
469
  if (spinner)
@@ -375,9 +495,10 @@ program
375
495
  maybeCompactBanner();
376
496
  const rootPath = getRootPath();
377
497
  const format = getFormat();
498
+ const config = await loadProjectConfig();
378
499
  const spinner = format === 'console' ? ora('Scanning...').start() : null;
379
500
  try {
380
- const scan = await scanRepository(rootPath);
501
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
381
502
  if (spinner)
382
503
  spinner.stop();
383
504
  switch (format) {
@@ -434,6 +555,65 @@ program
434
555
  process.exit(1);
435
556
  }
436
557
  });
558
+ // ── Command: hotspots ─────────────────────────────────────
559
+ program
560
+ .command('hotspots')
561
+ .description('Rank files by risk (git churn × complexity × open issues)')
562
+ .option('--limit <n>', 'number of hotspots to show')
563
+ .option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")')
564
+ .action(async (cmdOpts) => {
565
+ setupLogLevel();
566
+ maybeCompactBanner();
567
+ const rootPath = getRootPath();
568
+ const format = getFormat();
569
+ const config = await loadProjectConfig();
570
+ const spinner = format === 'console' ? ora('Analyzing hotspots...').start() : null;
571
+ try {
572
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
573
+ let issues = await collectIssues(rootPath, scan.files);
574
+ issues = applyConfigToIssues(issues, config);
575
+ const limitRaw = cmdOpts.limit ?? config.hotspots?.limit ?? 10;
576
+ const limit = Math.max(1, Math.min(100, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 10 : limitRaw));
577
+ const since = cmdOpts.since ?? config.hotspots?.since ?? '12 months ago';
578
+ const report = await analyzeHotspots(rootPath, scan.files, issues, {
579
+ since,
580
+ limit,
581
+ });
582
+ if (spinner)
583
+ spinner.stop();
584
+ switch (format) {
585
+ case 'json':
586
+ reportHotspotsJson(report);
587
+ break;
588
+ case 'markdown':
589
+ reportHotspotsMarkdown(report);
590
+ break;
591
+ default:
592
+ reportHotspots(report);
593
+ }
594
+ }
595
+ catch (error) {
596
+ if (spinner)
597
+ spinner.fail('Hotspot analysis failed');
598
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
599
+ process.exit(1);
600
+ }
601
+ });
602
+ // ── Command: mcp ──────────────────────────────────────────
603
+ program
604
+ .command('mcp')
605
+ .description('Run projscan as an MCP server (stdio) for AI coding agents')
606
+ .action(async () => {
607
+ setLogLevel('quiet');
608
+ const rootPath = getRootPath();
609
+ try {
610
+ await runMcpServer(rootPath);
611
+ }
612
+ catch (error) {
613
+ console.error(chalk.red(error instanceof Error ? error.message : String(error)));
614
+ process.exit(1);
615
+ }
616
+ });
437
617
  // ── Command: badge ────────────────────────────────────────
438
618
  program
439
619
  .command('badge')
@@ -444,9 +624,11 @@ program
444
624
  maybeCompactBanner();
445
625
  const rootPath = getRootPath();
446
626
  const spinner = ora('Calculating health score...').start();
627
+ const config = await loadProjectConfig();
447
628
  try {
448
- const scan = await scanRepository(rootPath);
449
- const issues = await collectIssues(rootPath, scan.files);
629
+ const scan = await scanRepository(rootPath, { ignore: config.ignore });
630
+ let issues = await collectIssues(rootPath, scan.files);
631
+ issues = applyConfigToIssues(issues, config);
450
632
  const { score, grade } = calculateScore(issues);
451
633
  spinner.stop();
452
634
  const gradeColor = grade === 'A' || grade === 'B' ? chalk.green : grade === 'C' ? chalk.yellow : chalk.red;
@@ -473,7 +655,7 @@ function analyzeFile(filePath, content) {
473
655
  const lines = content.split('\n');
474
656
  const imports = extractImports(content);
475
657
  const exports = extractExports(content);
476
- const purpose = inferPurpose(filePath, imports, exports);
658
+ const purpose = inferPurpose(filePath, exports);
477
659
  const potentialIssues = detectFileIssues(content, lines.length);
478
660
  return {
479
661
  filePath: path.relative(process.cwd(), filePath),
@@ -484,143 +666,6 @@ function analyzeFile(filePath, content) {
484
666
  lineCount: lines.length,
485
667
  };
486
668
  }
487
- function extractImports(content) {
488
- const imports = [];
489
- const seen = new Set();
490
- // ES import
491
- const esImportRegex = /import\s+(?:(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/gm;
492
- let match;
493
- while ((match = esImportRegex.exec(content)) !== null) {
494
- const source = match[1];
495
- if (!seen.has(source)) {
496
- seen.add(source);
497
- imports.push({
498
- source,
499
- specifiers: [],
500
- isRelative: source.startsWith('.') || source.startsWith('/'),
501
- });
502
- }
503
- }
504
- // CommonJS require
505
- const requireRegex = /(?:const|let|var)\s+(?:\{[^}]*\}|\w+)\s*=\s*require\(\s*['"]([^'"]+)['"]\s*\)/gm;
506
- while ((match = requireRegex.exec(content)) !== null) {
507
- const source = match[1];
508
- if (!seen.has(source)) {
509
- seen.add(source);
510
- imports.push({
511
- source,
512
- specifiers: [],
513
- isRelative: source.startsWith('.') || source.startsWith('/'),
514
- });
515
- }
516
- }
517
- return imports;
518
- }
519
- function extractExports(content) {
520
- const exports = [];
521
- // export function
522
- const funcRegex = /^export\s+(?:async\s+)?function\s+(\w+)/gm;
523
- let match;
524
- while ((match = funcRegex.exec(content)) !== null) {
525
- exports.push({ name: match[1], type: 'function' });
526
- }
527
- // export class
528
- const classRegex = /^export\s+class\s+(\w+)/gm;
529
- while ((match = classRegex.exec(content)) !== null) {
530
- exports.push({ name: match[1], type: 'class' });
531
- }
532
- // export const/let/var
533
- const varRegex = /^export\s+(?:const|let|var)\s+(\w+)/gm;
534
- while ((match = varRegex.exec(content)) !== null) {
535
- exports.push({ name: match[1], type: 'variable' });
536
- }
537
- // export interface
538
- const interfaceRegex = /^export\s+interface\s+(\w+)/gm;
539
- while ((match = interfaceRegex.exec(content)) !== null) {
540
- exports.push({ name: match[1], type: 'interface' });
541
- }
542
- // export type
543
- const typeRegex = /^export\s+type\s+(\w+)/gm;
544
- while ((match = typeRegex.exec(content)) !== null) {
545
- exports.push({ name: match[1], type: 'type' });
546
- }
547
- // export default
548
- if (/^export\s+default/m.test(content)) {
549
- exports.push({ name: 'default', type: 'default' });
550
- }
551
- return exports;
552
- }
553
- function inferPurpose(filePath, imports, exports) {
554
- const name = path.basename(filePath, path.extname(filePath)).toLowerCase();
555
- const dir = path.dirname(filePath).toLowerCase();
556
- if (name.includes('test') || name.includes('spec'))
557
- return 'Test file';
558
- if (name.includes('config') || name.includes('rc'))
559
- return 'Configuration file';
560
- if (name === 'index')
561
- return 'Module entry point / barrel file';
562
- if (name === 'main' || name === 'app')
563
- return 'Application entry point';
564
- if (name.includes('route') || name.includes('router'))
565
- return 'Route definitions';
566
- if (name.includes('middleware'))
567
- return 'Middleware handler';
568
- if (name.includes('controller'))
569
- return 'Request controller';
570
- if (name.includes('service'))
571
- return 'Service layer logic';
572
- if (name.includes('model') || name.includes('schema'))
573
- return 'Data model / schema definition';
574
- if (name.includes('util') || name.includes('helper'))
575
- return 'Utility functions';
576
- if (name.includes('hook'))
577
- return 'Custom hook';
578
- if (name.includes('context') || name.includes('provider'))
579
- return 'Context / state provider';
580
- if (name.includes('type') || name.includes('interface'))
581
- return 'Type definitions';
582
- if (name.includes('constant') || name.includes('config'))
583
- return 'Constants / configuration';
584
- if (name.includes('migration'))
585
- return 'Database migration';
586
- if (name.includes('seed'))
587
- return 'Database seed data';
588
- if (name.includes('auth'))
589
- return 'Authentication logic';
590
- if (name.includes('api'))
591
- return 'API endpoint handler';
592
- if (dir.includes('component') || dir.includes('pages'))
593
- return 'UI component';
594
- if (dir.includes('service'))
595
- return 'Service module';
596
- if (dir.includes('model'))
597
- return 'Data model';
598
- if (dir.includes('util') || dir.includes('lib'))
599
- return 'Library / utility module';
600
- const exportTypes = exports.map((e) => e.type);
601
- if (exportTypes.includes('class'))
602
- return 'Class-based module';
603
- if (exportTypes.filter((t) => t === 'function').length > 2)
604
- return 'Function library';
605
- return 'Source module';
606
- }
607
- function detectFileIssues(content, lineCount) {
608
- const issues = [];
609
- if (lineCount > 500)
610
- issues.push(`Large file (${lineCount} lines) — consider splitting`);
611
- if (lineCount > 1000)
612
- issues.push('Very large file — strongly consider refactoring');
613
- if (/console\.(log|warn|error|debug)\s*\(/.test(content)) {
614
- issues.push('Contains console.log statements — consider using a proper logger');
615
- }
616
- if (/TODO|FIXME|HACK|XXX/i.test(content)) {
617
- issues.push('Contains TODO/FIXME comments');
618
- }
619
- if (/any\b/.test(content) && /\.tsx?$/.test(content)) {
620
- issues.push('Uses "any" type — consider using proper types');
621
- }
622
- return issues;
623
- }
624
669
  // ── Architecture Layer Detection ──────────────────────────
625
670
  function buildArchitectureLayers(files, frameworkNames) {
626
671
  const layers = [];