@chappibunny/repolens 1.5.3 โ†’ 1.6.1

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,24 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.6.0
6
+
7
+ ### โœจ Features
8
+
9
+ - **Deterministic enrichment**: All 6 AI-enhanced document types now produce significantly richer output when running without AI integration. Fallback generators receive dependency graph stats, data flows, monorepo info, routes, drift results, and module type classifications.
10
+ - **Executive Summary**: Now includes module composition breakdown, monorepo structure, codebase health metrics (cycles, orphans, hubs), data flow summaries, and test framework info.
11
+ - **System Overview**: Now includes module architecture by type, route summary (pages + APIs), dependency graph stats with hub modules, and monorepo info.
12
+ - **Business Domains**: Now maps routes to domains and shows cross-domain dependency hubs.
13
+ - **Architecture Overview**: Now includes module layers by type, dependency health with strengths/concerns heuristics, hub modules, architecture drift summary, and monorepo architecture.
14
+ - **Data Flows**: Now generates additional flows from dependency graph hub chains when heuristic flows are sparse, shows shared library and external service dependencies per flow, and includes import network summary.
15
+ - **Developer Onboarding**: Now includes monorepo navigation, key routes to explore, data flow overview, integration point warnings, module type classification in the module table, and test framework quickstart.
16
+ - **Activated dead code**: `identifyFlowDependencies()` from `flow-inference.js` is now imported and used in the data flows fallback to enrich each flow with shared library and external dependency context.
17
+
18
+ ### ๐Ÿงช Test Coverage
19
+
20
+ - **Deterministic enrichment tests** (`tests/deterministic-enrichment.test.js`): 36 tests covering all 6 enriched fallback generators โ€” module composition, monorepo sections, dependency health, route summaries, data flow generation from dep graph, backward compatibility.
21
+ - **347 tests** passing across **22 test files** (up from 311/21).
22
+
5
23
  ## 1.5.3
6
24
 
7
25
  ### ๐Ÿ”ง Improvements
package/README.md CHANGED
@@ -25,7 +25,7 @@ RepoLens scans your repository, generates living architecture documentation, and
25
25
 
26
26
  > **Try it now** โ€” no installation required. Run `npx @chappibunny/repolens demo` on any repo for an instant local preview.
27
27
 
