@chappibunny/repolens 1.15.3 → 1.15.5
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 +55 -0
- package/README.md +13 -9
- package/llms-install.md +2 -0
- package/package.json +2 -2
- package/src/ai/generate-sections.js +202 -80
- package/src/analyzers/context-builder.js +120 -63
- package/src/analyzers/dependency-graph.js +97 -2
- package/src/analyzers/domain-inference.js +55 -4
- package/src/analyzers/flow-inference.js +48 -9
- package/src/analyzers/maturity.js +151 -0
- package/src/analyzers/security-patterns.js +29 -3
- package/src/cli.js +21 -17
- package/src/core/scan.js +50 -4
- package/src/docs/generate-doc-set.js +28 -0
- package/src/init.js +2 -4
- package/src/mcp/tools.js +2 -1
- package/src/renderers/render.js +3 -3
- package/src/renderers/renderAnalysis.js +28 -9
- package/src/utils/ai-notice.js +20 -0
- package/src/utils/path-roles.js +58 -0
- package/src/utils/project-name.js +24 -0
|
@@ -12,7 +12,10 @@ import {
|
|
|
12
12
|
AI_SCHEMAS,
|
|
13
13
|
renderStructuredToMarkdown,
|
|
14
14
|
} from "./prompts.js";
|
|
15
|
-
import { identifyFlowDependencies } from "../analyzers/flow-inference.js";
|
|
15
|
+
import { identifyFlowDependencies, filterNoiseFlows } from "../analyzers/flow-inference.js";
|
|
16
|
+
import { isCliProject, isLibraryProject } from "../analyzers/context-builder.js";
|
|
17
|
+
import { scoreMaturity } from "../analyzers/maturity.js";
|
|
18
|
+
import { isNonProductPath, isTestPath } from "../utils/path-roles.js";
|
|
16
19
|
import { info, warn } from "../utils/logger.js";
|
|
17
20
|
|
|
18
21
|
// Strip conversational patterns that LLMs sometimes inject into documentation
|
|
@@ -152,23 +155,34 @@ export async function generateDeveloperOnboarding(context, enrichment = {}, conf
|
|
|
152
155
|
|
|
153
156
|
// Fallback generators (deterministic, no AI)
|
|
154
157
|
|
|
158
|
+
function withArticle(word) {
|
|
159
|
+
return `${/^[aeiou]/i.test(word) ? "an" : "a"} ${word}`;
|
|
160
|
+
}
|
|
161
|
+
|
|
155
162
|
function getFallbackExecutiveSummary(context, enrichment = {}) {
|
|
156
163
|
const { depGraph, flows } = enrichment;
|
|
157
164
|
const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose";
|
|
158
165
|
const languageList = context.techStack.languages.join(", ") || "multiple languages";
|
|
159
166
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
160
|
-
const isCLI = (context
|
|
167
|
+
const isCLI = isCliProject(context);
|
|
168
|
+
const isLibrary = isLibraryProject(context);
|
|
161
169
|
const projectName = context.project.name || "This system";
|
|
162
170
|
|
|
163
|
-
//
|
|
171
|
+
// Maturity comes from repo signals (version, history, tests, CI, docs, dependency health)
|
|
172
|
+
const maturity = context.maturity || scoreMaturity([
|
|
173
|
+
{ key: "tests", met: testFrameworks.length > 0 },
|
|
174
|
+
{ key: "build", met: context.techStack.buildTools.length > 0 },
|
|
175
|
+
{ key: "deps", met: depGraph?.stats?.cycles === undefined ? null : depGraph.stats.cycles === 0 },
|
|
176
|
+
]);
|
|
164
177
|
const hasTests = testFrameworks.length > 0;
|
|
165
|
-
const hasBuildTools = context.techStack.buildTools.length > 0;
|
|
166
178
|
const hasCleanDeps = depGraph?.stats?.cycles === 0;
|
|
167
|
-
const
|
|
168
|
-
const
|
|
179
|
+
const maturityLabel = maturity.label;
|
|
180
|
+
const maturityScore = maturity.score;
|
|
181
|
+
const maturityMax = maturity.max;
|
|
169
182
|
|
|
170
183
|
// Determine project type for better descriptions
|
|
171
|
-
const projectType = isCLI ? "command-line tool" :
|
|
184
|
+
const projectType = isCLI ? "command-line tool" :
|
|
185
|
+
isLibrary ? "library" :
|
|
172
186
|
(context.project.apiRoutesDetected > 0 && context.project.pagesDetected > 0) ? "full-stack application" :
|
|
173
187
|
context.project.apiRoutesDetected > 0 ? "API service" :
|
|
174
188
|
context.project.pagesDetected > 0 ? "web application" : "software system";
|
|
@@ -179,10 +193,10 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
|
|
|
179
193
|
|
|
180
194
|
| Aspect | Summary |
|
|
181
195
|
|--------|---------|
|
|
182
|
-
| **What** | ${projectName} is
|
|
183
|
-
| **Built With** | ${
|
|
196
|
+
| **What** | ${projectName} is ${withArticle(maturityLabel.toLowerCase())} ${projectType} |
|
|
197
|
+
| **Built With** | ${[...context.techStack.frameworks, languageList].join(", ")} |
|
|
184
198
|
| **Scale** | ${context.project.modulesDetected} modules across ${context.project.filesScanned} files |
|
|
185
|
-
| **Maturity** | ${maturityLabel} (${maturityScore}
|
|
199
|
+
| **Maturity** | ${maturityLabel} (${maturityScore}/${maturityMax} indicators) |
|
|
186
200
|
|
|
187
201
|
---
|
|
188
202
|
|
|
@@ -192,6 +206,8 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
|
|
|
192
206
|
|
|
193
207
|
if (isCLI) {
|
|
194
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.`;
|
|
195
211
|
} else if (context.project.apiRoutesDetected > 0 && context.project.pagesDetected > 0) {
|
|
196
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.`;
|
|
197
213
|
} else if (context.project.apiRoutesDetected > 0) {
|
|
@@ -212,7 +228,7 @@ The system is organized around **${context.domains.length} functional domains**,
|
|
|
212
228
|
output += `|--------|------------|-------|\n`;
|
|
213
229
|
for (const d of topDomains) {
|
|
214
230
|
const desc = d.description || inferDomainCapability(d.name);
|
|
215
|
-
output += `| **${d.name}** | ${desc} | ${d.moduleCount}
|
|
231
|
+
output += `| **${d.name}** | ${desc} | ${d.moduleCount} module${d.moduleCount === 1 ? "" : "s"}, ${d.fileCount} file${d.fileCount === 1 ? "" : "s"} |\n`;
|
|
216
232
|
}
|
|
217
233
|
if (context.domains.length > 6) {
|
|
218
234
|
output += `| *+${context.domains.length - 6} more* | Additional functional areas | — |\n`;
|
|
@@ -271,9 +287,9 @@ The system is organized around **${context.domains.length} functional domains**,
|
|
|
271
287
|
output += `| Metric | Value | Status |\n`;
|
|
272
288
|
output += `|--------|-------|--------|\n`;
|
|
273
289
|
output += `| Internal imports | ${stats.totalEdges} edges | ${stats.totalEdges > 0 ? "✅ Connected" : "⚠️ Isolated"} |\n`;
|
|
274
|
-
output += `| External Packages | ${stats
|
|
290
|
+
output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "✅ Manageable" : stats.externalDeps < 100 ? "⚠️ Consider audit" : "🔴 High dependency count"} |\n`;
|
|
275
291
|
output += `| Circular Dependencies | ${stats.cycles === 0 ? "cycle-free" : stats.cycles} | ${stats.cycles === 0 ? "✅ None" : `🔴 ${stats.cycles} detected`} |\n`;
|
|
276
|
-
output += `| Orphan Files | ${stats.orphanFiles} | ${stats
|
|
292
|
+
output += `| Orphan Files | ${stats.orphanFiles} | ${orphanStatus(stats)} |\n`;
|
|
277
293
|
|
|
278
294
|
// Key insights
|
|
279
295
|
output += `\n### Key Insights\n\n`;
|
|
@@ -284,19 +300,19 @@ The system is organized around **${context.domains.length} functional domains**,
|
|
|
284
300
|
}
|
|
285
301
|
if (stats.hubs && stats.hubs.length > 0) {
|
|
286
302
|
const topHub = stats.hubs[0];
|
|
287
|
-
if (topHub.importedBy > stats
|
|
288
|
-
output += `- ⚠️ **Coupling hotspot** — \`${topHub.key}\` is imported by ${topHub.importedBy} files (${Math.round(topHub.importedBy / stats
|
|
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`;
|
|
289
305
|
} else {
|
|
290
306
|
output += `- ✅ **Healthy coupling** — Most-imported module (\`${topHub.key}\`) serves ${topHub.importedBy} files without excessive centralization.\n`;
|
|
291
307
|
}
|
|
292
308
|
}
|
|
293
|
-
if (stats
|
|
309
|
+
if (orphanRatio(stats) > ORPHAN_WARN_RATIO) {
|
|
294
310
|
output += `- ⚠️ **Potential dead code** — ${stats.orphanFiles} files (${Math.round(stats.orphanFiles / stats.totalFiles * 100)}%) are not imported anywhere.\n`;
|
|
295
311
|
}
|
|
296
312
|
}
|
|
297
313
|
|
|
298
314
|
// Data flows summary
|
|
299
|
-
const summaryFlows = (flows
|
|
315
|
+
const summaryFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test') && !isTestPath(f.modules?.[0]));
|
|
300
316
|
if (summaryFlows.length > 0) {
|
|
301
317
|
output += `\n## Key Data Flows\n\n`;
|
|
302
318
|
output += `The system processes information through **${summaryFlows.length} identified flow${summaryFlows.length === 1 ? "" : "s"}**:\n\n`;
|
|
@@ -311,7 +327,7 @@ The system is organized around **${context.domains.length} functional domains**,
|
|
|
311
327
|
|
|
312
328
|
const observations = [];
|
|
313
329
|
if (context.patterns.length > 0) {
|
|
314
|
-
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.`);
|
|
315
331
|
}
|
|
316
332
|
if (context.domains.length >= 5) {
|
|
317
333
|
observations.push(`With ${context.domains.length} distinct domains, the system has clear separation of concerns suitable for team-based development.`);
|
|
@@ -321,6 +337,8 @@ The system is organized around **${context.domains.length} functional domains**,
|
|
|
321
337
|
}
|
|
322
338
|
if (isCLI) {
|
|
323
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.`);
|
|
324
342
|
}
|
|
325
343
|
|
|
326
344
|
for (const obs of observations) {
|
|
@@ -337,7 +355,8 @@ The system is organized around **${context.domains.length} functional domains**,
|
|
|
337
355
|
function getFallbackSystemOverview(context, enrichment = {}) {
|
|
338
356
|
const { depGraph } = enrichment;
|
|
339
357
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
340
|
-
const isCLI = (context
|
|
358
|
+
const isCLI = isCliProject(context);
|
|
359
|
+
const isLibrary = isLibraryProject(context);
|
|
341
360
|
const projectName = context.project.name || "This system";
|
|
342
361
|
|
|
343
362
|
// Calculate scale classification
|
|
@@ -371,6 +390,8 @@ function getFallbackSystemOverview(context, enrichment = {}) {
|
|
|
371
390
|
}
|
|
372
391
|
if (isCLI) {
|
|
373
392
|
output += `\n| **Interface Type** | Command-Line Tool |`;
|
|
393
|
+
} else if (isLibrary) {
|
|
394
|
+
output += `\n| **Interface Type** | Library (imported API) |`;
|
|
374
395
|
}
|
|
375
396
|
|
|
376
397
|
output += `
|
|
@@ -505,9 +526,9 @@ The system is built on a modern technology foundation:\n\n`;
|
|
|
505
526
|
output += `| Metric | Value | Assessment |\n`;
|
|
506
527
|
output += `|--------|-------|------------|\n`;
|
|
507
528
|
output += `| Internal Imports | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "Modules are connected" : "No internal imports"} |\n`;
|
|
508
|
-
output += `| External Packages | ${stats
|
|
529
|
+
output += `| External Packages | ${externalPackagesLabel(stats)} | ${stats.externalDeps < 50 ? "Lean dependencies" : stats.externalDeps < 100 ? "Moderate dependencies" : "Many dependencies"} |\n`;
|
|
509
530
|
output += `| Circular Dependencies | ${stats.cycles} | ${stats.cycles === 0 ? "✅ Clean" : "⚠️ Needs attention"} |\n`;
|
|
510
|
-
output += `| Orphan Files | ${stats.orphanFiles} | ${stats
|
|
531
|
+
output += `| Orphan Files | ${stats.orphanFiles} | ${orphanStatus(stats)} |\n`;
|
|
511
532
|
|
|
512
533
|
if (stats.hubs && stats.hubs.length > 0) {
|
|
513
534
|
output += `\n**Hub modules** (most imported):\n\n`;
|
|
@@ -672,7 +693,8 @@ ${context.domains.map(d => `| ${d.name} | ${d.moduleCount} | ${d.fileCount} | ${
|
|
|
672
693
|
|
|
673
694
|
function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
674
695
|
const { depGraph, driftResult } = enrichment;
|
|
675
|
-
const isCLI = (context
|
|
696
|
+
const isCLI = isCliProject(context);
|
|
697
|
+
const isLibrary = isLibraryProject(context);
|
|
676
698
|
const projectName = context.project.name || "This system";
|
|
677
699
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
678
700
|
|
|
@@ -683,9 +705,9 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
|
683
705
|
`;
|
|
684
706
|
|
|
685
707
|
if (context.patterns.length > 0) {
|
|
686
|
-
output += `**${projectName}** follows a **${context.patterns[0]}** architecture`;
|
|
708
|
+
output += `**${projectName}** follows a **${patternLabel(context.patterns[0])}** architecture`;
|
|
687
709
|
if (context.patterns.length > 1) {
|
|
688
|
-
output += ` with elements of **${context.patterns.slice(1).join(", ")}**`;
|
|
710
|
+
output += ` with elements of **${context.patterns.slice(1).map(patternLabel).join(", ")}**`;
|
|
689
711
|
}
|
|
690
712
|
output += `.\n\n`;
|
|
691
713
|
|
|
@@ -705,6 +727,14 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
|
705
727
|
output += `- Flexibility for rapid development\n\n`;
|
|
706
728
|
}
|
|
707
729
|
|
|
730
|
+
// Findings are warnings about the code, not architectural patterns
|
|
731
|
+
const findings = context.findings || [];
|
|
732
|
+
if (findings.length > 0) {
|
|
733
|
+
output += `### Findings\n\n`;
|
|
734
|
+
for (const finding of findings) output += `- ${finding}\n`;
|
|
735
|
+
output += `\n`;
|
|
736
|
+
}
|
|
737
|
+
|
|
708
738
|
// System layers with rich descriptions
|
|
709
739
|
output += `## System Layers\n\n`;
|
|
710
740
|
output += `The system is organized into **${context.domains.length} functional layer${context.domains.length === 1 ? '' : 's'}**, each encapsulating a distinct area of responsibility:\n\n`;
|
|
@@ -781,6 +811,8 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
|
781
811
|
}
|
|
782
812
|
if (isCLI) {
|
|
783
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`;
|
|
784
816
|
}
|
|
785
817
|
|
|
786
818
|
// Dependency health with deep analysis
|
|
@@ -795,9 +827,9 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
|
795
827
|
output += `| Metric | Value | Status | Recommendation |\n`;
|
|
796
828
|
output += `|--------|-------|--------|----------------|\n`;
|
|
797
829
|
output += `| Import Edges | ${stats.totalEdges} | ${stats.totalEdges > 0 ? "✅" : "⚠️"} | ${stats.totalEdges > 0 ? "Healthy module connectivity" : "Consider connecting modules"} |\n`;
|
|
798
|
-
output += `| External Packages | ${stats
|
|
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`;
|
|
799
831
|
output += `| Circular Deps | ${stats.cycles} | ${stats.cycles === 0 ? "✅" : "🔴"} | ${stats.cycles === 0 ? "Clean module boundaries" : "Refactor to break cycles"} |\n`;
|
|
800
|
-
output += `| Orphan Files | ${stats.orphanFiles} | ${stats
|
|
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`;
|
|
801
833
|
|
|
802
834
|
// Architectural strengths and concerns
|
|
803
835
|
output += `\n### Architectural Strengths\n\n`;
|
|
@@ -808,7 +840,7 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
|
808
840
|
if (stats.orphanFiles === 0) {
|
|
809
841
|
strengths.push("**Fully connected codebase** — Every file is part of the dependency graph. No dead code or forgotten modules.");
|
|
810
842
|
}
|
|
811
|
-
if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy < stats
|
|
843
|
+
if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy < sourceFileCount(stats) * 0.3) {
|
|
812
844
|
strengths.push("**Balanced coupling** — No single module dominates the import graph. This reduces change risk and enables parallel development.");
|
|
813
845
|
}
|
|
814
846
|
if (testFrameworks.length > 0) {
|
|
@@ -830,8 +862,8 @@ function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
|
830
862
|
if (stats.orphanFiles > stats.totalFiles * 0.2) {
|
|
831
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.`);
|
|
832
864
|
}
|
|
833
|
-
if (stats.hubs && stats.hubs.length > 0 && stats.hubs[0].importedBy >= stats
|
|
834
|
-
concerns.push(`**Central coupling hotspot** — \`${stats.hubs[0].key}\` is imported by ${stats.hubs[0].importedBy} files (${Math.round(stats.hubs[0].importedBy / stats
|
|
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.`);
|
|
835
867
|
}
|
|
836
868
|
if (stats.externalDeps > 100) {
|
|
837
869
|
concerns.push(`**Heavy external dependencies** — ${stats.externalDeps} packages increase security surface and maintenance burden. Audit regularly. Priority: Low.`);
|
|
@@ -914,11 +946,11 @@ Data flows describe how information moves through **${projectName}** — from ex
|
|
|
914
946
|
`;
|
|
915
947
|
|
|
916
948
|
// Combine heuristic flows with dep-graph-derived flows, filtering out test file flows
|
|
917
|
-
const allFlows =
|
|
949
|
+
const allFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test') && !isTestPath(f.modules?.[0]));
|
|
918
950
|
|
|
919
951
|
// Generate additional flows from dependency graph hub chains
|
|
920
952
|
if (depGraph?.nodes && depGraph.nodes.length > 0 && allFlows.length < 3) {
|
|
921
|
-
const hubFlows = inferFlowsFromDepGraph(depGraph);
|
|
953
|
+
const hubFlows = filterNoiseFlows(inferFlowsFromDepGraph(depGraph));
|
|
922
954
|
for (const hf of hubFlows) {
|
|
923
955
|
if (!allFlows.some(f => f.name === hf.name)) {
|
|
924
956
|
allFlows.push(hf);
|
|
@@ -1117,7 +1149,7 @@ function getFallbackDeveloperOnboarding(context, enrichment = {}) {
|
|
|
1117
1149
|
const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose tools";
|
|
1118
1150
|
const languageList = context.techStack.languages.join(", ") || "standard languages";
|
|
1119
1151
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
1120
|
-
const isCLI = (context
|
|
1152
|
+
const isCLI = isCliProject(context);
|
|
1121
1153
|
const projectName = context.project.name || "this project";
|
|
1122
1154
|
const routes = context.routes || {};
|
|
1123
1155
|
const pages = routes.pages || [];
|
|
@@ -1148,7 +1180,7 @@ This guide will help you get up to speed quickly. **${projectName}** is a ${fram
|
|
|
1148
1180
|
| **Build Tools** | ${context.techStack.buildTools.join(", ") || "Standard toolchain"} |
|
|
1149
1181
|
${testFrameworks.length > 0 ? `| **Testing** | ${testFrameworks.join(", ")} |\n` : ""}| **Size** | ${context.project.modulesDetected} modules, ${context.project.filesScanned} files |
|
|
1150
1182
|
| **Domains** | ${context.domains.length} functional areas |
|
|
1151
|
-
${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` : ""}
|
|
1152
1184
|
|
|
1153
1185
|
---
|
|
1154
1186
|
|
|
@@ -1274,7 +1306,7 @@ ${context.repoRoots.map(root => {
|
|
|
1274
1306
|
}
|
|
1275
1307
|
|
|
1276
1308
|
// Data flows for understanding behavior
|
|
1277
|
-
const onboardingFlows = (flows
|
|
1309
|
+
const onboardingFlows = filterNoiseFlows(flows).filter(f => !f.name?.toLowerCase().includes('test') && !isTestPath(f.modules?.[0]));
|
|
1278
1310
|
if (onboardingFlows.length > 0) {
|
|
1279
1311
|
output += `---\n\n## How Data Flows\n\n`;
|
|
1280
1312
|
output += `Understanding these flows helps you see how the system processes information end-to-end:\n\n`;
|
|
@@ -1336,8 +1368,8 @@ ${context.repoRoots.map(root => {
|
|
|
1336
1368
|
if (depGraph?.stats?.cycles > 0) {
|
|
1337
1369
|
pitfalls.push(`**Circular dependencies** — This codebase has ${depGraph.stats.cycles} circular import${depGraph.stats.cycles === 1 ? '' : 's'}. Be careful not to introduce more.`);
|
|
1338
1370
|
}
|
|
1339
|
-
if (depGraph?.stats?.hubs?.length > 0 && depGraph.stats.hubs[0].importedBy >= depGraph.stats
|
|
1340
|
-
pitfalls.push(`**High-impact modules** — \`${depGraph.stats.hubs[0].key.split("/").pop()}\` is used by ${Math.round(depGraph.stats.hubs[0].importedBy / depGraph.stats
|
|
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.`);
|
|
1341
1373
|
}
|
|
1342
1374
|
if (context.monorepo) {
|
|
1343
1375
|
pitfalls.push(`**Monorepo boundaries** — Remember to build dependent packages when making changes.`);
|
|
@@ -1391,38 +1423,46 @@ function formatModuleType(type) {
|
|
|
1391
1423
|
state: "State Management",
|
|
1392
1424
|
route: "Route/Page Modules",
|
|
1393
1425
|
app: "Application Core",
|
|
1426
|
+
source: "Source Modules",
|
|
1394
1427
|
other: "Other Modules",
|
|
1395
1428
|
};
|
|
1396
1429
|
return labels[type] || type;
|
|
1397
1430
|
}
|
|
1398
1431
|
|
|
1399
|
-
//
|
|
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.
|
|
1400
1438
|
function inferFlowsFromDepGraph(depGraph) {
|
|
1401
1439
|
const flows = [];
|
|
1402
1440
|
if (!depGraph?.nodes || depGraph.nodes.length === 0) return flows;
|
|
1403
1441
|
|
|
1404
|
-
|
|
1405
|
-
const hubThreshold = Math.max(3, Math.floor(
|
|
1406
|
-
const hubs =
|
|
1407
|
-
.filter(n => n.
|
|
1408
|
-
.
|
|
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)
|
|
1409
1450
|
.slice(0, 3);
|
|
1410
1451
|
|
|
1411
|
-
for (const hub of hubs) {
|
|
1412
|
-
const
|
|
1413
|
-
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);
|
|
1414
1454
|
const downstream = hub.imports.slice(0, 3);
|
|
1415
1455
|
const shortName = hub.key.split("/").pop();
|
|
1416
1456
|
|
|
1417
1457
|
flows.push({
|
|
1418
1458
|
name: `${shortName} Integration Flow`,
|
|
1419
|
-
description: `\`${hub.key}\` is a central module imported by ${
|
|
1459
|
+
description: `\`${hub.key}\` is a central module imported by ${allImporters.length} files, acting as an integration point for data and logic`,
|
|
1420
1460
|
steps: [
|
|
1421
1461
|
...importers.map(i => `\`${i.split("/").pop()}\` imports \`${shortName}\``),
|
|
1422
1462
|
...(downstream.length > 0 ? [`\`${shortName}\` depends on ${downstream.map(d => `\`${d.split("/").pop()}\``).join(", ")}`] : []),
|
|
1423
1463
|
],
|
|
1424
1464
|
modules: [hub.key, ...importers.slice(0, 3)],
|
|
1425
|
-
critical:
|
|
1465
|
+
critical: allImporters.length >= hubThreshold * 2,
|
|
1426
1466
|
});
|
|
1427
1467
|
}
|
|
1428
1468
|
|
|
@@ -1459,27 +1499,53 @@ function describeRoot(root) {
|
|
|
1459
1499
|
return "Project files";
|
|
1460
1500
|
}
|
|
1461
1501
|
|
|
1502
|
+
// Folder names that only say "code lives here". They never describe a module on their own.
|
|
1503
|
+
const GENERIC_CONTAINER_SEGMENTS = new Set(["src", "lib", "libs", "source", "sources", "app", "packages", "pkg", "internal"]);
|
|
1504
|
+
|
|
1505
|
+
const MODULE_DESCRIPTION_RULES = [
|
|
1506
|
+
[/auth/, "Authentication and authorization logic"],
|
|
1507
|
+
[/api|route|endpoint/, "API layer and route handlers"],
|
|
1508
|
+
[/component|widget|^ui$/, "User interface components"],
|
|
1509
|
+
[/hook/, "Custom hooks and shared state logic"],
|
|
1510
|
+
[/util|helper/, "Utility and helper functions"],
|
|
1511
|
+
[/^(common|shared)$/, "Code shared by the other modules"],
|
|
1512
|
+
[/service/, "Service layer / business logic"],
|
|
1513
|
+
[/model|schema|entity/, "Data models and schemas"],
|
|
1514
|
+
[/config|setting/, "Configuration management"],
|
|
1515
|
+
[/test|spec/, "Test infrastructure"],
|
|
1516
|
+
[/style|css|theme/, "Styling and theming"],
|
|
1517
|
+
[/page|view|screen/, "Application pages and views"],
|
|
1518
|
+
[/store|state|redux/, "State management"],
|
|
1519
|
+
[/middleware/, "Request/response middleware"],
|
|
1520
|
+
[/database|^db$|migration/, "Database layer"],
|
|
1521
|
+
[/render/, "Rendering and template logic"],
|
|
1522
|
+
[/publish/, "Publishing and output delivery"],
|
|
1523
|
+
[/scan/, "File and repository scanning"],
|
|
1524
|
+
[/analyz/, "Code analysis and inference"],
|
|
1525
|
+
];
|
|
1526
|
+
|
|
1527
|
+
function titleizeSegment(segment) {
|
|
1528
|
+
const clean = String(segment).replace(/\.[a-z0-9]+$/i, "").replace(/[-_]+/g, " ").trim();
|
|
1529
|
+
return clean ? clean.charAt(0).toUpperCase() + clean.slice(1) : "Project";
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1462
1532
|
function describeOnboardingModule(key) {
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (/publish/.test(lower)) return "Publishing and output delivery";
|
|
1480
|
-
if (/scan/.test(lower)) return "File and repository scanning";
|
|
1481
|
-
if (/analyz/.test(lower)) return "Code analysis and inference";
|
|
1482
|
-
return "Core application module";
|
|
1533
|
+
const segments = String(key).replace(/\\/g, "/").toLowerCase().split("/").filter(Boolean);
|
|
1534
|
+
const last = (segments[segments.length - 1] || "").replace(/\.[a-z0-9]+$/i, "");
|
|
1535
|
+
const parents = segments.slice(0, -1).filter((segment) => !GENERIC_CONTAINER_SEGMENTS.has(segment));
|
|
1536
|
+
|
|
1537
|
+
// The module's own name decides first. A parent folder like `lib/` is only a container,
|
|
1538
|
+
// so `lib/ogg` is not "utility functions" just because it lives under lib.
|
|
1539
|
+
for (const [pattern, description] of MODULE_DESCRIPTION_RULES) {
|
|
1540
|
+
if (pattern.test(last)) return description;
|
|
1541
|
+
}
|
|
1542
|
+
if (GENERIC_CONTAINER_SEGMENTS.has(last) && segments.length === 1) return "Application source code";
|
|
1543
|
+
for (const segment of parents) {
|
|
1544
|
+
for (const [pattern, description] of MODULE_DESCRIPTION_RULES) {
|
|
1545
|
+
if (pattern.test(segment)) return description;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return `${titleizeSegment(last || key)} module`;
|
|
1483
1549
|
}
|
|
1484
1550
|
|
|
1485
1551
|
// Helper: infer business capability from domain name
|
|
@@ -1522,11 +1588,39 @@ function getLayerResponsibility(type) {
|
|
|
1522
1588
|
state: "Manages application-wide data synchronization",
|
|
1523
1589
|
route: "Maps URL paths to page components and handles navigation",
|
|
1524
1590
|
app: "Orchestrates application initialization and core bootstrapping",
|
|
1591
|
+
source: "The project's own source code",
|
|
1525
1592
|
other: "General purpose functionality",
|
|
1526
1593
|
};
|
|
1527
1594
|
return responsibilities[type] || "Supporting application functionality";
|
|
1528
1595
|
}
|
|
1529
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
|
+
|
|
1530
1624
|
// Helper: calculate codebase health score
|
|
1531
1625
|
function calculateHealthScore(stats, context) {
|
|
1532
1626
|
let score = 100;
|
|
@@ -1535,9 +1629,10 @@ function calculateHealthScore(stats, context) {
|
|
|
1535
1629
|
if (stats.cycles > 0) score -= Math.min(30, stats.cycles * 10);
|
|
1536
1630
|
|
|
1537
1631
|
// Orphan files indicate potential dead code
|
|
1538
|
-
const
|
|
1539
|
-
if (
|
|
1540
|
-
else if (
|
|
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;
|
|
1541
1636
|
|
|
1542
1637
|
// Too many external dependencies
|
|
1543
1638
|
if (stats.externalDeps > 100) score -= 15;
|
|
@@ -1547,9 +1642,6 @@ function calculateHealthScore(stats, context) {
|
|
|
1547
1642
|
const hasTests = (context.techStack.testFrameworks || []).length > 0;
|
|
1548
1643
|
if (!hasTests) score -= 10;
|
|
1549
1644
|
|
|
1550
|
-
// Give bonus for clean, connected codebase
|
|
1551
|
-
if (stats.cycles === 0 && stats.totalEdges > 0) score = Math.min(100, score + 5);
|
|
1552
|
-
|
|
1553
1645
|
return Math.max(0, Math.min(100, score));
|
|
1554
1646
|
}
|
|
1555
1647
|
|
|
@@ -1598,10 +1690,40 @@ function inferFrameworkPurpose(frameworks) {
|
|
|
1598
1690
|
return purposes.length > 0 ? purposes.join(", ") : "Application runtime";
|
|
1599
1691
|
}
|
|
1600
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
|
+
|
|
1601
1721
|
// Helper: explain an architectural pattern
|
|
1602
1722
|
function explainPattern(pattern) {
|
|
1603
|
-
const
|
|
1604
|
-
|
|
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();
|
|
1605
1727
|
if (/monorepo/i.test(lower)) return "Multiple related packages are managed in a single repository, enabling coordinated development and shared tooling.";
|
|
1606
1728
|
if (/microservice/i.test(lower)) return "The system is decomposed into independently deployable services communicating over network protocols.";
|
|
1607
1729
|
if (/mvc|model.?view/i.test(lower)) return "The application separates data (Model), presentation (View), and business logic (Controller) into distinct layers.";
|
|
@@ -1609,14 +1731,15 @@ function explainPattern(pattern) {
|
|
|
1609
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.";
|
|
1610
1732
|
if (/layered/i.test(lower)) return "The codebase is organized into horizontal layers (e.g., presentation, business, data) with clear boundaries.";
|
|
1611
1733
|
if (/modular/i.test(lower)) return "Functionality is divided into self-contained modules with explicit interfaces and minimal coupling.";
|
|
1612
|
-
|
|
1613
|
-
return "A structured approach to organizing code and managing complexity.";
|
|
1734
|
+
return `Detected from the code layout: ${label}.`;
|
|
1614
1735
|
}
|
|
1615
1736
|
|
|
1616
1737
|
// Helper: get benefits of a pattern
|
|
1617
1738
|
function getBenefitsForPattern(pattern) {
|
|
1618
|
-
const
|
|
1619
|
-
|
|
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();
|
|
1620
1743
|
if (/monorepo/i.test(lower)) return "Shared tooling, atomic cross-package changes, unified versioning";
|
|
1621
1744
|
if (/microservice/i.test(lower)) return "Independent deployment, tech flexibility, team autonomy";
|
|
1622
1745
|
if (/mvc|model.?view/i.test(lower)) return "Separation of concerns, testability, parallel development";
|
|
@@ -1624,7 +1747,6 @@ function getBenefitsForPattern(pattern) {
|
|
|
1624
1747
|
if (/api.?first/i.test(lower)) return "Clear contracts, parallel development, documentation-driven";
|
|
1625
1748
|
if (/layered/i.test(lower)) return "Clear responsibilities, easier testing, technology swapping";
|
|
1626
1749
|
if (/modular/i.test(lower)) return "Reusability, isolated testing, team ownership boundaries";
|
|
1627
|
-
if (/plugin/i.test(lower)) return "Extensibility, community contributions, core stability";
|
|
1628
1750
|
return "Structured organization, maintainability, clarity";
|
|
1629
1751
|
}
|
|
1630
1752
|
|