@chappibunny/repolens 1.6.1 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/package.json +4 -1
- package/src/ai/generate-sections.js +62 -25
- package/src/ai/prompts.js +28 -7
- package/src/analyzers/context-builder.js +186 -14
- package/src/analyzers/flow-inference.js +117 -10
- package/src/analyzers/graphql-analyzer.js +7 -2
- package/src/docs/generate-doc-set.js +14 -9
- package/src/renderers/render.js +104 -54
- package/src/renderers/renderDiff.js +23 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to RepoLens will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 1.7.1
|
|
6
|
+
|
|
7
|
+
### 🛡️ AI Output Guardrails
|
|
8
|
+
|
|
9
|
+
- **System prompt hardening**: Added anti-conversational rules — AI now instructed to never offer additional work, never ask questions, never use second-person address (except onboarding), and to back every claim with context evidence.
|
|
10
|
+
- **Evidence-only constraints**: Architecture weaknesses, exec summary risks, and onboarding complexity hotspots now require concrete evidence from context data (cycle counts, coupling metrics, orphan files). No speculation.
|
|
11
|
+
- **Output sanitizer**: New `sanitizeAIOutput()` strips conversational patterns (`"If you want"`, `"I can produce"`, `"Shall I"`, `"Let me know"`) from both structured JSON and plain-text AI responses before they reach documents.
|
|
12
|
+
- **Structured renderer sanitization**: `renderArchitectureOverviewJSON` now sanitizes weakness bullet items, removing conversational lines even if they survive prompt-level constraints.
|
|
13
|
+
- **Dual-path coverage**: Sanitization applies to both structured JSON mode (Path A) and plain-text fallback (Path B), closing all AI output paths.
|
|
14
|
+
|
|
15
|
+
## 1.7.0
|
|
16
|
+
|
|
17
|
+
### ✨ Features
|
|
18
|
+
|
|
19
|
+
- **Dependency-weighted module roles**: Module catalog now shows import-based role labels (e.g. "Orchestrator — coordinates 54 modules", "Critical shared infrastructure — imported by 55 modules") derived from real dependency graph metrics.
|
|
20
|
+
- **Context-aware route map**: CLI tools now get a helpful explanation ("This project is a CLI tool — it does not expose HTTP routes") instead of an empty route table with a stale footer.
|
|
21
|
+
- **Verified architecture patterns**: Detected patterns now include confidence labels (e.g. "naming only" vs evidence-backed) based on dependency graph verification.
|
|
22
|
+
- **Import-chain data flows**: Data flows are now traced from real entry points via import chains, replacing hardcoded template flows. Shows actual module dependencies and downstream traces.
|
|
23
|
+
- **CLI-aware tech stack labels**: All 4 key documents (Executive Summary, System Overview, Architecture Overview, Developer Onboarding) now show "N/A (CLI tool)" instead of "Not detected" for Frameworks and Build Tools when the project is a CLI tool.
|
|
24
|
+
- **Test flow filtering**: Test file flows are now filtered from Executive Summary, Data Flows, and Developer Onboarding documents, keeping output focused on production code paths.
|
|
25
|
+
- **Hub flow cleanup**: Integration flows derived from dependency graph hubs no longer list test files as importers in their step chains.
|
|
26
|
+
|
|
27
|
+
### 🐛 Bug Fixes
|
|
28
|
+
|
|
29
|
+
- **GraphQL false positives**: The GraphQL analyzer now excludes test files and its own analysis/rendering source files from scanning, preventing self-detection of library patterns (e.g. detecting "Apollo Server" from regex pattern strings).
|
|
30
|
+
- **GraphQL pattern precision**: The `graphql-js` detection pattern now uses word boundaries (`\bGraphQLSchema\b`) to avoid false matches from function names like `renderGraphQLSchema`.
|
|
31
|
+
- **Change impact noise filtering**: Architecture diff now filters `node_modules/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `.repolens/`, and `.git/` from both the Impacted Modules list and the file lists (Added/Removed/Modified).
|
|
32
|
+
- **Zero-value suppression**: Documents no longer show "serves 0 application pages" or "0 API endpoints" for CLI tools — these rows are conditionally hidden when values are zero.
|
|
33
|
+
- **Module label enrichment**: `describeModule()` now recognizes 6 additional module types (plugin, prompt, provider, generate/section, ai/ml), reducing generic "Application module" fallbacks.
|
|
34
|
+
|
|
5
35
|
## 1.6.0
|
|
6
36
|
|
|
7
37
|
### ✨ Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chappibunny/repolens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -67,5 +67,8 @@
|
|
|
67
67
|
"devDependencies": {
|
|
68
68
|
"tinyexec": "1.0.2",
|
|
69
69
|
"vitest": "^4.0.18"
|
|
70
|
+
},
|
|
71
|
+
"overrides": {
|
|
72
|
+
"yauzl": ">=3.2.1"
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -18,6 +18,27 @@ import {
|
|
|
18
18
|
import { identifyFlowDependencies } from "../analyzers/flow-inference.js";
|
|
19
19
|
import { info, warn } from "../utils/logger.js";
|
|
20
20
|
|
|
21
|
+
// Strip conversational patterns that LLMs sometimes inject into documentation
|
|
22
|
+
const CONVERSATIONAL_PATTERNS = [
|
|
23
|
+
/^(?:[-*]\s*)?if you (?:want|need|would like|prefer)[^.\n]*[.\n]/gmi,
|
|
24
|
+
/^(?:[-*]\s*)?(?:shall|should) I [^.\n]*[.\n]/gmi,
|
|
25
|
+
/^(?:[-*]\s*)?(?:let me know|feel free)[^.\n]*[.\n]/gmi,
|
|
26
|
+
/^(?:[-*]\s*)?I can (?:also |additionally )?(?:produce|create|generate|help|provide|suggest|recommend)[^.\n]*[.\n]/gmi,
|
|
27
|
+
/^(?:[-*]\s*)?(?:would you like|do you want)[^.\n]*[.\n]/gmi,
|
|
28
|
+
/^(?:[-*]\s*)?(?:here is|here's) (?:a |the )?(?:summary|overview|breakdown)[^.\n]*:\s*$/gmi,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function sanitizeAIOutput(text) {
|
|
32
|
+
if (!text || typeof text !== "string") return text;
|
|
33
|
+
let cleaned = text;
|
|
34
|
+
for (const pattern of CONVERSATIONAL_PATTERNS) {
|
|
35
|
+
cleaned = cleaned.replace(pattern, "");
|
|
36
|
+
}
|
|
37
|
+
// Collapse multiple blank lines left by removals
|
|
38
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
|
|
39
|
+
return cleaned;
|
|
40
|
+
}
|
|
41
|
+
|
|
21
42
|
/**
|
|
22
43
|
* Try structured JSON mode first, fall back to plain-text AI, then deterministic.
|
|
23
44
|
*/
|
|
@@ -41,7 +62,7 @@ async function generateWithStructuredFallback(key, promptText, maxTokens, fallba
|
|
|
41
62
|
|
|
42
63
|
if (result.success && result.parsed) {
|
|
43
64
|
const md = renderStructuredToMarkdown(key, result.parsed);
|
|
44
|
-
if (md) return md;
|
|
65
|
+
if (md) return sanitizeAIOutput(md);
|
|
45
66
|
}
|
|
46
67
|
// If structured mode failed, fall through to plain-text
|
|
47
68
|
warn(`Structured AI failed for ${key}, trying plain-text mode...`);
|
|
@@ -60,7 +81,7 @@ async function generateWithStructuredFallback(key, promptText, maxTokens, fallba
|
|
|
60
81
|
return fallbackFn();
|
|
61
82
|
}
|
|
62
83
|
|
|
63
|
-
return result.text;
|
|
84
|
+
return sanitizeAIOutput(result.text);
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
export async function generateExecutiveSummary(context, enrichment = {}) {
|
|
@@ -125,6 +146,18 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
|
|
|
125
146
|
const languageList = context.techStack.languages.join(", ") || "multiple languages";
|
|
126
147
|
const domainSummary = context.domains.slice(0, 5).map(d => d.name).join(", ");
|
|
127
148
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
149
|
+
const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
|
|
150
|
+
|
|
151
|
+
// Build the "what it does" line based on project type
|
|
152
|
+
let interfaceLine;
|
|
153
|
+
if (isCLI) {
|
|
154
|
+
interfaceLine = "It operates as a **command-line tool**, interacting through terminal commands rather than a web interface.";
|
|
155
|
+
} else {
|
|
156
|
+
const parts = [];
|
|
157
|
+
if (context.project.apiRoutesDetected > 0) parts.push(`exposes **${context.project.apiRoutesDetected} API endpoint${context.project.apiRoutesDetected === 1 ? "" : "s"}**`);
|
|
158
|
+
if (context.project.pagesDetected > 0) parts.push(`serves **${context.project.pagesDetected} application page${context.project.pagesDetected === 1 ? "" : "s"}** to end users`);
|
|
159
|
+
interfaceLine = parts.length > 0 ? `It ${parts.join(" and ")}.` : "";
|
|
160
|
+
}
|
|
128
161
|
|
|
129
162
|
let output = `# Executive Summary
|
|
130
163
|
|
|
@@ -132,7 +165,7 @@ function getFallbackExecutiveSummary(context, enrichment = {}) {
|
|
|
132
165
|
|
|
133
166
|
${context.project.name} is a ${frameworkList} application built with ${languageList}. The codebase contains **${context.project.modulesDetected} modules** spread across **${context.project.filesScanned} files**, organized into ${context.domains.length} functional domain${context.domains.length === 1 ? "" : "s"}.
|
|
134
167
|
|
|
135
|
-
${
|
|
168
|
+
${interfaceLine}
|
|
136
169
|
|
|
137
170
|
## Primary Functional Areas
|
|
138
171
|
|
|
@@ -146,9 +179,9 @@ ${context.domains.map(d => `| ${d.name} | ${d.moduleCount} | ${d.description ||
|
|
|
146
179
|
|
|
147
180
|
| Category | Details |
|
|
148
181
|
|----------|---------|
|
|
149
|
-
| Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
|
|
182
|
+
| Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
150
183
|
| Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
|
|
151
|
-
| Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
|
|
184
|
+
| Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
152
185
|
${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}`;
|
|
153
186
|
|
|
154
187
|
// Module type breakdown
|
|
@@ -188,11 +221,12 @@ ${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")}
|
|
|
188
221
|
}
|
|
189
222
|
}
|
|
190
223
|
|
|
191
|
-
// Data flows
|
|
192
|
-
|
|
224
|
+
// Data flows (filter out test file flows for exec summary)
|
|
225
|
+
const summaryFlows = (flows || []).filter(f => !f.name?.toLowerCase().includes('test'));
|
|
226
|
+
if (summaryFlows.length > 0) {
|
|
193
227
|
output += `\n## Key Data Flows\n\n`;
|
|
194
|
-
output += `${
|
|
195
|
-
for (const flow of
|
|
228
|
+
output += `${summaryFlows.length} data flow${summaryFlows.length === 1 ? "" : "s"} identified:\n\n`;
|
|
229
|
+
for (const flow of summaryFlows) {
|
|
196
230
|
output += `- **${flow.name}**${flow.critical ? " (critical)" : ""} — ${flow.description}\n`;
|
|
197
231
|
}
|
|
198
232
|
}
|
|
@@ -213,6 +247,7 @@ function getFallbackSystemOverview(context, enrichment = {}) {
|
|
|
213
247
|
const sizeLabel = context.project.modulesDetected > 50 ? "large-scale" :
|
|
214
248
|
context.project.modulesDetected > 20 ? "medium-sized" : "focused";
|
|
215
249
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
250
|
+
const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
|
|
216
251
|
|
|
217
252
|
let output = `# System Overview
|
|
218
253
|
|
|
@@ -224,16 +259,14 @@ This is a ${sizeLabel} codebase organized into **${context.project.modulesDetect
|
|
|
224
259
|
|--------|-------|
|
|
225
260
|
| Files scanned | ${context.project.filesScanned} |
|
|
226
261
|
| Modules | ${context.project.modulesDetected} |
|
|
227
|
-
|
|
228
|
-
| API endpoints | ${context.project.apiRoutesDetected} |
|
|
229
|
-
|
|
262
|
+
${context.project.pagesDetected > 0 ? `| Application pages | ${context.project.pagesDetected} |\n` : ""}${context.project.apiRoutesDetected > 0 ? `| API endpoints | ${context.project.apiRoutesDetected} |\n` : ""}
|
|
230
263
|
## Technology Stack
|
|
231
264
|
|
|
232
265
|
| Category | Technologies |
|
|
233
266
|
|----------|-------------|
|
|
234
|
-
| Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
|
|
267
|
+
| Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
235
268
|
| Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
|
|
236
|
-
| Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
|
|
269
|
+
| Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
237
270
|
${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}
|
|
238
271
|
## Detected Patterns
|
|
239
272
|
|
|
@@ -378,6 +411,7 @@ function getFallbackBusinessDomains(context, enrichment = {}) {
|
|
|
378
411
|
|
|
379
412
|
function getFallbackArchitectureOverview(context, enrichment = {}) {
|
|
380
413
|
const { depGraph, driftResult } = enrichment;
|
|
414
|
+
const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
|
|
381
415
|
const patternDesc = context.patterns.length > 0
|
|
382
416
|
? `The detected architectural patterns are **${context.patterns.join(", ")}**. These patterns shape how data and control flow through the system.`
|
|
383
417
|
: "No specific architectural patterns were detected. The project appears to follow a straightforward directory-based organization.";
|
|
@@ -415,13 +449,13 @@ ${context.domains.slice(0, 8).map(d => `| **${d.name}** | ${d.description || "Ha
|
|
|
415
449
|
|
|
416
450
|
| Category | Technologies |
|
|
417
451
|
|----------|-------------|
|
|
418
|
-
| Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
|
|
452
|
+
| Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
419
453
|
| Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
|
|
420
|
-
| Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
|
|
454
|
+
| Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
421
455
|
|
|
422
456
|
## Scale & Complexity
|
|
423
457
|
|
|
424
|
-
The repository comprises **${context.project.filesScanned} files** organized into **${context.project.modulesDetected} modules
|
|
458
|
+
The repository comprises **${context.project.filesScanned} files** organized into **${context.project.modulesDetected} modules**.${context.project.apiRoutesDetected > 0 ? ` It exposes **${context.project.apiRoutesDetected} API endpoint${context.project.apiRoutesDetected === 1 ? "" : "s"}**.` : ""}${context.project.pagesDetected > 0 ? ` It serves **${context.project.pagesDetected} application page${context.project.pagesDetected === 1 ? "" : "s"}**.` : ""}${isCLI ? " It operates as a command-line tool." : ""}
|
|
425
459
|
`;
|
|
426
460
|
|
|
427
461
|
// Dependency graph health
|
|
@@ -502,8 +536,8 @@ function getFallbackDataFlows(flows, context, enrichment = {}) {
|
|
|
502
536
|
let output = `# Data Flows\n\n`;
|
|
503
537
|
output += `> Data flows describe how information moves through the system — from external inputs through processing layers to storage or presentation.\n\n`;
|
|
504
538
|
|
|
505
|
-
// Combine heuristic flows with dep-graph-derived flows
|
|
506
|
-
const allFlows = [...(flows || [])];
|
|
539
|
+
// Combine heuristic flows with dep-graph-derived flows, filtering out test file flows
|
|
540
|
+
const allFlows = [...(flows || [])].filter(f => !f.name?.toLowerCase().includes('test'));
|
|
507
541
|
|
|
508
542
|
// Generate additional flows from dependency graph hub chains
|
|
509
543
|
if (depGraph?.nodes && depGraph.nodes.length > 0 && allFlows.length < 3) {
|
|
@@ -571,6 +605,7 @@ function getFallbackDeveloperOnboarding(context, enrichment = {}) {
|
|
|
571
605
|
const frameworkList = context.techStack.frameworks.join(", ") || "general-purpose tools";
|
|
572
606
|
const languageList = context.techStack.languages.join(", ") || "standard languages";
|
|
573
607
|
const testFrameworks = context.techStack.testFrameworks || [];
|
|
608
|
+
const isCLI = (context.patterns || []).some(p => p.toLowerCase().includes("cli"));
|
|
574
609
|
const routes = context.routes || {};
|
|
575
610
|
const pages = routes.pages || [];
|
|
576
611
|
const apis = routes.apis || [];
|
|
@@ -604,9 +639,9 @@ ${context.repoRoots.map(root => `| \`${root}\` | ${describeRoot(root)} |`).join(
|
|
|
604
639
|
|
|
605
640
|
| Category | Technologies |
|
|
606
641
|
|----------|-------------|
|
|
607
|
-
| Frameworks | ${context.techStack.frameworks.join(", ") || "Not detected"} |
|
|
642
|
+
| Frameworks | ${context.techStack.frameworks.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
608
643
|
| Languages | ${context.techStack.languages.join(", ") || "Not detected"} |
|
|
609
|
-
| Build Tools | ${context.techStack.buildTools.join(", ") || "Not detected"} |
|
|
644
|
+
| Build Tools | ${context.techStack.buildTools.join(", ") || (isCLI ? "N/A (CLI tool)" : "Not detected")} |
|
|
610
645
|
${testFrameworks.length > 0 ? `| Test Frameworks | ${testFrameworks.join(", ")} |\n` : ""}
|
|
611
646
|
## Largest Modules
|
|
612
647
|
|
|
@@ -637,11 +672,12 @@ ${context.topModules.slice(0, 10).map(m => `| \`${m.key}\` | ${m.fileCount} | ${
|
|
|
637
672
|
}
|
|
638
673
|
}
|
|
639
674
|
|
|
640
|
-
// Data flows overview
|
|
641
|
-
|
|
675
|
+
// Data flows overview (filter out test flows)
|
|
676
|
+
const onboardingFlows = (flows || []).filter(f => !f.name?.toLowerCase().includes('test'));
|
|
677
|
+
if (onboardingFlows.length > 0) {
|
|
642
678
|
output += `## How Data Flows\n\n`;
|
|
643
679
|
output += `Understanding these flows will help you see how the system works end-to-end:\n\n`;
|
|
644
|
-
for (const flow of
|
|
680
|
+
for (const flow of onboardingFlows) {
|
|
645
681
|
output += `- **${flow.name}** — ${flow.description}\n`;
|
|
646
682
|
}
|
|
647
683
|
output += "\n";
|
|
@@ -715,7 +751,8 @@ function inferFlowsFromDepGraph(depGraph) {
|
|
|
715
751
|
.slice(0, 3);
|
|
716
752
|
|
|
717
753
|
for (const hub of hubs) {
|
|
718
|
-
const
|
|
754
|
+
const testPattern = /(?:^|\/)(?:tests?|__tests?__|spec|__spec__)\/|\.(test|spec)\.[jt]sx?$/i;
|
|
755
|
+
const importers = hub.importedBy.filter(i => !testPattern.test(i)).slice(0, 5);
|
|
719
756
|
const downstream = hub.imports.slice(0, 3);
|
|
720
757
|
const shortName = hub.key.split("/").pop();
|
|
721
758
|
|
package/src/ai/prompts.js
CHANGED
|
@@ -57,7 +57,12 @@ Rules:
|
|
|
57
57
|
- Do not mention AI, LLMs, or that you are an assistant.
|
|
58
58
|
- No markdown tables unless specifically requested.
|
|
59
59
|
- Use simple formatting: headings, paragraphs, lists.
|
|
60
|
-
- Maximum 2 heading levels deep within sections
|
|
60
|
+
- Maximum 2 heading levels deep within sections.
|
|
61
|
+
- You are producing a static document, not participating in a conversation.
|
|
62
|
+
- Never offer to do additional work (no "If you want", "I can also", "Let me know", "Shall I").
|
|
63
|
+
- Never ask the reader questions or invite follow-up.
|
|
64
|
+
- Never address the reader in second person ("you") unless the document type requires it (e.g. onboarding).
|
|
65
|
+
- Every claim must be supported by concrete evidence from the supplied context data.`;
|
|
61
66
|
|
|
62
67
|
export function createExecutiveSummaryPrompt(context) {
|
|
63
68
|
return `Write an executive summary for a mixed audience of technical and non-technical readers.
|
|
@@ -70,7 +75,7 @@ Requirements:
|
|
|
70
75
|
- Explain the main system areas using the domain information.
|
|
71
76
|
- Explain the business capabilities implied by the codebase structure.
|
|
72
77
|
- Mention key external dependencies only if they are present in the context.
|
|
73
|
-
- Mention architectural or operational risks if they are
|
|
78
|
+
- Mention architectural or operational risks only if they are directly supported by concrete data in the context (e.g. cycle counts, orphan files, coupling metrics).
|
|
74
79
|
- Do not mention file counts more than once.
|
|
75
80
|
- Maximum 500 words.
|
|
76
81
|
- Use this structure:
|
|
@@ -89,7 +94,9 @@ Requirements:
|
|
|
89
94
|
|
|
90
95
|
## Operational and architectural risks
|
|
91
96
|
|
|
92
|
-
## Recommended focus areas
|
|
97
|
+
## Recommended focus areas
|
|
98
|
+
|
|
99
|
+
IMPORTANT: Only list risks and focus areas that are directly evidenced by the context data. Do not speculate.`;
|
|
93
100
|
}
|
|
94
101
|
|
|
95
102
|
export function createSystemOverviewPrompt(context) {
|
|
@@ -183,7 +190,9 @@ Requirements:
|
|
|
183
190
|
|
|
184
191
|
## Architectural strengths
|
|
185
192
|
|
|
186
|
-
## Architectural weaknesses
|
|
193
|
+
## Architectural weaknesses
|
|
194
|
+
|
|
195
|
+
IMPORTANT: Only list weaknesses that are directly evidenced by the context data (e.g. cycle counts, orphan files, high coupling metrics, missing layers). Do not speculate about what the system lacks.`;
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
export function createDataFlowsPrompt(flows, context) {
|
|
@@ -249,7 +258,9 @@ Requirements:
|
|
|
249
258
|
|
|
250
259
|
## What to understand first
|
|
251
260
|
|
|
252
|
-
## Known complexity hotspots
|
|
261
|
+
## Known complexity hotspots
|
|
262
|
+
|
|
263
|
+
IMPORTANT: Only cite complexity hotspots that are supported by concrete evidence in the context (e.g. high import counts, circular dependencies, large file counts). Do not speculate about what might be complex.`;
|
|
253
264
|
}
|
|
254
265
|
|
|
255
266
|
export function createModuleSummaryPrompt(module, context) {
|
|
@@ -520,12 +531,22 @@ function renderBusinessDomainsJSON(d) {
|
|
|
520
531
|
return md;
|
|
521
532
|
}
|
|
522
533
|
|
|
534
|
+
function sanitizeBulletList(val) {
|
|
535
|
+
const raw = toBulletList(val);
|
|
536
|
+
if (!raw) return raw;
|
|
537
|
+
// Strip conversational lines from bullet lists
|
|
538
|
+
return raw.split("\n").filter(line => {
|
|
539
|
+
const lower = line.toLowerCase();
|
|
540
|
+
return !/(^|\s)(if you (?:want|need|would)|shall i |let me know|i can (?:also )?(?:produce|create|generate|help)|would you like|do you want|feel free)/i.test(lower);
|
|
541
|
+
}).join("\n");
|
|
542
|
+
}
|
|
543
|
+
|
|
523
544
|
function renderArchitectureOverviewJSON(d) {
|
|
524
545
|
let md = `# Architecture Overview\n\n`;
|
|
525
546
|
md += `## Architecture Style\n\n${safeStr(d.style)}\n\n`;
|
|
526
547
|
md += `## Layers\n\n${toHeadingSections(d.layers)}\n\n`;
|
|
527
|
-
md += `## Architectural Strengths\n\n${
|
|
528
|
-
md += `## Architectural Weaknesses\n\n${
|
|
548
|
+
md += `## Architectural Strengths\n\n${sanitizeBulletList(d.strengths)}\n\n`;
|
|
549
|
+
md += `## Architectural Weaknesses\n\n${sanitizeBulletList(d.weaknesses)}\n`;
|
|
529
550
|
return md;
|
|
530
551
|
}
|
|
531
552
|
|
|
@@ -2,7 +2,83 @@
|
|
|
2
2
|
|
|
3
3
|
import { groupModulesByDomain } from "./domain-inference.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Compute per-module dependency metrics from the dep graph.
|
|
7
|
+
* Returns a Map<normalizedKey, { fanIn, fanOut, isHub, isOrphan, isLeaf, isOrchestrator }>.
|
|
8
|
+
*/
|
|
9
|
+
export function computeModuleDepMetrics(depGraph) {
|
|
10
|
+
const metrics = new Map();
|
|
11
|
+
if (!depGraph?.nodes) return metrics;
|
|
12
|
+
|
|
13
|
+
const hubThreshold = Math.max(3, Math.floor(depGraph.nodes.length * 0.05));
|
|
14
|
+
|
|
15
|
+
for (const node of depGraph.nodes) {
|
|
16
|
+
const fanIn = node.importedBy.length;
|
|
17
|
+
const fanOut = node.imports.length;
|
|
18
|
+
metrics.set(node.key, {
|
|
19
|
+
fanIn,
|
|
20
|
+
fanOut,
|
|
21
|
+
isHub: fanIn >= hubThreshold,
|
|
22
|
+
isOrphan: fanIn === 0 && fanOut === 0,
|
|
23
|
+
isLeaf: fanOut === 0 && fanIn > 0,
|
|
24
|
+
isOrchestrator: fanOut >= 5 && fanIn < fanOut,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return metrics;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a module-level dep metrics map (aggregating file-level metrics).
|
|
32
|
+
* moduleKey → { fanIn, fanOut, isHub, isOrphan, isLeaf, isOrchestrator }
|
|
33
|
+
*/
|
|
34
|
+
export function computeModuleLevelMetrics(depGraph, modules) {
|
|
35
|
+
const fileMetrics = computeModuleDepMetrics(depGraph);
|
|
36
|
+
if (fileMetrics.size === 0 || !modules) return new Map();
|
|
37
|
+
|
|
38
|
+
const moduleLevelMap = new Map();
|
|
39
|
+
|
|
40
|
+
for (const mod of modules) {
|
|
41
|
+
const moduleKey = mod.key;
|
|
42
|
+
const lowerKey = moduleKey.toLowerCase();
|
|
43
|
+
|
|
44
|
+
// Find all file-level nodes that belong to this module
|
|
45
|
+
let totalFanIn = 0;
|
|
46
|
+
let totalFanOut = 0;
|
|
47
|
+
let fileCount = 0;
|
|
48
|
+
|
|
49
|
+
for (const [nodeKey, m] of fileMetrics) {
|
|
50
|
+
if (nodeKey === lowerKey || nodeKey.startsWith(lowerKey + "/") || nodeKey === moduleKey || nodeKey.startsWith(moduleKey + "/")) {
|
|
51
|
+
// Count only external edges (crossing module boundary)
|
|
52
|
+
if (depGraph?.nodes) {
|
|
53
|
+
const node = depGraph.nodes.find(n => n.key === nodeKey);
|
|
54
|
+
if (node) {
|
|
55
|
+
const externalIn = node.importedBy.filter(imp => !imp.startsWith(moduleKey + "/") && imp !== moduleKey).length;
|
|
56
|
+
const externalOut = node.imports.filter(imp => !imp.startsWith(moduleKey + "/") && imp !== moduleKey).length;
|
|
57
|
+
totalFanIn += externalIn;
|
|
58
|
+
totalFanOut += externalOut;
|
|
59
|
+
fileCount++;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (fileCount > 0) {
|
|
66
|
+
const hubThreshold = Math.max(5, Math.floor(modules.length * 0.15));
|
|
67
|
+
moduleLevelMap.set(moduleKey, {
|
|
68
|
+
fanIn: totalFanIn,
|
|
69
|
+
fanOut: totalFanOut,
|
|
70
|
+
isHub: totalFanIn >= hubThreshold,
|
|
71
|
+
isOrphan: totalFanIn === 0 && totalFanOut === 0,
|
|
72
|
+
isLeaf: totalFanOut === 0 && totalFanIn > 0,
|
|
73
|
+
isOrchestrator: totalFanOut >= 5 && totalFanIn < totalFanOut,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return moduleLevelMap;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function buildAIContext(scanResult, config, depGraph = null) {
|
|
6
82
|
const { filesCount, modules, api, pages, metadata } = scanResult;
|
|
7
83
|
|
|
8
84
|
// Get domain hints from config if available
|
|
@@ -17,13 +93,17 @@ export function buildAIContext(scanResult, config) {
|
|
|
17
93
|
// Group modules by business domain
|
|
18
94
|
const domainGroups = groupModulesByDomain(modules, customHints);
|
|
19
95
|
|
|
20
|
-
//
|
|
96
|
+
// Compute module-level dependency metrics
|
|
97
|
+
const moduleMetrics = computeModuleLevelMetrics(depGraph, modules);
|
|
98
|
+
|
|
99
|
+
// Identify top modules with enriched types
|
|
21
100
|
const topModules = modules
|
|
22
101
|
.slice(0, 15)
|
|
23
102
|
.map(m => ({
|
|
24
103
|
key: m.key,
|
|
25
104
|
fileCount: m.fileCount,
|
|
26
|
-
type: inferModuleType(m.key)
|
|
105
|
+
type: inferModuleType(m.key),
|
|
106
|
+
depRole: describeModuleDepRole(m.key, moduleMetrics),
|
|
27
107
|
}));
|
|
28
108
|
|
|
29
109
|
// Categorize routes
|
|
@@ -48,8 +128,8 @@ export function buildAIContext(scanResult, config) {
|
|
|
48
128
|
testFrameworks: metadata?.testFrameworks || []
|
|
49
129
|
};
|
|
50
130
|
|
|
51
|
-
// Identify key architectural patterns
|
|
52
|
-
const patterns = inferArchitecturalPatterns(modules);
|
|
131
|
+
// Identify key architectural patterns (verified against dep graph when available)
|
|
132
|
+
const patterns = inferArchitecturalPatterns(modules, depGraph);
|
|
53
133
|
|
|
54
134
|
// Build compact context object
|
|
55
135
|
return {
|
|
@@ -124,11 +204,31 @@ function inferModuleType(modulePath) {
|
|
|
124
204
|
return "other";
|
|
125
205
|
}
|
|
126
206
|
|
|
127
|
-
function inferArchitecturalPatterns(modules) {
|
|
207
|
+
function inferArchitecturalPatterns(modules, depGraph = null) {
|
|
128
208
|
const patterns = [];
|
|
129
209
|
const keys = modules.map(m => m.key.toLowerCase());
|
|
130
210
|
|
|
131
211
|
const has = (keyword) => keys.some(k => k.includes(keyword));
|
|
212
|
+
|
|
213
|
+
// Compute module-level metrics for verification
|
|
214
|
+
const moduleMetrics = depGraph ? computeModuleLevelMetrics(depGraph, modules) : new Map();
|
|
215
|
+
const hasCycles = depGraph?.cycles?.length > 0;
|
|
216
|
+
|
|
217
|
+
// Verify pattern: a module matching `keyword` has high fan-in (truly central)
|
|
218
|
+
const verifyHub = (keyword) => {
|
|
219
|
+
for (const [mKey, m] of moduleMetrics) {
|
|
220
|
+
if (mKey.toLowerCase().includes(keyword) && m.fanIn >= 3) return true;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Verify pattern: modules matching keyword have mostly outbound deps (orchestrators)
|
|
226
|
+
const verifyOrchestrator = (keyword) => {
|
|
227
|
+
for (const [mKey, m] of moduleMetrics) {
|
|
228
|
+
if (mKey.toLowerCase().includes(keyword) && m.fanOut >= 3) return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
};
|
|
132
232
|
|
|
133
233
|
// Web framework patterns
|
|
134
234
|
if (has("app/")) patterns.push("Next.js App Router");
|
|
@@ -137,25 +237,95 @@ function inferArchitecturalPatterns(modules) {
|
|
|
137
237
|
if (has("hook")) patterns.push("React hooks pattern");
|
|
138
238
|
if (has("store") || has("state") || has("redux") || has("zustand")) patterns.push("Centralized state management");
|
|
139
239
|
|
|
140
|
-
// General patterns
|
|
240
|
+
// General patterns — verified against dep graph when available
|
|
141
241
|
if (has("api") || has("endpoint")) patterns.push("API route pattern");
|
|
142
|
-
|
|
143
|
-
if (has("
|
|
242
|
+
|
|
243
|
+
if (has("core") || has("kernel")) {
|
|
244
|
+
if (moduleMetrics.size > 0 && verifyHub("core")) {
|
|
245
|
+
patterns.push("Core/kernel architecture (verified — high fan-in)");
|
|
246
|
+
} else if (moduleMetrics.size > 0) {
|
|
247
|
+
patterns.push("Core/kernel architecture (naming only)");
|
|
248
|
+
} else {
|
|
249
|
+
patterns.push("Core/kernel architecture");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (has("plugin") || has("extension")) {
|
|
254
|
+
if (moduleMetrics.size > 0 && (verifyHub("plugin") || verifyOrchestrator("plugin") || verifyHub("loader"))) {
|
|
255
|
+
patterns.push("Plugin system (verified — loader/registry detected)");
|
|
256
|
+
} else if (moduleMetrics.size > 0) {
|
|
257
|
+
patterns.push("Plugin system (naming only)");
|
|
258
|
+
} else {
|
|
259
|
+
patterns.push("Plugin system");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
144
263
|
if (has("middleware")) patterns.push("Middleware pipeline");
|
|
145
|
-
|
|
146
|
-
if (has("
|
|
264
|
+
|
|
265
|
+
if (has("render") || has("template")) {
|
|
266
|
+
if (moduleMetrics.size > 0 && verifyOrchestrator("render")) {
|
|
267
|
+
patterns.push("Renderer pipeline (verified — multi-output)");
|
|
268
|
+
} else {
|
|
269
|
+
patterns.push("Renderer pipeline");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (has("publish") || has("output")) {
|
|
274
|
+
if (moduleMetrics.size > 0 && verifyOrchestrator("publish")) {
|
|
275
|
+
patterns.push("Multi-output publishing (verified — multiple targets)");
|
|
276
|
+
} else {
|
|
277
|
+
patterns.push("Multi-output publishing");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
147
281
|
if (has("analyz") || has("detect") || has("inspect")) patterns.push("Analysis pipeline");
|
|
148
282
|
if (has("cli") || has("command") || has("bin")) patterns.push("CLI tool architecture");
|
|
149
|
-
|
|
283
|
+
|
|
284
|
+
if (has("util") || has("helper") || has("lib")) {
|
|
285
|
+
if (moduleMetrics.size > 0 && verifyHub("util") || verifyHub("helper") || verifyHub("lib")) {
|
|
286
|
+
patterns.push("Shared utility layer (verified — high fan-in)");
|
|
287
|
+
} else {
|
|
288
|
+
patterns.push("Shared utility layer");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
150
292
|
if (has("integrat") || has("adapter") || has("connect")) patterns.push("Integration adapters");
|
|
151
293
|
if (has("ai") || has("prompt") || has("provider")) patterns.push("AI/LLM integration");
|
|
152
294
|
if (has("deliver") || has("dispatch")) patterns.push("Delivery pipeline");
|
|
153
295
|
if (has("test") || has("spec")) patterns.push("Dedicated test infrastructure");
|
|
154
|
-
|
|
296
|
+
|
|
297
|
+
// Structural patterns from dep graph (not keyword-based)
|
|
298
|
+
if (depGraph?.stats) {
|
|
299
|
+
if (depGraph.stats.cycles === 0 && depGraph.stats.totalEdges > 10) {
|
|
300
|
+
patterns.push("Acyclic dependency graph — clean layering");
|
|
301
|
+
}
|
|
302
|
+
if (hasCycles) {
|
|
303
|
+
patterns.push(`⚠️ ${depGraph.stats.cycles} circular dependency cycle(s) detected`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
155
307
|
return patterns;
|
|
156
308
|
}
|
|
157
309
|
|
|
158
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Generate a dependency-aware role description for a module.
|
|
312
|
+
*/
|
|
313
|
+
function describeModuleDepRole(moduleKey, moduleMetrics) {
|
|
314
|
+
const m = moduleMetrics.get(moduleKey);
|
|
315
|
+
if (!m) return null;
|
|
316
|
+
|
|
317
|
+
if (m.isOrphan) return "Isolated (no imports or importers)";
|
|
318
|
+
if (m.isHub && m.fanIn >= 15) return `Critical shared infrastructure (imported by ${m.fanIn} modules)`;
|
|
319
|
+
if (m.isHub) return `Shared infrastructure (imported by ${m.fanIn} modules)`;
|
|
320
|
+
if (m.isOrchestrator) return `Orchestrator (coordinates ${m.fanOut} modules)`;
|
|
321
|
+
if (m.isLeaf) return `Leaf module (consumed only, ${m.fanIn} importer${m.fanIn === 1 ? "" : "s"})`;
|
|
322
|
+
if (m.fanIn > 0 && m.fanOut > 0) return `Connector (${m.fanIn} in, ${m.fanOut} out)`;
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export { describeModuleDepRole };
|
|
327
|
+
|
|
328
|
+
export function buildModuleContext(modules, config, depGraph = null) {
|
|
159
329
|
const customHints = config.domains
|
|
160
330
|
? Object.entries(config.domains).map(([key, domain]) => ({
|
|
161
331
|
match: domain.match || [],
|
|
@@ -165,6 +335,7 @@ export function buildModuleContext(modules, config) {
|
|
|
165
335
|
: [];
|
|
166
336
|
|
|
167
337
|
const domainGroups = groupModulesByDomain(modules, customHints);
|
|
338
|
+
const moduleMetrics = computeModuleLevelMetrics(depGraph, modules);
|
|
168
339
|
|
|
169
340
|
return modules.map(module => {
|
|
170
341
|
const domain = domainGroups.find(d =>
|
|
@@ -175,6 +346,7 @@ export function buildModuleContext(modules, config) {
|
|
|
175
346
|
key: module.key,
|
|
176
347
|
fileCount: module.fileCount,
|
|
177
348
|
type: inferModuleType(module.key),
|
|
349
|
+
depRole: describeModuleDepRole(module.key, moduleMetrics),
|
|
178
350
|
domain: domain?.name || "Other",
|
|
179
351
|
domainDescription: domain?.description || "Uncategorized"
|
|
180
352
|
};
|
|
@@ -1,27 +1,134 @@
|
|
|
1
|
-
// Infer data flows through the application using heuristics
|
|
1
|
+
// Infer data flows through the application using import graph analysis + heuristics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Primary flow inference: uses dep graph when available for real import-chain flows,
|
|
5
|
+
* falls back to keyword heuristics for repos without dep graph data.
|
|
6
|
+
*/
|
|
7
|
+
export function inferDataFlows(scanResult, config, depGraph = null) {
|
|
4
8
|
const flows = [];
|
|
5
9
|
|
|
6
|
-
//
|
|
10
|
+
// If we have dep graph data, build real import-chain flows from entry points
|
|
11
|
+
if (depGraph?.nodes && depGraph.nodes.length > 0) {
|
|
12
|
+
const entryFlows = inferFlowsFromEntryPoints(scanResult, depGraph);
|
|
13
|
+
flows.push(...entryFlows);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Keyword-based heuristic flows (only add if not already covered by import-chain analysis)
|
|
17
|
+
const existingNames = new Set(flows.map(f => f.name.toLowerCase()));
|
|
18
|
+
|
|
7
19
|
const marketFlow = inferMarketDataFlow(scanResult);
|
|
8
|
-
if (marketFlow) flows.push(marketFlow);
|
|
20
|
+
if (marketFlow && !existingNames.has(marketFlow.name.toLowerCase())) flows.push(marketFlow);
|
|
9
21
|
|
|
10
|
-
// Authentication Flow
|
|
11
22
|
const authFlow = inferAuthFlow(scanResult);
|
|
12
|
-
if (authFlow) flows.push(authFlow);
|
|
23
|
+
if (authFlow && !existingNames.has(authFlow.name.toLowerCase())) flows.push(authFlow);
|
|
13
24
|
|
|
14
|
-
// Content/Article Flow
|
|
15
25
|
const contentFlow = inferContentFlow(scanResult);
|
|
16
|
-
if (contentFlow) flows.push(contentFlow);
|
|
26
|
+
if (contentFlow && !existingNames.has(contentFlow.name.toLowerCase())) flows.push(contentFlow);
|
|
17
27
|
|
|
18
|
-
// API Integration Flow
|
|
19
28
|
const apiFlow = inferApiIntegrationFlow(scanResult);
|
|
20
|
-
if (apiFlow) flows.push(apiFlow);
|
|
29
|
+
if (apiFlow && !existingNames.has(apiFlow.name.toLowerCase())) flows.push(apiFlow);
|
|
21
30
|
|
|
22
31
|
return flows;
|
|
23
32
|
}
|
|
24
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Trace real import chains from entry points through the dep graph.
|
|
36
|
+
* Entry points: CLI/bin files, page components, API route handlers, index files.
|
|
37
|
+
*/
|
|
38
|
+
function inferFlowsFromEntryPoints(scanResult, depGraph) {
|
|
39
|
+
const flows = [];
|
|
40
|
+
if (!depGraph?.nodes) return flows;
|
|
41
|
+
|
|
42
|
+
// Find entry points (files with high fan-out and low/zero fan-in)
|
|
43
|
+
const entryPoints = depGraph.nodes.filter(n => {
|
|
44
|
+
const key = n.key.toLowerCase();
|
|
45
|
+
const isEntry = (
|
|
46
|
+
key.includes("bin/") ||
|
|
47
|
+
key.includes("cli") ||
|
|
48
|
+
key.endsWith("/index") ||
|
|
49
|
+
key.includes("pages/") ||
|
|
50
|
+
key.includes("app/") ||
|
|
51
|
+
(n.importedBy.length === 0 && n.imports.length >= 2) // True entry: nothing imports it
|
|
52
|
+
);
|
|
53
|
+
return isEntry && n.imports.length >= 2;
|
|
54
|
+
}).sort((a, b) => {
|
|
55
|
+
// Prioritize src/ entry points over tests/
|
|
56
|
+
const aIsTest = a.key.toLowerCase().includes("test");
|
|
57
|
+
const bIsTest = b.key.toLowerCase().includes("test");
|
|
58
|
+
if (aIsTest !== bIsTest) return aIsTest ? 1 : -1;
|
|
59
|
+
return b.imports.length - a.imports.length;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Limit: all src entries + max 2 test entries
|
|
63
|
+
const srcEntries = entryPoints.filter(n => !n.key.toLowerCase().includes("test"));
|
|
64
|
+
const testEntries = entryPoints.filter(n => n.key.toLowerCase().includes("test")).slice(0, 2);
|
|
65
|
+
const selectedEntries = [...srcEntries, ...testEntries].slice(0, 5);
|
|
66
|
+
|
|
67
|
+
for (const entry of selectedEntries) {
|
|
68
|
+
const chain = traceImportChain(entry.key, depGraph, 6);
|
|
69
|
+
if (chain.length < 2) continue;
|
|
70
|
+
|
|
71
|
+
const shortName = entry.key.split("/").pop();
|
|
72
|
+
const isCliEntry = entry.key.toLowerCase().includes("cli") || entry.key.toLowerCase().includes("bin/");
|
|
73
|
+
const flowType = isCliEntry ? "Command" : "Request";
|
|
74
|
+
|
|
75
|
+
// Classify each step in the chain
|
|
76
|
+
const steps = chain.map((nodeKey, i) => {
|
|
77
|
+
const node = depGraph.nodes.find(n => n.key === nodeKey);
|
|
78
|
+
const name = nodeKey.split("/").pop();
|
|
79
|
+
const fanIn = node?.importedBy?.length || 0;
|
|
80
|
+
const fanOut = node?.imports?.length || 0;
|
|
81
|
+
|
|
82
|
+
if (i === 0) return `\`${name}\` (entry point, imports ${fanOut} modules)`;
|
|
83
|
+
if (fanIn >= 5) return `\`${name}\` (shared infrastructure, used by ${fanIn} files)`;
|
|
84
|
+
if (fanOut === 0) return `\`${name}\` (leaf — no further dependencies)`;
|
|
85
|
+
return `\`${name}\` → imports ${fanOut} module${fanOut === 1 ? "" : "s"}`;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
flows.push({
|
|
89
|
+
name: `${shortName} ${flowType} Flow`,
|
|
90
|
+
description: `Import chain starting from \`${entry.key}\` — traces how this entry point depends on ${chain.length - 1} downstream module${chain.length - 1 === 1 ? "" : "s"}`,
|
|
91
|
+
steps,
|
|
92
|
+
modules: chain.slice(0, 8),
|
|
93
|
+
critical: entry.imports.length >= 5,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return flows;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Trace from an entry node through the dep graph via BFS, following highest-fan-out paths.
|
|
102
|
+
* Returns an ordered chain of module keys representing the primary dependency path.
|
|
103
|
+
*/
|
|
104
|
+
function traceImportChain(startKey, depGraph, maxDepth = 6) {
|
|
105
|
+
const chain = [startKey];
|
|
106
|
+
const visited = new Set([startKey]);
|
|
107
|
+
let current = startKey;
|
|
108
|
+
|
|
109
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
110
|
+
const node = depGraph.nodes.find(n => n.key === current);
|
|
111
|
+
if (!node || node.imports.length === 0) break;
|
|
112
|
+
|
|
113
|
+
// Follow the most-imported (highest fan-in) dependency first — it's the most interesting path
|
|
114
|
+
const candidates = node.imports
|
|
115
|
+
.filter(imp => !visited.has(imp))
|
|
116
|
+
.map(imp => {
|
|
117
|
+
const target = depGraph.nodes.find(n => n.key === imp);
|
|
118
|
+
return { key: imp, fanIn: target?.importedBy?.length || 0 };
|
|
119
|
+
})
|
|
120
|
+
.sort((a, b) => b.fanIn - a.fanIn);
|
|
121
|
+
|
|
122
|
+
if (candidates.length === 0) break;
|
|
123
|
+
|
|
124
|
+
current = candidates[0].key;
|
|
125
|
+
visited.add(current);
|
|
126
|
+
chain.push(current);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return chain;
|
|
130
|
+
}
|
|
131
|
+
|
|
25
132
|
function inferMarketDataFlow(scanResult) {
|
|
26
133
|
const { modules, pages, api } = scanResult;
|
|
27
134
|
|
|
@@ -31,7 +31,7 @@ const SCHEMA_LIBRARY_PATTERNS = [
|
|
|
31
31
|
{ name: "Apollo Server", pattern: /ApolloServer|@apollo\/server|apollo-server/ },
|
|
32
32
|
{ name: "GraphQL Yoga", pattern: /graphql-yoga|createYoga/ },
|
|
33
33
|
{ name: "Mercurius", pattern: /mercurius/ },
|
|
34
|
-
{ name: "graphql-js", pattern: /graphql\b
|
|
34
|
+
{ name: "graphql-js", pattern: /graphql\b.*\bbuildSchema\b|\bGraphQLSchema\b|\bGraphQLObjectType\b/ },
|
|
35
35
|
{ name: "type-graphql", pattern: /type-graphql|@Resolver|@Query|@Mutation/ },
|
|
36
36
|
{ name: "Nexus", pattern: /nexus|makeSchema|objectType\(/ },
|
|
37
37
|
{ name: "Pothos", pattern: /pothos|SchemaBuilder/ },
|
|
@@ -84,8 +84,13 @@ export async function analyzeGraphQL(files, repoRoot) {
|
|
|
84
84
|
const schemaFiles = files.filter(f => GRAPHQL_EXTENSIONS.some(ext => f.endsWith(ext)));
|
|
85
85
|
|
|
86
86
|
// Phase 2: Find JS/TS files with inline schema or resolvers
|
|
87
|
+
// Exclude test files and our own analysis/rendering files (they reference library names as strings, not usage)
|
|
88
|
+
const testPattern = /(?:^|\/)(?:tests?|__tests?__|spec|__spec__)\/|\.(test|spec)\.[jt]sx?$/i;
|
|
89
|
+
const selfPatterns = ["graphql-analyzer.js", "renderAnalysis.js"];
|
|
87
90
|
const codeFiles = files.filter(f =>
|
|
88
|
-
f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".jsx") || f.endsWith(".tsx")
|
|
91
|
+
(f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".jsx") || f.endsWith(".tsx")) &&
|
|
92
|
+
!testPattern.test(f) &&
|
|
93
|
+
!selfPatterns.some(s => f.endsWith(s))
|
|
89
94
|
);
|
|
90
95
|
|
|
91
96
|
// Parse .graphql/.gql files
|
|
@@ -33,10 +33,10 @@ import path from "node:path";
|
|
|
33
33
|
export async function generateDocumentSet(scanResult, config, diffData = null, pluginManager = null) {
|
|
34
34
|
info("Building structured context for AI...");
|
|
35
35
|
|
|
36
|
-
// Build AI context from scan results
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
// Build AI context from scan results (dep graph computed later, patched in after)
|
|
37
|
+
let aiContext = buildAIContext(scanResult, config);
|
|
38
|
+
let moduleContext = buildModuleContext(scanResult.modules, config);
|
|
39
|
+
let flows = inferDataFlows(scanResult, config);
|
|
40
40
|
|
|
41
41
|
// Run extended analysis (v0.8.0)
|
|
42
42
|
const repoRoot = config.__repoRoot || process.cwd();
|
|
@@ -63,6 +63,11 @@ export async function generateDocumentSet(scanResult, config, diffData = null, p
|
|
|
63
63
|
await saveBaseline(snapshot, outputDir);
|
|
64
64
|
} catch (e) { warn(`Drift detection failed: ${e.message}`); }
|
|
65
65
|
|
|
66
|
+
// Rebuild AI context with dep graph for enriched module roles and pattern verification
|
|
67
|
+
aiContext = buildAIContext(scanResult, config, depGraph);
|
|
68
|
+
moduleContext = buildModuleContext(scanResult.modules, config, depGraph);
|
|
69
|
+
flows = inferDataFlows(scanResult, config, depGraph);
|
|
70
|
+
|
|
66
71
|
// CODEOWNERS integration
|
|
67
72
|
const codeowners = await parseCodeowners(repoRoot);
|
|
68
73
|
const ownershipMap = codeowners.found
|
|
@@ -196,19 +201,19 @@ async function generateDocument(docPlan, context) {
|
|
|
196
201
|
return await generateArchitectureOverview(aiContext, { depGraph, driftResult });
|
|
197
202
|
|
|
198
203
|
case "module_catalog":
|
|
199
|
-
// Hybrid: deterministic skeleton + ownership info
|
|
200
|
-
return renderModuleCatalogOriginal(config, scanResult, ownershipMap);
|
|
204
|
+
// Hybrid: deterministic skeleton + ownership info + dep-graph roles
|
|
205
|
+
return renderModuleCatalogOriginal(config, scanResult, ownershipMap, depGraph);
|
|
201
206
|
|
|
202
207
|
case "route_map":
|
|
203
|
-
// Hybrid: deterministic skeleton +
|
|
204
|
-
return renderRouteMapOriginal(config, scanResult);
|
|
208
|
+
// Hybrid: deterministic skeleton + context-aware messaging
|
|
209
|
+
return renderRouteMapOriginal(config, scanResult, aiContext);
|
|
205
210
|
|
|
206
211
|
case "api_surface":
|
|
207
212
|
// Hybrid: deterministic skeleton + AI enhancement (for now, just deterministic)
|
|
208
213
|
return renderApiSurfaceOriginal(config, scanResult);
|
|
209
214
|
|
|
210
215
|
case "data_flows":
|
|
211
|
-
return await generateDataFlows(flows, aiContext, { depGraph, scanResult });
|
|
216
|
+
return await generateDataFlows(flows, aiContext, { depGraph, scanResult, moduleContext });
|
|
212
217
|
|
|
213
218
|
case "arch_diff":
|
|
214
219
|
if (!diffData) {
|
package/src/renderers/render.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { computeModuleLevelMetrics, describeModuleDepRole } from "../analyzers/context-builder.js";
|
|
2
|
+
|
|
3
|
+
export function renderSystemOverview(cfg, scan, depGraph = null) {
|
|
2
4
|
const projectName = cfg.project?.name || "Project";
|
|
3
5
|
const date = new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
|
|
4
6
|
|
|
@@ -17,8 +19,8 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
17
19
|
`|--------|-------|`,
|
|
18
20
|
`| Files scanned | ${scan.filesCount} |`,
|
|
19
21
|
`| Modules detected | ${scan.modules.length} |`,
|
|
20
|
-
`| Application pages | ${scan.pages
|
|
21
|
-
`| API endpoints | ${scan.api.length}
|
|
22
|
+
...(scan.pages?.length ? [`| Application pages | ${scan.pages.length} |`] : []),
|
|
23
|
+
...(scan.api.length ? [`| API endpoints | ${scan.api.length} |`] : []),
|
|
22
24
|
``
|
|
23
25
|
];
|
|
24
26
|
|
|
@@ -35,9 +37,9 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
35
37
|
`| Category | Technologies |`,
|
|
36
38
|
`|----------|-------------|`
|
|
37
39
|
);
|
|
38
|
-
|
|
40
|
+
lines.push(`| Frameworks | ${frameworks.join(", ") || 'N/A'} |`);
|
|
39
41
|
if (languages.length) lines.push(`| Languages | ${languages.join(", ")} |`);
|
|
40
|
-
|
|
42
|
+
lines.push(`| Build Tools | ${buildTools.join(", ") || 'N/A'} |`);
|
|
41
43
|
if (testFrameworks.length) lines.push(`| Testing | ${testFrameworks.join(", ")} |`);
|
|
42
44
|
lines.push(``);
|
|
43
45
|
}
|
|
@@ -53,8 +55,8 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
53
55
|
`## Architecture Summary`,
|
|
54
56
|
``,
|
|
55
57
|
`The repository is organized as ${sizeDesc} with **${scan.modules.length} modules** spanning **${scan.filesCount} files**. `
|
|
56
|
-
+ (scan.api.length > 0 ? `It exposes **${scan.api.length} API endpoint${scan.api.length === 1 ? "" : "s"}
|
|
57
|
-
+ (scan.pages?.length > 0 ? `
|
|
58
|
+
+ (scan.api.length > 0 ? `It exposes **${scan.api.length} API endpoint${scan.api.length === 1 ? "" : "s"}**. ` : "")
|
|
59
|
+
+ (scan.pages?.length > 0 ? `It serves **${scan.pages.length} application page${scan.pages.length === 1 ? "" : "s"}**. ` : "")
|
|
58
60
|
+ `The largest modules are listed below, ranked by file count.`,
|
|
59
61
|
``
|
|
60
62
|
);
|
|
@@ -62,6 +64,7 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
62
64
|
|
|
63
65
|
// Largest modules as a table instead of bullets
|
|
64
66
|
const topModules = scan.modules.slice(0, 10);
|
|
67
|
+
const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
|
|
65
68
|
if (topModules.length > 0) {
|
|
66
69
|
lines.push(
|
|
67
70
|
`## Largest Modules`,
|
|
@@ -70,7 +73,8 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
70
73
|
`|--------|-------|-------------|`
|
|
71
74
|
);
|
|
72
75
|
for (const m of topModules) {
|
|
73
|
-
const
|
|
76
|
+
const depRole = describeModuleDepRole(m.key, moduleMetrics);
|
|
77
|
+
const desc = describeModule(m.key, depRole);
|
|
74
78
|
lines.push(`| \`${m.key}\` | ${m.fileCount} | ${desc} |`);
|
|
75
79
|
}
|
|
76
80
|
lines.push(``);
|
|
@@ -105,36 +109,50 @@ export function renderSystemOverview(cfg, scan) {
|
|
|
105
109
|
return lines.join("\n");
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
function describeModule(key) {
|
|
112
|
+
function describeModule(key, depRole = null) {
|
|
109
113
|
const normalized = key.toLowerCase();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (normalized.includes("
|
|
113
|
-
if (normalized.includes("
|
|
114
|
-
if (normalized.includes("
|
|
115
|
-
if (normalized.includes("
|
|
116
|
-
if (normalized.includes("
|
|
117
|
-
if (normalized.includes("
|
|
118
|
-
if (normalized.includes("
|
|
119
|
-
if (normalized.includes("
|
|
120
|
-
if (normalized.includes("
|
|
121
|
-
if (normalized.includes("
|
|
122
|
-
if (normalized.includes("
|
|
123
|
-
if (normalized.includes("
|
|
124
|
-
if (normalized.includes("
|
|
125
|
-
if (normalized.includes("
|
|
126
|
-
if (normalized.includes("
|
|
127
|
-
if (normalized.includes("
|
|
128
|
-
if (normalized.includes("
|
|
129
|
-
if (normalized.includes("
|
|
130
|
-
if (normalized.includes("
|
|
131
|
-
if (normalized.includes("
|
|
132
|
-
if (normalized.includes("
|
|
133
|
-
|
|
114
|
+
// Base description from path keywords
|
|
115
|
+
let base;
|
|
116
|
+
if (normalized.includes("core")) base = "Core business logic and shared foundations";
|
|
117
|
+
else if (normalized.includes("util")) base = "Shared utilities and helper functions";
|
|
118
|
+
else if (normalized.includes("api")) base = "API route handlers and endpoint definitions";
|
|
119
|
+
else if (normalized.includes("component")) base = "Reusable UI components";
|
|
120
|
+
else if (normalized.includes("hook")) base = "Custom React hooks";
|
|
121
|
+
else if (normalized.includes("page")) base = "Application page components";
|
|
122
|
+
else if (normalized.includes("lib")) base = "Library code and third-party integrations";
|
|
123
|
+
else if (normalized.includes("service")) base = "Service layer and external integrations";
|
|
124
|
+
else if (normalized.includes("model")) base = "Data models and schema definitions";
|
|
125
|
+
else if (normalized.includes("store") || normalized.includes("state")) base = "State management";
|
|
126
|
+
else if (normalized.includes("config")) base = "Configuration and settings";
|
|
127
|
+
else if (normalized.includes("test")) base = "Test suites and fixtures";
|
|
128
|
+
else if (normalized.includes("style") || normalized.includes("css")) base = "Styling and design tokens";
|
|
129
|
+
else if (normalized.includes("type")) base = "Type definitions and interfaces";
|
|
130
|
+
else if (normalized.includes("middleware")) base = "Request middleware and interceptors";
|
|
131
|
+
else if (normalized.includes("auth")) base = "Authentication and authorization";
|
|
132
|
+
else if (normalized.includes("render")) base = "Rendering logic and output formatters";
|
|
133
|
+
else if (normalized.includes("publish")) base = "Publishing and delivery integrations";
|
|
134
|
+
else if (normalized.includes("analyz")) base = "Code analysis and intelligence";
|
|
135
|
+
else if (normalized.includes("delivery")) base = "Content delivery and distribution";
|
|
136
|
+
else if (normalized.includes("integrat")) base = "Third-party service integrations";
|
|
137
|
+
else if (normalized.includes("doc")) base = "Documentation generation";
|
|
138
|
+
else if (normalized.includes("bin") || normalized.includes("cli")) base = "CLI entry point and commands";
|
|
139
|
+
else if (normalized.includes("plugin") || normalized.includes("extension")) base = "Plugin system and extensions";
|
|
140
|
+
else if (normalized.includes("prompt")) base = "Prompt engineering and templates";
|
|
141
|
+
else if (normalized.includes("provider")) base = "Service provider adapters";
|
|
142
|
+
else if (normalized.includes("generate") || normalized.includes("section")) base = "Content generation pipeline";
|
|
143
|
+
else if (normalized.includes("ai") || normalized.includes("ml")) base = "AI and machine learning integration";
|
|
144
|
+
else base = "Application module";
|
|
145
|
+
|
|
146
|
+
// Enrich with dependency role if available
|
|
147
|
+
if (depRole) {
|
|
148
|
+
return `${base} · ${depRole}`;
|
|
149
|
+
}
|
|
150
|
+
return base;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
|
-
export function renderModuleCatalog(cfg, scan, ownershipMap = {}) {
|
|
153
|
+
export function renderModuleCatalog(cfg, scan, ownershipMap = {}, depGraph = null) {
|
|
137
154
|
const hasOwnership = Object.keys(ownershipMap).length > 0;
|
|
155
|
+
const moduleMetrics = computeModuleLevelMetrics(depGraph, scan.modules);
|
|
138
156
|
const lines = [
|
|
139
157
|
`# Module Catalog`,
|
|
140
158
|
``,
|
|
@@ -172,7 +190,8 @@ export function renderModuleCatalog(cfg, scan, ownershipMap = {}) {
|
|
|
172
190
|
}
|
|
173
191
|
|
|
174
192
|
for (const module of scan.modules.slice(0, 100)) {
|
|
175
|
-
const
|
|
193
|
+
const depRole = describeModuleDepRole(module.key, moduleMetrics);
|
|
194
|
+
const desc = describeModule(module.key, depRole);
|
|
176
195
|
const owners = ownershipMap[module.key];
|
|
177
196
|
if (hasOwnership) {
|
|
178
197
|
lines.push(`| \`${module.key}\` | ${module.fileCount} | ${desc} | ${owners ? owners.join(", ") : "—"} |`);
|
|
@@ -277,7 +296,7 @@ export function renderApiSurface(cfg, scan) {
|
|
|
277
296
|
return lines.join("\n");
|
|
278
297
|
}
|
|
279
298
|
|
|
280
|
-
export function renderRouteMap(cfg, scan) {
|
|
299
|
+
export function renderRouteMap(cfg, scan, aiContext = null) {
|
|
281
300
|
const lines = [
|
|
282
301
|
`# Route Map`,
|
|
283
302
|
``,
|
|
@@ -330,30 +349,61 @@ export function renderRouteMap(cfg, scan) {
|
|
|
330
349
|
}
|
|
331
350
|
|
|
332
351
|
if (!scan.pages?.length && !scan.api?.length) {
|
|
352
|
+
// Determine project type for context-aware messaging
|
|
353
|
+
const patterns = aiContext?.patterns || [];
|
|
354
|
+
const isCLI = patterns.some(p => p.toLowerCase().includes("cli"));
|
|
355
|
+
const isLibrary = patterns.some(p => p.toLowerCase().includes("library") || p.toLowerCase().includes("shared"));
|
|
356
|
+
const techStack = aiContext?.techStack || {};
|
|
357
|
+
const hasWebFramework = (techStack.frameworks || []).some(f =>
|
|
358
|
+
/next|react|vue|angular|express|fastify|hono|koa|django|flask|rails|spring/i.test(f)
|
|
359
|
+
);
|
|
360
|
+
|
|
333
361
|
lines.push(
|
|
334
362
|
`## Route Detection`,
|
|
335
|
-
``,
|
|
336
|
-
`No routes were auto-detected in this scan. RepoLens currently supports:`,
|
|
337
|
-
``,
|
|
338
|
-
`| Framework | Pattern | Status |`,
|
|
339
|
-
`|-----------|---------|--------|`,
|
|
340
|
-
`| Next.js | \`pages/\` and \`app/\` directories | Supported |`,
|
|
341
|
-
`| Next.js API | \`pages/api/\` and App Router | Supported |`,
|
|
342
|
-
`| Express.js | \`app.get\`, \`router.post\`, etc. | Supported |`,
|
|
343
|
-
`| React Router | \`<Route>\` components | Supported |`,
|
|
344
|
-
`| Vue Router | \`routes\` array definitions | Supported |`,
|
|
345
|
-
``,
|
|
346
|
-
`If your project uses a different routing framework, open an issue at [github.com/CHAPIBUNNY/repolens](https://github.com/CHAPIBUNNY/repolens/issues) to request support.`,
|
|
347
363
|
``
|
|
348
364
|
);
|
|
365
|
+
|
|
366
|
+
if (isCLI) {
|
|
367
|
+
lines.push(
|
|
368
|
+
`This project is a **CLI tool** — it does not expose HTTP routes or serve web pages. This is expected behavior, not a detection failure.`,
|
|
369
|
+
``,
|
|
370
|
+
`CLI tools interact through terminal commands rather than URLs. See the **System Overview** or **Developer Onboarding** documents for command documentation.`,
|
|
371
|
+
``
|
|
372
|
+
);
|
|
373
|
+
} else if (isLibrary && !hasWebFramework) {
|
|
374
|
+
lines.push(
|
|
375
|
+
`This project is a **library/package** — it does not define its own routes or pages. Libraries are consumed by other applications that define their own routing.`,
|
|
376
|
+
``
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
lines.push(
|
|
380
|
+
`No routes were auto-detected in this scan. RepoLens currently supports:`,
|
|
381
|
+
``,
|
|
382
|
+
`| Framework | Pattern | Status |`,
|
|
383
|
+
`|-----------|---------|--------|`,
|
|
384
|
+
`| Next.js | \`pages/\` and \`app/\` directories | Supported |`,
|
|
385
|
+
`| Next.js API | \`pages/api/\` and App Router | Supported |`,
|
|
386
|
+
`| Express.js | \`app.get\`, \`router.post\`, etc. | Supported |`,
|
|
387
|
+
`| Fastify | \`fastify.get\`, \`fastify.post\`, etc. | Supported |`,
|
|
388
|
+
`| Hono | \`app.get\`, \`app.post\`, etc. | Supported |`,
|
|
389
|
+
`| React Router | \`<Route>\` components | Supported |`,
|
|
390
|
+
`| Vue Router | \`routes\` array definitions | Supported |`,
|
|
391
|
+
``,
|
|
392
|
+
`If your project uses a different routing framework, open an issue at [github.com/CHAPIBUNNY/repolens](https://github.com/CHAPIBUNNY/repolens/issues) to request support.`,
|
|
393
|
+
``
|
|
394
|
+
);
|
|
395
|
+
}
|
|
349
396
|
}
|
|
350
397
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
398
|
+
// Only add the route hint footer for projects that actually have routes
|
|
399
|
+
if (scan.pages?.length || scan.api?.length) {
|
|
400
|
+
lines.push(
|
|
401
|
+
`---`,
|
|
402
|
+
``,
|
|
403
|
+
`*Paths starting with \`/api/\` are backend endpoints; all others are user-facing pages.*`,
|
|
404
|
+
``
|
|
405
|
+
);
|
|
406
|
+
}
|
|
357
407
|
|
|
358
408
|
return lines.join("\n");
|
|
359
409
|
}
|
|
@@ -31,6 +31,20 @@ function routePathFromFile(file) {
|
|
|
31
31
|
return file;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Files/dirs to exclude from impacted modules (build artifacts, lockfiles, etc.)
|
|
35
|
+
const NOISE_PATTERNS = [
|
|
36
|
+
/^node_modules/,
|
|
37
|
+
/^package-lock\.json$/,
|
|
38
|
+
/^yarn\.lock$/,
|
|
39
|
+
/^pnpm-lock\.yaml$/,
|
|
40
|
+
/^\.repolens\//,
|
|
41
|
+
/^\.git\//,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
function isNoiseFile(file) {
|
|
45
|
+
return NOISE_PATTERNS.some(p => p.test(file));
|
|
46
|
+
}
|
|
47
|
+
|
|
34
48
|
function moduleFromFile(file) {
|
|
35
49
|
const normalized = file.replace(/\\/g, "/");
|
|
36
50
|
const parts = normalized.split("/");
|
|
@@ -50,14 +64,20 @@ export function buildArchitectureDiffData(diff) {
|
|
|
50
64
|
const addedRoutes = diff.added.filter(isRouteFile).map(routePathFromFile);
|
|
51
65
|
const removedRoutes = diff.removed.filter(isRouteFile).map(routePathFromFile);
|
|
52
66
|
|
|
67
|
+
// Filter out noise files (node_modules, lockfiles, etc.) from all lists
|
|
68
|
+
const added = diff.added.filter(f => !isNoiseFile(f));
|
|
69
|
+
const removed = diff.removed.filter(f => !isNoiseFile(f));
|
|
70
|
+
const modified = diff.modified.filter(f => !isNoiseFile(f));
|
|
71
|
+
const allFiles = [...added, ...removed, ...modified];
|
|
53
72
|
const impactedModules = [
|
|
54
|
-
...new Set(
|
|
55
|
-
[...diff.added, ...diff.removed, ...diff.modified].map(moduleFromFile)
|
|
56
|
-
)
|
|
73
|
+
...new Set(allFiles.map(moduleFromFile))
|
|
57
74
|
].sort();
|
|
58
75
|
|
|
59
76
|
return {
|
|
60
77
|
...diff,
|
|
78
|
+
added,
|
|
79
|
+
removed,
|
|
80
|
+
modified,
|
|
61
81
|
addedRoutes,
|
|
62
82
|
removedRoutes,
|
|
63
83
|
impactedModules
|