@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 CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.12.0
6
+
7
+ ### 🛡️ Security Pattern Detection
8
+
9
+ New static analysis for security anti-patterns with CWE classification:
10
+
11
+ - **16 anti-patterns** across 8 categories: Code Injection (eval), XSS (innerHTML), SQL Injection, Command Injection, Path Traversal, Prototype Pollution, Hardcoded Credentials, Insecure Randomness, ReDoS
12
+ - Each finding includes severity (high/medium/low), file location, code snippet, and CWE ID
13
+ - New renderer: `renderSecurityHotspots()` with severity breakdown, category grouping, and remediation guidance
14
+ - New document type: **Security Hotspots** (`security_hotspots`)
15
+ - Skips test files, build outputs, configs, `.min.js`, `.d.ts` to reduce noise
16
+ - 30 new tests in `security-patterns.test.js`
17
+
18
+ ### 📊 Complexity & Code Health Scoring
19
+
20
+ Cyclomatic complexity analysis with unified health scoring:
21
+
22
+ - **Per-file and per-function complexity**: Counts if/else/for/while/case/catch/&&/||/??/ternary with comment and string stripping
23
+ - **Unified health score** (0–100, A–F grades) per module synthesizing:
24
+ - Cyclomatic complexity penalties
25
+ - Coupling metrics (fan-in × fan-out from dependency graph)
26
+ - JSDoc documentation coverage
27
+ - Security findings severity
28
+ - New renderer: `renderCodeHealth()` with grade distribution, hotspot table, top complex functions, and scoring methodology
29
+ - New document type: **Code Health** (`code_health`)
30
+ - 41 new tests in `code-health.test.js`
31
+
32
+ ### 📝 JSDoc/TSDoc Extraction
33
+
34
+ API Surface enrichment with documentation extraction:
35
+
36
+ - Parses `@param`, `@returns`, `@deprecated`, `@example`, `@throws`, `@since` tags
37
+ - Enriches API Surface document with parameter types, descriptions, and deprecation notices
38
+ - Coverage reporting (e.g., "89/167 exports documented (53%)")
39
+ - 16 new tests in `jsdoc-analyzer.test.js`
40
+
41
+ ### 📈 Numbers
42
+
43
+ - **17 document types** (up from 15): added Security Hotspots + Code Health
44
+ - **480 tests** passing across 25 test files (up from 393 across 22)
45
+ - **12 analyzers** in the pipeline
46
+
5
47
  ## 1.11.0
6
48
 
7
49
  ### 🧙 Smart URL Parsing in Wizard
