@chappibunny/repolens 1.15.4 → 1.16.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.
@@ -13,7 +13,9 @@ import {
13
13
  renderStructuredToMarkdown,
14
14
  } from "./prompts.js";
15
15
  import { identifyFlowDependencies, filterNoiseFlows } from "../analyzers/flow-inference.js";
16
+ import { isCliProject, isLibraryProject } from "../analyzers/context-builder.js";
16
17
  import { scoreMaturity } from "../analyzers/maturity.js";
18
+ import { isNonProductPath, isTestPath } from "../utils/path-roles.js";
17
19
  import { info, warn } from "../utils/logger.js";
18
20
 
19
21
  // Strip conversational patterns that LLMs sometimes inject into documentation
@@ -162,7 +164,8 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
162
164
  const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose";
163
165
  const languageList = context.techStack.languages.join(", ") || "multiple languages";
164
166
  const testFrameworks = context.techStack.testFrameworks || [];
165
- const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
167
+ const isCLI = isCliProject(context);
168
+ const isLibrary = isLibraryProject(context);
166
169
  const projectName = context.project.name || "This system";
167
170
 
168
171
  // Maturity comes from repo signals (version, history, tests, CI, docs, dependency health)
@@ -178,7 +181,8 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
178
181
  const maturityMax = maturity.max;
179
182
 
180
183
  // Determine project type for better descriptions
181
- const projectType = isCLI ? "command-line tool" :
184
+ const projectType = isCLI ? "command-line tool" :
185
+ isLibrary ? "library" :
182
186
  (context.project.apiRoutesDetected > 0 && context.project.pagesDetected > 0) ? "full-stack application" :
183
187
  context.project.apiRoutesDetected > 0 ? "API service" :
184
188
  context.project.pagesDetected > 0 ? "web application" : "software system";
@@ -190,7 +194,7 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
190
194
  | Aspect | Summary |
191
195
  |--------|---------|
192
196
  | **What** | ${projectName} is ${withArticle(maturityLabel.toLowerCase())} ${projectType} |
193
- | **Built With** | ${frameworkList} + ${languageList} |
197
+ | **Built With** | ${[...context.techStack.frameworks, languageList].join(", ")} |
194
198
  | **Scale** | ${context.project.modulesDetected} modules across ${context.project.filesScanned} files |
195
199
  | **Maturity** | ${maturityLabel} (${maturityScore}/${maturityMax} indicators) |
196
200
 
@@ -202,6 +206,8 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
202
206
 
203
207
  if (isCLI) {
204
208
  output += `It operates as a **command-line interface**, enabling users to interact with the system through terminal commands. This architecture is optimized for automation, scripting, and integration into CI/CD pipelines.`;
209
+ } else if (isLibrary) {
210
+ output += `It is a **library**: other programs import it as a dependency and call its public API. It serves no routes or pages of its own.`;
205
211
  } else if (context.project.apiRoutesDetected > 0 && context.project.pagesDetected > 0) {
206
212
  output += `It provides both a **user-facing interface** (${context.project.pagesDetected} pages) and a **programmatic API** (${context.project.apiRoutesDetected} endpoints), serving as a complete solution for end users and system integrations alike.`;
207
213
  } else if (context.project.apiRoutesDetected > 0) {
@@ -222,7 +228,7 @@ The system is organized around **${context.domains.length} functional domains**,
222
228
  output += `|--------|------------|-------|\n`;
223
229
  for (const d of topDomains) {
224
230
  const desc = d.description || inferDomainCapability(d.name);
225
- output += `| **${d.name}** | ${desc} | ${d.moduleCount} modules, ${d.fileCount} files |\n`;
231
+ output += `| **${d.name}** | ${desc} | ${d.moduleCount} module${d.moduleCount === 1 ? "" : "s"}, ${d.fileCount} file${d.fileCount === 1 ? "" : "s"} |\n`;
226
232
  }
227
233
  if (context.domains.length > 6) {
228
234
  output += `| *+${context.domains.length - 6} more* | Additional functional areas | — |\n`;
@@ -281,9 +287,9 @@ The system is organized around **${context.domains.length} functional domains**,
281
287
  output += `| Metric | Value | Status |\n`;
282
288
  output += `|--------|-------|--------|\n`;
283
289
  output += `| Internal imports | ${stats.totalEdges} edges | ${stats.totalEdges > 0 ? "✅ Connected" : "⚠️ Isolated"} |\n`;
284
- output += `| External Packages | ${stats.externalDeps} | ${stats.externalDeps < 50 ? "✅ Manageable" : stats.externalDeps < 100 ? "⚠️ Consider audit" : "🔴 High dependency count"} |\n`;
290
+ output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "✅ Manageable" : stats.externalDeps < 100 ? "⚠️ Consider audit" : "🔴 High dependency count"} |\n`;
285
291
  output += `| Circular Dependencies | ${stats.cycles === 0 ? "cycle-free" : stats.cycles} | ${stats.cycles === 0 ? "✅ None" : `🔴 ${stats.cycles} detected`} |\n`;
286
- output += `| Orphan Files | ${stats.orphanFiles} | ${stats.orphanFiles === 0 ? "✅ All connected" : `⚠️ ${stats.orphanFiles} unused`} |\n`;
292
+ output += `| Orphan Files | ${stats.orphanFiles} | ${orphanStatus(stats)} |\n`;
287
293
 
288
294
  // Key insights
289
295
  output += `\n### Key Insights\n\n`;
@@ -294,19 +300,19 @@ The system is organized around **${context.domains.length} functional domains**,
294
300
  }
