@chappibunny/repolens 1.3.0 → 1.4.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/RELEASE.md CHANGED
@@ -22,7 +22,7 @@ RepoLens uses semantic versioning:
22
22
  8. Push branch and tag: `git push --follow-tags`
23
23
  9. GitHub Actions `release.yml` runs automatically:
24
24
  - Security audit (dependency + secrets scanning)
25
- - Test suite (163 tests)
25
+ - Test suite (185 tests)
26
26
  - Create GitHub Release with tarball
27
27
  - Publish to npm (`npm publish --access public`)
28
28
  10. Verify on npm: `npm view @chappibunny/repolens version`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/ai/prompts.js CHANGED
@@ -1,5 +1,48 @@
1
1
  // Strict prompt templates for AI-generated documentation sections
2
2
 
3
+ const MAX_CONTEXT_CHARS = 12000; // ~3000 tokens, safe for all models
4
+
5
+ /**
6
+ * Truncate a context object to fit within token limits.
7
+ * Prunes large arrays (routes, modules, domains) to keep context compact.
8
+ */
9
+ function truncateContext(context) {
10
+ let json = JSON.stringify(context, null, 2);
11
+ if (json.length <= MAX_CONTEXT_CHARS) return json;
12
+
13
+ // Progressively shrink: reduce array sizes
14
+ const trimmed = { ...context };
15
+
16
+ // Trim routes
17
+ if (trimmed.routes) {
18
+ if (trimmed.routes.pages && trimmed.routes.pages.length > 15) {
19
+ trimmed.routes = { ...trimmed.routes, pages: trimmed.routes.pages.slice(0, 15) };
20
+ }
21
+ if (trimmed.routes.apis && trimmed.routes.apis.length > 15) {
22
+ trimmed.routes = { ...trimmed.routes, apis: trimmed.routes.apis.slice(0, 15) };
23
+ }
24
+ }
25
+
26
+ // Trim domains
27
+ if (Array.isArray(trimmed.domains) && trimmed.domains.length > 8) {
28
+ trimmed.domains = trimmed.domains.slice(0, 8);
29
+ }
30
+
31
+ // Trim top modules
32
+ if (Array.isArray(trimmed.topModules) && trimmed.topModules.length > 10) {
33
+ trimmed.topModules = trimmed.topModules.slice(0, 10);
34
+ }
35
+
36
+ json = JSON.stringify(trimmed, null, 2);
37
+
38
+ // Final hard truncation if still over limit
39
+ if (json.length > MAX_CONTEXT_CHARS) {
40
+ json = json.slice(0, MAX_CONTEXT_CHARS) + "\n... (context truncated for token limit)";
41
+ }
42
+
43
+ return json;
44
+ }
45
+
3
46
  export const SYSTEM_PROMPT = `You are a senior software architect and technical writer.
4
47
  Your job is to turn structured repository analysis into clear documentation.
5
48
 
@@ -20,7 +63,7 @@ export function createExecutiveSummaryPrompt(context) {
20
63
  return `Write an executive summary for a mixed audience of technical and non-technical readers.
21
64
 
22
65
  Use this context:
23
- ${JSON.stringify(context, null, 2)}
66
+ ${truncateContext(context)}
24
67
 
25
68
  Requirements:
26
69
  - Explain what the system appears to do based on the modules and routes.
