@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.
@@ -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 + AI enhancement (for now, just deterministic)
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`;
@@ -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",