@chappibunny/repolens 1.16.1 → 1.16.3

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.
@@ -130,7 +130,7 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
130
130
 
131
131
  for (const docPlan of activeDocuments) {
132
132
  docIndex++;
133
- const emptyReason = emptyDocumentReason(docPlan, { config, scanResult, graphqlResult });
133
+ const emptyReason = emptyDocumentReason(docPlan, { config, scanResult, graphqlResult, jsdocResult });
134
134
  if (emptyReason) {
135
135
  info(`[${docIndex}/${totalDocs}] Skipping ${docPlan.key}: ${emptyReason}`);
136
136
  continue;
@@ -244,10 +244,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
244
244
 
245
245
  /**
246
246
  * Documents that would only say "nothing found" are not written. A library has no GraphQL
247
- * schema and no routes, so those two pages would be noise. A document the config lists
247
+ * schema and no routes, so those two pages would be noise, and a CLI or library with no
248
+ * routes, no external API clients, and no documented exports has no API Surface to show. A document the config lists
248
249
  * explicitly in `documentation.sections` is always written.
249
250
  */
250
- export function emptyDocumentReason(docPlan, { config, scanResult, graphqlResult }) {
251
+ export function emptyDocumentReason(docPlan, { config, scanResult, graphqlResult, jsdocResult = null }) {
251
252
  if (config?.documentation?.sections?.includes(docPlan.key)) return null;
252
253
  if (docPlan.key === "graphql_schema" && !graphqlResult?.detected) {
253
254
  return "no GraphQL schema detected";
@@ -255,9 +256,16 @@ export function emptyDocumentReason(docPlan, { config, scanResult, graphqlResult
255
256
  if (docPlan.key === "route_map" && !(scanResult?.pages?.length) && !(scanResult?.api?.length)) {
256
257
  return "no pages or API routes detected";
257
258
  }
259
+ if (docPlan.key === "api_surface" && !(scanResult?.api?.length) && !(scanResult?.externalApis?.length) && !hasDocumentedExports(jsdocResult)) {
260
+ return "no API routes, external API integrations, or documented exports detected";
261
+ }
258
262
  return null;
259
263
  }
260
264
 
265
+ function hasDocumentedExports(jsdocResult) {
266
+ return Boolean(jsdocResult?.detected && Array.isArray(jsdocResult.exports) && jsdocResult.exports.length > 0);
267
+ }
268
+
261
269
  async function generateDocument(docPlan, context) {
262
270
  const { key } = docPlan;
263
271
  const { scanResult, config, aiContext, moduleContext, flows, diffData, graphqlResult, tsResult, jsdocResult, depGraph, driftResult, securityResult, codeHealthResult, ownershipMap, pluginManager } = context;
package/src/help.js CHANGED
@@ -32,6 +32,8 @@ It detects Python, Go, Rust, Java/Kotlin, JavaScript/TypeScript, Ruby, PHP,
32
32
  C#, Swift, and C/C++ (and combinations) and sets scan.include plus
33
33
  module_roots from the files it finds. A repository with no recognized
34
34
  language gets multi-language globs, not JavaScript-only defaults.
35
+ Detection is not full analysis: the dependency graph and code health read
36
+ JavaScript/TypeScript, Python, and Go only.
35
37
 
36
38
  The target directory must already exist.
37
39
 
@@ -129,9 +131,12 @@ Options:
129
131
  --help, -h Show this help
130
132
 
131
133
  With no config, demo detects the repository language and scans matching
132
- files (Python, Go, Rust, Java, JavaScript, and others). It does not publish
133
- to Notion or Confluence. A scan that matches 0 files exits 2 and does not
134
- write empty docs.
134
+ files (Python, Go, Rust, Java, JavaScript, and others) for modules and file
135
+ structure. The dependency graph and code health cover JavaScript/TypeScript,
136
+ Python, and Go only; for other languages (for example Java, Kotlin, or C#)
137
+ those pages say the language is not supported yet instead of showing a
138
+ score. It does not publish to Notion or Confluence. A scan that matches
139
+ 0 files exits 2 and does not write empty docs.
135
140
 
136
141
  The target directory must already exist.
137
142
 
@@ -151,6 +151,18 @@ function describeModule(key, depRole = null, evidence = {}) {
151
151
  return base;
152
152
  }
153
153
 
154
+ // A module whose files are mostly tests and fixtures is not "Application module".
155
+ // The file count stays the same; the role says how much of it is test code.
156
+ export function describeModuleWithTests(module, desc) {
157
+ const total = Number(module?.fileCount) || 0;
158
+ const tests = Number(module?.testFileCount) || 0;
159
+ if (!total || tests * 2 < total) return desc;
160
+ if (/^Test suites and fixtures/.test(desc)) return desc;
161
+ const role = desc.replace(/^Application module( · )?/, "");
162
+ const base = `Mostly tests and fixtures (${tests} of ${total} files)`;
163
+ return role ? `${base} · ${role}` : base;
164
+ }
165
+
154
166
  export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = null) {
155
167
  const hasOwnership = Object.keys(ownershipMap).length > 0;
156
168
  const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
@@ -192,7 +204,7 @@ export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = nul
192
204
 
193
205
  for (const module of scan.modules.slice(0, 100)) {
194
206
  const depRole = describeModuleDepRole(module.key, moduleMetrics);
195
- const desc = describeModule(module.key, depRole, moduleEvidence(module.key, depGraph));
207
+ const desc = describeModuleWithTests(module, describeModule(module.key, depRole, moduleEvidence(module.key, depGraph)));
196
208
  const owners = ownershipMap[module.key];
197
209
  if (hasOwnership) {
198
210
  lines.push(`| \`${module.key}\` | ${module.fileCount} | ${desc} | ${owners ? owners.join(", ") : "—"} |`);
@@ -236,7 +248,7 @@ export function renderApiSurface(cfg, scan, jsdocResult = null) {
236
248
 
237
249
  if (!scan.api.length) {
238
250
  lines.push(
239
- `No API routes were detected in this scan. If your project uses a routing framework (Express, Fastify, NestJS, etc.), ensure the relevant directories are included in your \`scan.include\` patterns.`,
251
+ `No HTTP API routes were detected. RepoLens reads Next.js API routes and Express-style \`app.get(...)\` / \`router.post(...)\` handlers in JavaScript and TypeScript; libraries and CLIs usually have none.`,
240
252
  ``
241
253
  );
242
254
  } else {
@@ -4,6 +4,8 @@
4
4
  // - Dependency Graph
5
5
  // - Architecture Drift
6
6
 
7
+ import { coverageNotice } from "../analyzers/language-coverage.js";
8
+
7
9
  export function renderGraphQLSchema(graphqlResult) {
8
10
  if (!graphqlResult?.detected) {
9
11
  return [
@@ -316,6 +318,10 @@ export function renderTypeGraph(tsResult) {
316
318
  }
317
319
 
318
320
  export function renderDependencyGraph(depResult) {
321
+ const coverageLines = coverageNotice(depResult?.coverage, "import analysis");
322
+ if (!depResult?.nodes?.length && depResult?.coverage?.status === "unsupported") {
323
+ return ["# Dependency Graph", "", ...coverageLines].join("\n");
324
+ }
319
325
  if (!depResult?.nodes?.length) {
320
326
  return [
321
327
  "# Dependency Graph",
@@ -332,6 +338,7 @@ export function renderDependencyGraph(depResult) {
332
338
  lines.push("");
333
339
  lines.push(`> ${depResult.summary}`);
334
340
  lines.push("");
341
+ lines.push(...coverageLines);
335
342
  lines.push("This document maps every import relationship in the codebase, identifies the most-connected modules, and flags circular dependencies that may complicate refactoring or increase build times.");
336
343
  lines.push("");
337
344
 
@@ -526,6 +533,10 @@ export function renderArchitectureDrift(driftResult) {
526
533
  }
527
534
 
528
535
  export function renderCodeHealth(healthResult) {
536
+ const coverageLines = coverageNotice(healthResult?.coverage, "code health");
537
+ if (!healthResult?.modules?.length && healthResult?.coverage?.status === "unsupported") {
538
+ return ["# Code Health Report", "", ...coverageLines].join("\n");
539
+ }
529
540
  if (!healthResult?.modules?.length) {
530
541
  return [
531
542
  "# Code Health Report",
@@ -544,6 +555,7 @@ export function renderCodeHealth(healthResult) {
544
555
  lines.push("");
545
556
  lines.push(`> ${healthResult.summary}`);
546
557
  lines.push("");
558
+ lines.push(...coverageLines);
547
559
  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.");
548
560
  lines.push("");
549
561
 
@@ -0,0 +1,146 @@
1
+ // Remove JavaScript/TypeScript comments while keeping every string, template literal
2
+ // (including `${...}` expressions, which may hold their own comments and strings),
3
+ // and regular-expression literal intact. Comment text becomes spaces; newlines stay,
4
+ // so offsets and line numbers still match the source.
5
+
6
+ const REGEX_PREFIX_WORDS = new Set([
7
+ "return", "typeof", "instanceof", "in", "of", "new", "delete", "void", "throw",
8
+ "case", "do", "else", "yield", "await",
9
+ ]);
10
+
11
+ function blank(text) {
12
+ return text.replace(/[^\n]/g, " ");
13
+ }
14
+
15
+ function isIdentPart(ch) {
16
+ return /[A-Za-z0-9_$]/.test(ch || "");
17
+ }
18
+
19
+ export function stripJsComments(source) {
20
+ const src = String(source ?? "");
21
+ const n = src.length;
22
+ let out = "";
23
+ let i = 0;
24
+ // Stack of contexts: { type: "code", depth, inTemplate } or { type: "template" }.
25
+ const stack = [{ type: "code", depth: 0, inTemplate: false }];
26
+ // Last significant code token, to tell a regex literal from division.
27
+ let lastChar = "";
28
+ let lastWord = "";
29
+
30
+ while (i < n) {
31
+ const ctx = stack[stack.length - 1];
32
+ const ch = src[i];
33
+
34
+ if (ctx.type === "template") {
35
+ if (ch === "\\") {
36
+ out += src.slice(i, i + 2);
37
+ i += 2;
38
+ continue;
39
+ }
40
+ if (ch === "`") {
41
+ out += ch;
42
+ i += 1;
43
+ stack.pop();
44
+ lastChar = "`";
45
+ lastWord = "";
46
+ continue;
47
+ }
48
+ if (ch === "$" && src[i + 1] === "{") {
49
+ out += "${";
50
+ i += 2;
51
+ stack.push({ type: "code", depth: 0, inTemplate: true });
52
+ lastChar = "{";
53
+ lastWord = "";
54
+ continue;
55
+ }
56
+ out += ch;
57
+ i += 1;
58
+ continue;
59
+ }
60
+
61
+ // code context
62
+ const next = src[i + 1];
63
+ if (ch === "/" && next === "/") {
64
+ const nl = src.indexOf("\n", i);
65
+ const end = nl === -1 ? n : nl;
66
+ out += blank(src.slice(i, end));
67
+ i = end;
68
+ continue;
69
+ }
70
+ if (ch === "/" && next === "*") {
71
+ const close = src.indexOf("*/", i + 2);
72
+ const end = close === -1 ? n : close + 2;
73
+ out += blank(src.slice(i, end));
74
+ i = end;
75
+ continue;
76
+ }
77
+ if (ch === "'" || ch === '"') {
78
+ let j = i + 1;
79
+ while (j < n) {
80
+ if (src[j] === "\\") { j += 2; continue; }
81
+ if (src[j] === ch || src[j] === "\n") { j += 1; break; }
82
+ j += 1;
83
+ }
84
+ out += src.slice(i, Math.min(j, n));
85
+ i = Math.min(j, n);
86
+ lastChar = ch;
87
+ lastWord = "";
88
+ continue;
89
+ }
90
+ if (ch === "`") {
91
+ out += ch;
92
+ i += 1;
93
+ stack.push({ type: "template" });
94
+ continue;
95
+ }
96
+ if (ch === "/") {
97
+ const regexAllowed = lastChar === "" || "([{,;:!?&|~^=<>+-*%".includes(lastChar) || REGEX_PREFIX_WORDS.has(lastWord);
98
+ if (regexAllowed) {
99
+ let j = i + 1;
100
+ let inClass = false;
101
+ while (j < n) {
102
+ const c = src[j];
103
+ if (c === "\\") { j += 2; continue; }
104
+ if (c === "\n") break;
105
+ if (c === "[") inClass = true;
106
+ else if (c === "]") inClass = false;
107
+ else if (c === "/" && !inClass) { j += 1; break; }
108
+ j += 1;
109
+ }
110
+ while (j < n && /[a-z]/i.test(src[j])) j += 1;
111
+ out += src.slice(i, j);
112
+ i = j;
113
+ lastChar = ")";
114
+ lastWord = "";
115
+ continue;
116
+ }
117
+ }
118
+ if (ch === "{") ctx.depth += 1;
119
+ if (ch === "}") {
120
+ if (ctx.inTemplate && ctx.depth === 0) {
121
+ out += ch;
122
+ i += 1;
123
+ stack.pop();
124
+ continue;
125
+ }
126
+ ctx.depth -= 1;
127
+ }
128
+ if (isIdentPart(ch)) {
129
+ let j = i;
130
+ while (j < n && isIdentPart(src[j])) j += 1;
131
+ const word = src.slice(i, j);
132
+ out += word;
133
+ i = j;
134
+ lastWord = word;
135
+ lastChar = word[word.length - 1];
136
+ continue;
137
+ }
138
+ out += ch;
139
+ i += 1;
140
+ if (!/\s/.test(ch)) {
141
+ lastChar = ch;
142
+ lastWord = "";
143
+ }
144
+ }
145
+ return out;
146
+ }