28
- [![RepoLens Demo](https://img.youtube.com/vi/hNd7xim_rTE/maxresdefault.jpg)](https://youtu.be/hNd7xim_rTE)
28
+ [![RepoLens Demo](https://img.youtube.com/vi/Lpyg0dGsiws/maxresdefault.jpg)](https://youtu.be/Lpyg0dGsiws)
29
29
 
30
30
  โ–ถ๏ธ *Click to watch on YouTube*
31
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.5.3",
3
+ "version": "1.6.1",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,10 +64,8 @@
64
64
  "js-yaml": "^4.1.0",
65
65
  "node-fetch": "^3.3.2"
66
66
  },
67
- "optionalDependencies": {
68
- "@mermaid-js/mermaid-cli": "^11.12.0"
69
- },
70
67
  "devDependencies": {
68
+ "tinyexec": "1.0.2",
71
69
  "vitest": "^4.0.18"
72
70
  }
73
71
  }
@@ -15,6 +15,7 @@ import {
15
15
  AI_SCHEMAS,
16
16
  renderStructuredToMarkdown,
17
17
  } from "./prompts.js";
18
+ import { identifyFlowDependencies } from "../analyzers/flow-inference.js";
18
19
  import { info, warn } from "../utils/logger.js";
19
20
 
20
21
  /**
@@ -62,68 +63,70 @@ async function generateWithStructuredFallback(key, promptText, maxTokens, fallba
62
63
  return result.text;
63
64
  }
64
65
 
65
- export async function generateExecutiveSummary(context) {
66
+ export async function generateExecutiveSummary(context, enrichment = {}) {
66
67
  return generateWithStructuredFallback(
67
68
  "executive_summary",
68
69
  createExecutiveSummaryPrompt(context),
69
70
  1500,
70
- () => getFallbackExecutiveSummary(context),
71
+ () => getFallbackExecutiveSummary(context, enrichment),
71
72
  );
72
73
  }
73
74
 
74
- export async function generateSystemOverview(context) {
75
+ export async function generateSystemOverview(context, enrichment = {}) {
75
76
  return generateWithStructuredFallback(
76
77
  "system_overview",
77
78
  createSystemOverviewPrompt(context),
78
79
  1200,
79
- () => getFallbackSystemOverview(context),
80
+ () => getFallbackSystemOverview(context, enrichment),
80
81
  );
81
82
  }
82
83
 
83
- export async function generateBusinessDomains(context) {
84
+ export async function generateBusinessDomains(context, enrichment = {}) {
84
85
  return generateWithStructuredFallback(
85
86
  "business_domains",
86
87
  createBusinessDomainsPrompt(context),
87
88
  2000,
88
- () => getFallbackBusinessDomains(context),
89
+ () => getFallbackBusinessDomains(context, enrichment),
89
90
  );
90
91
  }
91
92
 
92
- export async function generateArchitectureOverview(context) {
93
+ export async function generateArchitectureOverview(context, enrichment = {}) {
93
94
  return generateWithStructuredFallback(
94
95
  "architecture_overview",
95
96
  createArchitectureOverviewPrompt(context),
96
97
  1800,
97
- () => getFallbackArchitectureOverview(context),
98
+ () => getFallbackArchitectureOverview(context, enrichment),
98
99
  );
99
100
  }
100
101
 
101
- export async function generateDataFlows(flows, context) {
102
+ export async function generateDataFlows(flows, context, enrichment = {}) {
102
103
  return generateWithStructuredFallback(
103
104
  "data_flows",
104
105
  createDataFlowsPrompt(flows, context),
105
106
  1800,
106
- () => getFallbackDataFlows(flows),
107
+ () => getFallbackDataFlows(flows, context, enrichment),
107
108
  );
108
109
  }
109
110
 
110
- export async function generateDeveloperOnboarding(context) {
111
+ export async function generateDeveloperOnboarding(context, enrichment = {}) {
111
112
  return generateWithStructuredFallback(
112
113
  "developer_onboarding",
113
114
  createDeveloperOnboardingPrompt(context),
114
115
  2200,
115
- () => getFallbackDeveloperOnboarding(context),
116
+ () => getFallbackDeveloperOnboarding(context, enrichment),
116
117
  );
117
118
  }
118
119
 
119
120
  // Fallback generators (deterministic, no AI)
120
121
 
121
- function getFallbackExecutiveSummary(context) {
122
+ function getFallbackExecutiveSummary(context, enrichment = {}) {
123
+ const { depGraph, flows } = enrichment;
122
124
  const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose";
123
125
  const languageList = context.techStack.languages.join(", ") || "multiple languages";
124
126
  const domainSummary = context.domains.slice(0, 5).map(d => d.name).join(", ");
127
+ const testFrameworks = context.techStack.testFrameworks || [];
125
128
 
126
- return `# Executive Summary
129
+ let output = `# Executive Summary
127
130
 
128
131
  ## What This System Does
129
132
 
@@ -146,21 +149,72 @@ ${context.domains.map(d => `| ${d.name} | ${d.moduleCount} | ${d.description ||
146
149
  | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
147
150
  | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
148
151
  | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
152
+ ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}`;
153
+
154
+ // Module type breakdown
155
+ const typeGroups = groupModulesByType(context.topModules);
156
+ if (Object.keys(typeGroups).length > 1) {
157
+ output += `\n## Module Composition\n\n`;
158
+ output += `| Layer | Modules | Examples |\n`;
159
+ output += `|-------|---------|----------|\n`;
160
+ for (const [type, mods] of Object.entries(typeGroups)) {
161
+ const examples = mods.slice(0, 3).map(m => `\`${m.key}\``).join(", ");
162
+ output += `| ${formatModuleType(type)} | ${mods.length} | ${examples} |\n`;
163
+ }
164
+ }
149
165
 
150
- ## Key Observations
166
+ // Monorepo info
167
+ if (context.monorepo) {
168
+ output += `\n## Monorepo Structure\n\n`;
169
+ output += `This is a **${context.monorepo.tool}** monorepo containing **${context.monorepo.packageCount} package${context.monorepo.packageCount === 1 ? "" : "s"}**:\n\n`;
170
+ output += context.monorepo.packages.slice(0, 10).map(p => `- \`${p.name}\` (${p.path})`).join("\n");
171
+ output += "\n";
172
+ }
173
+
174
+ // Dependency health
175
+ if (depGraph?.stats) {
176
+ const stats = depGraph.stats;
177
+ output += `\n## Codebase Health\n\n`;
178
+ output += `| Metric | Value |\n`;
179
+ output += `|--------|-------|\n`;
180
+ output += `| Internal imports | ${stats.totalEdges} |\n`;
181
+ output += `| External dependencies | ${stats.externalDeps} |\n`;
182
+ output += `| Circular dependencies | ${stats.cycles} |\n`;
183
+ output += `| Orphan files | ${stats.orphanFiles} |\n`;
184
+ if (stats.cycles === 0) {
185
+ output += `\nThe dependency graph is **cycle-free**, indicating clean module boundaries.\n`;
186
+ } else {
187
+ output += `\nโš ๏ธ **${stats.cycles} circular dependenc${stats.cycles === 1 ? "y" : "ies"}** detected โ€” these may complicate refactoring and testing.\n`;
188
+ }
189
+ }
190
+
191
+ // Data flows
192
+ if (flows && flows.length > 0) {
193
+ 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) {
196
+ output += `- **${flow.name}**${flow.critical ? " (critical)" : ""} โ€” ${flow.description}\n`;
197
+ }
198
+ }
199
+
200
+ output += `\n## Key Observations
151
201
 
152
202
  The codebase follows ${context.patterns.length > 0 ? context.patterns.join(", ") : "standard"} architectural patterns. ${domainSummary ? `The core functional areas โ€” ${domainSummary} โ€” account for the majority of the application logic.` : ""}
153
203
 
154
204
  ---
155
205
 
156
206
  *This summary is generated deterministically from repository structure. Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for natural language insights tailored to non-technical readers.*`;
207
+
208
+ return output;
157
209
  }
158
210
 
159
- function getFallbackSystemOverview(context) {
211
+ function getFallbackSystemOverview(context, enrichment = {}) {
212
+ const { depGraph } = enrichment;
160
213
  const sizeLabel = context.project.modulesDetected > 50 ? "large-scale" :
161
214
  context.project.modulesDetected > 20 ? "medium-sized" : "focused";
215
+ const testFrameworks = context.techStack.testFrameworks || [];
162
216
 
163
- return `# System Overview
217
+ let output = `# System Overview
164
218
 
165
219
  ## Repository Snapshot
166
220
 
@@ -173,24 +227,102 @@ This is a ${sizeLabel} codebase organized into **${context.project.modulesDetect
173
227
  | Application pages | ${context.project.pagesDetected} |
174
228
  | API endpoints | ${context.project.apiRoutesDetected} |
175
229
 
230
+ ## Technology Stack
231
+
232
+ | Category | Technologies |
233
+ |----------|-------------|
234
+ | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
235
+ | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
236
+ | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
237
+ ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}
176
238
  ## Detected Patterns
177
239
 
178
240
  ${context.patterns.length > 0 ? context.patterns.map(p => `- ${p}`).join("\n") : "No specific architectural patterns were detected. This typically means the project uses a straightforward directory-based organization."}
241
+ `;
242
+
243
+ // Module type breakdown
244
+ const typeGroups = groupModulesByType(context.topModules);
245
+ if (Object.keys(typeGroups).length > 1) {
246
+ output += `\n## Module Architecture\n\n`;
247
+ output += `The codebase is organized into the following module layers:\n\n`;
248
+ for (const [type, mods] of Object.entries(typeGroups)) {
249
+ const moduleList = mods.slice(0, 5).map(m => `\`${m.key}\` (${m.fileCount} files)`).join(", ");
250
+ output += `- **${formatModuleType(type)}** (${mods.length}): ${moduleList}\n`;
251
+ }
252
+ }
179
253
 
180
- ## Dominant Domains
254
+ output += `\n## Dominant Domains
181
255
 
182
256
  The following domains represent the largest areas of the codebase by file count:
183
257
 
184
258
  | Rank | Domain | Files |
185
259
  |------|--------|-------|
186
260
  ${context.domains.slice(0, 5).map((d, i) => `| ${i + 1} | ${d.name} | ${d.fileCount} |`).join("\n")}
261
+ `;
262
+
263
+ // Route highlights
264
+ const routes = context.routes || {};
265
+ const pages = routes.pages || [];
266
+ const apis = routes.apis || [];
267
+ if (pages.length > 0 || apis.length > 0) {
268
+ output += `\n## Route Summary\n\n`;
269
+ if (pages.length > 0) {
270
+ output += `**Application Pages** (${pages.length} total):\n\n`;
271
+ for (const p of pages.slice(0, 8)) {
272
+ output += `- \`${p.path}\`\n`;
273
+ }
274
+ if (pages.length > 8) output += `- โ€ฆ and ${pages.length - 8} more\n`;
275
+ output += "\n";
276
+ }
277
+ if (apis.length > 0) {
278
+ output += `**API Endpoints** (${apis.length} total):\n\n`;
279
+ for (const a of apis.slice(0, 8)) {
280
+ const methods = a.methods ? ` [${a.methods.join(", ")}]` : "";
281
+ output += `- \`${a.path}\`${methods}\n`;
282
+ }
283
+ if (apis.length > 8) output += `- โ€ฆ and ${apis.length - 8} more\n`;
284
+ output += "\n";
285
+ }
286
+ }
187
287
 
188
- ---
288
+ // Dependency graph highlights
289
+ if (depGraph?.stats) {
290
+ const stats = depGraph.stats;
291
+ output += `## Dependency Graph\n\n`;
292
+ output += `| Metric | Value |\n`;
293
+ output += `|--------|-------|\n`;
294
+ output += `| Internal imports | ${stats.totalEdges} |\n`;
295
+ output += `| External packages | ${stats.externalDeps} |\n`;
296
+ output += `| Circular dependencies | ${stats.cycles} |\n`;
297
+ output += `| Orphan files | ${stats.orphanFiles} |\n`;
298
+ if (stats.hubs && stats.hubs.length > 0) {
299
+ output += `\n**Hub modules** (most imported):\n\n`;
300
+ for (const hub of stats.hubs.slice(0, 5)) {
301
+ output += `- \`${hub.key}\` โ€” imported by ${hub.importedBy} files\n`;
302
+ }
303
+ }
304
+ output += "\n";
305
+ }
306
+
307
+ // Monorepo
308
+ if (context.monorepo) {
309
+ output += `## Monorepo\n\n`;
310
+ output += `Managed by **${context.monorepo.tool}** with **${context.monorepo.packageCount} packages**.\n\n`;
311
+ }
312
+
313
+ output += `---
189
314
 
190
315
  *This overview is generated deterministically. Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for richer contextual explanations.*`;
316
+
317
+ return output;
191
318
  }
192
319
 
193
- function getFallbackBusinessDomains(context) {
320
+ function getFallbackBusinessDomains(context, enrichment = {}) {
321
+ const { depGraph } = enrichment;
322
+ const routes = context.routes || {};
323
+ const pages = routes.pages || [];
324
+ const apis = routes.apis || [];
325
+
194
326
  let output = `# Business Domains\n\n`;
195
327
  output += `> This document maps the codebase into business-oriented functional areas. Each domain represents a distinct area of responsibility.\n\n`;
196
328
  output += `**Total domains identified:** ${context.domains.length}\n\n`;
@@ -206,6 +338,37 @@ function getFallbackBusinessDomains(context) {
206
338
  if (domain.topModules?.length > 0) {
207
339
  output += `**Key modules:** ${domain.topModules.slice(0, 5).map(m => `\`${m}\``).join(", ")}\n\n`;
208
340
  }
341
+
342
+ // Match routes to this domain by checking if any domain module paths appear in routes
343
+ const domainModules = domain.topModules || [];
344
+ const domainPages = pages.filter(p =>
345
+ domainModules.some(m => p.path.toLowerCase().includes(m.toLowerCase().split("/").pop()))
346
+ );
347
+ const domainApis = apis.filter(a =>
348
+ domainModules.some(m => a.path.toLowerCase().includes(m.toLowerCase().split("/").pop()))
349
+ );
350
+
351
+ if (domainPages.length > 0 || domainApis.length > 0) {
352
+ output += `**Routes in this domain:**\n\n`;
353
+ for (const p of domainPages.slice(0, 5)) {
354
+ output += `- ๐Ÿ“„ \`${p.path}\`\n`;
355
+ }
356
+ for (const a of domainApis.slice(0, 5)) {
357
+ const methods = a.methods ? ` [${a.methods.join(", ")}]` : "";
358
+ output += `- ๐Ÿ”Œ \`${a.path}\`${methods}\n`;
359
+ }
360
+ output += "\n";
361
+ }
362
+ }
363
+
364
+ // Cross-domain dependency insights
365
+ if (depGraph?.stats?.hubs && depGraph.stats.hubs.length > 0) {
366
+ output += `## Cross-Domain Dependencies\n\n`;
367
+ output += `The following modules are heavily imported across the codebase, likely serving as shared infrastructure:\n\n`;
368
+ for (const hub of depGraph.stats.hubs.slice(0, 5)) {
369
+ output += `- \`${hub.key}\` โ€” imported by ${hub.importedBy} modules\n`;
370
+ }
371
+ output += "\n";
209
372
  }
210
373
 
211
374
  output += `---\n\n*Domain mapping is based on directory naming conventions. Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for natural language descriptions aimed at non-technical stakeholders.*`;
@@ -213,12 +376,13 @@ function getFallbackBusinessDomains(context) {
213
376
  return output;
214
377
  }
215
378
 
216
- function getFallbackArchitectureOverview(context) {
379
+ function getFallbackArchitectureOverview(context, enrichment = {}) {
380
+ const { depGraph, driftResult } = enrichment;
217
381
  const patternDesc = context.patterns.length > 0
218
382
  ? `The detected architectural patterns are **${context.patterns.join(", ")}**. These patterns shape how data and control flow through the system.`
219
383
  : "No specific architectural patterns were detected. The project appears to follow a straightforward directory-based organization.";
220
384
 
221
- return `# Architecture Overview
385
+ let output = `# Architecture Overview
222
386
 
223
387
  ## Architectural Style
224
388
 
@@ -231,8 +395,23 @@ The codebase is organized into ${context.domains.length} functional layers, each
231
395
  | Layer | Description |
232
396
  |-------|-------------|
233
397
  ${context.domains.slice(0, 8).map(d => `| **${d.name}** | ${d.description || "Handles a distinct functional concern"} |`).join("\n")}
398
+ `;
399
+
400
+ // Module layer breakdown by type
401
+ const typeGroups = groupModulesByType(context.topModules);
402
+ if (Object.keys(typeGroups).length > 1) {
403
+ output += `\n## Module Layers\n\n`;
404
+ output += `Modules are classified into architectural layers based on their role:\n\n`;
405
+ for (const [type, mods] of Object.entries(typeGroups)) {
406
+ output += `### ${formatModuleType(type)}\n\n`;
407
+ for (const m of mods.slice(0, 5)) {
408
+ output += `- \`${m.key}\` (${m.fileCount} files)\n`;
409
+ }
410
+ output += "\n";
411
+ }
412
+ }
234
413
 
235
- ## Technology Stack
414
+ output += `## Technology Stack
236
415
 
237
416
  | Category | Technologies |
238
417
  |----------|-------------|
@@ -243,25 +422,108 @@ ${context.domains.slice(0, 8).map(d => `| **${d.name}** | ${d.description || "Ha
243
422
  ## Scale & Complexity
244
423
 
245
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"}**.
425
+ `;
246
426
 
247
- ---
427
+ // Dependency graph health
428
+ if (depGraph?.stats) {
429
+ const stats = depGraph.stats;
430
+ output += `\n## Dependency Health\n\n`;
431
+ output += `| Metric | Value |\n`;
432
+ output += `|--------|-------|\n`;
433
+ output += `| Import edges | ${stats.totalEdges} |\n`;
434
+ output += `| External packages | ${stats.externalDeps} |\n`;
435
+ output += `| Circular dependencies | ${stats.cycles} |\n`;
436
+ output += `| Orphan files | ${stats.orphanFiles} |\n\n`;
437
+
438
+ // Strengths
439
+ const strengths = [];
440
+ const concerns = [];
441
+ if (stats.cycles === 0) strengths.push("No circular dependencies โ€” clean module boundaries");
442
+ if (stats.orphanFiles === 0) strengths.push("No orphan files โ€” all code is connected");
443
+ if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy < stats.totalFiles * 0.3)
444
+ strengths.push("No excessively coupled hubs");
445
+
446
+ if (stats.cycles > 0) concerns.push(`${stats.cycles} circular dependenc${stats.cycles === 1 ? "y" : "ies"} โ€” may hinder testing and refactoring`);
447
+ if (stats.orphanFiles > stats.totalFiles * 0.2)
448
+ concerns.push(`High orphan file ratio (${stats.orphanFiles}/${stats.totalFiles}) โ€” possible dead code`);
449
+ if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy >= stats.totalFiles * 0.3)
450
+ concerns.push(`\`${stats.hubs[0].key}\` is imported by ${stats.hubs[0].importedBy} files โ€” high coupling risk`);
451
+
452
+ if (strengths.length > 0) {
453
+ output += `**Strengths:**\n\n`;
454
+ for (const s of strengths) output += `- โœ… ${s}\n`;
455
+ output += "\n";
456
+ }
457
+ if (concerns.length > 0) {
458
+ output += `**Concerns:**\n\n`;
459
+ for (const c of concerns) output += `- โš ๏ธ ${c}\n`;
460
+ output += "\n";
461
+ }
462
+
463
+ if (stats.hubs && stats.hubs.length > 0) {
464
+ output += `**Hub modules** (most imported):\n\n`;
465
+ for (const hub of stats.hubs.slice(0, 5)) {
466
+ output += `- \`${hub.key}\` โ€” imported by ${hub.importedBy} files\n`;
467
+ }
468
+ output += "\n";
469
+ }
470
+ }
471
+
472
+ // Drift summary
473
+ if (driftResult?.drifts && driftResult.drifts.length > 0) {
474
+ output += `## Architecture Drift\n\n`;
475
+ output += `${driftResult.drifts.length} drift${driftResult.drifts.length === 1 ? "" : "s"} detected since last baseline:\n\n`;
476
+ for (const drift of driftResult.drifts.slice(0, 5)) {
477
+ output += `- **${drift.type}**: ${drift.description || drift.message || "Change detected"}\n`;
478
+ }
479
+ output += "\n";
480
+ }
481
+
482
+ // Monorepo
483
+ if (context.monorepo) {
484
+ output += `## Monorepo Architecture\n\n`;
485
+ output += `This project is a **${context.monorepo.tool}** monorepo with **${context.monorepo.packageCount} packages**:\n\n`;
486
+ for (const pkg of context.monorepo.packages.slice(0, 10)) {
487
+ output += `- \`${pkg.name}\` โ€” ${pkg.path}\n`;
488
+ }
489
+ output += "\n";
490
+ }
491
+
492
+ output += `---
248
493
 
249
494
  *This architecture overview is generated deterministically. Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for deeper architectural narratives.*`;
495
+
496
+ return output;
250
497
  }
251
498
 
252
- function getFallbackDataFlows(flows) {
499
+ function getFallbackDataFlows(flows, context, enrichment = {}) {
500
+ const { depGraph, scanResult } = enrichment;
501
+
253
502
  let output = `# Data Flows\n\n`;
254
503
  output += `> Data flows describe how information moves through the system โ€” from external inputs through processing layers to storage or presentation.\n\n`;
255
504
 
256
- if (!flows || flows.length === 0) {
505
+ // Combine heuristic flows with dep-graph-derived flows
506
+ const allFlows = [...(flows || [])];
507
+
508
+ // Generate additional flows from dependency graph hub chains
509
+ if (depGraph?.nodes && depGraph.nodes.length > 0 && allFlows.length < 3) {
510
+ const hubFlows = inferFlowsFromDepGraph(depGraph);
511
+ for (const hf of hubFlows) {
512
+ if (!allFlows.some(f => f.name === hf.name)) {
513
+ allFlows.push(hf);
514
+ }
515
+ }
516
+ }
517
+
518
+ if (allFlows.length === 0) {
257
519
  output += `No data flows were detected. This typically means the system uses straightforward requestโ€“response patterns without distinct multi-step pipelines.\n\n`;
258
520
  output += `---\n\n*Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for heuristic-based flow descriptions.*`;
259
521
  return output;
260
522
  }
261
523
 
262
- output += `**${flows.length} flow${flows.length === 1 ? "" : "s"} detected** in the codebase.\n\n---\n\n`;
524
+ output += `**${allFlows.length} flow${allFlows.length === 1 ? "" : "s"} detected** in the codebase.\n\n---\n\n`;
263
525
 
264
- for (const flow of flows) {
526
+ for (const flow of allFlows) {
265
527
  output += `## ${flow.name}\n\n`;
266
528
  output += `${flow.description}\n\n`;
267
529
  if (flow.steps && flow.steps.length > 0) {
@@ -273,6 +535,30 @@ function getFallbackDataFlows(flows) {
273
535
  if (flow.modules && flow.modules.length > 0) {
274
536
  output += `**Involved modules:** ${flow.modules.map(m => `\`${m}\``).join(", ")}\n\n`;
275
537
  }
538
+
539
+ // Add dependency context if available
540
+ if (scanResult && flow.modules) {
541
+ const deps = identifyFlowDependencies(flow, scanResult);
542
+ if (deps.sharedLibraries.length > 0) {
543
+ output += `**Shared libraries used:** ${deps.sharedLibraries.map(m => `\`${m}\``).join(", ")}\n\n`;
544
+ }
545
+ if (deps.externalDependencies.length > 0) {
546
+ output += `**External services:** ${deps.externalDependencies.join(", ")}\n\n`;
547
+ }
548
+ }
549
+ }
550
+
551
+ // Import chain summary
552
+ if (depGraph?.stats) {
553
+ output += `## Import Network\n\n`;
554
+ output += `The system has **${depGraph.stats.totalEdges} internal import edges** connecting ${depGraph.stats.totalFiles} source files.\n\n`;
555
+ if (depGraph.stats.hubs && depGraph.stats.hubs.length > 0) {
556
+ output += `Key integration points (most referenced modules):\n\n`;
557
+ for (const hub of depGraph.stats.hubs.slice(0, 5)) {
558
+ output += `- \`${hub.key}\` โ€” referenced by ${hub.importedBy} files\n`;
559
+ }
560
+ output += "\n";
561
+ }
276
562
  }
277
563
 
278
564
  output += `---\n\n*Flow detection is based on naming conventions and import patterns. Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for natural language flow narratives.*`;
@@ -280,11 +566,16 @@ function getFallbackDataFlows(flows) {
280
566
  return output;
281
567
  }
282
568
 
283
- function getFallbackDeveloperOnboarding(context) {
569
+ function getFallbackDeveloperOnboarding(context, enrichment = {}) {
570
+ const { flows, depGraph } = enrichment;
284
571
  const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose tools";
285
572
  const languageList = context.techStack.languages.join(", ") || "standard languages";
573
+ const testFrameworks = context.techStack.testFrameworks || [];
574
+ const routes = context.routes || {};
575
+ const pages = routes.pages || [];
576
+ const apis = routes.apis || [];
286
577
 
287
- return `# Developer Onboarding
578
+ let output = `# Developer Onboarding
288
579
 
289
580
  ## Welcome
290
581
 
@@ -297,51 +588,179 @@ The top-level directory is organized as follows:
297
588
  | Directory | Purpose |
298
589
  |-----------|---------|
299
590
  ${context.repoRoots.map(root => `| \`${root}\` | ${describeRoot(root)} |`).join("\n")}
591
+ `;
592
+
593
+ // Monorepo navigation
594
+ if (context.monorepo) {
595
+ output += `\n## Monorepo Navigation\n\n`;
596
+ output += `This is a **${context.monorepo.tool}** monorepo. Key packages:\n\n`;
597
+ for (const pkg of context.monorepo.packages.slice(0, 10)) {
598
+ output += `- \`${pkg.name}\` โ€” \`${pkg.path}\`\n`;
599
+ }
600
+ output += "\n";
601
+ }
300
602
 
301
- ## Technology Stack
603
+ output += `## Technology Stack
302
604
 
303
605
  | Category | Technologies |
304
606
  |----------|-------------|
305
607
  | Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
306
608
  | Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
307
609
  | Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
308
-
610
+ ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}
309
611
  ## Largest Modules
310
612
 
311
613
  These are the primary modules by file count โ€” good starting points for understanding the system:
312
614
 
313
- | Module | Files | Description |
314
- |--------|-------|-------------|
315
- ${context.topModules.slice(0, 10).map((m, i) => `| \`${m.key}\` | ${m.fileCount} | ${describeOnboardingModule(m.key)} |`).join("\n")}
615
+ | Module | Files | Type |
616
+ |--------|-------|------|
617
+ ${context.topModules.slice(0, 10).map(m => `| \`${m.key}\` | ${m.fileCount} | ${formatModuleType(m.type)} |`).join("\n")}
618
+ `;
619
+
620
+ // Key routes to explore
621
+ if (pages.length > 0 || apis.length > 0) {
622
+ output += `\n## Key Routes\n\n`;
623
+ if (pages.length > 0) {
624
+ output += `**Pages to explore:**\n\n`;
625
+ for (const p of pages.slice(0, 5)) {
626
+ output += `- \`${p.path}\` โ€” ${p.file}\n`;
627
+ }
628
+ output += "\n";
629
+ }
630
+ if (apis.length > 0) {
631
+ output += `**API endpoints:**\n\n`;
632
+ for (const a of apis.slice(0, 5)) {
633
+ const methods = a.methods ? ` [${a.methods.join(", ")}]` : "";
634
+ output += `- \`${a.path}\`${methods} โ€” ${a.file}\n`;
635
+ }
636
+ output += "\n";
637
+ }
638
+ }
316
639
 
317
- ## Getting Started
640
+ // Data flows overview
641
+ if (flows && flows.length > 0) {
642
+ output += `## How Data Flows\n\n`;
643
+ output += `Understanding these flows will help you see how the system works end-to-end:\n\n`;
644
+ for (const flow of flows) {
645
+ output += `- **${flow.name}** โ€” ${flow.description}\n`;
646
+ }
647
+ output += "\n";
648
+ }
649
+
650
+ // Dependency orientation
651
+ if (depGraph?.stats?.hubs && depGraph.stats.hubs.length > 0) {
652
+ output += `## Key Integration Points\n\n`;
653
+ output += `These modules are the most widely imported โ€” changes to them may have broad impact:\n\n`;
654
+ for (const hub of depGraph.stats.hubs.slice(0, 5)) {
655
+ output += `- \`${hub.key}\` โ€” imported by ${hub.importedBy} files\n`;
656
+ }
657
+ output += "\n";
658
+ }
659
+
660
+ output += `## Getting Started
318
661
 
319
662
  1. Clone the repository and install dependencies
320
663
  2. Review the top-level directories above to understand the project layout
321
664
  3. Start with the largest modules listed above โ€” they contain the core functionality
322
- 4. Check for a \`README.md\` or \`CONTRIBUTING.md\` file for project-specific setup instructions
665
+ 4. Check for a \`README.md\` or \`CONTRIBUTING.md\` file for project-specific setup instructions`;
323
666
 
324
- ---
667
+ // Test framework quickstart
668
+ if (testFrameworks.length > 0) {
669
+ output += `\n5. Run tests with your ${testFrameworks[0]} setup to verify everything works`;
670
+ }
671
+
672
+ output += `\n\n---
325
673
 
326
674
  *This onboarding guide is generated deterministically. Enable AI enhancement (\`REPOLENS_AI_ENABLED=true\`) for a narrative-style guide with tips and common pitfalls.*`;
675
+
676
+ return output;
677
+ }
678
+
679
+ // Helper: group top modules by their inferred type
680
+ function groupModulesByType(topModules) {
681
+ const groups = {};
682
+ for (const m of topModules) {
683
+ const type = m.type || "other";
684
+ if (!groups[type]) groups[type] = [];
685
+ groups[type].push(m);
686
+ }
687
+ return groups;
688
+ }
689
+
690
+ // Helper: human-readable module type label
691
+ function formatModuleType(type) {
692
+ const labels = {
693
+ api: "API Layer",
694
+ ui: "UI Components",
695
+ library: "Shared Libraries",
696
+ hooks: "React Hooks",
697
+ state: "State Management",
698
+ route: "Route/Page Modules",
699
+ app: "Application Core",
700
+ other: "Other Modules",
701
+ };
702
+ return labels[type] || type;
703
+ }
704
+
705
+ // Helper: derive flows from dependency graph hub chains
706
+ function inferFlowsFromDepGraph(depGraph) {
707
+ const flows = [];
708
+ if (!depGraph?.nodes || depGraph.nodes.length === 0) return flows;
709
+
710
+ // Find hub nodes โ€“ modules imported by many others
711
+ const hubThreshold = Math.max(3, Math.floor(depGraph.nodes.length * 0.05));
712
+ const hubs = depGraph.nodes
713
+ .filter(n => n.importedBy.length >= hubThreshold)
714
+ .sort((a, b) => b.importedBy.length - a.importedBy.length)
715
+ .slice(0, 3);
716
+
717
+ for (const hub of hubs) {
718
+ const importers = hub.importedBy.slice(0, 5);
719
+ const downstream = hub.imports.slice(0, 3);
720
+ const shortName = hub.key.split("/").pop();
721
+
722
+ flows.push({
723
+ name: `${shortName} Integration Flow`,
724
+ description: `\`${hub.key}\` is a central module imported by ${hub.importedBy.length} files, acting as an integration point for data and logic`,
725
+ steps: [
726
+ ...importers.map(i => `\`${i.split("/").pop()}\` imports \`${shortName}\``),
727
+ ...(downstream.length > 0 ? [`\`${shortName}\` depends on ${downstream.map(d => `\`${d.split("/").pop()}\``).join(", ")}`] : []),
728
+ ],
729
+ modules: [hub.key, ...importers.slice(0, 3)],
730
+ critical: hub.importedBy.length >= hubThreshold * 2,
731
+ });
732
+ }
733
+
734
+ return flows;
327
735
  }
328
736
 
329
737
  function describeRoot(root) {
330
738
  const lower = root.toLowerCase().replace(/\/$/, "");
739
+ // Check the last segment for nested paths like src/core, src/analyzers
740
+ const lastSeg = lower.split("/").pop();
331
741
  if (/^src$|^lib$/.test(lower)) return "Application source code";
332
- if (/^test|^__test|^spec/.test(lower)) return "Test suites";
333
- if (/^doc/.test(lower)) return "Documentation";
334
- if (/^bin$|^scripts?$/.test(lower)) return "CLI entry points and scripts";
335
- if (/^config/.test(lower)) return "Configuration files";
336
- if (/^public$|^static$|^assets$/.test(lower)) return "Static assets";
337
- if (/^dist$|^build$|^out$/.test(lower)) return "Build output";
338
- if (/^\.github$/.test(lower)) return "GitHub Actions and workflows";
339
- if (/^api$/.test(lower)) return "API definitions";
340
- if (/^components?$/.test(lower)) return "Shared UI components";
341
- if (/^pages?$|^views?$|^screens?$/.test(lower)) return "Application pages/views";
342
- if (/^utils?$|^helpers?$/.test(lower)) return "Utility functions";
343
- if (/^services?$/.test(lower)) return "Service layer";
344
- if (/^hooks?$/.test(lower)) return "Custom hooks";
742
+ if (/^test|^__test|^spec/.test(lastSeg)) return "Test suites";
743
+ if (/^doc/.test(lastSeg)) return "Documentation";
744
+ if (/^bin$|^scripts?$/.test(lastSeg)) return "CLI entry points and scripts";
745
+ if (/^config/.test(lastSeg)) return "Configuration files";
746
+ if (/^public$|^static$|^assets$/.test(lastSeg)) return "Static assets";
747
+ if (/^dist$|^build$|^out$/.test(lastSeg)) return "Build output";
748
+ if (/^\.github$/.test(lastSeg)) return "GitHub Actions and workflows";
749
+ if (/^api$|^endpoint/.test(lastSeg)) return "API definitions";
750
+ if (/^components?$|^ui$/.test(lastSeg)) return "Shared UI components";
751
+ if (/^pages?$|^views?$|^screens?$/.test(lastSeg)) return "Application pages/views";
752
+ if (/^utils?$|^helpers?$/.test(lastSeg)) return "Utility functions";
753
+ if (/^services?$/.test(lastSeg)) return "Service layer";
754
+ if (/^hooks?$/.test(lastSeg)) return "Custom hooks";
755
+ if (/^core$|^kernel$|^engine$/.test(lastSeg)) return "Core logic and foundations";
756
+ if (/^analyz/.test(lastSeg)) return "Code analysis and detection";
757
+ if (/^render/.test(lastSeg)) return "Rendering and output formatting";
758
+ if (/^publish/.test(lastSeg)) return "Publishing and distribution";
759
+ if (/^deliver/.test(lastSeg)) return "Content delivery";
760
+ if (/^integrat/.test(lastSeg)) return "Third-party integrations";
761
+ if (/^plugin/.test(lastSeg)) return "Plugin and extension system";
762
+ if (/^ai$|^ml$|^llm$/.test(lastSeg)) return "AI/ML features and providers";
763
+ if (/^middleware/.test(lastSeg)) return "Middleware pipeline";
345
764
  return "Project files";
346
765
  }
347
766
 
@@ -97,12 +97,28 @@ export function buildAIContext(scanResult, config) {
97
97
  function inferModuleType(modulePath) {
98
98
  const lower = modulePath.toLowerCase();
99
99
 
100
- if (lower.includes("api")) return "api";
101
- if (lower.includes("component")) return "ui";
102
- if (lower.includes("lib") || lower.includes("util")) return "library";
100
+ if (lower.includes("test") || lower.includes("spec") || lower.includes("__test")) return "test";
101
+ if (lower.includes("api") || lower.includes("endpoint")) return "api";
102
+ if (lower.includes("component") || lower.includes("widget")) return "ui";
103
+ if (lower.includes("lib") || lower.includes("util") || lower.includes("helper") || lower.includes("common") || lower.includes("shared")) return "library";
103
104
  if (lower.includes("hook")) return "hooks";
104
- if (lower.includes("store") || lower.includes("state")) return "state";
105
- if (lower.includes("page") || lower.includes("route")) return "route";
105
+ if (lower.includes("store") || lower.includes("state") || lower.includes("redux") || lower.includes("zustand")) return "state";
106
+ if (lower.includes("page") || lower.includes("route") || lower.includes("view")) return "route";
107
+ if (lower.includes("config") || lower.includes("setting") || lower.includes("env")) return "config";
108
+ if (lower.includes("core") || lower.includes("kernel") || lower.includes("foundation")) return "core";
109
+ if (lower.includes("render") || lower.includes("template") || lower.includes("format")) return "renderer";
110
+ if (lower.includes("publish") || lower.includes("output") || lower.includes("export")) return "publisher";
111
+ if (lower.includes("analyz") || lower.includes("inspect") || lower.includes("detect")) return "analyzer";
112
+ if (lower.includes("plugin") || lower.includes("extension") || lower.includes("addon")) return "plugin";
113
+ if (lower.includes("deliver") || lower.includes("dispatch") || lower.includes("send")) return "delivery";
114
+ if (lower.includes("doc") || lower.includes("generate")) return "documentation";
115
+ if (lower.includes("integrat") || lower.includes("connect") || lower.includes("adapter")) return "integration";
116
+ if (lower.includes("cli") || lower.includes("command") || lower.includes("bin")) return "cli";
117
+ if (lower.includes("ai") || lower.includes("ml") || lower.includes("model") || lower.includes("prompt")) return "ai";
118
+ if (lower.includes("auth") || lower.includes("login") || lower.includes("session")) return "auth";
119
+ if (lower.includes("data") || lower.includes("db") || lower.includes("model") || lower.includes("schema")) return "data";
120
+ if (lower.includes("middleware")) return "middleware";
121
+ if (lower.includes("service")) return "service";
106
122
  if (lower.includes("app")) return "app";
107
123
 
108
124
  return "other";
@@ -110,21 +126,31 @@ function inferModuleType(modulePath) {
110
126
 
111
127
  function inferArchitecturalPatterns(modules) {
112
128
  const patterns = [];
129
+ const keys = modules.map(m => m.key.toLowerCase());
113
130
 
114
- const hasAppRouter = modules.some(m => m.key.includes("app/"));
115
- const hasPagesRouter = modules.some(m => m.key.includes("pages/"));
116
- const hasComponents = modules.some(m => m.key.includes("component"));
117
- const hasLib = modules.some(m => m.key.includes("lib"));
118
- const hasHooks = modules.some(m => m.key.includes("hook"));
119
- const hasStore = modules.some(m => m.key.includes("store") || m.key.includes("state"));
120
- const hasApi = modules.some(m => m.key.includes("api"));
121
-
122
- if (hasAppRouter) patterns.push("Next.js App Router");
123
- if (hasPagesRouter) patterns.push("Next.js Pages Router");
124
- if (hasComponents && hasLib) patterns.push("Layered component architecture");
125
- if (hasHooks) patterns.push("React hooks pattern");
126
- if (hasStore) patterns.push("Centralized state management");
127
- if (hasApi) patterns.push("API route pattern");
131
+ const has = (keyword) => keys.some(k => k.includes(keyword));
132
+
133
+ // Web framework patterns
134
+ if (has("app/")) patterns.push("Next.js App Router");
135
+ if (has("pages/")) patterns.push("Next.js Pages Router");
136
+ if (has("component") && has("lib")) patterns.push("Layered component architecture");
137
+ if (has("hook")) patterns.push("React hooks pattern");
138
+ if (has("store") || has("state") || has("redux") || has("zustand")) patterns.push("Centralized state management");
139
+
140
+ // General patterns
141
+ 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");
144
+ 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");
147
+ if (has("analyz") || has("detect") || has("inspect")) patterns.push("Analysis pipeline");
148
+ 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");
150
+ if (has("integrat") || has("adapter") || has("connect")) patterns.push("Integration adapters");
151
+ if (has("ai") || has("prompt") || has("provider")) patterns.push("AI/LLM integration");
152
+ if (has("deliver") || has("dispatch")) patterns.push("Delivery pipeline");
153
+ if (has("test") || has("spec")) patterns.push("Dedicated test infrastructure");
128
154
 
129
155
  return patterns;
130
156
  }
@@ -32,7 +32,7 @@ const DEFAULT_DOMAIN_HINTS = [
32
32
  description: "Payment processing and subscription management"
33
33
  },
34
34
  {
35
- match: ["api", "endpoint", "route", "handler", "controller", "middleware"],
35
+ match: ["api", "endpoint", "handler", "controller", "middleware"],
36
36
  domain: "API Layer",
37
37
  description: "Backend API endpoints and request handling"
38
38
  },
@@ -75,6 +75,61 @@ const DEFAULT_DOMAIN_HINTS = [
75
75
  match: ["job", "queue", "worker", "cron", "task", "scheduler", "background"],
76
76
  domain: "Background Jobs",
77
77
  description: "Background processing, job queues, and scheduled tasks"
78
+ },
79
+ {
80
+ match: ["core", "kernel", "foundation", "engine"],
81
+ domain: "Core Engine",
82
+ description: "Core business logic and foundational modules"
83
+ },
84
+ {
85
+ match: ["render", "template", "format", "output"],
86
+ domain: "Rendering & Output",
87
+ description: "Content rendering, formatting, and output generation"
88
+ },
89
+ {
90
+ match: ["publish", "deploy", "release", "distribute"],
91
+ domain: "Publishing & Delivery",
92
+ description: "Content and artifact publishing, deployment, and distribution"
93
+ },
94
+ {
95
+ match: ["analyz", "inspect", "detect", "lint", "scan", "parse"],
96
+ domain: "Analysis & Detection",
97
+ description: "Code analysis, pattern detection, and static inspection"
98
+ },
99
+ {
100
+ match: ["plugin", "extension", "addon", "module"],
101
+ domain: "Plugin System",
102
+ description: "Extensibility framework, plugins, and add-ons"
103
+ },
104
+ {
105
+ match: ["deliver", "dispatch", "send", "transport"],
106
+ domain: "Delivery",
107
+ description: "Content delivery and distribution channels"
108
+ },
109
+ {
110
+ match: ["doc", "generate", "markdown", "readme"],
111
+ domain: "Documentation",
112
+ description: "Documentation generation and management"
113
+ },
114
+ {
115
+ match: ["integrat", "connect", "adapter", "bridge", "gateway"],
116
+ domain: "Integrations",
117
+ description: "Third-party service integrations and adapters"
118
+ },
119
+ {
120
+ match: ["cli", "command", "bin", "terminal", "shell", "prompt"],
121
+ domain: "CLI & Commands",
122
+ description: "Command-line interface and terminal commands"
123
+ },
124
+ {
125
+ match: ["ai", "ml", "llm", "gpt", "openai", "anthropic", "gemini"],
126
+ domain: "AI & Machine Learning",
127
+ description: "AI/ML integration, LLM providers, and intelligent features"
128
+ },
129
+ {
130
+ match: ["service", "provider", "client", "sdk"],
131
+ domain: "Services",
132
+ description: "Service layer, providers, and external client SDKs"
78
133
  }
79
134
  ];
80
135
 
package/src/core/scan.js CHANGED
@@ -29,13 +29,22 @@ function isExpressRoute(content) {
29
29
  }
30
30
 
31
31
  function isReactRouterFile(content) {
32
- // Detect React Router patterns
33
- return content.includes("<Route") || content.includes("createBrowserRouter") || content.includes("createRoutesFromElements");
32
+ // Detect React Router patterns โ€” require import evidence, not just string mentions
33
+ const hasImport = /import\s+.*?from\s+['"]react-router/.test(content)
34
+ || /require\s*\(\s*['"]react-router/.test(content);
35
+ const hasJSX = /<Route\s/.test(content);
36
+ const hasFactory = /createBrowserRouter\s*\(/.test(content)
37
+ || /createRoutesFromElements\s*\(/.test(content);
38
+ return hasImport || hasJSX || hasFactory;
34
39
  }
35
40
 
36
41
  function isVueRouterFile(content) {
37
- // Detect Vue Router patterns
38
- return (content.includes("createRouter") || content.includes("VueRouter")) && content.includes("routes");
42
+ // Detect Vue Router patterns โ€” require import evidence, not just string mentions
43
+ const hasImport = /import\s+.*?from\s+['"]vue-router/.test(content)
44
+ || /require\s*\(\s*['"]vue-router/.test(content);
45
+ const hasConstructor = /new\s+VueRouter\s*\(/.test(content);
46
+ const hasFactory = /createRouter\s*\(/.test(content) && hasImport;
47
+ return hasImport || hasConstructor || hasFactory;
39
48
  }
40
49
 
41
50
  function isNextPage(file) {
@@ -159,6 +168,10 @@ async function extractRepoMetadata(repoRoot) {
159
168
  if (allDeps["nestjs"] || allDeps["@nestjs/core"]) metadata.frameworks.push("NestJS");
160
169
  if (allDeps["svelte"]) metadata.frameworks.push("Svelte");
161
170
  if (allDeps["solid-js"]) metadata.frameworks.push("Solid");
171
+ if (allDeps["hono"]) metadata.frameworks.push("Hono");
172
+ if (allDeps["koa"]) metadata.frameworks.push("Koa");
173
+ if (allDeps["hapi"] || allDeps["@hapi/hapi"]) metadata.frameworks.push("Hapi");
174
+ if (allDeps["electron"]) metadata.frameworks.push("Electron");
162
175
 
163
176
  // Detect test frameworks
164
177
  if (allDeps["vitest"]) metadata.testFrameworks.push("Vitest");
@@ -166,6 +179,7 @@ async function extractRepoMetadata(repoRoot) {
166
179
  if (allDeps["mocha"]) metadata.testFrameworks.push("Mocha");
167
180
  if (allDeps["playwright"]) metadata.testFrameworks.push("Playwright");
168
181
  if (allDeps["cypress"]) metadata.testFrameworks.push("Cypress");
182
+ if (allDeps["ava"]) metadata.testFrameworks.push("Ava");
169
183
 
170
184
  // Detect build tools
171
185
  if (allDeps["vite"]) metadata.buildTools.push("Vite");
@@ -173,9 +187,25 @@ async function extractRepoMetadata(repoRoot) {
173
187
  if (allDeps["rollup"]) metadata.buildTools.push("Rollup");
174
188
  if (allDeps["esbuild"]) metadata.buildTools.push("esbuild");
175
189
  if (allDeps["turbo"]) metadata.buildTools.push("Turborepo");
190
+ if (allDeps["tsup"]) metadata.buildTools.push("tsup");
191
+ if (allDeps["swc"] || allDeps["@swc/core"]) metadata.buildTools.push("SWC");
192
+ if (allDeps["parcel"]) metadata.buildTools.push("Parcel");
176
193
 
177
- // Detect TypeScript
194
+ // Detect languages
178
195
  if (allDeps["typescript"]) metadata.languages.add("TypeScript");
196
+
197
+ // Infer JavaScript if package.json exists (any npm project uses JS/Node)
198
+ metadata.languages.add("JavaScript");
199
+
200
+ // Detect Node.js runtime indicators
201
+ const hasNodeEngines = pkg.engines && pkg.engines.node;
202
+ const hasBin = pkg.bin != null;
203
+ const hasNodeDeps = allDeps["node-fetch"] || allDeps["fs-extra"] || allDeps["dotenv"]
204
+ || allDeps["commander"] || allDeps["yargs"] || allDeps["chalk"]
205
+ || allDeps["inquirer"] || allDeps["ora"] || allDeps["execa"];
206
+ if (hasNodeEngines || hasBin || hasNodeDeps || pkg.type === "module") {
207
+ metadata.languages.add("Node.js");
208
+ }
179
209
  } catch {
180
210
  // No package.json or invalid JSON
181
211
  }
@@ -241,30 +271,29 @@ function extractExpressRoutes(content) {
241
271
 
242
272
  function extractReactRoutes(content, file) {
243
273
  const routes = [];
274
+ const lines = content.split("\n");
244
275
 
245
276
  // Match <Route path="..." />
246
277
  const routePattern = /<Route\s+[^>]*path\s*=\s*['"`]([^'"`]+)['"`][^>]*\/?>/gi;
247
278
  let match;
248
279
 
249
280
  while ((match = routePattern.exec(content)) !== null) {
250
- const [, path] = match;
251
- routes.push({
252
- file,
253
- path,
254
- framework: "React Router"
255
- });
281
+ const [, routePath] = match;
282
+ if (isValidRoutePath(routePath)) {
283
+ routes.push({ file, path: routePath, framework: "React Router" });
284
+ }
256
285
  }
257
286
 
258
- // Match path: "..." in route objects
259
- const objectPattern = /path\s*:\s*['"`]([^'"`]+)['"`]/gi;
260
- while ((match = objectPattern.exec(content)) !== null) {
261
- const [, path] = match;
262
- if (!routes.some(r => r.path === path)) {
263
- routes.push({
264
- file,
265
- path,
266
- framework: "React Router"
267
- });
287
+ // Match path: "..." in route objects (skip comment lines)
288
+ for (const line of lines) {
289
+ const trimmed = line.trim();
290
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
291
+ const objectMatch = /path\s*:\s*['"`]([^'"`]+)['"`]/i.exec(trimmed);
292
+ if (objectMatch) {
293
+ const routePath = objectMatch[1];
294
+ if (isValidRoutePath(routePath) && !routes.some(r => r.path === routePath)) {
295
+ routes.push({ file, path: routePath, framework: "React Router" });
296
+ }
268
297
  }
269
298
  }
270
299
 
@@ -273,23 +302,31 @@ function extractReactRoutes(content, file) {
273
302
 
274
303
  function extractVueRoutes(content, file) {
275
304
  const routes = [];
276
-
277
- // Match path: '...' or path: "..." in Vue router definitions
278
- const pathPattern = /path\s*:\s*['"`]([^'"`]+)['"`]/gi;
279
- let match;
280
-
281
- while ((match = pathPattern.exec(content)) !== null) {
282
- const [, path] = match;
283
- routes.push({
284
- file,
285
- path,
286
- framework: "Vue Router"
287
- });
305
+ const lines = content.split("\n");
306
+
307
+ // Match path: '...' or path: "..." in Vue router definitions (skip comment lines)
308
+ for (const line of lines) {
309
+ const trimmed = line.trim();
310
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
311
+ const match = /path\s*:\s*['"`]([^'"`]+)['"`]/i.exec(trimmed);
312
+ if (match) {
313
+ const routePath = match[1];
314
+ if (isValidRoutePath(routePath) && !routes.some(r => r.path === routePath)) {
315
+ routes.push({ file, path: routePath, framework: "Vue Router" });
316
+ }
317
+ }
288
318
  }
289
319
 
290
320
  return routes;
291
321
  }
292
322
 
323
+ function isValidRoutePath(p) {
324
+ // Filter out placeholder/documentation strings and non-path values
325
+ if (!p || p === "..." || p === "*" || p.length > 200) return false;
326
+ // Must look like a URL path (starts with / or is a relative segment)
327
+ return p.startsWith("/") || /^[a-zA-Z0-9]/.test(p);
328
+ }
329
+
293
330
  export async function scanRepo(cfg) {
294
331
  const repoRoot = cfg.__repoRoot;
295
332
 
@@ -184,16 +184,16 @@ async function generateDocument(docPlan, context) {
184
184
 
185
185
  switch (key) {
186
186
  case "executive_summary":
187
- return await generateExecutiveSummary(aiContext);
187
+ return await generateExecutiveSummary(aiContext, { depGraph, flows });
188
188
 
189
189
  case "system_overview":
190
- return await generateSystemOverview(aiContext);
190
+ return await generateSystemOverview(aiContext, { depGraph });
191
191
 
192
192
  case "business_domains":
193
- return await generateBusinessDomains(aiContext);
193
+ return await generateBusinessDomains(aiContext, { depGraph });
194
194
 
195
195
  case "architecture_overview":
196
- return await generateArchitectureOverview(aiContext);
196
+ return await generateArchitectureOverview(aiContext, { depGraph, driftResult });
197
197
 
198
198
  case "module_catalog":
199
199
  // Hybrid: deterministic skeleton + ownership info
@@ -208,7 +208,7 @@ async function generateDocument(docPlan, context) {
208
208
  return renderApiSurfaceOriginal(config, scanResult);
209
209
 
210
210
  case "data_flows":
211
- return await generateDataFlows(flows, aiContext);
211
+ return await generateDataFlows(flows, aiContext, { depGraph, scanResult });
212
212
 
213
213
  case "arch_diff":
214
214
  if (!diffData) {
@@ -221,7 +221,7 @@ async function generateDocument(docPlan, context) {
221
221
  return renderSystemMap(scanResult, config, depGraph);
222
222
 
223
223
  case "developer_onboarding":
224
- return await generateDeveloperOnboarding(aiContext);
224
+ return await generateDeveloperOnboarding(aiContext, { flows, depGraph });
225
225
 
226
226
  case "graphql_schema":
227
227
  return renderGraphQLSchema(graphqlResult);
@@ -90,15 +90,23 @@ function buildModuleGraph(modules, depGraph) {
90
90
 
91
91
  /**
92
92
  * Find which module a file belongs to.
93
+ * Edge keys from the dep graph are extensionless (e.g. "src/core/config")
94
+ * while module keys may include extensions (e.g. "src/core/config.js").
95
+ * We try both direct match and extension-stripped match.
93
96
  */
94
97
  function findModuleForFile(fileKey, modules) {
95
98
  const normalized = fileKey.replace(/\\/g, "/");
96
- // Find the most specific (longest) matching module key
97
99
  let bestMatch = null;
98
100
  for (const mod of modules) {
99
- if (normalized.startsWith(mod.key) || normalized.startsWith(mod.key + "/")) {
100
- if (!bestMatch || mod.key.length > bestMatch.length) {
101
- bestMatch = mod.key;
101
+ const modKey = mod.key;
102
+ // Direct match (with or without extension)
103
+ if (normalized === modKey || normalized === modKey.replace(/\.[^/.]+$/, "")) {
104
+ return modKey;
105
+ }
106
+ // Prefix match: file is inside this module
107
+ if (normalized.startsWith(modKey + "/") || normalized.startsWith(modKey.replace(/\.[^/.]+$/, "") + "/")) {
108
+ if (!bestMatch || modKey.length > bestMatch.length) {
109
+ bestMatch = modKey;
102
110
  }
103
111
  }
104
112
  }
@@ -107,13 +115,25 @@ function findModuleForFile(fileKey, modules) {
107
115
 
108
116
  function categorizeModule(key) {
109
117
  const normalized = key.toLowerCase();
110
- if (normalized.includes("core")) return "core";
111
- if (normalized.includes("publisher")) return "integration";
112
- if (normalized.includes("renderer")) return "business";
113
- if (normalized.includes("delivery")) return "integration";
114
- if (normalized.includes("util")) return "util";
115
- if (normalized.includes("test")) return "test";
116
- if (normalized.includes("bin")) return "cli";
118
+ if (normalized.includes("test") || normalized.includes("spec")) return "test";
119
+ if (normalized.includes("core") || normalized.includes("kernel")) return "core";
120
+ if (normalized.includes("analyz") || normalized.includes("detect") || normalized.includes("inspect")) return "analyzer";
121
+ if (normalized.includes("render") || normalized.includes("format") || normalized.includes("template")) return "renderer";
122
+ if (normalized.includes("publish") || normalized.includes("output")) return "publisher";
123
+ if (normalized.includes("deliver") || normalized.includes("dispatch")) return "delivery";
124
+ if (normalized.includes("integrat") || normalized.includes("connect") || normalized.includes("adapter")) return "integration";
125
+ if (normalized.includes("util") || normalized.includes("helper") || normalized.includes("lib") || normalized.includes("common")) return "util";
126
+ if (normalized.includes("ai") || normalized.includes("llm") || normalized.includes("prompt")) return "ai";
127
+ if (normalized.includes("doc") || normalized.includes("generate")) return "docs";
128
+ if (normalized.includes("plugin") || normalized.includes("extension")) return "plugin";
129
+ if (normalized.includes("config") || normalized.includes("setting")) return "config";
130
+ if (normalized.includes("cli") || normalized.includes("bin") || normalized.includes("command")) return "cli";
131
+ if (normalized.includes("api") || normalized.includes("endpoint")) return "api";
132
+ if (normalized.includes("component") || normalized.includes("ui")) return "ui";
133
+ if (normalized.includes("page") || normalized.includes("route") || normalized.includes("view")) return "page";
134
+ if (normalized.includes("store") || normalized.includes("state")) return "state";
135
+ if (normalized.includes("middleware")) return "middleware";
136
+ if (normalized.includes("service")) return "service";
117
137
  return "other";
118
138
  }
119
139
 
@@ -122,8 +142,21 @@ function generateUnicodeArchitectureDiagram(nodes, relationships) {
122
142
  const categories = {
123
143
  cli: { icon: "๐ŸŽฏ", label: "CLI Entry", nodes: [] },
124
144
  core: { icon: "โš™๏ธ", label: "Core Logic", nodes: [] },
125
- business: { icon: "๐Ÿ“‹", label: "Business Logic", nodes: [] },
145
+ config: { icon: "๐Ÿ”ง", label: "Configuration", nodes: [] },
146
+ analyzer: { icon: "๐Ÿ”", label: "Analysis", nodes: [] },
147
+ ai: { icon: "๐Ÿค–", label: "AI / ML", nodes: [] },
148
+ docs: { icon: "๐Ÿ“", label: "Documentation", nodes: [] },
149
+ renderer: { icon: "๐Ÿ“‹", label: "Rendering", nodes: [] },
150
+ publisher: { icon: "๐Ÿ“ค", label: "Publishing", nodes: [] },
151
+ delivery: { icon: "๐Ÿ“ฌ", label: "Delivery", nodes: [] },
126
152
  integration: { icon: "๐Ÿ”Œ", label: "Integration", nodes: [] },
153
+ plugin: { icon: "๐Ÿงฉ", label: "Plugins", nodes: [] },
154
+ api: { icon: "๐ŸŒ", label: "API Layer", nodes: [] },
155
+ ui: { icon: "๐Ÿ–ผ๏ธ", label: "UI Components", nodes: [] },
156
+ page: { icon: "๐Ÿ“„", label: "Pages / Routes", nodes: [] },
157
+ state: { icon: "๐Ÿ’พ", label: "State Management", nodes: [] },
158
+ middleware: { icon: "๐Ÿ”€", label: "Middleware", nodes: [] },
159
+ service: { icon: "โšก", label: "Services", nodes: [] },
127
160
  util: { icon: "๐Ÿ› ๏ธ", label: "Utilities", nodes: [] },
128
161
  test: { icon: "โœ…", label: "Testing", nodes: [] },
129
162
  other: { icon: "๐Ÿ“ฆ", label: "Other", nodes: [] }
@@ -235,5 +268,53 @@ export function renderSystemMap(scan, config, depGraph) {
235
268
  ""
236
269
  ];
237
270
 
271
+ // Add key connections summary when we have relationships
272
+ if (relationships.length > 0) {
273
+ lines.push("---", "", "## Key Connections", "");
274
+
275
+ // Find most-depended-on modules (highest in-degree)
276
+ const inDegree = new Map();
277
+ const outDegree = new Map();
278
+ for (const rel of relationships) {
279
+ inDegree.set(rel.to, (inDegree.get(rel.to) || 0) + (rel.weight || 1));
280
+ outDegree.set(rel.from, (outDegree.get(rel.from) || 0) + (rel.weight || 1));
281
+ }
282
+
283
+ const topDeps = [...inDegree.entries()]
284
+ .sort((a, b) => b[1] - a[1])
285
+ .slice(0, 5);
286
+
287
+ if (topDeps.length > 0) {
288
+ lines.push("**Most depended-on modules** (highest import count):", "");
289
+ lines.push("| Module | Imported by |");
290
+ lines.push("|--------|------------|");
291
+ for (const [nodeId, count] of topDeps) {
292
+ const node = nodes.find(n => n.id === nodeId);
293
+ if (node) {
294
+ lines.push(`| \`${node.label}\` | ${count} module${count !== 1 ? "s" : ""} |`);
295
+ }
296
+ }
297
+ lines.push("");
298
+ }
299
+
300
+ // Find modules with highest out-degree (most dependencies)
301
+ const topConsumers = [...outDegree.entries()]
302
+ .sort((a, b) => b[1] - a[1])
303
+ .slice(0, 5);
304
+
305
+ if (topConsumers.length > 0) {
306
+ lines.push("**Most dependent modules** (highest dependency count):", "");
307
+ lines.push("| Module | Depends on |");
308
+ lines.push("|--------|-----------|");
309
+ for (const [nodeId, count] of topConsumers) {
310
+ const node = nodes.find(n => n.id === nodeId);
311
+ if (node) {
312
+ lines.push(`| \`${node.label}\` | ${count} module${count !== 1 ? "s" : ""} |`);
313
+ }
314
+ }
315
+ lines.push("");
316
+ }
317
+ }
318
+
238
319
  return lines.join("\n");
239
320
  }