@chappibunny/repolens 1.6.1 → 1.7.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,26 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.7.0
6
+
7
+ ### ✨ Features
8
+
9
+ - **Dependency-weighted module roles**: Module catalog now shows import-based role labels (e.g. "Orchestrator — coordinates 54 modules", "Critical shared infrastructure — imported by 55 modules") derived from real dependency graph metrics.
10
+ - **Context-aware route map**: CLI tools now get a helpful explanation ("This project is a CLI tool — it does not expose HTTP routes") instead of an empty route table with a stale footer.
11
+ - **Verified architecture patterns**: Detected patterns now include confidence labels (e.g. "naming only" vs evidence-backed) based on dependency graph verification.
12
+ - **Import-chain data flows**: Data flows are now traced from real entry points via import chains, replacing hardcoded template flows. Shows actual module dependencies and downstream traces.
13
+ - **CLI-aware tech stack labels**: All 4 key documents (Executive Summary, System Overview, Architecture Overview, Developer Onboarding) now show "N/A (CLI tool)" instead of "Not detected" for Frameworks and Build Tools when the project is a CLI tool.
14
+ - **Test flow filtering**: Test file flows are now filtered from Executive Summary, Data Flows, and Developer Onboarding documents, keeping output focused on production code paths.
15
+ - **Hub flow cleanup**: Integration flows derived from dependency graph hubs no longer list test files as importers in their step chains.
16
+
17
+ ### 🐛 Bug Fixes
18
+
19
+ - **GraphQL false positives**: The GraphQL analyzer now excludes test files and its own analysis/rendering source files from scanning, preventing self-detection of library patterns (e.g. detecting "Apollo Server" from regex pattern strings).
20
+ - **GraphQL pattern precision**: The `graphql-js` detection pattern now uses word boundaries (`\bGraphQLSchema\b`) to avoid false matches from function names like `renderGraphQLSchema`.
21
+ - **Change impact noise filtering**: Architecture diff now filters `node_modules/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `.repolens/`, and `.git/` from both the Impacted Modules list and the file lists (Added/Removed/Modified).
22
+ - **Zero-value suppression**: Documents no longer show "serves 0 application pages" or "0 API endpoints" for CLI tools — these rows are conditionally hidden when values are zero.
23
+ - **Module label enrichment**: `describeModule()` now recognizes 6 additional module types (plugin, prompt, provider, generate/section, ai/ml), reducing generic "Application module" fallbacks.
24
+
5
25
  ## 1.6.0
6
26
 
7
27
  ### ✨ Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -67,5 +67,8 @@
67
67
  "devDependencies": {
68
68
  "tinyexec": "1.0.2",
69
69
  "vitest": "^4.0.18"
70
+ },
71
+ "overrides": {
72
+ "yauzl": ">=3.2.1"
70
73
  }
71
74
  }
@@ -125,6 +125,18 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
125
125
  const languageList = context.techStack.languages.join(", ") || "multiple languages";
126
126
  const domainSummary = context.domains.slice(0, 5).map(d => d.name).join(", ");
127
127
  const testFrameworks = context.techStack.testFrameworks || [];
128
+ const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
129
+
130
+ // Build the "what it does" line based on project type
131
+ let interfaceLine;
132
+ if (isCLI) {
133
+ interfaceLine = "It operates as a **command-line tool**, interacting through terminal commands rather than a web interface.";
134
+ } else {
135
+ const parts = [];
136
+ if (context.project.apiRoutesDetected > 0) parts.push(`exposes **${context.project.apiRoutesDetected} API endpoint${context.project.apiRoutesDetected === 1 ? "" : "s"}**`);
137
+ if (context.project.pagesDetected > 0) parts.push(`serves **${context.project.pagesDetected} application page${context.project.pagesDetected === 1 ? "" : "s"}** to end users`);
138
+ interfaceLine = parts.length > 0 ? `It ${parts.join(" and ")}.` : "";
139
+ }
128
140
 
129
141
  let output = `# Executive Summary
130
142
 
@@ -132,7 +144,7 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
132
144
 
133
145
  ${context.project.name} is a ${frameworkList} application built with ${languageList}. The codebase contains **${context.project.modulesDetected} modules** spread across **${context.project.filesScanned} files**, organized into ${context.domains.length} functional domain${context.domains.length === 1 ? "" : "s"}.
134
146
 
135
- ${context.project.apiRoutesDetected > 0 ? `The system exposes **${context.project.apiRoutesDetected} API endpoint${context.project.apiRoutesDetected === 1 ? "" : "s"}** and` : "It"} serves **${context.project.pagesDetected} application page${context.project.pagesDetected === 1 ? "" : "s"}** to end users.
147
+ ${interfaceLine}
136
148
 
137
149
  ## Primary Functional Areas
138
150
 
@@ -146,9 +158,9 @@ ${context.domains.map(d => `| ${d.name} | ${d.moduleCount} | ${d.description ||
146
158
 
147
159
  | Category | Details |
148
160
  |----------|---------|
149
- | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
161
+ | Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
150
162
  | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
151
- | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
163
+ | Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
152
164
  ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}`;
153
165
 
154
166
  // Module type breakdown
@@ -188,11 +200,12 @@ ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")}
188
200
  }
189
201
  }
190
202
 
