@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.
- package/CHANGELOG.md +93 -0
- package/README.md +5 -5
- package/package.json +1 -1
- package/src/ai/document-plan.js +16 -0
- package/src/ai/generate-sections.js +6 -6
- package/src/ai/provider.js +27 -3
- package/src/analyzers/complexity-analyzer.js +297 -0
- package/src/analyzers/jsdoc-analyzer.js +354 -0
- package/src/analyzers/security-patterns.js +329 -0
- package/src/docs/generate-doc-set.js +34 -4
- package/src/init.js +484 -41
- package/src/publishers/github-wiki.js +10 -1
- package/src/publishers/markdown.js +3 -1
- package/src/renderers/render.js +96 -1
- package/src/renderers/renderAnalysis.js +184 -0
package/src/renderers/render.js
CHANGED
|
@@ -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",
|