package/README.md CHANGED
@@ -10,14 +10,14 @@
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/@chappibunny/repolens)](https://www.npmjs.com/package/@chappibunny/repolens)
12
12
  [![VS Code Extension](https://img.shields.io/visual-studio-marketplace/v/CHAPIBUNNY.repolens-architecture?label=VS%20Code)](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture)
13
- [![Tests](https://img.shields.io/badge/tests-380%20passing-brightgreen)](https://github.com/CHAPIBUNNY/repolens/actions)
13
+ [![Tests](https://img.shields.io/badge/tests-480%20passing-brightgreen)](https://github.com/CHAPIBUNNY/repolens/actions)
14
14
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
15
15
 
16
16
  **Your architecture docs are already outdated.** RepoLens fixes that.
17
17
 
18
18
  RepoLens scans your repository, generates living architecture documentation, and publishes it to Notion, Confluence, GitHub Wiki, or Markdown — automatically on every push. Engineers get technical docs. Stakeholders get readable system overviews. Nobody writes a word.
19
19
 
20
- > Stable as of v1.0 — [API guarantees](docs/STABILITY.md) · [Security hardened](SECURITY.md) · v1.9.12
20
+ > Stable as of v1.0 — [API guarantees](docs/STABILITY.md) · [Security hardened](SECURITY.md) · v1.12.0
21
21
 
22
22
  ---
23
23
 
@@ -92,13 +92,13 @@ Run `npx @chappibunny/repolens migrate` to automatically update your workflow fi
92
92
 
93
93
  ## 📋 What It Generates
94
94
 
95
- **15 document types** for three audiences — no manual writing required:
95
+ **17 document types** for three audiences — no manual writing required:
96
96
 
97
97
  | Audience | Documents |
98
98
  |---|---|
99
99
  | **Stakeholders** (founders, PMs, ops) | Executive Summary · Business Domains · Data Flows |
100
- | **Everyone** | System Overview · Developer Onboarding · Change Impact · Architecture Drift |
101
- | **Engineers** | Architecture Overview · Module Catalog · API Surface · Route Map · System Map · GraphQL Schema · TypeScript Type Graph · Dependency Graph |
100
+ | **Everyone** | System Overview · Developer Onboarding · Change Impact · Architecture Drift · Code Health |
101
+ | **Engineers** | Architecture Overview · Module Catalog · API Surface · Route Map · System Map · GraphQL Schema · TypeScript Type Graph · Dependency Graph · Security Hotspots |
102
102
 
103
103
  **Two modes:** Deterministic (free, fast, always works) or AI-Enhanced (optional — GitHub Models, OpenAI, Anthropic, Google, Azure, Ollama).
104
104
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.11.0",
3
+ "version": "1.12.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -120,6 +120,22 @@ export const DOCUMENT_PLAN = [
120
120
  audience: "mixed",
121
121
  ai: false,
122
122
  description: "Structural changes compared to baseline snapshot"
123
+ },
124
+ {
125
+ key: "security_hotspots",
126
+ filename: "15-security-hotspots.md",
127
+ title: "Security Hotspots",
128
+ audience: "technical",
129
+ ai: false,
130
+ description: "Security anti-pattern detection with CWE classification"
131
+ },
132
+ {
133
+ key: "code_health",
134
+ filename: "16-code-health.md",
135
+ title: "Code Health Report",
136
+ audience: "mixed",
137
+ ai: false,
138
+ description: "Per-module health scores from complexity, coupling, docs, and security"
123
139
  }
124
140
  ];
125
141
 
@@ -94,7 +94,7 @@ export async function generateExecutiveSummary(context, enrichment = {}, config)
94
94
  return generateWithStructuredFallback(
95
95
  "executive_summary",
96
96
  createExecutiveSummaryPrompt(context),
97
- 1500,
97
+ 3000,
98
98
  () => getFallbackExecutiveSummary(context, enrichment),
99
99
  config,
100
100
  );
@@ -104,7 +104,7 @@ export async function generateSystemOverview(context, enrichment = {}, config) {
104
104
  return generateWithStructuredFallback(
105
105
  "system_overview",
106
106
  createSystemOverviewPrompt(context),
107
- 1200,
107
+ 2500,
108
108
  () => getFallbackSystemOverview(context, enrichment),
109
109
  config,
110
110
  );
@@ -114,7 +114,7 @@ export async function generateBusinessDomains(context, enrichment = {}, config)
114
114
  return generateWithStructuredFallback(
115
115
  "business_domains",
116
116
  createBusinessDomainsPrompt(context),
117
- 2000,
117
+ 3500,
118
118
  () => getFallbackBusinessDomains(context, enrichment),
119
119
  config,
120
120
  );
@@ -124,7 +124,7 @@ export async function generateArchitectureOverview(context, enrichment = {}, con
124
124
  return generateWithStructuredFallback(
125
125
  "architecture_overview",
126
126
  createArchitectureOverviewPrompt(context),
127
- 1800,
127
+ 3500,
128
128
  () => getFallbackArchitectureOverview(context, enrichment),
129
129
  config,
130
130
  );
@@ -134,7 +134,7 @@ export async function generateDataFlows(flows, context, enrichment = {}, config)
134
134
  return generateWithStructuredFallback(
135
135
  "data_flows",
136
136
  createDataFlowsPrompt(flows, context),
137
- 1800,
137
+ 3500,
138
138
  () => getFallbackDataFlows(flows, context, enrichment),
139
139
  config,
140
140
  );
@@ -144,7 +144,7 @@ export async function generateDeveloperOnboarding(context, enrichment = {}, conf
144
144
  return generateWithStructuredFallback(
145
145
  "developer_onboarding",
146
146
  createDeveloperOnboardingPrompt(context),
147
- 2200,
147
+ 4000,
148
148
  () => getFallbackDeveloperOnboarding(context, enrichment),
149
149
  config,
150
150
  );
@@ -4,7 +4,7 @@ import { warn, info } from "../utils/logger.js";
4
4
  import { executeAIRequest } from "../utils/rate-limit.js";
5
5
 
6
6
  const DEFAULT_TIMEOUT_MS = 60000;
7
- const DEFAULT_MAX_TOKENS = 2500;
7
+ const DEFAULT_MAX_TOKENS = 4000;
8
8
 
9
9
  /**
10
10
  * AI Provider Presets - one env var to configure common providers.
@@ -245,8 +245,16 @@ async function callOpenAICompatibleAPI({ baseUrl, apiKey, model, system, user, t
245
245
  { role: "system", content: system },
246
246
  { role: "user", content: user }
247
247
  ],
248
- max_completion_tokens: maxTokens
249
248
  };
249
+
250
+ // GPT-5+ and o-series models require max_completion_tokens; older models use max_tokens
251
+ const usesNewTokenParam = /^(gpt-5|gpt-4\.1|o[34]-|o3)/.test(model);
252
+ if (usesNewTokenParam) {
253
+ body.max_completion_tokens = maxTokens;
254
+ } else {
255
+ body.max_tokens = maxTokens;
256
+ }
257
+
250
258
  // Only send temperature when explicitly configured — some models
251
259
  // (e.g. gpt-5-mini) reject any non-default value
252
260
  if (temperature != null) {
@@ -279,7 +287,23 @@ async function callOpenAICompatibleAPI({ baseUrl, apiKey, model, system, user, t
279
287
  throw new Error("No completion returned from API");
280
288
  }
281
289
 
282
- return data.choices[0].message.content;
290
+ const choice = data.choices[0];
291
+ const content = choice.message?.content;
292
+ const finishReason = choice.finish_reason || "unknown";
293
+
294
+ // Handle truncation - warn with actionable advice
295
+ if (finishReason === "length") {
296
+ if (!content || content.trim().length === 0) {
297
+ warn(`AI response truncated (hit ${maxTokens} token limit). Increase REPOLENS_AI_MAX_TOKENS or the per-document limit.`);
298
+ } else {
299
+ warn(`AI response may be incomplete (finish_reason: length). Consider increasing token limits.`);
300
+ }
301
+ } else if (!content || content.trim().length === 0) {
302
+ warn(`AI response empty (finish_reason: ${finishReason})`);
303
+ }
304
+
305
+ // Handle null/undefined content (some models return null in certain cases)
306
+ return content ?? "";
283
307
 
284
308
  } catch (error) {
285
309
  clearTimeout(timeoutId);
@@ -0,0 +1,297 @@
1
+ // Complexity & Code Health analyzer
2
+ // Computes per-file cyclomatic complexity, then synthesizes all analyzers
3
+ // (dep graph, JSDoc, security) into a per-module health score.
4
+
5
+ import fs from "node:fs/promises";
6
+ import path from "node:path";
7
+ import { info } from "../utils/logger.js";
8
+
9
+ const JS_EXTENSIONS = [".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx"];
10
+
11
+ // Skip test files, build outputs, etc.
12
+ const SKIP_PATTERNS = [
13
+ /node_modules\//,
14
+ /\.test\.[jt]sx?$/,
15
+ /\.spec\.[jt]sx?$/,
16
+ /\btest[s]?\//,
17
+ /\bdist\//,
18
+ /\bbuild\//,
19
+ /\.min\.[jt]s$/,
20
+ /\.d\.ts$/,
21
+ /\.repolens\//,
22
+ ];
23
+
24
+ function shouldSkip(filePath) {
25
+ return SKIP_PATTERNS.some(p => p.test(filePath));
26
+ }
27
+
28
+ /**
29
+ * Count cyclomatic complexity for a source string.
30
+ * Each decision point adds 1 to the baseline of 1.
31
+ */
32
+ export function computeComplexity(source) {
33
+ // Strip comments to avoid false positives
34
+ let stripped = source.replace(/\/\*[\s\S]*?\*\//g, m => m.replace(/[^\n]/g, " "));
35
+ stripped = stripped.replace(/(?<!:)\/\/.*$/gm, "");
36
+ // Strip string literals to avoid matching keywords inside strings
37
+ stripped = stripped.replace(/`[^`]*`/g, '""');
38
+ stripped = stripped.replace(/"(?:[^"\\]|\\.)*"/g, '""');
39
+ stripped = stripped.replace(/'(?:[^'\\]|\\.)*'/g, "''");
40
+
41
+ let complexity = 1; // baseline
42
+
43
+ // Decision point patterns
44
+ const patterns = [
45
+ /\bif\s*\(/g,
46
+ /\belse\s+if\s*\(/g,
47
+ /\bfor\s*\(/g,
48
+ /\bwhile\s*\(/g,
49
+ /\bcase\s+/g,
50
+ /\bcatch\s*\(/g,
51
+ /(?<!\?)\?\s*(?![.?])/g, // ternary ? but not optional chaining ?. or nullish ??
52
+ /&&/g,
53
+ /\|\|/g,
54
+ /\?\?/g, // nullish coalescing
55
+ ];
56
+
57
+ for (const pattern of patterns) {
58
+ const matches = stripped.match(pattern);
59
+ if (matches) complexity += matches.length;
60
+ }
61
+
62
+ // Subtract double-counted else-if (already counted by if)
63
+ const elseIfMatches = stripped.match(/\belse\s+if\s*\(/g);
64
+ if (elseIfMatches) complexity -= elseIfMatches.length;
65
+
66
+ return complexity;
67
+ }
68
+
69
+ /**
70
+ * Extract function-level complexity from source code.
71
+ * Returns per-function complexity for the top-level functions.
72
+ */
73
+ export function extractFunctions(source) {
74
+ const functions = [];
75
+ const lines = source.split("\n");
76
+
77
+ // Match exported/declared function signatures
78
+ const funcPattern = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g;
79
+ const arrowPattern = /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|\w+)\s*=>/g;
80
+
81
+ let match;
82
+
83
+ while ((match = funcPattern.exec(source)) !== null) {
84
+ const startLine = source.slice(0, match.index).split("\n").length;
85
+ functions.push({ name: match[1], line: startLine, index: match.index });
86
+ }
87
+
88
+ while ((match = arrowPattern.exec(source)) !== null) {
89
+ const startLine = source.slice(0, match.index).split("\n").length;
90
+ functions.push({ name: match[1], line: startLine, index: match.index });
91
+ }
92
+
93
+ // Sort by position in file
94
+ functions.sort((a, b) => a.index - b.index);
95
+
96
+ // Compute complexity for each function's body (approximation: from start to next function)
97
+ const results = [];
98
+ for (let i = 0; i < functions.length; i++) {
99
+ const start = functions[i].index;
100
+ const end = i + 1 < functions.length ? functions[i + 1].index : source.length;
101
+ const body = source.slice(start, end);
102
+ const complexity = computeComplexity(body);
103
+ results.push({
104
+ name: functions[i].name,
105
+ line: functions[i].line,
106
+ complexity,
107
+ });
108
+ }
109
+
110
+ return results;
111
+ }
112
+
113
+ /**
114
+ * Analyze complexity for all JS/TS files in the project.
115
+ * @param {string[]} files - File paths relative to repoRoot
116
+ * @param {string} repoRoot - Absolute path to repo root
117
+ * @returns {Promise<Object>} Complexity analysis results
118
+ */
119
+ export async function analyzeComplexity(files, repoRoot) {
120
+ const jsFiles = files
121
+ .filter(f => JS_EXTENSIONS.some(ext => f.endsWith(ext)))
122
+ .filter(f => !shouldSkip(f));
123
+
124
+ const result = {
125
+ files: [],
126
+ functions: [],
127
+ filesAnalyzed: jsFiles.length,
128
+ };
129
+
130
+ for (const file of jsFiles) {
131
+ let content;
132
+ try {
133
+ content = await fs.readFile(path.join(repoRoot, file), "utf8");
134
+ } catch {
135
+ continue;
136
+ }
137
+
138
+ const fileComplexity = computeComplexity(content);
139
+ const lineCount = content.split("\n").length;
140
+ const funcs = extractFunctions(content);
141
+
142
+ result.files.push({
143
+ file,
144
+ complexity: fileComplexity,
145
+ lines: lineCount,
146
+ functions: funcs.length,
147
+ maxFunctionComplexity: funcs.length > 0 ? Math.max(...funcs.map(f => f.complexity)) : fileComplexity,
148
+ });
149
+
150
+ for (const fn of funcs) {
151
+ result.functions.push({ ...fn, file });
152
+ }
153
+ }
154
+
155
+ return result;
156
+ }
157
+
158
+ /**
159
+ * Synthesize signals from all analyzers into per-module health scores.
160
+ *
161
+ * Health score (0–100) is computed from:
162
+ * - Complexity penalty: high cyclomatic complexity
163
+ * - Coupling penalty: high fan-in * fan-out product
164
+ * - Documentation bonus: JSDoc coverage
165
+ * - Security penalty: findings in this file
166
+ *
167
+ * @param {Object} complexityResult - From analyzeComplexity()
168
+ * @param {Object} depGraph - From analyzeDependencyGraph()
169
+ * @param {Object} jsdocResult - From analyzeJSDoc()
170
+ * @param {Object} securityResult - From analyzeSecurityPatterns()
171
+ * @returns {Object} Code health report
172
+ */
173
+ export function computeCodeHealth(complexityResult, depGraph, jsdocResult, securityResult) {
174
+ const modules = [];
175
+
176
+ // Build lookup maps
177
+ const depMap = new Map();
178
+ if (depGraph?.nodes) {
179
+ for (const node of depGraph.nodes) {
180
+ depMap.set(node.file, {
181
+ fanIn: node.importedBy?.length || 0,
182
+ fanOut: node.imports?.length || 0,
183
+ });
184
+ }
185
+ }
186
+
187
+ const securityByFile = new Map();
188
+ if (securityResult?.findings) {
189
+ for (const finding of securityResult.findings) {
190
+ if (!securityByFile.has(finding.file)) securityByFile.set(finding.file, []);
191
+ securityByFile.get(finding.file).push(finding);
192
+ }
193
+ }
194
+
195
+ const jsdocByFile = new Map();
196
+ if (jsdocResult?.byFile) {
197
+ for (const [file, exports] of Object.entries(jsdocResult.byFile)) {
198
+ const total = exports.length;
199
+ const documented = exports.filter(e => e.jsdoc != null).length;
200
+ jsdocByFile.set(file, { total, documented, coverage: total > 0 ? documented / total : 1 });
201
+ }
202
+ }
203
+
204
+ for (const fileInfo of complexityResult.files) {
205
+ const { file, complexity, lines, maxFunctionComplexity } = fileInfo;
206
+
207
+ // Coupling
208
+ const dep = depMap.get(file) || { fanIn: 0, fanOut: 0 };
209
+ const coupling = dep.fanIn * dep.fanOut;
210
+
211
+ // Documentation coverage (default 100% if not tracked)
212
+ const docInfo = jsdocByFile.get(file) || { total: 0, documented: 0, coverage: 1 };
213
+
214
+ // Security findings
215
+ const findings = securityByFile.get(file) || [];
216
+ const highFindings = findings.filter(f => f.severity === "high").length;
217
+ const mediumFindings = findings.filter(f => f.severity === "medium").length;
218
+
219
+ // Score calculation (0–100, higher is healthier)
220
+ let score = 100;
221
+
222
+ // Complexity penalty: -1 per complexity point above 10, -2 above 30
223
+ if (complexity > 10) score -= Math.min(25, (complexity - 10) * 1);
224
+ if (complexity > 30) score -= Math.min(15, (complexity - 30) * 2);
225
+
226
+ // Max function complexity penalty: -2 per point above 8
227
+ if (maxFunctionComplexity > 8) score -= Math.min(15, (maxFunctionComplexity - 8) * 2);
228
+
229
+ // Coupling penalty: penalize files with high fan-in AND fan-out
230
+ if (coupling > 20) score -= Math.min(15, Math.floor((coupling - 20) / 5));
231
+
232
+ // Documentation bonus/penalty
233
+ if (docInfo.total > 0) {
234
+ if (docInfo.coverage < 0.5) score -= 10;
235
+ else if (docInfo.coverage < 0.8) score -= 5;
236
+ }
237
+
238
+ // Security penalty
239
+ score -= highFindings * 10;
240
+ score -= mediumFindings * 5;
241
+
242
+ score = Math.max(0, Math.min(100, score));
243
+
244
+ const grade = score >= 80 ? "A" : score >= 60 ? "B" : score >= 40 ? "C" : score >= 20 ? "D" : "F";
245
+
246
+ modules.push({
247
+ file,
248
+ score,
249
+ grade,
250
+ lines,
251
+ complexity,
252
+ maxFunctionComplexity,
253
+ fanIn: dep.fanIn,
254
+ fanOut: dep.fanOut,
255
+ coupling,
256
+ docCoverage: docInfo.total > 0 ? Math.round(docInfo.coverage * 100) : null,
257
+ securityFindings: findings.length,
258
+ highSecurityFindings: highFindings,
259
+ });
260
+ }
261
+
262
+ // Sort by score ascending (worst first)
263
+ modules.sort((a, b) => a.score - b.score);
264
+
265
+ // Aggregate stats
266
+ const totalFiles = modules.length;
267
+ const avgScore = totalFiles > 0 ? Math.round(modules.reduce((s, m) => s + m.score, 0) / totalFiles) : 100;
268
+ const avgComplexity = totalFiles > 0 ? Math.round(modules.reduce((s, m) => s + m.complexity, 0) / totalFiles) : 0;
269
+
270
+ const gradeDistribution = { A: 0, B: 0, C: 0, D: 0, F: 0 };
271
+ for (const m of modules) gradeDistribution[m.grade]++;
272
+
273
+ const topComplexFunctions = [...complexityResult.functions]
274
+ .sort((a, b) => b.complexity - a.complexity)
275
+ .slice(0, 15);
276
+
277
+ const hotspots = modules.filter(m => m.score < 60).slice(0, 20);
278
+
279
+ const summary = totalFiles > 0
280
+ ? `${totalFiles} files analyzed · Average health: ${avgScore}/100 · ${gradeDistribution.A} A, ${gradeDistribution.B} B, ${gradeDistribution.C} C, ${gradeDistribution.D} D, ${gradeDistribution.F} F`
281
+ : "No source files analyzed.";
282
+
283
+ info(`Code health: avg ${avgScore}/100 across ${totalFiles} files (${hotspots.length} hotspots)`);
284
+
285
+ return {
286
+ modules,
287
+ summary,
288
+ stats: {
289
+ totalFiles,
290
+ avgScore,
291
+ avgComplexity,
292
+ gradeDistribution,
293
+ },
294
+ hotspots,
295
+ topComplexFunctions,
296
+ };
297
+ }