191
- // Data flows
192
- if (flows && flows.length > 0) {
203
+ // Data flows (filter out test file flows for exec summary)
204
+ const summaryFlows = (flows || []).filter(f => !f.name?.toLowerCase().includes('test'));
205
+ if (summaryFlows.length > 0) {
193
206
  output += `\n## Key Data Flows\n\n`;
194
- output += `${flows.length} data flow${flows.length === 1 ? "" : "s"} identified:\n\n`;
195
- for (const flow of flows) {
207
+ output += `${summaryFlows.length} data flow${summaryFlows.length === 1 ? "" : "s"} identified:\n\n`;
208
+ for (const flow of summaryFlows) {
196
209
  output += `- **${flow.name}**${flow.critical ? " (critical)" : ""} — ${flow.description}\n`;
197
210
  }
198
211
  }
@@ -213,6 +226,7 @@ function getFallbackSystemOverview(context, enrichment = {}) {
213
226
  const sizeLabel = context.project.modulesDetected > 50 ? "large-scale" :
214
227
  context.project.modulesDetected > 20 ? "medium-sized" : "focused";
215
228
  const testFrameworks = context.techStack.testFrameworks || [];
229
+ const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
216
230
 
217
231
  let output = `# System Overview
218
232
 
@@ -224,16 +238,14 @@ This is a ${sizeLabel} codebase organized into **${context.project.modulesDetect
224
238
  |--------|-------|
225
239
  | Files scanned | ${context.project.filesScanned} |
226
240
  | Modules | ${context.project.modulesDetected} |
227
- | Application pages | ${context.project.pagesDetected} |
228
- | API endpoints | ${context.project.apiRoutesDetected} |
229
-
241
+ ${context.project.pagesDetected > 0 ? `| Application pages | ${context.project.pagesDetected} |\n` : ""}${context.project.apiRoutesDetected > 0 ? `| API endpoints | ${context.project.apiRoutesDetected} |\n` : ""}
230
242
  ## Technology Stack
231
243
 
232
244
  | Category | Technologies |
233
245
  |----------|-------------|
234
- | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
246
+ | Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
235
247
  | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
236
- | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
248
+ | Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
237
249
  ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}
238
250
  ## Detected Patterns
239
251
 
@@ -378,6 +390,7 @@ function getFallbackBusinessDomains(context, enrichment = {}) {
378
390
 
379
391
  function getFallbackArchitectureOverview(context, enrichment = {}) {
380
392
  const { depGraph, driftResult } = enrichment;
393
+ const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
381
394
  const patternDesc = context.patterns.length > 0
382
395
  ? `The detected architectural patterns are **${context.patterns.join(", ")}**. These patterns shape how data and control flow through the system.`
383
396
  : "No specific architectural patterns were detected. The project appears to follow a straightforward directory-based organization.";
@@ -415,13 +428,13 @@ ${context.domains.slice(0, 8).map(d => `| **${d.name}** | ${d.description || "Ha
415
428
 
416
429
  | Category | Technologies |
417
430
  |----------|-------------|
418
- | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
431
+ | Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
419
432
  | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
420
- | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
433
+ | Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
421
434
 
422
435
  ## Scale & Complexity
423
436
 
424
- The repository comprises **${context.project.filesScanned} files** organized into **${context.project.modulesDetected} modules**. ${context.project.apiRoutesDetected > 0 ? `It exposes **${context.project.apiRoutesDetected} API endpoint${context.project.apiRoutesDetected === 1 ? "" : "s"}** and` : "It"} serves **${context.project.pagesDetected} application page${context.project.pagesDetected === 1 ? "" : "s"}**.
437
+ The repository comprises **${context.project.filesScanned} files** organized into **${context.project.modulesDetected} modules**.${context.project.apiRoutesDetected > 0 ? ` It exposes **${context.project.apiRoutesDetected} API endpoint${context.project.apiRoutesDetected === 1 ? "" : "s"}**.` : ""}${context.project.pagesDetected > 0 ? ` It serves **${context.project.pagesDetected} application page${context.project.pagesDetected === 1 ? "" : "s"}**.` : ""}${isCLI ? " It operates as a command-line tool." : ""}
425
438
  `;
426
439
 
427
440
  // Dependency graph health
@@ -502,8 +515,8 @@ function getFallbackDataFlows(flows, context, enrichment = {}) {
502
515
  let output = `# Data Flows\n\n`;
503
516
  output += `> Data flows describe how information moves through the system — from external inputs through processing layers to storage or presentation.\n\n`;
504
517
 
505
- // Combine heuristic flows with dep-graph-derived flows
506
- const allFlows = [...(flows || [])];
518
+ // Combine heuristic flows with dep-graph-derived flows, filtering out test file flows
519
+ const allFlows = [...(flows || [])].filter(f => !f.name?.toLowerCase().includes('test'));
507
520
 
508
521
  // Generate additional flows from dependency graph hub chains
509
522
  if (depGraph?.nodes && depGraph.nodes.length > 0 && allFlows.length < 3) {
@@ -571,6 +584,7 @@ function getFallbackDeveloperOnboarding(context, enrichment = {}) {
571
584
  const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose tools";
572
585
  const languageList = context.techStack.languages.join(", ") || "standard languages";
573
586
  const testFrameworks = context.techStack.testFrameworks || [];
587
+ const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
574
588
  const routes = context.routes || {};
575
589
  const pages = routes.pages || [];
576
590
  const apis = routes.apis || [];
@@ -604,9 +618,9 @@ ${context.repoRoots.map(root => `| \`${root}\` | ${describeRoot(root)} |`).join(
604
618
 
605
619
  | Category | Technologies |
606
620
  |----------|-------------|
607
- | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
621
+ | Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
608
622
  | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
609
- | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
623
+ | Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
610
624
  ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}
611
625
  ## Largest Modules
612
626
 
@@ -637,11 +651,12 @@ ${context.topModules.slice(0, 10).map(m => `| \`${m.key}\` | ${m.fileCount} | ${
637
651
  }
638
652
  }
639
653
 
640
- // Data flows overview
641
- if (flows && flows.length > 0) {
654
+ // Data flows overview (filter out test flows)
655
+ const onboardingFlows = (flows || []).filter(f => !f.name?.toLowerCase().includes('test'));
656
+ if (onboardingFlows.length > 0) {
642
657
  output += `## How Data Flows\n\n`;
643
658
  output += `Understanding these flows will help you see how the system works end-to-end:\n\n`;
644
- for (const flow of flows) {
659
+ for (const flow of onboardingFlows) {
645
660
  output += `- **${flow.name}** — ${flow.description}\n`;
646
661
  }
647
662
  output += "\n";
@@ -715,7 +730,8 @@ function inferFlowsFromDepGraph(depGraph) {
715
730
  .slice(0, 3);
716
731
 
717
732
  for (const hub of hubs) {
718
- const importers = hub.importedBy.slice(0, 5);
733
+ const testPattern = /(?:^|\/)(?:tests?|__tests?__|spec|__spec__)\/|\.(test|spec)\.[jt]sx?$/i;
734
+ const importers = hub.importedBy.filter(i => !testPattern.test(i)).slice(0, 5);
719
735
  const downstream = hub.imports.slice(0, 3);
720
736
  const shortName = hub.key.split("/").pop();
721
737
 
@@ -2,7 +2,83 @@
2
2
 
3
3
  import { groupModulesByDomain } from "./domain-inference.js";
4
4
 
5
- export function buildAIContext(scanResult, config) {
5
+ /**
6
+ * Compute per-module dependency metrics from the dep graph.
7
+ * Returns a Map<normalizedKey, { fanIn, fanOut, isHub, isOrphan, isLeaf, isOrchestrator }>.
8
+ */
9
+ export function computeModuleDepMetrics(depGraph) {
10
+ const metrics = new Map();
11
+ if (!depGraph?.nodes) return metrics;
12
+
13
+ const hubThreshold = Math.max(3, Math.floor(depGraph.nodes.length * 0.05));
14
+
15
+ for (const node of depGraph.nodes) {
16
+ const fanIn = node.importedBy.length;
17
+ const fanOut = node.imports.length;
18
+ metrics.set(node.key, {
19
+ fanIn,
20
+ fanOut,
21
+ isHub: fanIn >= hubThreshold,
22
+ isOrphan: fanIn === 0 && fanOut === 0,
23
+ isLeaf: fanOut === 0 && fanIn > 0,
24
+ isOrchestrator: fanOut >= 5 && fanIn < fanOut,
25
+ });
26
+ }
27
+ return metrics;
28
+ }
29
+
30
+ /**
31
+ * Build a module-level dep metrics map (aggregating file-level metrics).
32
+ * moduleKey → { fanIn, fanOut, isHub, isOrphan, isLeaf, isOrchestrator }
33
+ */
34
+ export function computeModuleLevelMetrics(depGraph, modules) {
35
+ const fileMetrics = computeModuleDepMetrics(depGraph);
36
+ if (fileMetrics.size === 0 || !modules) return new Map();
37
+
38
+ const moduleLevelMap = new Map();
39
+
40
+ for (const mod of modules) {
41
+ const moduleKey = mod.key;
42
+ const lowerKey = moduleKey.toLowerCase();
43
+
44
+ // Find all file-level nodes that belong to this module
45
+ let totalFanIn = 0;
46
+ let totalFanOut = 0;
47
+ let fileCount = 0;
48
+
49
+ for (const [nodeKey, m] of fileMetrics) {
50
+ if (nodeKey === lowerKey || nodeKey.startsWith(lowerKey + "/") || nodeKey === moduleKey || nodeKey.startsWith(moduleKey + "/")) {
51
+ // Count only external edges (crossing module boundary)
52
+ if (depGraph?.nodes) {
53
+ const node = depGraph.nodes.find(n => n.key === nodeKey);
54
+ if (node) {
55
+ const externalIn = node.importedBy.filter(imp => !imp.startsWith(moduleKey + "/") && imp !== moduleKey).length;
56
+ const externalOut = node.imports.filter(imp => !imp.startsWith(moduleKey + "/") && imp !== moduleKey).length;
57
+ totalFanIn += externalIn;
58
+ totalFanOut += externalOut;
59
+ fileCount++;
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ if (fileCount > 0) {
66
+ const hubThreshold = Math.max(5, Math.floor(modules.length * 0.15));
67
+ moduleLevelMap.set(moduleKey, {
68
+ fanIn: totalFanIn,
69
+ fanOut: totalFanOut,
70
+ isHub: totalFanIn >= hubThreshold,
71
+ isOrphan: totalFanIn === 0 && totalFanOut === 0,
72
+ isLeaf: totalFanOut === 0 && totalFanIn > 0,
73
+ isOrchestrator: totalFanOut >= 5 && totalFanIn < totalFanOut,
74
+ });
75
+ }
76
+ }
77
+
78
+ return moduleLevelMap;
79
+ }
80
+
81
+ export function buildAIContext(scanResult, config, depGraph = null) {
6
82
  const { filesCount, modules, api, pages, metadata } = scanResult;
7
83
 
8
84
  // Get domain hints from config if available
@@ -17,13 +93,17 @@ export function buildAIContext(scanResult, config) {
17
93
  // Group modules by business domain
18
94
  const domainGroups = groupModulesByDomain(modules, customHints);
19
95
 
20
- // Identify top modules
96
+ // Compute module-level dependency metrics
97
+ const moduleMetrics = computeModuleLevelMetrics(depGraph, modules);
98
+
99
+ // Identify top modules with enriched types
21
100
  const topModules = modules
22
101
  .slice(0, 15)
23
102
  .map(m => ({
24
103
  key: m.key,
25
104
  fileCount: m.fileCount,
26
- type: inferModuleType(m.key)
105
+ type: inferModuleType(m.key),
106
+ depRole: describeModuleDepRole(m.key, moduleMetrics),
27
107
  }));
28
108
 
29
109
  // Categorize routes
@@ -48,8 +128,8 @@ export function buildAIContext(scanResult, config) {
48
128
  testFrameworks: metadata?.testFrameworks || []
49
129
  };
50
130
 
51
- // Identify key architectural patterns
52
- const patterns = inferArchitecturalPatterns(modules);
131
+ // Identify key architectural patterns (verified against dep graph when available)
132
+ const patterns = inferArchitecturalPatterns(modules, depGraph);
53
133
 
54
134
  // Build compact context object
55
135
  return {
@@ -124,11 +204,31 @@ function inferModuleType(modulePath) {
124
204
  return "other";
125
205
  }
126
206
 
127
- function inferArchitecturalPatterns(modules) {
207
+ function inferArchitecturalPatterns(modules, depGraph = null) {
128
208
  const patterns = [];
129
209
  const keys = modules.map(m => m.key.toLowerCase());
130
210
 
131
211
  const has = (keyword) => keys.some(k => k.includes(keyword));
212
+
213
+ // Compute module-level metrics for verification
214
+ const moduleMetrics = depGraph ? computeModuleLevelMetrics(depGraph, modules) : new Map();
215
+ const hasCycles = depGraph?.cycles?.length > 0;
216
+
217
+ // Verify pattern: a module matching `keyword` has high fan-in (truly central)
218
+ const verifyHub = (keyword) => {
219
+ for (const [mKey, m] of moduleMetrics) {
220
+ if (mKey.toLowerCase().includes(keyword) && m.fanIn >= 3) return true;
221
+ }
222
+ return false;
223
+ };
224
+
225
+ // Verify pattern: modules matching keyword have mostly outbound deps (orchestrators)
226
+ const verifyOrchestrator = (keyword) => {
227
+ for (const [mKey, m] of moduleMetrics) {
228
+ if (mKey.toLowerCase().includes(keyword) && m.fanOut >= 3) return true;
229
+ }
230
+ return false;
231
+ };
132
232
 
133
233
  // Web framework patterns
134
234
  if (has("app/")) patterns.push("Next.js App Router");
@@ -137,25 +237,95 @@ function inferArchitecturalPatterns(modules) {
137
237
  if (has("hook")) patterns.push("React hooks pattern");
138
238
  if (has("store") || has("state") || has("redux") || has("zustand")) patterns.push("Centralized state management");
139
239
 
140
- // General patterns
240
+ // General patterns — verified against dep graph when available
141
241
  if (has("api") || has("endpoint")) patterns.push("API route pattern");
142
- if (has("core") || has("kernel")) patterns.push("Core/kernel architecture");
143
- if (has("plugin") || has("extension")) patterns.push("Plugin system");
242
+
243
+ if (has("core") || has("kernel")) {
244
+ if (moduleMetrics.size > 0 && verifyHub("core")) {
245
+ patterns.push("Core/kernel architecture (verified — high fan-in)");
246
+ } else if (moduleMetrics.size > 0) {
247
+ patterns.push("Core/kernel architecture (naming only)");
248
+ } else {
249
+ patterns.push("Core/kernel architecture");
250
+ }
251
+ }
252
+
253
+ if (has("plugin") || has("extension")) {
254
+ if (moduleMetrics.size > 0 && (verifyHub("plugin") || verifyOrchestrator("plugin") || verifyHub("loader"))) {
255
+ patterns.push("Plugin system (verified — loader/registry detected)");
256
+ } else if (moduleMetrics.size > 0) {
257
+ patterns.push("Plugin system (naming only)");
258
+ } else {
259
+ patterns.push("Plugin system");
260
+ }
261
+ }
262
+
144
263
  if (has("middleware")) patterns.push("Middleware pipeline");
145
- if (has("render") || has("template")) patterns.push("Renderer pipeline");
146
- if (has("publish") || has("output")) patterns.push("Multi-output publishing");
264
+
265
+ if (has("render") || has("template")) {
266
+ if (moduleMetrics.size > 0 && verifyOrchestrator("render")) {
267
+ patterns.push("Renderer pipeline (verified — multi-output)");
268
+ } else {
269
+ patterns.push("Renderer pipeline");
270
+ }
271
+ }
272
+
273
+ if (has("publish") || has("output")) {
274
+ if (moduleMetrics.size > 0 && verifyOrchestrator("publish")) {
275
+ patterns.push("Multi-output publishing (verified — multiple targets)");
276
+ } else {
277
+ patterns.push("Multi-output publishing");
278
+ }
279
+ }
280
+
147
281
  if (has("analyz") || has("detect") || has("inspect")) patterns.push("Analysis pipeline");
148
282
  if (has("cli") || has("command") || has("bin")) patterns.push("CLI tool architecture");
149
- if (has("util") || has("helper") || has("lib")) patterns.push("Shared utility layer");
283
+
284
+ if (has("util") || has("helper") || has("lib")) {
285
+ if (moduleMetrics.size > 0 && verifyHub("util") || verifyHub("helper") || verifyHub("lib")) {
286
+ patterns.push("Shared utility layer (verified — high fan-in)");
287
+ } else {
288
+ patterns.push("Shared utility layer");
289
+ }
290
+ }
291
+
150
292
  if (has("integrat") || has("adapter") || has("connect")) patterns.push("Integration adapters");
151
293
  if (has("ai") || has("prompt") || has("provider")) patterns.push("AI/LLM integration");
152
294
  if (has("deliver") || has("dispatch")) patterns.push("Delivery pipeline");
153
295
  if (has("test") || has("spec")) patterns.push("Dedicated test infrastructure");
154
-
296
+
297
+ // Structural patterns from dep graph (not keyword-based)
298
+ if (depGraph?.stats) {
299
+ if (depGraph.stats.cycles === 0 && depGraph.stats.totalEdges > 10) {
300
+ patterns.push("Acyclic dependency graph — clean layering");
301
+ }
302
+ if (hasCycles) {
303
+ patterns.push(`⚠️ ${depGraph.stats.cycles} circular dependency cycle(s) detected`);
304
+ }
305
+ }
306
+
155
307
  return patterns;
156
308
  }
157
309
 
158
- export function buildModuleContext(modules, config) {
310
+ /**
311
+ * Generate a dependency-aware role description for a module.
312
+ */
313
+ function describeModuleDepRole(moduleKey, moduleMetrics) {
314
+ const m = moduleMetrics.get(moduleKey);
315
+ if (!m) return null;
316
+
317
+ if (m.isOrphan) return "Isolated (no imports or importers)";
318
+ if (m.isHub && m.fanIn >= 15) return `Critical shared infrastructure (imported by ${m.fanIn} modules)`;
319
+ if (m.isHub) return `Shared infrastructure (imported by ${m.fanIn} modules)`;
320
+ if (m.isOrchestrator) return `Orchestrator (coordinates ${m.fanOut} modules)`;
321
+ if (m.isLeaf) return `Leaf module (consumed only, ${m.fanIn} importer${m.fanIn === 1 ? "" : "s"})`;
322
+ if (m.fanIn > 0 && m.fanOut > 0) return `Connector (${m.fanIn} in, ${m.fanOut} out)`;
323
+ return null;
324
+ }
325
+
326
+ export { describeModuleDepRole };
327
+
328
+ export function buildModuleContext(modules, config, depGraph = null) {
159
329
  const customHints = config.domains
160
330
  ? Object.entries(config.domains).map(([key, domain]) => ({
161
331
  match: domain.match || [],
@@ -165,6 +335,7 @@ export function buildModuleContext(modules, config) {
165
335
  : [];
166
336
 
167
337
  const domainGroups = groupModulesByDomain(modules, customHints);
338
+ const moduleMetrics = computeModuleLevelMetrics(depGraph, modules);
168
339
 
169
340
  return modules.map(module => {
170
341
  const domain = domainGroups.find(d =>
@@ -175,6 +346,7 @@ export function buildModuleContext(modules, config) {
175
346
  key: module.key,
176
347
  fileCount: module.fileCount,
177
348
  type: inferModuleType(module.key),
349
+ depRole: describeModuleDepRole(module.key, moduleMetrics),
178
350
  domain: domain?.name || "Other",
179
351
  domainDescription: domain?.description || "Uncategorized"
180
352
  };
@@ -1,27 +1,134 @@
1
- // Infer data flows through the application using heuristics
1
+ // Infer data flows through the application using import graph analysis + heuristics
2
2
 
3
- export function inferDataFlows(scanResult, config) {
3
+ /**
4
+ * Primary flow inference: uses dep graph when available for real import-chain flows,
5
+ * falls back to keyword heuristics for repos without dep graph data.
6
+ */
7
+ export function inferDataFlows(scanResult, config, depGraph = null) {
4
8
  const flows = [];
5
9
 
6
- // Stock/Market Data Flow (if applicable)
10
+ // If we have dep graph data, build real import-chain flows from entry points
11
+ if (depGraph?.nodes && depGraph.nodes.length > 0) {
12
+ const entryFlows = inferFlowsFromEntryPoints(scanResult, depGraph);
13
+ flows.push(...entryFlows);
14
+ }
15
+
16
+ // Keyword-based heuristic flows (only add if not already covered by import-chain analysis)
17
+ const existingNames = new Set(flows.map(f => f.name.toLowerCase()));
18
+
7
19
  const marketFlow = inferMarketDataFlow(scanResult);
8
- if (marketFlow) flows.push(marketFlow);
20
+ if (marketFlow && !existingNames.has(marketFlow.name.toLowerCase())) flows.push(marketFlow);
9
21
 
10
- // Authentication Flow
11
22
  const authFlow = inferAuthFlow(scanResult);
12
- if (authFlow) flows.push(authFlow);
23
+ if (authFlow && !existingNames.has(authFlow.name.toLowerCase())) flows.push(authFlow);
13
24
 
14
- // Content/Article Flow
15
25
  const contentFlow = inferContentFlow(scanResult);
16
- if (contentFlow) flows.push(contentFlow);
26
+ if (contentFlow && !existingNames.has(contentFlow.name.toLowerCase())) flows.push(contentFlow);
17
27
 
18
- // API Integration Flow
19
28
  const apiFlow = inferApiIntegrationFlow(scanResult);
20
- if (apiFlow) flows.push(apiFlow);
29
+ if (apiFlow && !existingNames.has(apiFlow.name.toLowerCase())) flows.push(apiFlow);
21
30
 
22
31
  return flows;
23
32
  }
24
33
 
34
+ /**
35
+ * Trace real import chains from entry points through the dep graph.
36
+ * Entry points: CLI/bin files, page components, API route handlers, index files.
37
+ */
38
+ function inferFlowsFromEntryPoints(scanResult, depGraph) {
39
+ const flows = [];
40
+ if (!depGraph?.nodes) return flows;
41
+
42
+ // Find entry points (files with high fan-out and low/zero fan-in)
43
+ const entryPoints = depGraph.nodes.filter(n => {
44
+ const key = n.key.toLowerCase();
45
+ const isEntry = (
46
+ key.includes("bin/") ||
47
+ key.includes("cli") ||
48
+ key.endsWith("/index") ||
49
+ key.includes("pages/") ||
50
+ key.includes("app/") ||
51
+ (n.importedBy.length === 0 && n.imports.length >= 2) // True entry: nothing imports it
52
+ );
53
+ return isEntry && n.imports.length >= 2;
54
+ }).sort((a, b) => {
55
+ // Prioritize src/ entry points over tests/
56
+ const aIsTest = a.key.toLowerCase().includes("test");
57
+ const bIsTest = b.key.toLowerCase().includes("test");
58
+ if (aIsTest !== bIsTest) return aIsTest ? 1 : -1;
59
+ return b.imports.length - a.imports.length;
60
+ });
61
+
62
+ // Limit: all src entries + max 2 test entries
63
+ const srcEntries = entryPoints.filter(n => !n.key.toLowerCase().includes("test"));
64
+ const testEntries = entryPoints.filter(n => n.key.toLowerCase().includes("test")).slice(0, 2);
65
+ const selectedEntries = [...srcEntries, ...testEntries].slice(0, 5);
66
+
67
+ for (const entry of selectedEntries) {
68
+ const chain = traceImportChain(entry.key, depGraph, 6);
69
+ if (chain.length < 2) continue;
70
+
71
+ const shortName = entry.key.split("/").pop();
72
+ const isCliEntry = entry.key.toLowerCase().includes("cli") || entry.key.toLowerCase().includes("bin/");
73
+ const flowType = isCliEntry ? "Command" : "Request";
74
+
75
+ // Classify each step in the chain
76
+ const steps = chain.map((nodeKey, i) => {
77
+ const node = depGraph.nodes.find(n => n.key === nodeKey);
78
+ const name = nodeKey.split("/").pop();
79
+ const fanIn = node?.importedBy?.length || 0;
80
+ const fanOut = node?.imports?.length || 0;
81
+
82
+ if (i === 0) return `\`${name}\` (entry point, imports ${fanOut} modules)`;
83
+ if (fanIn >= 5) return `\`${name}\` (shared infrastructure, used by ${fanIn} files)`;
84
+ if (fanOut === 0) return `\`${name}\` (leaf — no further dependencies)`;
85
+ return `\`${name}\` → imports ${fanOut} module${fanOut === 1 ? "" : "s"}`;
86
+ });
87
+
88
+ flows.push({
89
+ name: `${shortName} ${flowType} Flow`,
90
+ description: `Import chain starting from \`${entry.key}\` — traces how this entry point depends on ${chain.length - 1} downstream module${chain.length - 1 === 1 ? "" : "s"}`,
91
+ steps,
92
+ modules: chain.slice(0, 8),
93
+ critical: entry.imports.length >= 5,
94
+ });
95
+ }
96
+
97
+ return flows;
98
+ }
99
+
100
+ /**
101
+ * Trace from an entry node through the dep graph via BFS, following highest-fan-out paths.
102
+ * Returns an ordered chain of module keys representing the primary dependency path.
103
+ */
104
+ function traceImportChain(startKey, depGraph, maxDepth = 6) {
105
+ const chain = [startKey];
106
+ const visited = new Set([startKey]);
107
+ let current = startKey;
108
+
109
+ for (let depth = 0; depth < maxDepth; depth++) {
110
+ const node = depGraph.nodes.find(n => n.key === current);
111
+ if (!node || node.imports.length === 0) break;
112
+
113
+ // Follow the most-imported (highest fan-in) dependency first — it's the most interesting path
114
+ const candidates = node.imports
115
+ .filter(imp => !visited.has(imp))
116
+ .map(imp => {
117
+ const target = depGraph.nodes.find(n => n.key === imp);
118
+ return { key: imp, fanIn: target?.importedBy?.length || 0 };
119
+ })
120
+ .sort((a, b) => b.fanIn - a.fanIn);
121
+
122
+ if (candidates.length === 0) break;
123
+
124
+ current = candidates[0].key;
125
+ visited.add(current);
126
+ chain.push(current);
127
+ }
128
+
129
+ return chain;
130
+ }
131
+
25
132
  function inferMarketDataFlow(scanResult) {
26
133
  const { modules, pages, api } = scanResult;
27
134
 
@@ -31,7 +31,7 @@ const SCHEMA_LIBRARY_PATTERNS = [
31
31
  { name: "Apollo Server", pattern: /ApolloServer|@apollo\/server|apollo-server/ },
32
32
  { name: "GraphQL Yoga", pattern: /graphql-yoga|createYoga/ },
33
33
  { name: "Mercurius", pattern: /mercurius/ },
34
- { name: "graphql-js", pattern: /graphql\b.*buildSchema|GraphQLSchema|GraphQLObjectType/ },
34
+ { name: "graphql-js", pattern: /graphql\b.*\bbuildSchema\b|\bGraphQLSchema\b|\bGraphQLObjectType\b/ },
35
35
  { name: "type-graphql", pattern: /type-graphql|@Resolver|@Query|@Mutation/ },
36
36
  { name: "Nexus", pattern: /nexus|makeSchema|objectType\(/ },
37
37
  { name: "Pothos", pattern: /pothos|SchemaBuilder/ },
@@ -84,8 +84,13 @@ export async function analyzeGraphQL(files, repoRoot) {
84
84
  const schemaFiles = files.filter(f => GRAPHQL_EXTENSIONS.some(ext => f.endsWith(ext)));
85
85
 
86
86
  // Phase 2: Find JS/TS files with inline schema or resolvers
87
+ // Exclude test files and our own analysis/rendering files (they reference library names as strings, not usage)
88
+ const testPattern = /(?:^|\/)(?:tests?|__tests?__|spec|__spec__)\/|\.(test|spec)\.[jt]sx?$/i;
89
+ const selfPatterns = ["graphql-analyzer.js", "renderAnalysis.js"];
87
90
  const codeFiles = files.filter(f =>
88
- f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".jsx") || f.endsWith(".tsx")
91
+ (f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".jsx") || f.endsWith(".tsx")) &&
92
+ !testPattern.test(f) &&
93
+ !selfPatterns.some(s => f.endsWith(s))
89
94
  );
90
95
 
91
96
  // Parse .graphql/.gql files
@@ -33,10 +33,10 @@ import path from "node:path";
33
33
  export async function generateDocumentSet(scanResult, config, diffData = null, pluginManager = null) {
34
34
  info("Building structured context for AI...");
35
35
 
36
- // Build AI context from scan results
37
- const aiContext = buildAIContext(scanResult, config);
38
- const moduleContext = buildModuleContext(scanResult.modules, config);
39
- const flows = inferDataFlows(scanResult, config);
36
+ // Build AI context from scan results (dep graph computed later, patched in after)
37
+ let aiContext = buildAIContext(scanResult, config);
38
+ let moduleContext = buildModuleContext(scanResult.modules, config);
39
+ let flows = inferDataFlows(scanResult, config);
40
40
 
41
41
  // Run extended analysis (v0.8.0)
42
42
  const repoRoot = config.__repoRoot || process.cwd();
@@ -63,6 +63,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
63
63
  await saveBaseline(snapshot, outputDir);
64
64
  } catch (e) { warn(`Drift detection failed: ${e.message}`); }
65
65
 
66
+ // Rebuild AI context with dep graph for enriched module roles and pattern verification
67
+ aiContext = buildAIContext(scanResult, config, depGraph);
68
+ moduleContext = buildModuleContext(scanResult.modules, config, depGraph);
69
+ flows = inferDataFlows(scanResult, config, depGraph);
70
+
66
71
  // CODEOWNERS integration
67
72
  const codeowners = await parseCodeowners(repoRoot);
68
73
  const ownershipMap = codeowners.found
@@ -196,19 +201,19 @@ async function generateDocument(docPlan, context) {
196
201
  return await generateArchitectureOverview(aiContext, { depGraph, driftResult });
197
202
 
198
203
  case "module_catalog":
199
- // Hybrid: deterministic skeleton + ownership info
200
- return renderModuleCatalogOriginal(config, scanResult, ownershipMap);
204
+ // Hybrid: deterministic skeleton + ownership info + dep-graph roles
205
+ return renderModuleCatalogOriginal(config, scanResult, ownershipMap, depGraph);
201
206
 
202
207
  case "route_map":
203
- // Hybrid: deterministic skeleton + AI enhancement (for now, just deterministic)
204
- return renderRouteMapOriginal(config, scanResult);
208
+ // Hybrid: deterministic skeleton + context-aware messaging
209
+ return renderRouteMapOriginal(config, scanResult, aiContext);
205
210
 
206
211
  case "api_surface":
207
212
  // Hybrid: deterministic skeleton + AI enhancement (for now, just deterministic)
208
213
  return renderApiSurfaceOriginal(config, scanResult);
209
214
 
210
215
  case "data_flows":
211
- return await generateDataFlows(flows, aiContext, { depGraph, scanResult });
216
+ return await generateDataFlows(flows, aiContext, { depGraph, scanResult, moduleContext });
212
217
 
213
218
  case "arch_diff":
214
219
  if (!diffData) {
@@ -1,4 +1,6 @@
1
- export function renderSystemOverview(cfg, scan) {
1
+ import { computeModuleLevelMetrics, describeModuleDepRole } from "../analyzers/context-builder.js";
2
+
3
+ export function renderSystemOverview(cfg, scan, depGraph = null) {
2
4
  const projectName = cfg.project?.name || "Project";
3
5
  const date = new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
4
6
 
@@ -17,8 +19,8 @@ export function renderSystemOverview(cfg, scan) {
17
19
  `|--------|-------|`,
18
20
  `| Files scanned | ${scan.filesCount} |`,
19
21
  `| Modules detected | ${scan.modules.length} |`,
20
- `| Application pages | ${scan.pages?.length || 0} |`,
21
- `| API endpoints | ${scan.api.length} |`,
22
+ ...(scan.pages?.length ? [`| Application pages | ${scan.pages.length} |`] : []),
23
+ ...(scan.api.length ? [`| API endpoints | ${scan.api.length} |`] : []),
22
24
  ``
23
25
  ];
24
26
 
@@ -35,9 +37,9 @@ export function renderSystemOverview(cfg, scan) {
35
37
  `| Category | Technologies |`,
36
38
  `|----------|-------------|`
37
39
  );
38
- if (frameworks.length) lines.push(`| Frameworks | ${frameworks.join(", ")} |`);
40
+ lines.push(`| Frameworks | ${frameworks.join(", ") || 'N/A'} |`);
39
41
  if (languages.length) lines.push(`| Languages | ${languages.join(", ")} |`);
40
- if (buildTools.length) lines.push(`| Build Tools | ${buildTools.join(", ")} |`);
42
+ lines.push(`| Build Tools | ${buildTools.join(", ") || 'N/A'} |`);
41
43
  if (testFrameworks.length) lines.push(`| Testing | ${testFrameworks.join(", ")} |`);
42
44
  lines.push(``);
43
45
  }
@@ -53,8 +55,8 @@ export function renderSystemOverview(cfg, scan) {
53
55
  `## Architecture Summary`,
54
56
  ``,
55
57
  `The repository is organized as ${sizeDesc} with **${scan.modules.length} modules** spanning **${scan.filesCount} files**. `
56
- + (scan.api.length > 0 ? `It exposes **${scan.api.length} API endpoint${scan.api.length === 1 ? "" : "s"}** ` : "")
57
- + (scan.pages?.length > 0 ? `and serves **${scan.pages.length} application page${scan.pages.length === 1 ? "" : "s"}**. ` : ". ")
58
+ + (scan.api.length > 0 ? `It exposes **${scan.api.length} API endpoint${scan.api.length === 1 ? "" : "s"}**. ` : "")
59
+ + (scan.pages?.length > 0 ? `It serves **${scan.pages.length} application page${scan.pages.length === 1 ? "" : "s"}**. ` : "")
58
60
  + `The largest modules are listed below, ranked by file count.`,
59
61
  ``
60
62
  );
@@ -62,6 +64,7 @@ export function renderSystemOverview(cfg, scan) {
62
64
 
63
65
  // Largest modules as a table instead of bullets
64
66
  const topModules = scan.modules.slice(0, 10);
67
+ const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
65
68
  if (topModules.length > 0) {
66
69
  lines.push(
67
70
  `## Largest Modules`,
@@ -70,7 +73,8 @@ export function renderSystemOverview(cfg, scan) {
70
73
  `|--------|-------|-------------|`
71
74
  );
72
75
  for (const m of topModules) {
73
- const desc = describeModule(m.key);
76
+ const depRole = describeModuleDepRole(m.key, moduleMetrics);
77
+ const desc = describeModule(m.key, depRole);
74
78
  lines.push(`| \`${m.key}\` | ${m.fileCount} | ${desc} |`);
75
79
  }
76
80
  lines.push(``);
@@ -105,36 +109,50 @@ export function renderSystemOverview(cfg, scan) {
105
109
  return lines.join("\n");
106
110
  }
107
111
 
108
- function describeModule(key) {
112
+ function describeModule(key, depRole = null) {
109
113
  const normalized = key.toLowerCase();
110
- if (normalized.includes("core")) return "Core business logic and shared foundations";
111
- if (normalized.includes("util")) return "Shared utilities and helper functions";
112
- if (normalized.includes("api")) return "API route handlers and endpoint definitions";
113
- if (normalized.includes("component")) return "Reusable UI components";
114
- if (normalized.includes("hook")) return "Custom React hooks";
115
- if (normalized.includes("page")) return "Application page components";
116
- if (normalized.includes("lib")) return "Library code and third-party integrations";
117
- if (normalized.includes("service")) return "Service layer and external integrations";
118
- if (normalized.includes("model")) return "Data models and schema definitions";
119
- if (normalized.includes("store") || normalized.includes("state")) return "State management";
120
- if (normalized.includes("config")) return "Configuration and settings";
121
- if (normalized.includes("test")) return "Test suites and fixtures";
122
- if (normalized.includes("style") || normalized.includes("css")) return "Styling and design tokens";
123
- if (normalized.includes("type")) return "Type definitions and interfaces";
124
- if (normalized.includes("middleware")) return "Request middleware and interceptors";
125
- if (normalized.includes("auth")) return "Authentication and authorization";
126
- if (normalized.includes("render")) return "Rendering logic and output formatters";
127
- if (normalized.includes("publish")) return "Publishing and delivery integrations";
128
- if (normalized.includes("analyz")) return "Code analysis and intelligence";
129
- if (normalized.includes("delivery")) return "Content delivery and distribution";
130
- if (normalized.includes("integrat")) return "Third-party service integrations";
131
- if (normalized.includes("doc")) return "Documentation generation";
132
- if (normalized.includes("bin") || normalized.includes("cli")) return "CLI entry point and commands";
133
- return "Application module";
114
+ // Base description from path keywords
115
+ let base;
116
+ if (normalized.includes("core")) base = "Core business logic and shared foundations";
117
+ else if (normalized.includes("util")) base = "Shared utilities and helper functions";
118
+ else if (normalized.includes("api")) base = "API route handlers and endpoint definitions";
119
+ else if (normalized.includes("component")) base = "Reusable UI components";
120
+ else if (normalized.includes("hook")) base = "Custom React hooks";
121
+ else if (normalized.includes("page")) base = "Application page components";
122
+ else if (normalized.includes("lib")) base = "Library code and third-party integrations";
123
+ else if (normalized.includes("service")) base = "Service layer and external integrations";
124
+ else if (normalized.includes("model")) base = "Data models and schema definitions";
125
+ else if (normalized.includes("store") || normalized.includes("state")) base = "State management";
126
+ else if (normalized.includes("config")) base = "Configuration and settings";
127
+ else if (normalized.includes("test")) base = "Test suites and fixtures";
128
+ else if (normalized.includes("style") || normalized.includes("css")) base = "Styling and design tokens";
129
+ else if (normalized.includes("type")) base = "Type definitions and interfaces";
130
+ else if (normalized.includes("middleware")) base = "Request middleware and interceptors";
131
+ else if (normalized.includes("auth")) base = "Authentication and authorization";
132
+ else if (normalized.includes("render")) base = "Rendering logic and output formatters";
133
+ else if (normalized.includes("publish")) base = "Publishing and delivery integrations";
134
+ else if (normalized.includes("analyz")) base = "Code analysis and intelligence";
135
+ else if (normalized.includes("delivery")) base = "Content delivery and distribution";
136
+ else if (normalized.includes("integrat")) base = "Third-party service integrations";
137
+ else if (normalized.includes("doc")) base = "Documentation generation";
138
+ else if (normalized.includes("bin") || normalized.includes("cli")) base = "CLI entry point and commands";
139
+ else if (normalized.includes("plugin") || normalized.includes("extension")) base = "Plugin system and extensions";
140
+ else if (normalized.includes("prompt")) base = "Prompt engineering and templates";
141
+ else if (normalized.includes("provider")) base = "Service provider adapters";
142
+ else if (normalized.includes("generate") || normalized.includes("section")) base = "Content generation pipeline";
143
+ else if (normalized.includes("ai") || normalized.includes("ml")) base = "AI and machine learning integration";
144
+ else base = "Application module";
145
+
146
+ // Enrich with dependency role if available
147
+ if (depRole) {
148
+ return `${base} · ${depRole}`;
149
+ }
150
+ return base;
134
151
  }
135
152
 
136
- export function renderModuleCatalog(cfg, scan, ownershipMap = {}) {
153
+ export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = null) {
137
154
  const hasOwnership = Object.keys(ownershipMap).length > 0;
155
+ const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
138
156
  const lines = [
139
157
  `# Module Catalog`,
140
158
  ``,
@@ -172,7 +190,8 @@ export function renderModuleCatalog(cfg, scan, ownershipMap = {}) {
172
190
  }
173
191
 
174
192
  for (const module of scan.modules.slice(0, 100)) {
175
- const desc = describeModule(module.key);
193
+ const depRole = describeModuleDepRole(module.key, moduleMetrics);
194
+ const desc = describeModule(module.key, depRole);
176
195
  const owners = ownershipMap[module.key];
177
196
  if (hasOwnership) {
178
197
  lines.push(`| \`${module.key}\` | ${module.fileCount} | ${desc} | ${owners ? owners.join(", ") : "—"} |`);
@@ -277,7 +296,7 @@ export function renderApiSurface(cfg, scan) {
277
296
  return lines.join("\n");
278
297
  }
279
298
 
280
- export function renderRouteMap(cfg, scan) {
299
+ export function renderRouteMap(cfg, scan, aiContext = null) {
281
300
  const lines = [
282
301
  `# Route Map`,
283
302
  ``,
@@ -330,30 +349,61 @@ export function renderRouteMap(cfg, scan) {
330
349
  }
331
350
 
332
351
  if (!scan.pages?.length && !scan.api?.length) {
352
+ // Determine project type for context-aware messaging
353
+ const patterns = aiContext?.patterns || [];
354
+ const isCLI = patterns.some(p => p.toLowerCase().includes("cli"));
355
+ const isLibrary = patterns.some(p => p.toLowerCase().includes("library") || p.toLowerCase().includes("shared"));
356
+ const techStack = aiContext?.techStack || {};
357
+ const hasWebFramework = (techStack.frameworks || []).some(f =>
358
+ /next|react|vue|angular|express|fastify|hono|koa|django|flask|rails|spring/i.test(f)
359
+ );
360
+
333
361
  lines.push(
334
362
  `## Route Detection`,
335
- ``,
336
- `No routes were auto-detected in this scan. RepoLens currently supports:`,
337
- ``,
338
- `| Framework | Pattern | Status |`,
339
- `|-----------|---------|--------|`,
340
- `| Next.js | \`pages/\` and \`app/\` directories | Supported |`,
341
- `| Next.js API | \`pages/api/\` and App Router | Supported |`,
342
- `| Express.js | \`app.get\`, \`router.post\`, etc. | Supported |`,
343
- `| React Router | \`<Route>\` components | Supported |`,
344
- `| Vue Router | \`routes\` array definitions | Supported |`,
345
- ``,
346
- `If your project uses a different routing framework, open an issue at [github.com/CHAPIBUNNY/repolens](https://github.com/CHAPIBUNNY/repolens/issues) to request support.`,
347
363
  ``
348
364
  );
365
+
366
+ if (isCLI) {
367
+ lines.push(
368
+ `This project is a **CLI tool** — it does not expose HTTP routes or serve web pages. This is expected behavior, not a detection failure.`,
369
+ ``,
370
+ `CLI tools interact through terminal commands rather than URLs. See the **System Overview** or **Developer Onboarding** documents for command documentation.`,
371
+ ``
372
+ );
373
+ } else if (isLibrary && !hasWebFramework) {
374
+ lines.push(
375
+ `This project is a **library/package** — it does not define its own routes or pages. Libraries are consumed by other applications that define their own routing.`,
376
+ ``
377
+ );
378
+ } else {
379
+ lines.push(
380
+ `No routes were auto-detected in this scan. RepoLens currently supports:`,
381
+ ``,
382
+ `| Framework | Pattern | Status |`,
383
+ `|-----------|---------|--------|`,
384
+ `| Next.js | \`pages/\` and \`app/\` directories | Supported |`,
385
+ `| Next.js API | \`pages/api/\` and App Router | Supported |`,
386
+ `| Express.js | \`app.get\`, \`router.post\`, etc. | Supported |`,
387
+ `| Fastify | \`fastify.get\`, \`fastify.post\`, etc. | Supported |`,
388
+ `| Hono | \`app.get\`, \`app.post\`, etc. | Supported |`,
389
+ `| React Router | \`<Route>\` components | Supported |`,
390
+ `| Vue Router | \`routes\` array definitions | Supported |`,
391
+ ``,
392
+ `If your project uses a different routing framework, open an issue at [github.com/CHAPIBUNNY/repolens](https://github.com/CHAPIBUNNY/repolens/issues) to request support.`,
393
+ ``
394
+ );
395
+ }
349
396
  }
350
397
 
351
- lines.push(
352
- `---`,
353
- ``,
354
- `*Paths starting with \`/api/\` are backend endpoints; all others are user-facing pages.*`,
355
- ``
356
- );
398
+ // Only add the route hint footer for projects that actually have routes
399
+ if (scan.pages?.length || scan.api?.length) {
400
+ lines.push(
401
+ `---`,
402
+ ``,
403
+ `*Paths starting with \`/api/\` are backend endpoints; all others are user-facing pages.*`,
404
+ ``
405
+ );
406
+ }
357
407
 
358
408
  return lines.join("\n");
359
409
  }
@@ -31,6 +31,20 @@ function routePathFromFile(file) {
31
31
  return file;
32
32
  }
33
33
 
34
+ // Files/dirs to exclude from impacted modules (build artifacts, lockfiles, etc.)
35
+ const NOISE_PATTERNS = [
36
+ /^node_modules/,
37
+ /^package-lock\.json$/,
38
+ /^yarn\.lock$/,
39
+ /^pnpm-lock\.yaml$/,
40
+ /^\.repolens\//,
41
+ /^\.git\//,
42
+ ];
43
+
44
+ function isNoiseFile(file) {
45
+ return NOISE_PATTERNS.some(p => p.test(file));
46
+ }
47
+
34
48
  function moduleFromFile(file) {
35
49
  const normalized = file.replace(/\\/g, "/");
36
50
  const parts = normalized.split("/");
@@ -50,14 +64,20 @@ export function buildArchitectureDiffData(diff) {
50
64
  const addedRoutes = diff.added.filter(isRouteFile).map(routePathFromFile);
51
65
  const removedRoutes = diff.removed.filter(isRouteFile).map(routePathFromFile);
52
66
 
67
+ // Filter out noise files (node_modules, lockfiles, etc.) from all lists
68
+ const added = diff.added.filter(f => !isNoiseFile(f));
69
+ const removed = diff.removed.filter(f => !isNoiseFile(f));
70
+ const modified = diff.modified.filter(f => !isNoiseFile(f));
71
+ const allFiles = [...added, ...removed, ...modified];
53
72
  const impactedModules = [
54
- ...new Set(
55
- [...diff.added, ...diff.removed, ...diff.modified].map(moduleFromFile)
56
- )
73
+ ...new Set(allFiles.map(moduleFromFile))
57
74
  ].sort();
58
75
 
59
76
  return {
60
77
  ...diff,
78
+ added,
79
+ removed,
80
+ modified,
61
81
  addedRoutes,
62
82
  removedRoutes,
63
83
  impactedModules