@chappibunny/repolens 1.11.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 +42 -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/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
|
@@ -5,6 +5,9 @@ import { inferDataFlows } from "../analyzers/flow-inference.js";
|
|
|
5
5
|
import { analyzeGraphQL } from "../analyzers/graphql-analyzer.js";
|
|
6
6
|
import { analyzeTypeScript } from "../analyzers/typescript-analyzer.js";
|
|
7
7
|
import { analyzeDependencyGraph } from "../analyzers/dependency-graph.js";
|
|
8
|
+
import { analyzeJSDoc } from "../analyzers/jsdoc-analyzer.js";
|
|
9
|
+
import { analyzeSecurityPatterns } from "../analyzers/security-patterns.js";
|
|
10
|
+
import { analyzeComplexity, computeCodeHealth } from "../analyzers/complexity-analyzer.js";
|
|
8
11
|
import { buildSnapshot, loadBaseline, saveBaseline, detectDrift } from "../analyzers/drift-detector.js";
|
|
9
12
|
import { parseCodeowners, buildOwnershipMap } from "../analyzers/codeowners.js";
|
|
10
13
|
import { getActiveDocuments } from "../ai/document-plan.js";
|
|
@@ -25,7 +28,9 @@ import {
|
|
|
25
28
|
renderGraphQLSchema,
|
|
26
29
|
renderTypeGraph,
|
|
27
30
|
renderDependencyGraph,
|
|
28
|
-
renderArchitectureDrift as renderDriftReport
|
|
31
|
+
renderArchitectureDrift as renderDriftReport,
|
|
32
|
+
renderCodeHealth,
|
|
33
|
+
renderSecurityHotspots
|
|
29
34
|
} from "../renderers/renderAnalysis.js";
|
|
30
35
|
import { info, warn } from "../utils/logger.js";
|
|
31
36
|
import path from "node:path";
|
|
@@ -46,9 +51,15 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
46
51
|
let graphqlResult = { detected: false };
|
|
47
52
|
let tsResult = { detected: false };
|
|
48
53
|
let depGraph = { stats: {}, graph: {} };
|
|
54
|
+
let jsdocResult = { detected: false, exports: [], summary: null };
|
|
49
55
|
try { graphqlResult = await analyzeGraphQL(scanFiles, repoRoot); } catch (e) { warn(`GraphQL analysis failed: ${e.message}`); }
|
|
50
56
|
try { tsResult = await analyzeTypeScript(scanFiles, repoRoot); } catch (e) { warn(`TypeScript analysis failed: ${e.message}`); }
|
|
51
57
|
try { depGraph = await analyzeDependencyGraph(scanFiles, repoRoot); } catch (e) { warn(`Dependency graph analysis failed: ${e.message}`); }
|
|
58
|
+
try { jsdocResult = await analyzeJSDoc(scanFiles, repoRoot); } catch (e) { warn(`JSDoc analysis failed: ${e.message}`); }
|
|
59
|
+
let securityResult = { detected: false, findings: [], bySeverity: { high: 0, medium: 0, low: 0 } };
|
|
60
|
+
try { securityResult = await analyzeSecurityPatterns(scanFiles, repoRoot); } catch (e) { warn(`Security pattern analysis failed: ${e.message}`); }
|
|
61
|
+
let complexityResult = { files: [], functions: [], filesAnalyzed: 0 };
|
|
62
|
+
try { complexityResult = await analyzeComplexity(scanFiles, repoRoot); } catch (e) { warn(`Complexity analysis failed: ${e.message}`); }
|
|
52
63
|
|
|
53
64
|
// Architecture drift detection
|
|
54
65
|
const outputDir = path.join(repoRoot, ".repolens");
|
|
@@ -73,6 +84,10 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
73
84
|
const ownershipMap = codeowners.found
|
|
74
85
|
? buildOwnershipMap(scanResult.modules, scanFiles, codeowners.rules)
|
|
75
86
|
: {};
|
|
87
|
+
|
|
88
|
+
// Compute unified code health scores (synthesizes complexity + coupling + docs + security)
|
|
89
|
+
let codeHealthResult = { modules: [], summary: "No health data", stats: { totalFiles: 0, avgScore: 100, avgComplexity: 0, gradeDistribution: { A: 0, B: 0, C: 0, D: 0, F: 0 } }, hotspots: [], topComplexFunctions: [] };
|
|
90
|
+
try { codeHealthResult = computeCodeHealth(complexityResult, depGraph, jsdocResult, securityResult); } catch (e) { warn(`Code health computation failed: ${e.message}`); }
|
|
76
91
|
|
|
77
92
|
// Get active documents based on config
|
|
78
93
|
const activeDocuments = getActiveDocuments(config);
|
|
@@ -86,8 +101,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
86
101
|
flows,
|
|
87
102
|
graphql: graphqlResult.detected ? graphqlResult : undefined,
|
|
88
103
|
typescript: tsResult.detected ? tsResult : undefined,
|
|
104
|
+
jsdoc: jsdocResult.detected ? jsdocResult : undefined,
|
|
89
105
|
dependencyGraph: depGraph.stats,
|
|
90
106
|
drift: driftResult,
|
|
107
|
+
security: securityResult.detected ? securityResult : undefined,
|
|
108
|
+
codeHealth: complexityResult.files.length > 0 ? true : undefined,
|
|
91
109
|
codeowners: codeowners.found ? { file: codeowners.file, ruleCount: codeowners.rules.length } : undefined,
|
|
92
110
|
ownershipMap: Object.keys(ownershipMap).length > 0 ? ownershipMap : undefined,
|
|
93
111
|
};
|
|
@@ -117,8 +135,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
117
135
|
diffData,
|
|
118
136
|
graphqlResult,
|
|
119
137
|
tsResult,
|
|
138
|
+
jsdocResult,
|
|
120
139
|
depGraph,
|
|
121
140
|
driftResult,
|
|
141
|
+
securityResult,
|
|
142
|
+
codeHealthResult,
|
|
122
143
|
ownershipMap,
|
|
123
144
|
pluginManager,
|
|
124
145
|
});
|
|
@@ -167,8 +188,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
167
188
|
diffData,
|
|
168
189
|
graphqlResult,
|
|
169
190
|
tsResult,
|
|
191
|
+
jsdocResult,
|
|
170
192
|
depGraph,
|
|
171
193
|
driftResult,
|
|
194
|
+
securityResult,
|
|
195
|
+
codeHealthResult,
|
|
172
196
|
});
|
|
173
197
|
|
|
174
198
|
documents.push({
|
|
@@ -203,7 +227,7 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
203
227
|
|
|
204
228
|
async function generateDocument(docPlan, context) {
|
|
205
229
|
const { key } = docPlan;
|
|
206
|
-
const { scanResult, config, aiContext, moduleContext, flows, diffData, graphqlResult, tsResult, depGraph, driftResult, ownershipMap, pluginManager } = context;
|
|
230
|
+
const { scanResult, config, aiContext, moduleContext, flows, diffData, graphqlResult, tsResult, jsdocResult, depGraph, driftResult, securityResult, codeHealthResult, ownershipMap, pluginManager } = context;
|
|
207
231
|
|
|
208
232
|
switch (key) {
|
|
209
233
|
case "executive_summary":
|
|
@@ -227,8 +251,8 @@ async function generateDocument(docPlan, context) {
|
|
|
227
251
|
return renderRouteMapOriginal(config, scanResult, aiContext);
|
|
228
252
|
|
|
229
253
|
case "api_surface":
|
|
230
|
-
// Hybrid: deterministic skeleton +
|
|
231
|
-
return renderApiSurfaceOriginal(config, scanResult);
|
|
254
|
+
// Hybrid: deterministic skeleton + JSDoc documentation
|
|
255
|
+
return renderApiSurfaceOriginal(config, scanResult, jsdocResult);
|
|
232
256
|
|
|
233
257
|
case "data_flows":
|
|
234
258
|
return await generateDataFlows(flows, aiContext, { depGraph, scanResult, moduleContext }, config);
|
|
@@ -258,6 +282,12 @@ async function generateDocument(docPlan, context) {
|
|
|
258
282
|
case "architecture_drift":
|
|
259
283
|
return renderDriftReport(driftResult);
|
|
260
284
|
|
|
285
|
+
case "security_hotspots":
|
|
286
|
+
return renderSecurityHotspots(securityResult);
|
|
287
|
+
|
|
288
|
+
case "code_health":
|
|
289
|
+
return renderCodeHealth(codeHealthResult);
|
|
290
|
+
|
|
261
291
|
default: {
|
|
262
292
|
// Check if a plugin provides this renderer
|
|
263
293
|
if (pluginManager) {
|
|
@@ -37,6 +37,8 @@ const PAGE_ORDER = [
|
|
|
37
37
|
"dependency_graph",
|
|
38
38
|
"architecture_drift",
|
|
39
39
|
"arch_diff",
|
|
40
|
+
"security_hotspots",
|
|
41
|
+
"code_health",
|
|
40
42
|
];
|
|
41
43
|
|
|
42
44
|
const PAGE_TITLES = {
|
|
@@ -56,6 +58,8 @@ const PAGE_TITLES = {
|
|
|
56
58
|
dependency_graph: "Dependency Graph",
|
|
57
59
|
architecture_drift: "Architecture Drift",
|
|
58
60
|
arch_diff: "Architecture Diff",
|
|
61
|
+
security_hotspots: "Security Hotspots",
|
|
62
|
+
code_health: "Code Health",
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
const PAGE_DESCRIPTIONS = {
|
|
@@ -75,6 +79,8 @@ const PAGE_DESCRIPTIONS = {
|
|
|
75
79
|
dependency_graph: "Module and package dependency relationships.",
|
|
76
80
|
architecture_drift: "Detected drift between intended and current architecture patterns.",
|
|
77
81
|
arch_diff: "Architecture-level diff across branches or revisions.",
|
|
82
|
+
security_hotspots: "Security anti-patterns detected with CWE classification and severity ratings.",
|
|
83
|
+
code_health: "Cyclomatic complexity analysis with unified health scores per module.",
|
|
78
84
|
};
|
|
79
85
|
|
|
80
86
|
// Audience-based grouping for Home page
|
|
@@ -90,7 +96,7 @@ const AUDIENCE_GROUPS = [
|
|
|
90
96
|
keys: [
|
|
91
97
|
"architecture_overview", "module_catalog", "api_surface",
|
|
92
98
|
"route_map", "system_map", "graphql_schema", "type_graph",
|
|
93
|
-
"dependency_graph", "architecture_drift",
|
|
99
|
+
"dependency_graph", "architecture_drift", "security_hotspots", "code_health",
|
|
94
100
|
],
|
|
95
101
|
},
|
|
96
102
|
{
|
|
@@ -120,6 +126,7 @@ const SIDEBAR_GROUPS = [
|
|
|
120
126
|
"architecture_overview", "module_catalog", "route_map",
|
|
121
127
|
"api_surface", "system_map", "graphql_schema", "type_graph",
|
|
122
128
|
"dependency_graph", "architecture_drift", "arch_diff", "change_impact",
|
|
129
|
+
"security_hotspots", "code_health",
|
|
123
130
|
],
|
|
124
131
|
},
|
|
125
132
|
];
|
|
@@ -142,6 +149,8 @@ const PAGE_AUDIENCE = {
|
|
|
142
149
|
dependency_graph: "Engineers",
|
|
143
150
|
architecture_drift: "Engineers ยท Tech Leads",
|
|
144
151
|
arch_diff: "Engineers ยท Tech Leads",
|
|
152
|
+
security_hotspots: "Engineers ยท Security",
|
|
153
|
+
code_health: "All Audiences",
|
|
145
154
|
};
|
|
146
155
|
|
|
147
156
|
/**
|
|
@@ -22,7 +22,9 @@ function pageFileName(key) {
|
|
|
22
22
|
graphql_schema: "graphql_schema.md",
|
|
23
23
|
type_graph: "type_graph.md",
|
|
24
24
|
dependency_graph: "dependency_graph.md",
|
|
25
|
-
architecture_drift: "architecture_drift.md"
|
|
25
|
+
architecture_drift: "architecture_drift.md",
|
|
26
|
+
security_hotspots: "security_hotspots.md",
|
|
27
|
+
code_health: "code_health.md"
|
|
26
28
|
};
|
|
27
29
|
|
|
28
30
|
return mapping[key] || `${key}.md`;
|
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",
|