295
301
  if (stats.hubs && stats.hubs.length > 0) {
296
302
  const topHub = stats.hubs[0];
297
- if (topHub.importedBy > stats.totalFiles * 0.3) {
298
- output += `- ⚠️ **Coupling hotspot** — \`${topHub.key}\` is imported by ${topHub.importedBy} files (${Math.round(topHub.importedBy / stats.totalFiles * 100)}% of codebase). Consider splitting.\n`;
303
+ if (topHub.importedBy > sourceFileCount(stats) * 0.3) {
304
+ output += `- ⚠️ **Coupling hotspot** — \`${topHub.key}\` is imported by ${topHub.importedBy} files (${Math.round(topHub.importedBy / sourceFileCount(stats) * 100)}% of codebase). Consider splitting.\n`;
299
305
  } else {
300
306
  output += `- ✅ **Healthy coupling** — Most-imported module (\`${topHub.key}\`) serves ${topHub.importedBy} files without excessive centralization.\n`;
301
307
  }
302
308
  }
303
- if (stats.orphanFiles > stats.totalFiles * 0.1) {
309
+ if (orphanRatio(stats) > ORPHAN_WARN_RATIO) {
304
310
  output += `- ⚠️ **Potential dead code** — ${stats.orphanFiles} files (${Math.round(stats.orphanFiles / stats.totalFiles * 100)}%) are not imported anywhere.\n`;
305
311
  }
306
312
  }
307
313
 
308
314
  // Data flows summary
309
- const summaryFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test'));
315
+ const summaryFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test') && !isTestPath(f.modules?.[0]));
310
316
  if (summaryFlows.length > 0) {
311
317
  output += `\n## Key Data Flows\n\n`;
312
318
  output += `The system processes information through **${summaryFlows.length} identified flow${summaryFlows.length === 1 ? "" : "s"}**:\n\n`;
@@ -321,7 +327,7 @@ The system is organized around **${context.domains.length} functional domains**,
321
327
 
322
328
  const observations = [];
323
329
  if (context.patterns.length > 0) {
324
- observations.push(`The codebase follows **${context.patterns.join(", ")}** architectural patterns, providing a familiar structure for developers.`);
330
+ observations.push(`The codebase follows **${context.patterns.map(patternLabel).join(", ")}** architectural patterns, providing a familiar structure for developers.`);
325
331
  }
326
332
  if (context.domains.length >= 5) {
327
333
  observations.push(`With ${context.domains.length} distinct domains, the system has clear separation of concerns suitable for team-based development.`);
@@ -331,6 +337,8 @@ The system is organized around **${context.domains.length} functional domains**,
331
337
  }
332
338
  if (isCLI) {
333
339
  observations.push(`As a CLI tool, this system is automation-friendly and can integrate into existing workflows and pipelines.`);
340
+ } else if (isLibrary) {
341
+ observations.push(`As a library, its public API is the contract: changes to exported functions affect every project that depends on it.`);
334
342
  }
335
343
 
336
344
  for (const obs of observations) {
@@ -347,7 +355,8 @@ The system is organized around **${context.domains.length} functional domains**,
347
355
  function getFallbackSystemOverview(context, enrichment = {}) {
348
356
  const { depGraph } = enrichment;
349
357
  const testFrameworks = context.techStack.testFrameworks || [];
350
- const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
358
+ const isCLI = isCliProject(context);
359
+ const isLibrary = isLibraryProject(context);
351
360
  const projectName = context.project.name || "This system";
352
361
 
353
362
  // Calculate scale classification
@@ -381,6 +390,8 @@ function getFallbackSystemOverview(context, enrichment = {}) {
381
390
  }
382
391
  if (isCLI) {
383
392
  output += `\n| **Interface Type** | Command-Line Tool |`;
393
+ } else if (isLibrary) {
394
+ output += `\n| **Interface Type** | Library (imported API) |`;
384
395
  }
385
396
 
386
397
  output += `
@@ -515,9 +526,9 @@ The system is built on a modern technology foundation:\n\n`;
515
526
  output += `| Metric | Value | Assessment |\n`;
516
527
  output += `|--------|-------|------------|\n`;
517
528
  output += `| Internal Imports | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "Modules are connected" : "No internal imports"} |\n`;
518
- output += `| External Packages | ${stats.externalDeps} | ${stats.externalDeps < 50 ? "Lean dependencies" : stats.externalDeps < 100 ? "Moderate dependencies" : "Many dependencies"} |\n`;
529
+ output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "Lean dependencies" : stats.externalDeps < 100 ? "Moderate dependencies" : "Many dependencies"} |\n`;
519
530
  output += `| Circular Dependencies | ${stats.cycles} | ${stats.cycles === 0 ? "✅ Clean" : "⚠️ Needs attention"} |\n`;
520
- output += `| Orphan Files | ${stats.orphanFiles} | ${stats.orphanFiles === 0 ? "✅ All connected" : `⚠️ ${stats.orphanFiles} unused`} |\n`;
531
+ output += `| Orphan Files | ${stats.orphanFiles} | ${orphanStatus(stats)} |\n`;
521
532
 
522
533
  if (stats.hubs && stats.hubs.length > 0) {
523
534
  output += `\n**Hub modules** (most imported):\n\n`;
@@ -682,7 +693,8 @@ ${context.domains.map(d => `| ${d.name} | ${d.moduleCount} | ${d.fileCount} | ${
682
693
 
683
694
  function getFallbackArchitectureOverview(context, enrichment = {}) {
684
695
  const { depGraph, driftResult } = enrichment;
685
- const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
696
+ const isCLI = isCliProject(context);
697
+ const isLibrary = isLibraryProject(context);
686
698
  const projectName = context.project.name || "This system";
687
699
  const testFrameworks = context.techStack.testFrameworks || [];
688
700
 
@@ -693,9 +705,9 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
693
705
  `;
694
706
 
695
707
  if (context.patterns.length > 0) {
696
- output += `**${projectName}** follows a **${context.patterns[0]}** architecture`;
708
+ output += `**${projectName}** follows a **${patternLabel(context.patterns[0])}** architecture`;
697
709
  if (context.patterns.length > 1) {
698
- output += ` with elements of **${context.patterns.slice(1).join(", ")}**`;
710
+ output += ` with elements of **${context.patterns.slice(1).map(patternLabel).join(", ")}**`;
699
711
  }
700
712
  output += `.\n\n`;
701
713
 
@@ -799,6 +811,8 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
799
811
  }
800
812
  if (isCLI) {
801
813
  output += `| **Interface** | CLI | Terminal-driven interaction |\n`;
814
+ } else if (isLibrary) {
815
+ output += `| **Interface** | Library | Imported by other programs through its public API |\n`;
802
816
  }
803
817
 
804
818
  // Dependency health with deep analysis
@@ -813,9 +827,9 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
813
827
  output += `| Metric | Value | Status | Recommendation |\n`;
814
828
  output += `|--------|-------|--------|----------------|\n`;
815
829
  output += `| Import Edges | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "✅" : "⚠️"} | ${stats.totalEdges > 0 ? "Healthy module connectivity" : "Consider connecting modules"} |\n`;
816
- output += `| External Packages | ${stats.externalDeps} | ${stats.externalDeps < 50 ? "✅" : stats.externalDeps < 100 ? "⚠️" : "🔴"} | ${stats.externalDeps >= 100 ? "Audit for unused dependencies" : stats.externalDeps >= 50 ? "Review dependency necessity" : "Lean dependency set"} |\n`;
830
+ output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "✅" : stats.externalDeps < 100 ? "⚠️" : "🔴"} | ${stats.externalDeps >= 100 ? "Audit for unused dependencies" : stats.externalDeps >= 50 ? "Review dependency necessity" : "Lean dependency set"} |\n`;
817
831
  output += `| Circular Deps | ${stats.cycles} | ${stats.cycles === 0 ? "✅" : "🔴"} | ${stats.cycles === 0 ? "Clean module boundaries" : "Refactor to break cycles"} |\n`;
818
- output += `| Orphan Files | ${stats.orphanFiles} | ${stats.orphanFiles === 0 ? "✅" : "⚠️"} | ${stats.orphanFiles === 0 ? "All code is connected" : "Review for dead code"} |\n`;
832
+ output += `| Orphan Files | ${stats.orphanFiles} | ${orphanRatio(stats) > ORPHAN_WARN_RATIO ? "⚠️" : "✅"} | ${stats.orphanFiles === 0 ? "All code is connected" : orphanRatio(stats) > ORPHAN_WARN_RATIO ? "Review for dead code" : "A few unconnected files (examples, type tests, entry points) are normal"} |\n`;
819
833
 
820
834
  // Architectural strengths and concerns
821
835
  output += `\n### Architectural Strengths\n\n`;
@@ -826,7 +840,7 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
826
840
  if (stats.orphanFiles === 0) {
827
841
  strengths.push("**Fully connected codebase** — Every file is part of the dependency graph. No dead code or forgotten modules.");
828
842
  }
829
- if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy < stats.totalFiles * 0.3) {
843
+ if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy < sourceFileCount(stats) * 0.3) {
830
844
  strengths.push("**Balanced coupling** — No single module dominates the import graph. This reduces change risk and enables parallel development.");
831
845
  }
832
846
  if (testFrameworks.length > 0) {
@@ -848,8 +862,8 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
848
862
  if (stats.orphanFiles > stats.totalFiles * 0.2) {
849
863
  concerns.push(`**High orphan file ratio** — ${stats.orphanFiles}/${stats.totalFiles} files (${Math.round(stats.orphanFiles / stats.totalFiles * 100)}%) are not imported. Consider removing dead code. Priority: Medium.`);
850
864
  }
851
- if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy >= stats.totalFiles * 0.3) {
852
- concerns.push(`**Central coupling hotspot** — \`${stats.hubs[0].key}\` is imported by ${stats.hubs[0].importedBy} files (${Math.round(stats.hubs[0].importedBy / stats.totalFiles * 100)}%). Consider breaking into smaller modules. Priority: Medium.`);
865
+ if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy >= sourceFileCount(stats) * 0.3) {
866
+ concerns.push(`**Central coupling hotspot** — \`${stats.hubs[0].key}\` is imported by ${stats.hubs[0].importedBy} files (${Math.round(stats.hubs[0].importedBy / sourceFileCount(stats) * 100)}%). Consider breaking into smaller modules. Priority: Medium.`);
853
867
  }
854
868
  if (stats.externalDeps > 100) {
855
869
  concerns.push(`**Heavy external dependencies** — ${stats.externalDeps} packages increase security surface and maintenance burden. Audit regularly. Priority: Low.`);
@@ -932,7 +946,7 @@ Data flows describe how information moves through **${projectName}** — from ex
932
946
  `;
933
947
 
934
948
  // Combine heuristic flows with dep-graph-derived flows, filtering out test file flows
935
- const allFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test'));
949
+ const allFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test') && !isTestPath(f.modules?.[0]));
936
950
 
937
951
  // Generate additional flows from dependency graph hub chains
938
952
  if (depGraph?.nodes && depGraph.nodes.length > 0 && allFlows.length < 3) {
@@ -1135,7 +1149,7 @@ function getFallbackDeveloperOnboarding(context, enrichment = {}) {
1135
1149
  const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose tools";
1136
1150
  const languageList = context.techStack.languages.join(", ") || "standard languages";
1137
1151
  const testFrameworks = context.techStack.testFrameworks || [];
1138
- const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
1152
+ const isCLI = isCliProject(context);
1139
1153
  const projectName = context.project.name || "this project";
1140
1154
  const routes = context.routes || {};
1141
1155
  const pages = routes.pages || [];
@@ -1166,7 +1180,7 @@ This guide will help you get up to speed quickly. **${projectName}** is a ${fram
1166
1180
  | **Build Tools** | ${context.techStack.buildTools.join(", ") || "Standard toolchain"} |
1167
1181
  ${testFrameworks.length > 0 ? `| **Testing** | ${testFrameworks.join(", ")} |\n` : ""}| **Size** | ${context.project.modulesDetected} modules, ${context.project.filesScanned} files |
1168
1182
  | **Domains** | ${context.domains.length} functional areas |
1169
- ${isCLI ? "| **Type** | Command-line interface |\n" : ""}${context.project.apiRoutesDetected > 0 ? `| **API** | ${context.project.apiRoutesDetected} endpoints |\n` : ""}${context.project.pagesDetected > 0 ? `| **UI** | ${context.project.pagesDetected} pages |\n` : ""}
1183
+ ${isCLI ? "| **Type** | Command-line interface |\n" : isLibraryProject(context) ? "| **Type** | Library |\n" : ""}${context.project.apiRoutesDetected > 0 ? `| **API** | ${context.project.apiRoutesDetected} endpoints |\n` : ""}${context.project.pagesDetected > 0 ? `| **UI** | ${context.project.pagesDetected} pages |\n` : ""}
1170
1184
 
1171
1185
  ---
1172
1186
 
@@ -1292,7 +1306,7 @@ ${context.repoRoots.map(root => {
1292
1306
  }
1293
1307
 
1294
1308
  // Data flows for understanding behavior
1295
- const onboardingFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test'));
1309
+ const onboardingFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test') && !isTestPath(f.modules?.[0]));
1296
1310
  if (onboardingFlows.length > 0) {
1297
1311
  output += `---\n\n## How Data Flows\n\n`;
1298
1312
  output += `Understanding these flows helps you see how the system processes information end-to-end:\n\n`;
@@ -1354,8 +1368,8 @@ ${context.repoRoots.map(root => {
1354
1368
  if (depGraph?.stats?.cycles > 0) {
1355
1369
  pitfalls.push(`**Circular dependencies** — This codebase has ${depGraph.stats.cycles} circular import${depGraph.stats.cycles === 1 ? '' : 's'}. Be careful not to introduce more.`);
1356
1370
  }
1357
- if (depGraph?.stats?.hubs?.length > 0 && depGraph.stats.hubs[0].importedBy >= depGraph.stats.totalFiles * 0.3) {
1358
- pitfalls.push(`**High-impact modules** — \`${depGraph.stats.hubs[0].key.split("/").pop()}\` is used by ${Math.round(depGraph.stats.hubs[0].importedBy / depGraph.stats.totalFiles * 100)}% of the codebase. Changes here need thorough testing.`);
1371
+ if (depGraph?.stats?.hubs?.length > 0 && depGraph.stats.hubs[0].importedBy >= sourceFileCount(depGraph.stats) * 0.3) {
1372
+ pitfalls.push(`**High-impact modules** — \`${depGraph.stats.hubs[0].key.split("/").pop()}\` is used by ${Math.round(depGraph.stats.hubs[0].importedBy / sourceFileCount(depGraph.stats) * 100)}% of the codebase. Changes here need thorough testing.`);
1359
1373
  }
1360
1374
  if (context.monorepo) {
1361
1375
  pitfalls.push(`**Monorepo boundaries** — Remember to build dependent packages when making changes.`);
@@ -1409,38 +1423,46 @@ function formatModuleType(type) {
1409
1423
  state: "State Management",
1410
1424
  route: "Route/Page Modules",
1411
1425
  app: "Application Core",
1426
+ source: "Source Modules",
1412
1427
  other: "Other Modules",
1413
1428
  };
1414
1429
  return labels[type] || type;
1415
1430
  }
1416
1431
 
1417
- // Helper: derive flows from dependency graph hub chains
1432
+ // File names that name a folder or an entry point rather than a piece of the system.
1433
+ const BARREL_NAMES = new Set(["index", "main", "mod", "__init__", "lib", "src", "app", "source", "pkg", "internal"]);
1434
+
1435
+ // Helper: derive flows from dependency graph hub chains.
1436
+ // A hub is an integration point only when it imports something itself. Barrels (`lib/index`),
1437
+ // leaf helpers (error classes, constants), tests, and examples are not flows.
1418
1438
  function inferFlowsFromDepGraph(depGraph) {
1419
1439
  const flows = [];
1420
1440
  if (!depGraph?.nodes || depGraph.nodes.length === 0) return flows;
1421
1441
 
1422
- // Find hub nodes – modules imported by many others
1423
- const hubThreshold = Math.max(3, Math.floor(depGraph.nodes.length * 0.05));
1424
- const hubs = depGraph.nodes
1425
- .filter(n => n.importedBy.length >= hubThreshold)
1426
- .sort((a, b) => b.importedBy.length - a.importedBy.length)
1442
+ const productNodes = depGraph.nodes.filter(n => !isNonProductPath(n.key));
1443
+ const hubThreshold = Math.max(3, Math.floor(productNodes.length * 0.05));
1444
+ const hubs = productNodes
1445
+ .filter(n => !BARREL_NAMES.has(String(n.key.split("/").pop()).toLowerCase()))
1446
+ .filter(n => n.imports.length > 0)
1447
+ .map(n => ({ node: n, importers: n.importedBy.filter(i => !isNonProductPath(i)) }))
1448
+ .filter(h => h.importers.length >= hubThreshold)
1449
+ .sort((a, b) => b.importers.length - a.importers.length)
1427
1450
  .slice(0, 3);
1428
1451
 
1429
- for (const hub of hubs) {
1430
- const testPattern = /(?:^|\/)(?:tests?|__tests?__|spec|__spec__)\/|\.(test|spec)\.[jt]sx?$/i;
1431
- const importers = hub.importedBy.filter(i => !testPattern.test(i)).slice(0, 5);
1452
+ for (const { node: hub, importers: allImporters } of hubs) {
1453
+ const importers = allImporters.slice(0, 5);
1432
1454
  const downstream = hub.imports.slice(0, 3);
1433
1455
  const shortName = hub.key.split("/").pop();
1434
1456
 
1435
1457
  flows.push({
1436
1458
  name: `${shortName} Integration Flow`,
1437
- description: `\`${hub.key}\` is a central module imported by ${hub.importedBy.length} files, acting as an integration point for data and logic`,
1459
+ description: `\`${hub.key}\` is a central module imported by ${allImporters.length} files, acting as an integration point for data and logic`,
1438
1460
  steps: [
1439
1461
  ...importers.map(i => `\`${i.split("/").pop()}\` imports \`${shortName}\``),
1440
1462
  ...(downstream.length > 0 ? [`\`${shortName}\` depends on ${downstream.map(d => `\`${d.split("/").pop()}\``).join(", ")}`] : []),
1441
1463
  ],
1442
1464
  modules: [hub.key, ...importers.slice(0, 3)],
1443
- critical: hub.importedBy.length >= hubThreshold * 2,
1465
+ critical: allImporters.length >= hubThreshold * 2,
1444
1466
  });
1445
1467
  }
1446
1468
 
@@ -1566,11 +1588,39 @@ function getLayerResponsibility(type) {
1566
1588
  state: "Manages application-wide data synchronization",
1567
1589
  route: "Maps URL paths to page components and handles navigation",
1568
1590
  app: "Orchestrates application initialization and core bootstrapping",
1591
+ source: "The project's own source code",
1569
1592
  other: "General purpose functionality",
1570
1593
  };
1571
1594
  return responsibilities[type] || "Supporting application functionality";
1572
1595
  }
1573
1596
 
1597
+ // Orphan files above this share of all code files are a warning, and they cost health points.
1598
+ // Below it they are normal (generated entry points, type tests, examples) and shown as fine.
1599
+ const ORPHAN_WARN_RATIO = 0.05;
1600
+
1601
+ function orphanRatio(stats) {
1602
+ return stats.orphanFiles / Math.max(stats.totalFiles, 1);
1603
+ }
1604
+
1605
+ function orphanStatus(stats) {
1606
+ if (stats.orphanFiles === 0) return "✅ All connected";
1607
+ const pct = Math.round(orphanRatio(stats) * 100);
1608
+ return orphanRatio(stats) > ORPHAN_WARN_RATIO
1609
+ ? `⚠️ ${stats.orphanFiles} unused (${pct}%)`
1610
+ : `✅ ${stats.orphanFiles} (${pct}%), within normal range`;
1611
+ }
1612
+
1613
+ // Files the product itself is made of. Hub shares are measured against these, not against tests.
1614
+ function sourceFileCount(stats) {
1615
+ return Math.max(stats.sourceFiles ?? stats.totalFiles, 1);
1616
+ }
1617
+
1618
+ // "12 (1 runtime, 11 development tooling)" when the split is known.
1619
+ function externalPackagesLabel(stats) {
1620
+ if (stats.externalRuntime === undefined || stats.externalDev === undefined || stats.externalDev === 0) return `${stats.externalDeps}`;
1621
+ return `${stats.externalDeps} (${stats.externalRuntime} runtime, ${stats.externalDev} dev/test tooling)`;
1622
+ }
1623
+
1574
1624
  // Helper: calculate codebase health score
1575
1625
  function calculateHealthScore(stats, context) {
1576
1626
  let score = 100;
@@ -1579,9 +1629,10 @@ function calculateHealthScore(stats, context) {
1579
1629
  if (stats.cycles > 0) score -= Math.min(30, stats.cycles * 10);
1580
1630
 
1581
1631
  // Orphan files indicate potential dead code
1582
- const orphanRatio = stats.orphanFiles / Math.max(stats.totalFiles, 1);
1583
- if (orphanRatio > 0.2) score -= 20;
1584
- else if (orphanRatio > 0.1) score -= 10;
1632
+ const ratio = orphanRatio(stats);
1633
+ if (ratio > 0.2) score -= 20;
1634
+ else if (ratio > 0.1) score -= 10;
1635
+ else if (ratio > ORPHAN_WARN_RATIO) score -= 5;
1585
1636
 
1586
1637
  // Too many external dependencies
1587
1638
  if (stats.externalDeps > 100) score -= 15;
@@ -1591,9 +1642,6 @@ function calculateHealthScore(stats, context) {
1591
1642
  const hasTests = (context.techStack.testFrameworks || []).length > 0;
1592
1643
  if (!hasTests) score -= 10;
1593
1644
 
1594
- // Give bonus for clean, connected codebase
1595
- if (stats.cycles === 0 && stats.totalEdges > 0) score = Math.min(100, score + 5);
1596
-
1597
1645
  return Math.max(0, Math.min(100, score));
1598
1646
  }
1599
1647
 
@@ -1642,10 +1690,40 @@ function inferFrameworkPurpose(frameworks) {
1642
1690
  return purposes.length > 0 ? purposes.join(", ") : "Application runtime";
1643
1691
  }
1644
1692
 
1693
+ // A pattern line is "Label — evidence" (`CLI tool architecture — \`bin\``). Only the label
1694
+ // says what the pattern is; the evidence is file names and must not pick the description.
1695
+ function patternLabel(pattern) {
1696
+ return String(pattern).split(" — ")[0].trim();
1697
+ }
1698
+
1699
+ const PATTERN_COPY = [
1700
+ [/^next\.?js app router/i, "Routes are defined by folders under `app/`, with server and client components mixed per route.", "File-based routing, nested layouts, streaming"],
1701
+ [/^next\.?js pages router/i, "Routes are defined by files under `pages/`, one file per URL.", "File-based routing, simple mental model"],
1702
+ [/^layered component/i, "UI components sit in their own layer and call shared library code rather than reaching into each other.", "Reusable UI, clear dependency direction"],
1703
+ [/^react hooks/i, "Stateful logic is packaged as custom hooks that components share.", "Reusable stateful logic, testable in isolation"],
1704
+ [/^centralized state/i, "Application state lives in a dedicated store that components read from and write to.", "Predictable data flow, easier debugging"],
1705
+ [/^api route/i, "HTTP endpoints live in route or handler folders, separate from UI code.", "Clear external interface, versionable"],
1706
+ [/^core\/kernel/i, "Foundational logic lives in `core/` or `kernel/` folders that other modules build on.", "Stable foundation, shared behavior in one place"],
1707
+ [/^plugin system/i, "The system supports extensibility through plugins or extensions loaded at runtime.", "Extensibility, community contributions, core stability"],
1708
+ [/^middleware pipeline/i, "Requests or data pass through a chain of middleware steps.", "Composable steps, cross-cutting concerns in one place"],
1709
+ [/^renderer pipeline/i, "Output is produced by renderer or template modules, one per output type.", "Output formats can change without touching the data layer"],
1710
+ [/^multi-output publishing/i, "Results are published to several targets through separate publisher modules.", "Add a target without touching the others"],
1711
+ [/^analysis pipeline/i, "Analyzer or detector modules inspect input and feed their findings forward.", "Each analysis step is testable alone"],
1712
+ [/^cli tool/i, "The system runs as a command-line interface, accepting user input through terminal commands and producing structured output.", "Automation-friendly, scriptable, integrates with CI/CD pipelines"],
1713
+ [/^shared utility/i, "Common helpers sit in shared modules that other modules import instead of duplicating code.", "Less duplication, one place to fix a helper"],
1714
+ [/^integration adapters/i, "Adapters wrap outside services behind a local interface.", "Outside services can be swapped or mocked"],
1715
+ [/^ai\/llm/i, "The code calls an AI or LLM provider or SDK.", "Intelligent features behind a provider boundary"],
1716
+ [/^delivery pipeline/i, "Dispatch and delivery modules move finished content to its destination.", "Delivery channels are isolated from content creation"],
1717
+ [/^dedicated test/i, "Tests live in their own folders, apart from production code.", "Safe refactoring, regression protection"],
1718
+ [/^acyclic dependency/i, "No circular imports were found: modules depend on each other in one direction only, so each layer can be read, tested, and changed on its own.", "Safe refactoring, predictable build order, simple testing"],
1719
+ ];
1720
+
1645
1721
  // Helper: explain an architectural pattern
1646
1722
  function explainPattern(pattern) {
1647
- const lower = pattern.toLowerCase();
1648
- if (/cli/i.test(lower)) return "The system operates as a command-line interface, accepting user input through terminal commands and producing structured output.";
1723
+ const label = patternLabel(pattern);
1724
+ const known = PATTERN_COPY.find(([regex]) => regex.test(label));
1725
+ if (known) return known[1];
1726
+ const lower = label.toLowerCase();
1649
1727
  if (/monorepo/i.test(lower)) return "Multiple related packages are managed in a single repository, enabling coordinated development and shared tooling.";
1650
1728
  if (/microservice/i.test(lower)) return "The system is decomposed into independently deployable services communicating over network protocols.";
1651
1729
  if (/mvc|model.?view/i.test(lower)) return "The application separates data (Model), presentation (View), and business logic (Controller) into distinct layers.";
@@ -1653,14 +1731,15 @@ function explainPattern(pattern) {
1653
1731
  if (/api.?first/i.test(lower)) return "The system is designed API-first, with a well-defined interface contract that drives both backend and frontend development.";
1654
1732
  if (/layered/i.test(lower)) return "The codebase is organized into horizontal layers (e.g., presentation, business, data) with clear boundaries.";
1655
1733
  if (/modular/i.test(lower)) return "Functionality is divided into self-contained modules with explicit interfaces and minimal coupling.";
1656
- if (/plugin/i.test(lower)) return "The system supports extensibility through a plugin architecture allowing third-party additions.";
1657
- return "A structured approach to organizing code and managing complexity.";
1734
+ return `Detected from the code layout: ${label}.`;
1658
1735
  }
1659
1736
 
1660
1737
  // Helper: get benefits of a pattern
1661
1738
  function getBenefitsForPattern(pattern) {
1662
- const lower = pattern.toLowerCase();
1663
- if (/cli/i.test(lower)) return "Automation-friendly, scriptable, integrates with CI/CD pipelines";
1739
+ const label = patternLabel(pattern);
1740
+ const known = PATTERN_COPY.find(([regex]) => regex.test(label));
1741
+ if (known) return known[2];
1742
+ const lower = label.toLowerCase();
1664
1743
  if (/monorepo/i.test(lower)) return "Shared tooling, atomic cross-package changes, unified versioning";
1665
1744
  if (/microservice/i.test(lower)) return "Independent deployment, tech flexibility, team autonomy";
1666
1745
  if (/mvc|model.?view/i.test(lower)) return "Separation of concerns, testability, parallel development";
@@ -1668,7 +1747,6 @@ function getBenefitsForPattern(pattern) {
1668
1747
  if (/api.?first/i.test(lower)) return "Clear contracts, parallel development, documentation-driven";
1669
1748
  if (/layered/i.test(lower)) return "Clear responsibilities, easier testing, technology swapping";
1670
1749
  if (/modular/i.test(lower)) return "Reusability, isolated testing, team ownership boundaries";
1671
- if (/plugin/i.test(lower)) return "Extensibility, community contributions, core stability";
1672
1750
  return "Structured organization, maintainability, clarity";
1673
1751
  }
1674
1752