@@ -53,7 +96,7 @@ export function createSystemOverviewPrompt(context) {
53
96
  return `Write a system overview for a mixed audience.
54
97
 
55
98
  Use this context:
56
- ${JSON.stringify(context, null, 2)}
99
+ ${truncateContext(context)}
57
100
 
58
101
  Requirements:
59
102
  - Provide a concise, high-level orientation to the codebase.
@@ -81,7 +124,7 @@ export function createBusinessDomainsPrompt(context) {
81
124
  return `Write business domain documentation for a mixed audience, especially non-technical readers.
82
125
 
83
126
  Use this context:
84
- ${JSON.stringify(context, null, 2)}
127
+ ${truncateContext(context)}
85
128
 
86
129
  Requirements:
87
130
  - Translate codebase structure into business language.
@@ -113,7 +156,7 @@ export function createArchitectureOverviewPrompt(context) {
113
156
  return `Write an architecture overview for engineers, architects, and technical PMs.
114
157
 
115
158
  Use this context:
116
- ${JSON.stringify(context, null, 2)}
159
+ ${truncateContext(context)}
117
160
 
118
161
  Requirements:
119
162
  - Explain the layered architecture based on observable patterns.
@@ -147,10 +190,10 @@ export function createDataFlowsPrompt(flows, context) {
147
190
  return `Write data flow documentation for a mixed audience.
148
191
 
149
192
  Use this flow information:
150
- ${JSON.stringify(flows, null, 2)}
193
+ ${truncateContext(flows)}
151
194
 
152
195
  And this context:
153
- ${JSON.stringify(context, null, 2)}
196
+ ${truncateContext(context)}
154
197
 
155
198
  Requirements:
156
199
  - Explain major information flows in plain language.
@@ -179,7 +222,7 @@ export function createDeveloperOnboardingPrompt(context) {
179
222
  return `Write developer onboarding documentation to help new engineers get productive quickly.
180
223
 
181
224
  Use this context:
182
- ${JSON.stringify(context, null, 2)}
225
+ ${truncateContext(context)}
183
226
 
184
227
  Requirements:
185
228
  - Guide new developers through the codebase structure.
@@ -218,7 +261,7 @@ Type: ${module.type}
218
261
  Domain: ${module.domain}
219
262
 
220
263
  Additional context:
221
- ${JSON.stringify(context, null, 2)}
264
+ ${truncateContext(context)}
222
265
 
223
266
  Requirements:
224
267
  - Explain the module's likely purpose.
@@ -252,7 +295,7 @@ File: ${route.file}
252
295
  Type: ${route.type}
253
296
 
254
297
  Additional context:
255
- ${JSON.stringify(context, null, 2)}
298
+ ${truncateContext(context)}
256
299
 
257
300
  Requirements:
258
301
  - Explain the user purpose of this route.
@@ -283,7 +326,7 @@ API: ${api.methods.join(", ")} ${api.path}
283
326
  File: ${api.file}
284
327
 
285
328
  Additional context:
286
- ${JSON.stringify(context, null, 2)}
329
+ ${truncateContext(context)}
287
330
 
288
331
  Requirements:
289
332
  - Explain the purpose in plain language and technical language.
@@ -2,42 +2,42 @@
2
2
 
3
3
  const DEFAULT_DOMAIN_HINTS = [
4
4
  {
5
- match: ["auth", "login", "signup", "session", "user", "account"],
6
- domain: "Authentication",
7
- description: "User authentication and identity flows"
5
+ match: ["auth", "login", "signup", "session", "user", "account", "oauth", "sso"],
6
+ domain: "Authentication & Identity",
7
+ description: "User authentication, authorization, and identity management"
8
8
  },
9
9
  {
10
- match: ["stock", "chart", "price", "market", "watchlist", "ticker", "quote"],
11
- domain: "Market Data & Analysis",
12
- description: "Market data retrieval, analysis, and visualization"
10
+ match: ["dashboard", "analytics", "chart", "report", "metric", "stat", "insight"],
11
+ domain: "Analytics & Reporting",
12
+ description: "Data visualization, reporting, and analytics dashboards"
13
13
  },
14
14
  {
15
- match: ["article", "newsletter", "news", "research", "content", "blog", "post"],
16
- domain: "Content & Research",
17
- description: "Content publishing, research, and insight delivery"
15
+ match: ["article", "newsletter", "news", "research", "content", "blog", "post", "cms"],
16
+ domain: "Content Management",
17
+ description: "Content publishing, management, and delivery"
18
18
  },
19
19
  {
20
- match: ["portfolio", "positions", "holdings", "trades", "orders"],
21
- domain: "Portfolio Management",
22
- description: "Portfolio tracking and trading functionality"
20
+ match: ["search", "filter", "query", "index", "catalog", "browse"],
21
+ domain: "Search & Discovery",
22
+ description: "Search functionality, filtering, and content discovery"
23
23
  },
24
24
  {
25
- match: ["alert", "notification", "email", "sms", "webhook"],
26
- domain: "Alerts & Notifications",
27
- description: "User notification and alerting system"
25
+ match: ["alert", "notification", "email", "sms", "webhook", "push", "message"],
26
+ domain: "Notifications",
27
+ description: "User notifications and messaging system"
28
28
  },
29
29
  {
30
- match: ["payment", "subscription", "billing", "stripe", "checkout"],
30
+ match: ["payment", "subscription", "billing", "stripe", "checkout", "invoice"],
31
31
  domain: "Payments & Billing",
32
32
  description: "Payment processing and subscription management"
33
33
  },
34
34
  {
35
- match: ["api", "endpoint", "route", "handler"],
35
+ match: ["api", "endpoint", "route", "handler", "controller", "middleware"],
36
36
  domain: "API Layer",
37
37
  description: "Backend API endpoints and request handling"
38
38
  },
39
39
  {
40
- match: ["component", "ui", "button", "form", "modal", "dialog"],
40
+ match: ["component", "ui", "button", "form", "modal", "dialog", "layout", "widget"],
41
41
  domain: "UI Components",
42
42
  description: "Reusable user interface components"
43
43
  },
@@ -47,19 +47,34 @@ const DEFAULT_DOMAIN_HINTS = [
47
47
  description: "Custom React hooks for state and behavior"
48
48
  },
49
49
  {
50
- match: ["store", "state", "redux", "zustand", "context"],
50
+ match: ["store", "state", "redux", "zustand", "context", "atom"],
51
51
  domain: "State Management",
52
52
  description: "Application state management"
53
53
  },
54
54
  {
55
- match: ["lib", "util", "helper", "common", "shared"],
55
+ match: ["lib", "util", "helper", "common", "shared", "tool"],
56
56
  domain: "Shared Utilities",
57
57
  description: "Common utilities and helper functions"
58
58
  },
59
59
  {
60
- match: ["data", "database", "db", "prisma", "sql"],
60
+ match: ["data", "database", "db", "prisma", "sql", "model", "schema", "migration", "seed"],
61
61
  domain: "Data Layer",
62
- description: "Database access and data persistence"
62
+ description: "Database access, models, and data persistence"
63
+ },
64
+ {
65
+ match: ["config", "setting", "env", "constant"],
66
+ domain: "Configuration",
67
+ description: "Application configuration and environment settings"
68
+ },
69
+ {
70
+ match: ["test", "spec", "fixture", "mock", "stub", "e2e", "cypress", "playwright"],
71
+ domain: "Testing",
72
+ description: "Test suites, fixtures, and testing utilities"
73
+ },
74
+ {
75
+ match: ["job", "queue", "worker", "cron", "task", "scheduler", "background"],
76
+ domain: "Background Jobs",
77
+ description: "Background processing, job queues, and scheduled tasks"
63
78
  }
64
79
  ];
65
80
 
package/src/cli.js CHANGED
@@ -89,7 +89,7 @@ async function printBanner() {
89
89
  ██╔══██╗██╔══╝ ██╔═══╝ ██║ ██║██║ ██╔══╝ ██║╚██╗██║╚════██║
90
90
  ██║ ██║███████╗██║ ╚██████╔╝███████╗███████╗██║ ╚████║███████║
91
91
  ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═══╝╚══════╝
92
- 🔍 Repository Intelligence by RABITAI 🐰
92
+ 🔍 Repository Intelligence by RepoLens
93
93
  v${version}
94
94
  `);
95
95
  console.log("─".repeat(70));
@@ -133,7 +133,7 @@ async function findConfig(startDir = process.cwd()) {
133
133
 
134
134
  function printHelp() {
135
135
  console.log(`
136
- RepoLens — Repository Intelligence CLI by RABITAI 🐰
136
+ RepoLens — Repository Intelligence CLI
137
137
 
138
138
  Usage:
139
139
  repolens <command> [options]
@@ -200,7 +200,7 @@ async function generateDocument(docPlan, context) {
200
200
 
201
201
  case "system_map":
202
202
  // Hybrid: deterministic diagram + AI explanation (for now, just diagram)
203
- return renderSystemMap(scanResult, config);
203
+ return renderSystemMap(scanResult, config, depGraph);
204
204
 
205
205
  case "developer_onboarding":
206
206
  return await generateDeveloperOnboarding(aiContext);
package/src/doctor.js CHANGED
@@ -151,6 +151,57 @@ export async function runDoctor(targetDir = process.cwd()) {
151
151
  }
152
152
  }
153
153
 
154
+ // Validate environment variables for configured publishers
155
+ if (cfg && Array.isArray(cfg.publishers)) {
156
+ info("Environment:");
157
+ info("");
158
+
159
+ const envChecks = [];
160
+
161
+ if (cfg.publishers.includes("notion")) {
162
+ envChecks.push(
163
+ { key: "NOTION_TOKEN", required: true, publisher: "Notion" },
164
+ { key: "NOTION_PARENT_PAGE_ID", required: true, publisher: "Notion" },
165
+ );
166
+ }
167
+
168
+ if (cfg.publishers.includes("confluence")) {
169
+ envChecks.push(
170
+ { key: "CONFLUENCE_URL", required: true, publisher: "Confluence" },
171
+ { key: "CONFLUENCE_EMAIL", required: true, publisher: "Confluence" },
172
+ { key: "CONFLUENCE_API_TOKEN", required: true, publisher: "Confluence" },
173
+ { key: "CONFLUENCE_SPACE_KEY", required: true, publisher: "Confluence" },
174
+ { key: "CONFLUENCE_PARENT_PAGE_ID", required: true, publisher: "Confluence" },
175
+ );
176
+ }
177
+
178
+ if (cfg.publishers.includes("github_wiki")) {
179
+ envChecks.push(
180
+ { key: "GITHUB_TOKEN", required: true, publisher: "GitHub Wiki" },
181
+ );
182
+ }
183
+
184
+ if (cfg.ai?.enabled || process.env.REPOLENS_AI_ENABLED === "true") {
185
+ envChecks.push(
186
+ { key: "REPOLENS_AI_API_KEY", required: true, publisher: "AI" },
187
+ );
188
+ }
189
+
190
+ if (envChecks.length === 0) {
191
+ ok("No publisher-specific env vars required (Markdown only)");
192
+ }
193
+
194
+ for (const check of envChecks) {
195
+ if (process.env[check.key]) {
196
+ ok(`${check.key} is set (${check.publisher})`);
197
+ } else {
198
+ warn(`${check.key} is not set — required for ${check.publisher} publishing`);
199
+ }
200
+ }
201
+
202
+ info("");
203
+ }
204
+
154
205
  info("");
155
206
 
156
207
  const detectedRoots = await detectRepoRoots(repoRoot);
@@ -71,7 +71,7 @@ function buildEmbed(payload) {
71
71
  };
72
72
 
73
73
  return {
74
- title: title || "RABITAI Documentation Updated",
74
+ title: title || "RepoLens Documentation Updated",
75
75
  description: description || "Documentation has been regenerated",
76
76
  color: typeof color === "string" ? colorMap[color] || colorMap.info : color || colorMap.info,
77
77
  fields: fields.map((field) => ({
@@ -82,7 +82,7 @@ function buildEmbed(payload) {
82
82
  timestamp,
83
83
  footer: footer
84
84
  ? { text: footer }
85
- : { text: "RABITAI 🐰" },
85
+ : { text: "RepoLens 🔍" },
86
86
  url: url || undefined,
87
87
  };
88
88
  }
@@ -237,7 +237,7 @@ export function buildErrorNotification(options) {
237
237
  }
238
238
 
239
239
  return {
240
- title: "🚨 RABITAI Error",
240
+ title: "🚨 RepoLens Error",
241
241
  description: "Documentation generation failed",
242
242
  color: "error",
243
243
  fields,
@@ -136,7 +136,12 @@ async function writeCache(cache) {
136
136
 
137
137
  // Convert Markdown to Confluence Storage Format
138
138
  function markdownToConfluenceStorage(markdown) {
139
- const lines = markdown.split("\n");
139
+ // Rewrite relative file links that can't resolve in Confluence
140
+ const rewritten = markdown.replace(
141
+ /\[([^\]]+)\]\(\.{1,2}\/[^)]+\)/g,
142
+ "$1"
143
+ );
144
+ const lines = rewritten.split("\n");
140
145
  const output = [];
141
146
 
142
147
  let i = 0;
@@ -161,10 +166,12 @@ function markdownToConfluenceStorage(markdown) {
161
166
  }
162
167
  i++; // skip closing ```
163
168
  const code = codeLines.join("\n");
169
+ // Escape ]]> inside code to prevent CDATA injection
170
+ const safeCode = code.replace(/]]>/g, "]]]]><![CDATA[>");
164
171
  output.push(
165
172
  `<ac:structured-macro ac:name="code">` +
166
173
  `<ac:parameter ac:name="language">${escapeHtml(language)}</ac:parameter>` +
167
- `<ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>` +
174
+ `<ac:plain-text-body><![CDATA[${safeCode}]]></ac:plain-text-body>` +
168
175
  `</ac:structured-macro>`
169
176
  );
170
177
  continue;
@@ -8,12 +8,21 @@ function outputDir(cfg) {
8
8
 
9
9
  function pageFileName(key) {
10
10
  const mapping = {
11
+ executive_summary: "executive_summary.md",
11
12
  system_overview: "system_overview.md",
13
+ business_domains: "business_domains.md",
14
+ architecture_overview: "architecture_overview.md",
12
15
  module_catalog: "module_catalog.md",
13
16
  api_surface: "api_surface.md",
17
+ data_flows: "data_flows.md",
14
18
  arch_diff: "architecture_diff.md",
15
19
  route_map: "route_map.md",
16
- system_map: "system_map.md"
20
+ system_map: "system_map.md",
21
+ developer_onboarding: "developer_onboarding.md",
22
+ graphql_schema: "graphql_schema.md",
23
+ type_graph: "type_graph.md",
24
+ dependency_graph: "dependency_graph.md",
25
+ architecture_drift: "architecture_drift.md"
17
26
  };
18
27
 
19
28
  return mapping[key] || `${key}.md`;
@@ -172,6 +172,18 @@ export async function clearPage(pageId) {
172
172
  }
173
173
  }
174
174
 
175
+ /**
176
+ * Rewrite relative file links that cannot resolve in external publishers.
177
+ * Converts [text](./path.md) and [text](../path.md) to just "text",
178
+ * while preserving absolute URLs like [text](https://example.com).
179
+ */
180
+ function rewriteRelativeLinks(markdown) {
181
+ return markdown.replace(
182
+ /\[([^\]]+)\]\(\.{1,2}\/[^)]+\)/g,
183
+ "$1"
184
+ );
185
+ }
186
+
175
187
  function parseInlineRichText(text) {
176
188
  // Parse inline markdown: **bold**, *italic*, `code` into Notion rich_text annotations
177
189
  const segments = [];
@@ -246,8 +258,11 @@ function markdownToNotionBlocks(markdown) {
246
258
  warn(`markdownToNotionBlocks received invalid markdown: ${typeof markdown}`);
247
259
  return [];
248
260
  }
261
+
262
+ // Rewrite relative file links that can't resolve in Notion
263
+ const rewritten = rewriteRelativeLinks(markdown);
249
264
 
250
- const lines = markdown.split("\n");
265
+ const lines = rewritten.split("\n");
251
266
  const blocks = [];
252
267
  let i = 0;
253
268
 
@@ -326,28 +341,88 @@ function markdownToNotionBlocks(markdown) {
326
341
 
327
342
  if (tableRows.length > 0) {
328
343
  const columnCount = tableRows[0].length;
329
- const tableBlock = {
330
- object: "block",
331
- type: "table",
332
- table: {
333
- table_width: columnCount,
334
- has_column_header: true,
335
- has_row_header: false,
336
- children: tableRows.map((row) => ({
337
- type: "table_row",
338
- table_row: {
339
- cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
344
+ const headerRow = tableRows[0];
345
+ const dataRows = tableRows.slice(1);
346
+
347
+ // Notion API limit: max 100 rows per table (including header)
348
+ // Split into chunks if needed
349
+ if (dataRows.length >= 100) {
350
+ const CHUNK_SIZE = 99; // 99 data rows + 1 header = 100 total
351
+
352
+ for (let chunkIdx = 0; chunkIdx < dataRows.length; chunkIdx += CHUNK_SIZE) {
353
+ const chunkRows = dataRows.slice(chunkIdx, chunkIdx + CHUNK_SIZE);
354
+ const allRows = [headerRow, ...chunkRows];
355
+
356
+ const tableBlock = {
357
+ object: "block",
358
+ type: "table",
359
+ table: {
360
+ table_width: columnCount,
361
+ has_column_header: true,
362
+ has_row_header: false,
363
+ children: allRows.map((row) => ({
364
+ type: "table_row",
365
+ table_row: {
366
+ cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
367
+ }
368
+ }))
340
369
  }
341
- }))
370
+ };
371
+
372
+ // Pad rows that have fewer cells than the header
373
+ for (const child of tableBlock.table.children) {
374
+ while (child.table_row.cells.length < columnCount) {
375
+ child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
376
+ }
377
+ }
378
+
379
+ blocks.push(tableBlock);
380
+
381
+ // Add continuation note between chunks
382
+ if (chunkIdx + CHUNK_SIZE < dataRows.length) {
383
+ const remaining = dataRows.length - (chunkIdx + CHUNK_SIZE);
384
+ blocks.push({
385
+ object: "block",
386
+ type: "paragraph",
387
+ paragraph: {
388
+ rich_text: [{
389
+ type: "text",
390
+ text: {
391
+ content: `📋 Table continued below (${remaining} more rows)...`
392
+ },
393
+ annotations: { italic: true, color: "gray" }
394
+ }]
395
+ }
396
+ });
397
+ }
342
398
  }
343
- };
344
- // Pad rows that have fewer cells than the header
345
- for (const child of tableBlock.table.children) {
346
- while (child.table_row.cells.length < columnCount) {
347
- child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
399
+ } else {
400
+ // Table fits in one block
401
+ const tableBlock = {
402
+ object: "block",
403
+ type: "table",
404
+ table: {
405
+ table_width: columnCount,
406
+ has_column_header: true,
407
+ has_row_header: false,
408
+ children: tableRows.map((row) => ({
409
+ type: "table_row",
410
+ table_row: {
411
+ cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
412
+ }
413
+ }))
414
+ }
415
+ };
416
+
417
+ // Pad rows that have fewer cells than the header
418
+ for (const child of tableBlock.table.children) {
419
+ while (child.table_row.cells.length < columnCount) {
420
+ child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
421
+ }
348
422
  }
423
+
424
+ blocks.push(tableBlock);
349
425
  }
350
- blocks.push(tableBlock);
351
426
  }
352
427
  continue;
353
428
  }
@@ -448,7 +523,7 @@ function markdownToNotionBlocks(markdown) {
448
523
  }
449
524
 
450
525
  // Exported for testing
451
- export { markdownToNotionBlocks, parseInlineRichText };
526
+ export { markdownToNotionBlocks, parseInlineRichText, rewriteRelativeLinks };
452
527
 
453
528
  export async function replacePageContent(pageId, markdown) {
454
529
  // Ensure page is unarchived before editing
@@ -266,6 +266,10 @@ export function renderRouteMap(cfg, scan) {
266
266
  lines.push(`| \`${page.path}\` | \`${page.file}\` |`);
267
267
  }
268
268
 
269
+ if (scan.pages.length > 200) {
270
+ lines.push(``, `> **Note:** Showing 200 of ${scan.pages.length} pages. Configure \`scan.include\` to narrow scope.`);
271
+ }
272
+
269
273
  lines.push(``);
270
274
  }
271
275
 
@@ -283,6 +287,10 @@ export function renderRouteMap(cfg, scan) {
283
287
  lines.push(`| ${route.methods.join(", ")} | \`${route.path}\` | \`${route.file}\` |`);
284
288
  }
285
289
 
290
+ if (scan.api.length > 200) {
291
+ lines.push(``, `> **Note:** Showing 200 of ${scan.api.length} API endpoints.`);
292
+ }
293
+
286
294
  lines.push(``);
287
295
  }
288
296
 
@@ -88,6 +88,9 @@ export function renderArchitectureDiff(diff) {
88
88
  for (const route of data.addedRoutes.slice(0, 25)) {
89
89
  lines.push(`- \`${route}\``);
90
90
  }
91
+ if (data.addedRoutes.length > 25) {
92
+ lines.push(``, `> Showing 25 of ${data.addedRoutes.length} added routes.`);
93
+ }
91
94
  lines.push("");
92
95
  }
93
96
 
@@ -96,6 +99,9 @@ export function renderArchitectureDiff(diff) {
96
99
  for (const route of data.removedRoutes.slice(0, 25)) {
97
100
  lines.push(`- \`${route}\``);
98
101
  }
102
+ if (data.removedRoutes.length > 25) {
103
+ lines.push(``, `> Showing 25 of ${data.removedRoutes.length} removed routes.`);
104
+ }
99
105
  lines.push("");
100
106
  }
101
107
 
@@ -104,6 +110,9 @@ export function renderArchitectureDiff(diff) {
104
110
  for (const module of data.impactedModules.slice(0, 40)) {
105
111
  lines.push(`- \`${module}\``);
106
112
  }
113
+ if (data.impactedModules.length > 40) {
114
+ lines.push(``, `> Showing 40 of ${data.impactedModules.length} impacted modules.`);
115
+ }
107
116
  lines.push("");
108
117
  }
109
118
 
@@ -112,6 +121,9 @@ export function renderArchitectureDiff(diff) {
112
121
  for (const file of data.added.slice(0, 25)) {
113
122
  lines.push(`- \`${file}\``);
114
123
  }
124
+ if (data.added.length > 25) {
125
+ lines.push(``, `> Showing 25 of ${data.added.length} added files.`);
126
+ }
115
127
  lines.push("");
116
128
  }
117
129
 
@@ -120,6 +132,9 @@ export function renderArchitectureDiff(diff) {
120
132
  for (const file of data.removed.slice(0, 25)) {
121
133
  lines.push(`- \`${file}\``);
122
134
  }
135
+ if (data.removed.length > 25) {
136
+ lines.push(``, `> Showing 25 of ${data.removed.length} removed files.`);
137
+ }
123
138
  lines.push("");
124
139
  }
125
140
 
@@ -128,6 +143,9 @@ export function renderArchitectureDiff(diff) {
128
143
  for (const file of data.modified.slice(0, 25)) {
129
144
  lines.push(`- \`${file}\``);
130
145
  }
146
+ if (data.modified.length > 25) {
147
+ lines.push(``, `> Showing 25 of ${data.modified.length} modified files.`);
148
+ }
131
149
  lines.push("");
132
150
  }
133
151