@chappibunny/repolens 1.10.0 → 1.12.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.
@@ -215,7 +215,7 @@ export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = nul
215
215
  return lines.join("\n");
216
216
  }
217
217
 
218
- export function renderApiSurface(cfg, scan) {
218
+ export function renderApiSurface(cfg, scan, jsdocResult = null) {
219
219
  const lines = [
220
220
  `# API Surface`,
221
221
  ``,
@@ -286,6 +286,101 @@ export function renderApiSurface(cfg, scan) {
286
286
  }
287
287
  }
288
288
 
289
+ // Section 3: Documented Exports (from JSDoc analysis)
290
+ if (jsdocResult && jsdocResult.detected && jsdocResult.exports.length > 0) {
291
+ lines.push(
292
+ `---`,
293
+ ``,
294
+ `## Documented Exports`,
295
+ ``,
296
+ `Exported functions with JSDoc/TSDoc documentation. Documentation coverage: **${jsdocResult.summary?.coverage || "N/A"}**`,
297
+ ``
298
+ );
299
+
300
+ // Show deprecated functions first as warnings
301
+ if (jsdocResult.deprecated && jsdocResult.deprecated.length > 0) {
302
+ lines.push(`### ⚠️ Deprecated Functions`, ``);
303
+ for (const dep of jsdocResult.deprecated) {
304
+ lines.push(`- \`${dep.name}\` in \`${dep.source}\`: ${dep.reason}`);
305
+ }
306
+ lines.push(``);
307
+ }
308
+
309
+ // Group by file and show documented exports
310
+ const documentedExports = jsdocResult.exports.filter(e => e.jsdoc);
311
+ if (documentedExports.length > 0) {
312
+ lines.push(`### Function Documentation`, ``);
313
+
314
+ // Group by file for better organization
315
+ const byFile = {};
316
+ for (const exp of documentedExports) {
317
+ if (!byFile[exp.source]) byFile[exp.source] = [];
318
+ byFile[exp.source].push(exp);
319
+ }
320
+
321
+ // Show up to 30 documented functions to avoid overwhelming output
322
+ let shown = 0;
323
+ const maxToShow = 30;
324
+
325
+ for (const [file, exports] of Object.entries(byFile)) {
326
+ if (shown >= maxToShow) break;
327
+
328
+ lines.push(`#### \`${file}\``, ``);
329
+
330
+ for (const exp of exports) {
331
+ if (shown >= maxToShow) break;
332
+ shown++;
333
+
334
+ const jsdoc = exp.jsdoc;
335
+ const isDeprecated = jsdoc.deprecated ? " ⚠️" : "";
336
+
337
+ lines.push(`**\`${exp.name}()\`**${isDeprecated}`, ``);
338
+
339
+ if (jsdoc.description) {
340
+ lines.push(jsdoc.description, ``);
341
+ }
342
+
343
+ if (jsdoc.params.length > 0) {
344
+ lines.push(`**Parameters:**`);
345
+ for (const param of jsdoc.params) {
346
+ const opt = param.optional ? " *(optional)*" : "";
347
+ lines.push(`- \`${param.name}\` (\`${param.type}\`)${opt}${param.description ? `: ${param.description}` : ""}`);
348
+ }
349
+ lines.push(``);
350
+ }
351
+
352
+ if (jsdoc.returns) {
353
+ lines.push(`**Returns:** \`${jsdoc.returns.type}\`${jsdoc.returns.description ? ` — ${jsdoc.returns.description}` : ""}`, ``);
354
+ }
355
+
356
+ if (jsdoc.throws.length > 0) {
357
+ lines.push(`**Throws:**`);
358
+ for (const t of jsdoc.throws) {
359
+ lines.push(`- ${t}`);
360
+ }
361
+ lines.push(``);
362
+ }
363
+ }
364
+ }
365
+
366
+ if (documentedExports.length > maxToShow) {
367
+ lines.push(``, `> *Showing ${maxToShow} of ${documentedExports.length} documented exports.*`, ``);
368
+ }
369
+ }
370
+
371
+ // Summary stats
372
+ lines.push(
373
+ `---`,
374
+ ``,
375
+ `**Documentation Summary:**`,
376
+ `- Total exports: ${jsdocResult.summary?.totalExports || 0}`,
377
+ `- Documented: ${jsdocResult.summary?.documented || 0}`,
378
+ `- Undocumented: ${jsdocResult.summary?.undocumented || 0}`,
379
+ `- Coverage: ${jsdocResult.summary?.coverage || "0%"}`,
380
+ ``
381
+ );
382
+ }
383
+
289
384
  lines.push(
290
385
  `---`,
291
386
  ``,
@@ -494,6 +494,190 @@ export function renderArchitectureDrift(driftResult) {
494
494
  return lines.join("\n");
495
495
  }
496
496
 
497
+ export function renderCodeHealth(healthResult) {
498
+ if (!healthResult?.modules?.length) {
499
+ return [
500
+ "# Code Health Report",
501
+ "",
502
+ "> No source files were available for health analysis.",
503
+ "",
504
+ "RepoLens computes a health score (0–100) per module by synthesizing cyclomatic complexity, coupling (fan-in × fan-out), documentation coverage, and security findings. Ensure your `scan.include` patterns cover the relevant source directories.",
505
+ ""
506
+ ].join("\n");
507
+ }
508
+
509
+ const lines = [];
510
+ const { stats, hotspots, topComplexFunctions, modules } = healthResult;
511
+
512
+ lines.push("# Code Health Report");
513
+ lines.push("");
514
+ lines.push(`> ${healthResult.summary}`);
515
+ lines.push("");
516
+ lines.push("This report synthesizes complexity, coupling, documentation, and security signals into a single health score per module. Lower scores indicate higher risk and maintenance burden.");
517
+ lines.push("");
518
+
519
+ // Overall stats
520
+ lines.push("## Overview");
521
+ lines.push("");
522
+ lines.push("| Metric | Value |");
523
+ lines.push("|--------|-------|");
524
+ lines.push(`| Files analyzed | ${stats.totalFiles} |`);
525
+ lines.push(`| Average health score | ${stats.avgScore}/100 |`);
526
+ lines.push(`| Average complexity | ${stats.avgComplexity} |`);
527
+ lines.push(`| Hotspots (score < 60) | ${hotspots.length} |`);
528
+ lines.push("");
529
+
530
+ // Grade distribution
531
+ lines.push("## Grade Distribution");
532
+ lines.push("");
533
+ lines.push("| Grade | Count | Meaning |");
534
+ lines.push("|-------|-------|---------|");
535
+ lines.push(`| 🟢 A (80–100) | ${stats.gradeDistribution.A} | Healthy — low complexity, well-documented |`);
536
+ lines.push(`| 🔵 B (60–79) | ${stats.gradeDistribution.B} | Acceptable — minor improvements possible |`);
537
+ lines.push(`| 🟡 C (40–59) | ${stats.gradeDistribution.C} | Needs attention — elevated complexity or coupling |`);
538
+ lines.push(`| 🟠 D (20–39) | ${stats.gradeDistribution.D} | At risk — multiple quality signals degraded |`);
539
+ lines.push(`| 🔴 F (0–19) | ${stats.gradeDistribution.F} | Critical — immediate refactoring recommended |`);
540
+ lines.push("");
541
+
542
+ // Hotspots (worst modules)
543
+ if (hotspots.length > 0) {
544
+ lines.push("## Hotspots — Priority Refactoring Targets");
545
+ lines.push("");
546
+ lines.push("These modules have the lowest health scores and should be prioritized for improvement:");
547
+ lines.push("");
548
+ lines.push("| Module | Score | Grade | Complexity | Coupling | Doc% | Security |");
549
+ lines.push("|--------|-------|-------|------------|----------|------|----------|");
550
+ for (const m of hotspots) {
551
+ const gradeIcon = m.grade === "C" ? "🟡" : m.grade === "D" ? "🟠" : "🔴";
552
+ const docCol = m.docCoverage !== null ? `${m.docCoverage}%` : "—";
553
+ const secCol = m.securityFindings > 0 ? `${m.securityFindings} (${m.highSecurityFindings} high)` : "—";
554
+ lines.push(`| \`${m.file}\` | ${m.score} | ${gradeIcon} ${m.grade} | ${m.complexity} | ${m.fanIn}↓ ${m.fanOut}↑ | ${docCol} | ${secCol} |`);
555
+ }
556
+ lines.push("");
557
+ }
558
+
559
+ // Most complex functions
560
+ if (topComplexFunctions.length > 0) {
561
+ lines.push("## Most Complex Functions");
562
+ lines.push("");
563
+ lines.push("These functions have the highest cyclomatic complexity and are the most likely sources of bugs:");
564
+ lines.push("");
565
+ lines.push("| Function | File | Line | Complexity |");
566
+ lines.push("|----------|------|------|------------|");
567
+ for (const fn of topComplexFunctions) {
568
+ lines.push(`| \`${fn.name}()\` | \`${fn.file}\` | ${fn.line} | ${fn.complexity} |`);
569
+ }
570
+ lines.push("");
571
+ }
572
+
573
+ // Full module listing (top 50, sorted by score)
574
+ const displayModules = modules.slice(0, 50);
575
+ lines.push("## All Modules by Health Score");
576
+ lines.push("");
577
+ if (modules.length > 50) {
578
+ lines.push(`*Showing 50 of ${modules.length} modules (sorted worst-first)*`);
579
+ lines.push("");
580
+ }
581
+ lines.push("| Module | Score | Grade | Lines | Complexity | Fan-In | Fan-Out |");
582
+ lines.push("|--------|-------|-------|-------|------------|--------|---------|");
583
+ for (const m of displayModules) {
584
+ const gradeIcon = m.grade === "A" ? "🟢" : m.grade === "B" ? "🔵" : m.grade === "C" ? "🟡" : m.grade === "D" ? "🟠" : "🔴";
585
+ lines.push(`| \`${m.file}\` | ${m.score} | ${gradeIcon} ${m.grade} | ${m.lines} | ${m.complexity} | ${m.fanIn} | ${m.fanOut} |`);
586
+ }
587
+ lines.push("");
588
+
589
+ // Scoring methodology
590
+ lines.push("## Scoring Methodology");
591
+ lines.push("");
592
+ lines.push("The health score (0–100) is computed from four dimensions:");
593
+ lines.push("");
594
+ lines.push("| Factor | Weight | How It's Measured |");
595
+ lines.push("|--------|--------|-------------------|");
596
+ lines.push("| Complexity | –1/pt above 10, –2/pt above 30 | Cyclomatic complexity (if/else/for/while/case/&&/\\|\\|/ternary) |");
597
+ lines.push("| Function complexity | –2/pt above 8 per function | Highest complexity function in the file |");
598
+ lines.push("| Coupling | –1/5 units above 20 | Fan-in × Fan-out (import connections) |");
599
+ lines.push("| Documentation | –10 if <50%, –5 if <80% | JSDoc/TSDoc coverage of exports |");
600
+ lines.push("| Security | –10/high, –5/medium finding | Security anti-pattern findings in file |");
601
+ lines.push("");
602
+
603
+ lines.push("---");
604
+ lines.push("");
605
+ lines.push("*Generated by RepoLens code health analysis. Complexity is regex-based and may differ from AST-based tools. Scores are relative indicators, not absolute quality measures.*");
606
+ lines.push("");
607
+
608
+ return lines.join("\n");
609
+ }
610
+
611
+ export function renderSecurityHotspots(secResult) {
612
+ if (!secResult?.detected) {
613
+ return [
614
+ "# Security Hotspots",
615
+ "",
616
+ "> No security anti-patterns were detected in the scanned source files.",
617
+ "",
618
+ "RepoLens scans JavaScript and TypeScript source files for common security risks including code injection (`eval`), XSS (`innerHTML`), SQL injection, command injection, prototype pollution, hardcoded credentials, and insecure randomness.",
619
+ "",
620
+ "Test files, configs, and build outputs are excluded from analysis to reduce false positives.",
621
+ ""
622
+ ].join("\n");
623
+ }
624
+
625
+ const lines = [];
626
+ lines.push("# Security Hotspots");
627
+ lines.push("");
628
+ lines.push(`> ${secResult.summary}`);
629
+ lines.push("");
630
+ lines.push("This report identifies code patterns that are commonly associated with security vulnerabilities. Each finding includes the relevant CWE classification and a recommended remediation.");
631
+ lines.push("");
632
+
633
+ // Severity summary
634
+ lines.push("## Severity Overview");
635
+ lines.push("");
636
+ lines.push("| Severity | Count |");
637
+ lines.push("|----------|-------|");
638
+ if (secResult.bySeverity.high > 0) lines.push(`| 🔴 High | ${secResult.bySeverity.high} |`);
639
+ if (secResult.bySeverity.medium > 0) lines.push(`| 🟡 Medium | ${secResult.bySeverity.medium} |`);
640
+ if (secResult.bySeverity.low > 0) lines.push(`| 🔵 Low | ${secResult.bySeverity.low} |`);
641
+ lines.push("");
642
+ lines.push(`**${secResult.filesScanned}** source files scanned · **${secResult.filesWithFindingsCount || secResult.filesWithFindings?.length || 0}** files with findings`);
643
+ lines.push("");
644
+
645
+ // Findings by category
646
+ const categories = Object.keys(secResult.byCategory);
647
+ for (const category of categories) {
648
+ const findings = secResult.byCategory[category];
649
+ lines.push(`## ${category}`);
650
+ lines.push("");
651
+ // Use description from first finding as category overview
652
+ lines.push(`> ${findings[0].description}`);
653
+ lines.push("");
654
+ lines.push("| File | Line | Pattern | Severity | CWE |");
655
+ lines.push("|------|------|---------|----------|-----|");
656
+ for (const f of findings) {
657
+ const sev = f.severity === "high" ? "🔴 High" : f.severity === "medium" ? "🟡 Medium" : "🔵 Low";
658
+ lines.push(`| \`${f.file}\` | ${f.line} | ${f.name} | ${sev} | ${f.cwe} |`);
659
+ }
660
+ lines.push("");
661
+ }
662
+
663
+ // Affected files list
664
+ if (secResult.filesWithFindings?.length > 0) {
665
+ lines.push("## Affected Files");
666
+ lines.push("");
667
+ for (const file of secResult.filesWithFindings) {
668
+ lines.push(`- \`${file}\``);
669
+ }
670
+ lines.push("");
671
+ }
672
+
673
+ lines.push("---");
674
+ lines.push("");
675
+ lines.push("*Generated by RepoLens security pattern analysis. Detection is regex-based and may produce false positives. Findings should be triaged by a security-aware engineer.*");
676
+ lines.push("");
677
+
678
+ return lines.join("\n");
679
+ }
680
+
497
681
  function formatCategoryLabel(category) {
498
682
  const labels = {
499
683
  modules: "Modules",