@chappibunny/repolens 1.5.3 โ†’ 1.6.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,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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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,33 +588,150 @@ 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) {
@@ -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: hookScanResult });
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);