ai-mind-map 1.14.0 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge-graph/graph.d.ts +26 -0
- package/dist/knowledge-graph/graph.d.ts.map +1 -1
- package/dist/knowledge-graph/graph.js +210 -131
- package/dist/knowledge-graph/graph.js.map +1 -1
- package/dist/tools/project-map-tool.d.ts +22 -0
- package/dist/tools/project-map-tool.d.ts.map +1 -0
- package/dist/tools/project-map-tool.js +877 -0
- package/dist/tools/project-map-tool.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,877 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Mind Map — Project Map Tool (MCP)
|
|
3
|
+
*
|
|
4
|
+
* THE killer tool: One call gives AI a complete mental model of any project.
|
|
5
|
+
* Instead of AI wasting 10-20 tool calls and thousands of tokens to understand
|
|
6
|
+
* a codebase, this returns a structured, token-efficient project map that covers:
|
|
7
|
+
*
|
|
8
|
+
* - What the project IS (purpose, tech stack, type)
|
|
9
|
+
* - HOW it's organized (modules, layers, directory structure)
|
|
10
|
+
* - WHAT talks to what (inter-module dependency graph)
|
|
11
|
+
* - WHERE the important stuff is (entry points, key abstractions)
|
|
12
|
+
* - WHO calls whom (top-level call flow)
|
|
13
|
+
* - WHAT patterns are used (design patterns, conventions)
|
|
14
|
+
*
|
|
15
|
+
* v1.14.0+
|
|
16
|
+
*/
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { relative, basename, extname, join } from 'node:path';
|
|
19
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
20
|
+
// ── Helpers ─────────────────────────────────────────────────
|
|
21
|
+
const defaultEstimator = {
|
|
22
|
+
estimate: (s) => Math.ceil(s.length / 4),
|
|
23
|
+
};
|
|
24
|
+
function ok(data, estimator) {
|
|
25
|
+
const json = JSON.stringify(data, null, 2);
|
|
26
|
+
const tokens = estimator.estimate(json);
|
|
27
|
+
return JSON.stringify({ success: true, data, tokenCount: tokens });
|
|
28
|
+
}
|
|
29
|
+
function fail(message) {
|
|
30
|
+
return JSON.stringify({ success: false, error: message });
|
|
31
|
+
}
|
|
32
|
+
function mcpText(text) {
|
|
33
|
+
return { content: [{ type: 'text', text }] };
|
|
34
|
+
}
|
|
35
|
+
const SKIP_DIRS = new Set([
|
|
36
|
+
'node_modules', '.git', '.svn', '.hg', 'dist', 'build', 'out',
|
|
37
|
+
'__pycache__', '.mypy_cache', '.pytest_cache', '.next', '.nuxt',
|
|
38
|
+
'target', 'bin', 'obj', '.vs', '.idea', '.vscode', 'vendor',
|
|
39
|
+
'coverage', '.tox', '.eggs', '.cache', '.gradle',
|
|
40
|
+
]);
|
|
41
|
+
// ── Framework Detection ─────────────────────────────────────
|
|
42
|
+
const FRAMEWORK_MARKERS = [
|
|
43
|
+
{ files: ['next.config.js', 'next.config.ts', 'next.config.mjs'], deps: ['next'], name: 'Next.js' },
|
|
44
|
+
{ files: ['nuxt.config.ts', 'nuxt.config.js'], deps: ['nuxt'], name: 'Nuxt.js' },
|
|
45
|
+
{ files: ['angular.json'], deps: ['@angular/core'], name: 'Angular' },
|
|
46
|
+
{ files: ['vite.config.ts', 'vite.config.js'], deps: ['vite'], name: 'Vite' },
|
|
47
|
+
{ files: [], deps: ['react', 'react-dom'], name: 'React' },
|
|
48
|
+
{ files: [], deps: ['vue'], name: 'Vue.js' },
|
|
49
|
+
{ files: [], deps: ['svelte'], name: 'Svelte' },
|
|
50
|
+
{ files: [], deps: ['express'], name: 'Express.js' },
|
|
51
|
+
{ files: [], deps: ['fastify'], name: 'Fastify' },
|
|
52
|
+
{ files: [], deps: ['koa'], name: 'Koa' },
|
|
53
|
+
{ files: [], deps: ['nestjs', '@nestjs/core'], name: 'NestJS' },
|
|
54
|
+
{ files: [], deps: ['electron'], name: 'Electron' },
|
|
55
|
+
{ files: [], deps: ['django'], name: 'Django' },
|
|
56
|
+
{ files: [], deps: ['flask'], name: 'Flask' },
|
|
57
|
+
{ files: [], deps: ['fastapi'], name: 'FastAPI' },
|
|
58
|
+
{ files: ['Gemfile'], deps: ['rails'], name: 'Ruby on Rails' },
|
|
59
|
+
{ files: [], deps: ['spring-boot', 'spring-core'], name: 'Spring Boot' },
|
|
60
|
+
{ files: [], deps: ['wpf', 'WindowsBase'], name: 'WPF' },
|
|
61
|
+
{ files: [], deps: ['Xamarin.Forms'], name: 'Xamarin' },
|
|
62
|
+
{ files: [], deps: ['MAUI', 'Microsoft.Maui'], name: '.NET MAUI' },
|
|
63
|
+
];
|
|
64
|
+
const BUILD_TOOL_MARKERS = [
|
|
65
|
+
{ files: ['webpack.config.js', 'webpack.config.ts'], name: 'Webpack' },
|
|
66
|
+
{ files: ['rollup.config.js', 'rollup.config.ts'], name: 'Rollup' },
|
|
67
|
+
{ files: ['esbuild.config.js'], name: 'esbuild' },
|
|
68
|
+
{ files: ['tsconfig.json'], name: 'TypeScript' },
|
|
69
|
+
{ files: ['babel.config.js', '.babelrc'], name: 'Babel' },
|
|
70
|
+
{ files: ['Makefile'], name: 'Make' },
|
|
71
|
+
{ files: ['CMakeLists.txt'], name: 'CMake' },
|
|
72
|
+
{ files: ['build.gradle', 'build.gradle.kts'], name: 'Gradle' },
|
|
73
|
+
{ files: ['pom.xml'], name: 'Maven' },
|
|
74
|
+
{ files: ['Cargo.toml'], name: 'Cargo' },
|
|
75
|
+
{ files: ['go.mod'], name: 'Go Modules' },
|
|
76
|
+
{ files: ['Dockerfile'], name: 'Docker' },
|
|
77
|
+
{ files: ['docker-compose.yml', 'docker-compose.yaml'], name: 'Docker Compose' },
|
|
78
|
+
];
|
|
79
|
+
// ============================================================
|
|
80
|
+
// Registration
|
|
81
|
+
// ============================================================
|
|
82
|
+
export function registerProjectMapTool(server, graph, config, estimator = defaultEstimator) {
|
|
83
|
+
server.tool('mindmap_project_map', 'Get a COMPLETE mental model of the entire project in ONE call. ' +
|
|
84
|
+
'Returns the full architecture map: tech stack, modules, inter-module dependencies, ' +
|
|
85
|
+
'entry points, key abstractions, data flow, conventions, and an AI-optimized quick-reference card. ' +
|
|
86
|
+
'USE THIS FIRST before any other tool — it saves thousands of tokens vs reading files individually.', {
|
|
87
|
+
projectPath: z.string().optional().describe('Project root path (defaults to configured root)'),
|
|
88
|
+
detail: z.enum(['quick', 'standard', 'deep']).default('standard')
|
|
89
|
+
.describe('Detail level: quick (~500 tokens), standard (~2000 tokens), deep (~5000 tokens)'),
|
|
90
|
+
focus: z.string().optional()
|
|
91
|
+
.describe('Optional focus area (e.g., "api", "auth", "database") to get deeper info on a specific module'),
|
|
92
|
+
}, async ({ projectPath, detail, focus }) => {
|
|
93
|
+
try {
|
|
94
|
+
const rootPath = projectPath ?? config.projectRoot;
|
|
95
|
+
const stats = graph.getStats();
|
|
96
|
+
const indexedFiles = graph.getIndexedFiles();
|
|
97
|
+
// ── 1. Project Identity ──────────────────────────────
|
|
98
|
+
const projectName = detectProjectName(rootPath);
|
|
99
|
+
const projectType = detectProjectType(rootPath, indexedFiles);
|
|
100
|
+
const description = generateProjectDescription(rootPath, projectType, stats);
|
|
101
|
+
// ── 2. Tech Stack ────────────────────────────────────
|
|
102
|
+
const languages = Object.entries(stats.languageBreakdown)
|
|
103
|
+
.sort(([, a], [, b]) => b - a)
|
|
104
|
+
.map(([name, files]) => ({
|
|
105
|
+
name,
|
|
106
|
+
files,
|
|
107
|
+
percentage: Math.round((files / Math.max(stats.totalFiles, 1)) * 100),
|
|
108
|
+
}));
|
|
109
|
+
const frameworks = detectFrameworks(rootPath);
|
|
110
|
+
const buildTools = detectBuildTools(rootPath);
|
|
111
|
+
const runtime = detectRuntime(rootPath);
|
|
112
|
+
// ── 3. Module Discovery ──────────────────────────────
|
|
113
|
+
const modules = discoverModules(rootPath, indexedFiles, graph, config);
|
|
114
|
+
const relations = discoverRelations(modules, graph, config);
|
|
115
|
+
// ── 4. Architecture Pattern ──────────────────────────
|
|
116
|
+
const archPattern = detectArchPattern(modules, indexedFiles, rootPath);
|
|
117
|
+
const layers = detectLayers(modules, indexedFiles, rootPath);
|
|
118
|
+
// ── 5. Entry Points ──────────────────────────────────
|
|
119
|
+
const entryPoints = detectEntryPoints(indexedFiles, rootPath);
|
|
120
|
+
// ── 6. Key Abstractions ──────────────────────────────
|
|
121
|
+
const keyAbstractions = extractKeyAbstractions(graph, config, detail, focus);
|
|
122
|
+
// ── 7. Data Flow ─────────────────────────────────────
|
|
123
|
+
const dataFlow = inferDataFlow(modules, relations, entryPoints);
|
|
124
|
+
// ── 8. Conventions ───────────────────────────────────
|
|
125
|
+
const conventions = detectConventions(indexedFiles, rootPath, graph);
|
|
126
|
+
// ── 9. Directory Map ─────────────────────────────────
|
|
127
|
+
const maxDepth = detail === 'quick' ? 2 : detail === 'standard' ? 3 : 4;
|
|
128
|
+
const directoryMap = buildDirectoryTree(rootPath, maxDepth);
|
|
129
|
+
// ── 10. Quick Reference Card ─────────────────────────
|
|
130
|
+
const quickRef = buildQuickRef(projectName, projectType, languages, frameworks, modules, entryPoints, archPattern, stats);
|
|
131
|
+
// ── Build Result ─────────────────────────────────────
|
|
132
|
+
const result = {
|
|
133
|
+
projectName,
|
|
134
|
+
projectRoot: rootPath,
|
|
135
|
+
projectType,
|
|
136
|
+
description,
|
|
137
|
+
techStack: { languages, frameworks, buildTools, runtime },
|
|
138
|
+
architecture: { pattern: archPattern, modules, relations, layers },
|
|
139
|
+
entryPoints,
|
|
140
|
+
keyAbstractions,
|
|
141
|
+
dataFlow,
|
|
142
|
+
conventions,
|
|
143
|
+
directoryMap,
|
|
144
|
+
quickRef,
|
|
145
|
+
};
|
|
146
|
+
// If focus specified, add deep-dive on that area
|
|
147
|
+
if (focus) {
|
|
148
|
+
const focusModule = modules.find(m => m.name.toLowerCase().includes(focus.toLowerCase()) ||
|
|
149
|
+
m.purpose.toLowerCase().includes(focus.toLowerCase()));
|
|
150
|
+
if (focusModule) {
|
|
151
|
+
result.focusArea = buildModuleDeepDive(focusModule, graph, config);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Trim based on detail level
|
|
155
|
+
if (detail === 'quick') {
|
|
156
|
+
return mcpText(ok({
|
|
157
|
+
quickRef: result.quickRef,
|
|
158
|
+
directoryMap: result.directoryMap,
|
|
159
|
+
modules: result.architecture.modules.map(m => ({
|
|
160
|
+
name: m.name, purpose: m.purpose, fileCount: m.fileCount,
|
|
161
|
+
})),
|
|
162
|
+
entryPoints: result.entryPoints.slice(0, 5),
|
|
163
|
+
tokensSaved: `~${Math.round(stats.totalNodes * 15)} tokens saved`,
|
|
164
|
+
}, estimator));
|
|
165
|
+
}
|
|
166
|
+
if (detail === 'standard') {
|
|
167
|
+
// Standard: everything except deep abstractions
|
|
168
|
+
const standardResult = { ...result };
|
|
169
|
+
standardResult.keyAbstractions = {
|
|
170
|
+
classes: result.keyAbstractions.classes.slice(0, 10),
|
|
171
|
+
interfaces: result.keyAbstractions.interfaces.slice(0, 5),
|
|
172
|
+
functions: result.keyAbstractions.functions.slice(0, 10),
|
|
173
|
+
};
|
|
174
|
+
standardResult.architecture.relations = result.architecture.relations.slice(0, 20);
|
|
175
|
+
return mcpText(ok(standardResult, estimator));
|
|
176
|
+
}
|
|
177
|
+
// Deep: everything
|
|
178
|
+
return mcpText(ok(result, estimator));
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return mcpText(fail(`Project map failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ============================================================
|
|
186
|
+
// Helper Functions
|
|
187
|
+
// ============================================================
|
|
188
|
+
function detectProjectName(rootPath) {
|
|
189
|
+
// Try package.json first
|
|
190
|
+
try {
|
|
191
|
+
const pkg = JSON.parse(readFileSync(join(rootPath, 'package.json'), 'utf-8'));
|
|
192
|
+
if (pkg.name)
|
|
193
|
+
return pkg.name;
|
|
194
|
+
}
|
|
195
|
+
catch { /* ignore */ }
|
|
196
|
+
// Try .csproj
|
|
197
|
+
try {
|
|
198
|
+
const entries = readdirSync(rootPath);
|
|
199
|
+
const csproj = entries.find(e => e.endsWith('.csproj'));
|
|
200
|
+
if (csproj)
|
|
201
|
+
return csproj.replace('.csproj', '');
|
|
202
|
+
}
|
|
203
|
+
catch { /* ignore */ }
|
|
204
|
+
// Try Cargo.toml
|
|
205
|
+
try {
|
|
206
|
+
const cargo = readFileSync(join(rootPath, 'Cargo.toml'), 'utf-8');
|
|
207
|
+
const match = cargo.match(/name\s*=\s*"([^"]+)"/);
|
|
208
|
+
if (match)
|
|
209
|
+
return match[1];
|
|
210
|
+
}
|
|
211
|
+
catch { /* ignore */ }
|
|
212
|
+
// Try go.mod
|
|
213
|
+
try {
|
|
214
|
+
const gomod = readFileSync(join(rootPath, 'go.mod'), 'utf-8');
|
|
215
|
+
const match = gomod.match(/^module\s+(.+)$/m);
|
|
216
|
+
if (match)
|
|
217
|
+
return match[1].trim().split('/').pop();
|
|
218
|
+
}
|
|
219
|
+
catch { /* ignore */ }
|
|
220
|
+
return basename(rootPath);
|
|
221
|
+
}
|
|
222
|
+
function detectProjectType(rootPath, indexedFiles) {
|
|
223
|
+
const markers = [
|
|
224
|
+
[['package.json'], 'node'],
|
|
225
|
+
[['tsconfig.json'], 'typescript'],
|
|
226
|
+
[['*.csproj', '*.sln'], 'dotnet'],
|
|
227
|
+
[['Cargo.toml'], 'rust'],
|
|
228
|
+
[['go.mod'], 'go'],
|
|
229
|
+
[['requirements.txt', 'setup.py', 'pyproject.toml'], 'python'],
|
|
230
|
+
[['build.gradle', 'pom.xml'], 'java'],
|
|
231
|
+
[['Gemfile'], 'ruby'],
|
|
232
|
+
[['Package.swift'], 'swift'],
|
|
233
|
+
];
|
|
234
|
+
const types = [];
|
|
235
|
+
for (const [files, type] of markers) {
|
|
236
|
+
for (const f of files) {
|
|
237
|
+
if (f.includes('*')) {
|
|
238
|
+
try {
|
|
239
|
+
const ext = f.replace('*', '');
|
|
240
|
+
if (readdirSync(rootPath).some(e => e.endsWith(ext)))
|
|
241
|
+
types.push(type);
|
|
242
|
+
}
|
|
243
|
+
catch { /* ignore */ }
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
if (existsSync(join(rootPath, f)))
|
|
247
|
+
types.push(type);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Also detect from file extensions
|
|
252
|
+
const extCounts = {};
|
|
253
|
+
for (const f of indexedFiles) {
|
|
254
|
+
const ext = extname(f).toLowerCase();
|
|
255
|
+
extCounts[ext] = (extCounts[ext] || 0) + 1;
|
|
256
|
+
}
|
|
257
|
+
if (extCounts['.xaml'] && extCounts['.cs'])
|
|
258
|
+
types.push('wpf');
|
|
259
|
+
if (extCounts['.razor'] || extCounts['.cshtml'])
|
|
260
|
+
types.push('asp.net');
|
|
261
|
+
return [...new Set(types)].join('+') || 'unknown';
|
|
262
|
+
}
|
|
263
|
+
function generateProjectDescription(rootPath, projectType, stats) {
|
|
264
|
+
// Try to read README or description from package.json
|
|
265
|
+
try {
|
|
266
|
+
const pkg = JSON.parse(readFileSync(join(rootPath, 'package.json'), 'utf-8'));
|
|
267
|
+
if (pkg.description)
|
|
268
|
+
return pkg.description;
|
|
269
|
+
}
|
|
270
|
+
catch { /* ignore */ }
|
|
271
|
+
try {
|
|
272
|
+
const readme = readFileSync(join(rootPath, 'README.md'), 'utf-8');
|
|
273
|
+
// Get first paragraph
|
|
274
|
+
const firstPara = readme.split('\n\n')[1]?.trim();
|
|
275
|
+
if (firstPara && firstPara.length < 200)
|
|
276
|
+
return firstPara;
|
|
277
|
+
}
|
|
278
|
+
catch { /* ignore */ }
|
|
279
|
+
return `${projectType} project with ${stats.totalFiles} files and ${stats.totalNodes} symbols`;
|
|
280
|
+
}
|
|
281
|
+
function detectFrameworks(rootPath) {
|
|
282
|
+
const found = [];
|
|
283
|
+
const deps = readDeps(rootPath);
|
|
284
|
+
for (const fw of FRAMEWORK_MARKERS) {
|
|
285
|
+
// Check marker files
|
|
286
|
+
if (fw.files.some(f => existsSync(join(rootPath, f)))) {
|
|
287
|
+
found.push(fw.name);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
// Check dependencies
|
|
291
|
+
if (fw.deps.some(d => deps.has(d))) {
|
|
292
|
+
found.push(fw.name);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return found;
|
|
296
|
+
}
|
|
297
|
+
function detectBuildTools(rootPath) {
|
|
298
|
+
const found = [];
|
|
299
|
+
for (const bt of BUILD_TOOL_MARKERS) {
|
|
300
|
+
if (bt.files.some(f => existsSync(join(rootPath, f)))) {
|
|
301
|
+
found.push(bt.name);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return found;
|
|
305
|
+
}
|
|
306
|
+
function detectRuntime(rootPath) {
|
|
307
|
+
if (existsSync(join(rootPath, 'package.json'))) {
|
|
308
|
+
try {
|
|
309
|
+
const pkg = JSON.parse(readFileSync(join(rootPath, 'package.json'), 'utf-8'));
|
|
310
|
+
if (pkg.engines?.node)
|
|
311
|
+
return `Node.js ${pkg.engines.node}`;
|
|
312
|
+
}
|
|
313
|
+
catch { /* ignore */ }
|
|
314
|
+
return 'Node.js';
|
|
315
|
+
}
|
|
316
|
+
if (existsSync(join(rootPath, 'go.mod')))
|
|
317
|
+
return 'Go';
|
|
318
|
+
if (existsSync(join(rootPath, 'Cargo.toml')))
|
|
319
|
+
return 'Rust';
|
|
320
|
+
try {
|
|
321
|
+
if (readdirSync(rootPath).some(e => e.endsWith('.csproj') || e.endsWith('.sln')))
|
|
322
|
+
return '.NET';
|
|
323
|
+
}
|
|
324
|
+
catch { /* ignore */ }
|
|
325
|
+
if (existsSync(join(rootPath, 'requirements.txt')))
|
|
326
|
+
return 'Python';
|
|
327
|
+
return 'unknown';
|
|
328
|
+
}
|
|
329
|
+
function readDeps(rootPath) {
|
|
330
|
+
const deps = new Set();
|
|
331
|
+
try {
|
|
332
|
+
const pkg = JSON.parse(readFileSync(join(rootPath, 'package.json'), 'utf-8'));
|
|
333
|
+
for (const d of Object.keys(pkg.dependencies || {}))
|
|
334
|
+
deps.add(d);
|
|
335
|
+
for (const d of Object.keys(pkg.devDependencies || {}))
|
|
336
|
+
deps.add(d);
|
|
337
|
+
}
|
|
338
|
+
catch { /* ignore */ }
|
|
339
|
+
return deps;
|
|
340
|
+
}
|
|
341
|
+
// ── Module Discovery ────────────────────────────────────────
|
|
342
|
+
function discoverModules(rootPath, indexedFiles, graph, config) {
|
|
343
|
+
// Group files by top-level directory
|
|
344
|
+
const dirGroups = new Map();
|
|
345
|
+
for (const f of indexedFiles) {
|
|
346
|
+
const rel = relative(rootPath, f).replace(/\\/g, '/');
|
|
347
|
+
const parts = rel.split('/');
|
|
348
|
+
// Use top 2 levels as module boundary
|
|
349
|
+
const moduleDir = parts.length > 1 ? parts[0] : '.';
|
|
350
|
+
if (!dirGroups.has(moduleDir))
|
|
351
|
+
dirGroups.set(moduleDir, []);
|
|
352
|
+
dirGroups.get(moduleDir).push(f);
|
|
353
|
+
}
|
|
354
|
+
const modules = [];
|
|
355
|
+
for (const [dirName, files] of dirGroups) {
|
|
356
|
+
if (dirName === '.' && files.length < 3)
|
|
357
|
+
continue;
|
|
358
|
+
// Get public symbols from this module
|
|
359
|
+
const publicSymbols = [];
|
|
360
|
+
const allSymbols = new Set();
|
|
361
|
+
for (const f of files.slice(0, 50)) { // Cap to prevent slowness
|
|
362
|
+
const nodes = graph.getNodesForFile(f);
|
|
363
|
+
for (const n of nodes) {
|
|
364
|
+
allSymbols.add(n.id);
|
|
365
|
+
if (n.isExported || n.visibility === 'public') {
|
|
366
|
+
publicSymbols.push(n.qualifiedName || n.name);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Find key files (entry points, index files, etc.)
|
|
371
|
+
const keyFiles = files
|
|
372
|
+
.filter(f => {
|
|
373
|
+
const name = basename(f).toLowerCase();
|
|
374
|
+
return name.startsWith('index') || name.startsWith('main') ||
|
|
375
|
+
name.startsWith('app') || name.startsWith('mod') ||
|
|
376
|
+
name === 'lib.rs' || name === 'init.py' || name === '__init__.py';
|
|
377
|
+
})
|
|
378
|
+
.map(f => relative(rootPath, f).replace(/\\/g, '/'))
|
|
379
|
+
.slice(0, 5);
|
|
380
|
+
const purpose = inferModulePurpose(dirName, files, graph);
|
|
381
|
+
modules.push({
|
|
382
|
+
name: dirName === '.' ? basename(rootPath) : dirName,
|
|
383
|
+
path: dirName,
|
|
384
|
+
purpose,
|
|
385
|
+
fileCount: files.length,
|
|
386
|
+
publicSymbols: publicSymbols.slice(0, 15),
|
|
387
|
+
dependencies: [], // filled in by discoverRelations
|
|
388
|
+
keyFiles,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
// Sort by file count desc
|
|
392
|
+
return modules.sort((a, b) => b.fileCount - a.fileCount);
|
|
393
|
+
}
|
|
394
|
+
function inferModulePurpose(dirName, files, graph) {
|
|
395
|
+
const name = dirName.toLowerCase();
|
|
396
|
+
// Common directory name patterns
|
|
397
|
+
const purposes = {
|
|
398
|
+
'src': 'Source code',
|
|
399
|
+
'lib': 'Library/shared code',
|
|
400
|
+
'api': 'API endpoints/routes',
|
|
401
|
+
'routes': 'URL routing',
|
|
402
|
+
'controllers': 'Request handlers (MVC controllers)',
|
|
403
|
+
'models': 'Data models/entities',
|
|
404
|
+
'views': 'UI views/templates',
|
|
405
|
+
'components': 'UI components',
|
|
406
|
+
'pages': 'Page-level components',
|
|
407
|
+
'utils': 'Utility/helper functions',
|
|
408
|
+
'helpers': 'Helper functions',
|
|
409
|
+
'services': 'Business logic services',
|
|
410
|
+
'middleware': 'Middleware/interceptors',
|
|
411
|
+
'hooks': 'React/framework hooks',
|
|
412
|
+
'store': 'State management',
|
|
413
|
+
'stores': 'State management stores',
|
|
414
|
+
'reducers': 'Redux reducers',
|
|
415
|
+
'actions': 'Redux/state actions',
|
|
416
|
+
'types': 'Type definitions',
|
|
417
|
+
'interfaces': 'Interface definitions',
|
|
418
|
+
'config': 'Configuration',
|
|
419
|
+
'constants': 'Constants/enums',
|
|
420
|
+
'tests': 'Test files',
|
|
421
|
+
'test': 'Test files',
|
|
422
|
+
'__tests__': 'Test files',
|
|
423
|
+
'spec': 'Test specifications',
|
|
424
|
+
'styles': 'Stylesheets',
|
|
425
|
+
'css': 'Stylesheets',
|
|
426
|
+
'assets': 'Static assets',
|
|
427
|
+
'public': 'Public static files',
|
|
428
|
+
'static': 'Static files',
|
|
429
|
+
'migrations': 'Database migrations',
|
|
430
|
+
'schemas': 'Data schemas',
|
|
431
|
+
'db': 'Database layer',
|
|
432
|
+
'database': 'Database layer',
|
|
433
|
+
'auth': 'Authentication/authorization',
|
|
434
|
+
'security': 'Security module',
|
|
435
|
+
'i18n': 'Internationalization',
|
|
436
|
+
'locales': 'Localization files',
|
|
437
|
+
'scripts': 'Build/utility scripts',
|
|
438
|
+
'tools': 'Developer tools',
|
|
439
|
+
'plugins': 'Plugin/extension system',
|
|
440
|
+
'core': 'Core business logic',
|
|
441
|
+
'domain': 'Domain model',
|
|
442
|
+
'infra': 'Infrastructure layer',
|
|
443
|
+
'infrastructure': 'Infrastructure layer',
|
|
444
|
+
'common': 'Shared/common code',
|
|
445
|
+
'shared': 'Shared across modules',
|
|
446
|
+
'features': 'Feature modules',
|
|
447
|
+
'modules': 'Application modules',
|
|
448
|
+
'handlers': 'Event/request handlers',
|
|
449
|
+
'workers': 'Background workers',
|
|
450
|
+
'jobs': 'Background jobs',
|
|
451
|
+
'queues': 'Message queues',
|
|
452
|
+
'events': 'Event system',
|
|
453
|
+
'commands': 'CLI commands',
|
|
454
|
+
'cli': 'Command-line interface',
|
|
455
|
+
'docs': 'Documentation',
|
|
456
|
+
'knowledge-graph': 'Knowledge graph/data model',
|
|
457
|
+
'parsers': 'Code/data parsers',
|
|
458
|
+
};
|
|
459
|
+
if (purposes[name])
|
|
460
|
+
return purposes[name];
|
|
461
|
+
// Try to infer from file contents
|
|
462
|
+
const exts = new Set(files.map(f => extname(f).toLowerCase()));
|
|
463
|
+
if (exts.has('.test.ts') || exts.has('.spec.ts') || exts.has('.test.js'))
|
|
464
|
+
return 'Test files';
|
|
465
|
+
if (exts.has('.css') || exts.has('.scss') || exts.has('.less'))
|
|
466
|
+
return 'Stylesheets';
|
|
467
|
+
if (exts.has('.html') || exts.has('.htm'))
|
|
468
|
+
return 'HTML templates';
|
|
469
|
+
// Try from symbol types
|
|
470
|
+
let classCount = 0, funcCount = 0, interfaceCount = 0;
|
|
471
|
+
for (const f of files.slice(0, 10)) {
|
|
472
|
+
for (const n of graph.getNodesForFile(f)) {
|
|
473
|
+
if (n.type === 'class')
|
|
474
|
+
classCount++;
|
|
475
|
+
if (n.type === 'function')
|
|
476
|
+
funcCount++;
|
|
477
|
+
if (n.type === 'interface')
|
|
478
|
+
interfaceCount++;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (interfaceCount > classCount)
|
|
482
|
+
return 'Type definitions/interfaces';
|
|
483
|
+
if (classCount > funcCount * 2)
|
|
484
|
+
return 'Class-based module';
|
|
485
|
+
return `Module (${files.length} files)`;
|
|
486
|
+
}
|
|
487
|
+
// ── Relations ───────────────────────────────────────────────
|
|
488
|
+
function discoverRelations(modules, graph, config) {
|
|
489
|
+
const relations = [];
|
|
490
|
+
const moduleByFile = new Map();
|
|
491
|
+
// Map each file to its module
|
|
492
|
+
for (const mod of modules) {
|
|
493
|
+
const rootPath = config.projectRoot;
|
|
494
|
+
const indexedFiles = graph.getIndexedFiles();
|
|
495
|
+
for (const f of indexedFiles) {
|
|
496
|
+
const rel = relative(rootPath, f).replace(/\\/g, '/');
|
|
497
|
+
const parts = rel.split('/');
|
|
498
|
+
const moduleDir = parts.length > 1 ? parts[0] : '.';
|
|
499
|
+
const modName = moduleDir === '.' ? basename(rootPath) : moduleDir;
|
|
500
|
+
moduleByFile.set(f, modName);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
// Count cross-module edges
|
|
504
|
+
const crossRefs = new Map();
|
|
505
|
+
const allNodes = graph.getAllNodes();
|
|
506
|
+
// Sample nodes (cap at 500 to prevent slowness)
|
|
507
|
+
const sampledNodes = allNodes.length > 500
|
|
508
|
+
? allNodes.filter((_, i) => i % Math.ceil(allNodes.length / 500) === 0)
|
|
509
|
+
: allNodes;
|
|
510
|
+
for (const node of sampledNodes) {
|
|
511
|
+
const fromModule = moduleByFile.get(node.filePath);
|
|
512
|
+
if (!fromModule)
|
|
513
|
+
continue;
|
|
514
|
+
const outEdges = graph.getOutEdges(node.id);
|
|
515
|
+
for (const edge of outEdges) {
|
|
516
|
+
const targetNode = graph.getNode(edge.targetId);
|
|
517
|
+
if (!targetNode)
|
|
518
|
+
continue;
|
|
519
|
+
const toModule = moduleByFile.get(targetNode.filePath);
|
|
520
|
+
if (!toModule || toModule === fromModule)
|
|
521
|
+
continue;
|
|
522
|
+
const key = `${fromModule}->${toModule}`;
|
|
523
|
+
if (!crossRefs.has(key))
|
|
524
|
+
crossRefs.set(key, { count: 0, types: new Set() });
|
|
525
|
+
const ref = crossRefs.get(key);
|
|
526
|
+
ref.count++;
|
|
527
|
+
ref.types.add(edge.type);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Convert to relations
|
|
531
|
+
for (const [key, ref] of crossRefs) {
|
|
532
|
+
const [from, to] = key.split('->');
|
|
533
|
+
const type = ref.types.has('calls') ? 'calls'
|
|
534
|
+
: ref.types.has('extends') ? 'extends'
|
|
535
|
+
: ref.types.has('implements') ? 'implements'
|
|
536
|
+
: 'imports';
|
|
537
|
+
relations.push({ from, to, strength: ref.count, type });
|
|
538
|
+
}
|
|
539
|
+
// Update module dependencies
|
|
540
|
+
for (const rel of relations) {
|
|
541
|
+
const mod = modules.find(m => m.name === rel.from);
|
|
542
|
+
if (mod && !mod.dependencies.includes(rel.to)) {
|
|
543
|
+
mod.dependencies.push(rel.to);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
return relations.sort((a, b) => b.strength - a.strength);
|
|
547
|
+
}
|
|
548
|
+
// ── Architecture Detection ──────────────────────────────────
|
|
549
|
+
function detectArchPattern(modules, indexedFiles, rootPath) {
|
|
550
|
+
const moduleNames = new Set(modules.map(m => m.name.toLowerCase()));
|
|
551
|
+
// MVC
|
|
552
|
+
if (moduleNames.has('models') && moduleNames.has('views') && moduleNames.has('controllers'))
|
|
553
|
+
return 'MVC (Model-View-Controller)';
|
|
554
|
+
// MVVM
|
|
555
|
+
if (moduleNames.has('models') && moduleNames.has('views') && moduleNames.has('viewmodels'))
|
|
556
|
+
return 'MVVM (Model-View-ViewModel)';
|
|
557
|
+
// Clean Architecture
|
|
558
|
+
if (moduleNames.has('domain') && moduleNames.has('infrastructure') && moduleNames.has('application'))
|
|
559
|
+
return 'Clean Architecture';
|
|
560
|
+
// Layered
|
|
561
|
+
if (moduleNames.has('api') && moduleNames.has('services') && moduleNames.has('data'))
|
|
562
|
+
return 'Layered Architecture';
|
|
563
|
+
// Feature-based
|
|
564
|
+
if (moduleNames.has('features') || moduleNames.has('modules'))
|
|
565
|
+
return 'Feature-based Architecture';
|
|
566
|
+
// Component-based (frontend)
|
|
567
|
+
if (moduleNames.has('components') && (moduleNames.has('pages') || moduleNames.has('views')))
|
|
568
|
+
return 'Component-based Architecture';
|
|
569
|
+
// Simple (flat)
|
|
570
|
+
if (modules.length <= 2)
|
|
571
|
+
return 'Simple/Flat Architecture';
|
|
572
|
+
// Library
|
|
573
|
+
if (moduleNames.has('lib') && modules.length <= 4)
|
|
574
|
+
return 'Library Architecture';
|
|
575
|
+
return 'Modular Architecture';
|
|
576
|
+
}
|
|
577
|
+
function detectLayers(modules, indexedFiles, rootPath) {
|
|
578
|
+
const layers = [];
|
|
579
|
+
const moduleNames = modules.map(m => m.name.toLowerCase());
|
|
580
|
+
// Presentation layer
|
|
581
|
+
const presentationModules = modules.filter(m => {
|
|
582
|
+
const n = m.name.toLowerCase();
|
|
583
|
+
return ['views', 'components', 'pages', 'ui', 'templates', 'layouts', 'screens'].includes(n);
|
|
584
|
+
});
|
|
585
|
+
if (presentationModules.length > 0) {
|
|
586
|
+
layers.push({
|
|
587
|
+
name: 'Presentation',
|
|
588
|
+
modules: presentationModules.map(m => m.name),
|
|
589
|
+
description: 'UI components, pages, and templates',
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
// Application/Business layer
|
|
593
|
+
const businessModules = modules.filter(m => {
|
|
594
|
+
const n = m.name.toLowerCase();
|
|
595
|
+
return ['services', 'controllers', 'handlers', 'core', 'domain', 'application', 'features'].includes(n);
|
|
596
|
+
});
|
|
597
|
+
if (businessModules.length > 0) {
|
|
598
|
+
layers.push({
|
|
599
|
+
name: 'Business Logic',
|
|
600
|
+
modules: businessModules.map(m => m.name),
|
|
601
|
+
description: 'Core business rules, services, and controllers',
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
// Data/Infrastructure layer
|
|
605
|
+
const dataModules = modules.filter(m => {
|
|
606
|
+
const n = m.name.toLowerCase();
|
|
607
|
+
return ['models', 'data', 'db', 'database', 'repositories', 'infra', 'infrastructure', 'migrations'].includes(n);
|
|
608
|
+
});
|
|
609
|
+
if (dataModules.length > 0) {
|
|
610
|
+
layers.push({
|
|
611
|
+
name: 'Data/Infrastructure',
|
|
612
|
+
modules: dataModules.map(m => m.name),
|
|
613
|
+
description: 'Data models, database, and external integrations',
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
// Shared/Utils layer
|
|
617
|
+
const sharedModules = modules.filter(m => {
|
|
618
|
+
const n = m.name.toLowerCase();
|
|
619
|
+
return ['utils', 'helpers', 'common', 'shared', 'lib', 'types', 'config', 'constants'].includes(n);
|
|
620
|
+
});
|
|
621
|
+
if (sharedModules.length > 0) {
|
|
622
|
+
layers.push({
|
|
623
|
+
name: 'Shared/Utilities',
|
|
624
|
+
modules: sharedModules.map(m => m.name),
|
|
625
|
+
description: 'Shared utilities, type definitions, and configuration',
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
return layers;
|
|
629
|
+
}
|
|
630
|
+
// ── Entry Points ────────────────────────────────────────────
|
|
631
|
+
function detectEntryPoints(indexedFiles, rootPath) {
|
|
632
|
+
const entryPoints = [];
|
|
633
|
+
const seen = new Set();
|
|
634
|
+
const patterns = [
|
|
635
|
+
[/[/\\]index\.(ts|js|tsx|jsx|mjs)$/, 'main', 'Application entry point'],
|
|
636
|
+
[/[/\\]main\.(ts|js|py|go|rs|java)$/, 'main', 'Application main file'],
|
|
637
|
+
[/[/\\]app\.(ts|js|tsx|jsx|py)$/, 'app', 'Application bootstrap'],
|
|
638
|
+
[/[/\\]App\.(tsx|jsx|vue|svelte)$/, 'app', 'Root application component'],
|
|
639
|
+
[/[/\\]server\.(ts|js|py)$/, 'server', 'Server entry point'],
|
|
640
|
+
[/[/\\]Program\.cs$/, 'main', '.NET program entry point'],
|
|
641
|
+
[/[/\\]Startup\.cs$/, 'startup', '.NET startup configuration'],
|
|
642
|
+
[/[/\\]App\.xaml\.cs$/, 'app', 'WPF application entry point'],
|
|
643
|
+
[/[/\\]MainWindow\.xaml\.cs$/, 'ui', 'Main window (WPF)'],
|
|
644
|
+
[/[/\\]manage\.py$/, 'cli', 'Django management script'],
|
|
645
|
+
[/[/\\]wsgi\.py$/, 'server', 'WSGI server entry'],
|
|
646
|
+
[/[/\\]asgi\.py$/, 'server', 'ASGI server entry'],
|
|
647
|
+
[/[/\\]lib\.rs$/, 'lib', 'Rust library crate root'],
|
|
648
|
+
[/[/\\]mod\.rs$/, 'module', 'Rust module root'],
|
|
649
|
+
[/[/\\]cmd[/\\].*\.go$/, 'cli', 'Go CLI command'],
|
|
650
|
+
[/[/\\]routes?\.(ts|js|py)$/i, 'routing', 'Route definitions'],
|
|
651
|
+
[/[/\\]api[/\\].*\.(ts|js|py)$/, 'api', 'API endpoint'],
|
|
652
|
+
];
|
|
653
|
+
for (const f of indexedFiles) {
|
|
654
|
+
for (const [pattern, type, desc] of patterns) {
|
|
655
|
+
if (pattern.test(f) && !seen.has(f)) {
|
|
656
|
+
seen.add(f);
|
|
657
|
+
entryPoints.push({
|
|
658
|
+
file: relative(rootPath, f).replace(/\\/g, '/'),
|
|
659
|
+
type,
|
|
660
|
+
description: desc,
|
|
661
|
+
});
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return entryPoints.slice(0, 15);
|
|
667
|
+
}
|
|
668
|
+
// ── Key Abstractions ────────────────────────────────────────
|
|
669
|
+
function extractKeyAbstractions(graph, config, detail, focus) {
|
|
670
|
+
const allNodes = graph.getAllNodes();
|
|
671
|
+
const limit = detail === 'deep' ? 20 : detail === 'standard' ? 10 : 5;
|
|
672
|
+
// Classes: sorted by edge count (most connected = most important)
|
|
673
|
+
const classes = allNodes
|
|
674
|
+
.filter(n => n.type === 'class' && (n.isExported || n.visibility === 'public'))
|
|
675
|
+
.map(n => {
|
|
676
|
+
const methods = allNodes.filter(m => m.type === 'method' && m.filePath === n.filePath &&
|
|
677
|
+
m.startLine >= n.startLine && m.endLine <= n.endLine).length;
|
|
678
|
+
return {
|
|
679
|
+
name: n.qualifiedName || n.name,
|
|
680
|
+
file: relative(config.projectRoot, n.filePath).replace(/\\/g, '/'),
|
|
681
|
+
methods,
|
|
682
|
+
description: n.docComment?.substring(0, 80) || `${n.name} class (${methods} methods)`,
|
|
683
|
+
};
|
|
684
|
+
})
|
|
685
|
+
.sort((a, b) => b.methods - a.methods)
|
|
686
|
+
.slice(0, limit);
|
|
687
|
+
// Interfaces
|
|
688
|
+
const interfaces = allNodes
|
|
689
|
+
.filter(n => n.type === 'interface' && (n.isExported || n.visibility === 'public'))
|
|
690
|
+
.map(n => ({
|
|
691
|
+
name: n.qualifiedName || n.name,
|
|
692
|
+
file: relative(config.projectRoot, n.filePath).replace(/\\/g, '/'),
|
|
693
|
+
description: n.docComment?.substring(0, 80) || `${n.name} interface`,
|
|
694
|
+
}))
|
|
695
|
+
.slice(0, limit);
|
|
696
|
+
// Top functions (exported, not methods)
|
|
697
|
+
const functions = allNodes
|
|
698
|
+
.filter(n => n.type === 'function' && (n.isExported || n.visibility === 'public'))
|
|
699
|
+
.map(n => ({
|
|
700
|
+
name: n.qualifiedName || n.name,
|
|
701
|
+
file: relative(config.projectRoot, n.filePath).replace(/\\/g, '/'),
|
|
702
|
+
signature: n.signature?.substring(0, 120) || n.name,
|
|
703
|
+
}))
|
|
704
|
+
.slice(0, limit);
|
|
705
|
+
return { classes, interfaces, functions };
|
|
706
|
+
}
|
|
707
|
+
// ── Data Flow ───────────────────────────────────────────────
|
|
708
|
+
function inferDataFlow(modules, relations, entryPoints) {
|
|
709
|
+
const flows = [];
|
|
710
|
+
// Find entry -> service -> data flows
|
|
711
|
+
const entryModules = new Set(entryPoints.map(e => {
|
|
712
|
+
const parts = e.file.split('/');
|
|
713
|
+
return parts.length > 1 ? parts[0] : '.';
|
|
714
|
+
}));
|
|
715
|
+
for (const rel of relations.slice(0, 10)) {
|
|
716
|
+
if (entryModules.has(rel.from)) {
|
|
717
|
+
flows.push(`${rel.from} → ${rel.to} (${rel.type}, ${rel.strength} refs)`);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
// Build chain: entry -> business -> data
|
|
721
|
+
if (flows.length === 0 && relations.length > 0) {
|
|
722
|
+
for (const rel of relations.slice(0, 5)) {
|
|
723
|
+
flows.push(`${rel.from} → ${rel.to} (${rel.type})`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (flows.length === 0) {
|
|
727
|
+
flows.push('(Run mindmap_reindex first for data flow analysis)');
|
|
728
|
+
}
|
|
729
|
+
return flows;
|
|
730
|
+
}
|
|
731
|
+
// ── Conventions ─────────────────────────────────────────────
|
|
732
|
+
function detectConventions(indexedFiles, rootPath, graph) {
|
|
733
|
+
const conventions = [];
|
|
734
|
+
// File naming convention
|
|
735
|
+
const names = indexedFiles.map(f => basename(f));
|
|
736
|
+
const camelCase = names.filter(n => /^[a-z][a-zA-Z]+\.\w+$/.test(n)).length;
|
|
737
|
+
const kebabCase = names.filter(n => /^[a-z]+-[a-z][\w-]*\.\w+$/.test(n)).length;
|
|
738
|
+
const pascalCase = names.filter(n => /^[A-Z][a-zA-Z]+\.\w+$/.test(n)).length;
|
|
739
|
+
const snakeCase = names.filter(n => /^[a-z]+_[a-z][\w_]*\.\w+$/.test(n)).length;
|
|
740
|
+
const max = Math.max(camelCase, kebabCase, pascalCase, snakeCase);
|
|
741
|
+
if (max > names.length * 0.3) {
|
|
742
|
+
if (camelCase === max)
|
|
743
|
+
conventions.push('File naming: camelCase');
|
|
744
|
+
if (kebabCase === max)
|
|
745
|
+
conventions.push('File naming: kebab-case');
|
|
746
|
+
if (pascalCase === max)
|
|
747
|
+
conventions.push('File naming: PascalCase');
|
|
748
|
+
if (snakeCase === max)
|
|
749
|
+
conventions.push('File naming: snake_case');
|
|
750
|
+
}
|
|
751
|
+
// Module style
|
|
752
|
+
if (existsSync(join(rootPath, 'tsconfig.json')))
|
|
753
|
+
conventions.push('TypeScript strict mode');
|
|
754
|
+
if (existsSync(join(rootPath, '.eslintrc.js')) || existsSync(join(rootPath, '.eslintrc.json')) || existsSync(join(rootPath, 'eslint.config.js')))
|
|
755
|
+
conventions.push('ESLint enforced');
|
|
756
|
+
if (existsSync(join(rootPath, '.prettierrc')) || existsSync(join(rootPath, '.prettierrc.json')))
|
|
757
|
+
conventions.push('Prettier formatting');
|
|
758
|
+
if (existsSync(join(rootPath, '.editorconfig')))
|
|
759
|
+
conventions.push('EditorConfig standards');
|
|
760
|
+
// Check for test patterns
|
|
761
|
+
const testFiles = indexedFiles.filter(f => f.includes('.test.') || f.includes('.spec.') || f.includes('__test'));
|
|
762
|
+
if (testFiles.length > 0) {
|
|
763
|
+
const ratio = Math.round((testFiles.length / indexedFiles.length) * 100);
|
|
764
|
+
conventions.push(`Test coverage: ${testFiles.length} test files (${ratio}%)`);
|
|
765
|
+
}
|
|
766
|
+
// Export style
|
|
767
|
+
const nodes = graph.getAllNodes();
|
|
768
|
+
const exportedCount = nodes.filter(n => n.isExported).length;
|
|
769
|
+
if (exportedCount > 0) {
|
|
770
|
+
const exportRatio = Math.round((exportedCount / nodes.length) * 100);
|
|
771
|
+
conventions.push(`Export ratio: ${exportRatio}% of symbols are exported`);
|
|
772
|
+
}
|
|
773
|
+
return conventions;
|
|
774
|
+
}
|
|
775
|
+
// ── Directory Tree ──────────────────────────────────────────
|
|
776
|
+
function buildDirectoryTree(rootPath, maxDepth) {
|
|
777
|
+
const lines = [];
|
|
778
|
+
const rootName = basename(rootPath);
|
|
779
|
+
lines.push(`${rootName}/`);
|
|
780
|
+
buildTreeRecursive(rootPath, '', maxDepth, 0, lines);
|
|
781
|
+
return lines.join('\n');
|
|
782
|
+
}
|
|
783
|
+
function buildTreeRecursive(dir, prefix, maxDepth, depth, lines) {
|
|
784
|
+
if (depth >= maxDepth)
|
|
785
|
+
return;
|
|
786
|
+
let entries;
|
|
787
|
+
try {
|
|
788
|
+
entries = readdirSync(dir, { withFileTypes: true })
|
|
789
|
+
.filter(e => !e.name.startsWith('.') && !SKIP_DIRS.has(e.name))
|
|
790
|
+
.map(e => ({ name: e.name, isDir: e.isDirectory() }))
|
|
791
|
+
.sort((a, b) => {
|
|
792
|
+
if (a.isDir !== b.isDir)
|
|
793
|
+
return a.isDir ? -1 : 1;
|
|
794
|
+
return a.name.localeCompare(b.name);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
// Cap entries to prevent huge trees
|
|
801
|
+
const maxEntries = depth === 0 ? 30 : 15;
|
|
802
|
+
const shown = entries.slice(0, maxEntries);
|
|
803
|
+
const hidden = entries.length - shown.length;
|
|
804
|
+
for (let i = 0; i < shown.length; i++) {
|
|
805
|
+
const entry = shown[i];
|
|
806
|
+
const isLast = i === shown.length - 1 && hidden === 0;
|
|
807
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
808
|
+
const nextPrefix = isLast ? ' ' : '│ ';
|
|
809
|
+
if (entry.isDir) {
|
|
810
|
+
// Count files in this dir
|
|
811
|
+
let fileCount = 0;
|
|
812
|
+
try {
|
|
813
|
+
fileCount = readdirSync(join(dir, entry.name)).filter(e => !e.startsWith('.')).length;
|
|
814
|
+
}
|
|
815
|
+
catch { /* ignore */ }
|
|
816
|
+
lines.push(`${prefix}${connector}${entry.name}/ (${fileCount})`);
|
|
817
|
+
buildTreeRecursive(join(dir, entry.name), prefix + nextPrefix, maxDepth, depth + 1, lines);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
lines.push(`${prefix}${connector}${entry.name}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
if (hidden > 0) {
|
|
824
|
+
lines.push(`${prefix}└── ... +${hidden} more`);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
// ── Quick Reference Card ────────────────────────────────────
|
|
828
|
+
function buildQuickRef(name, type, languages, frameworks, modules, entryPoints, archPattern, stats) {
|
|
829
|
+
const lines = [];
|
|
830
|
+
lines.push(`═══ ${name} ═══`);
|
|
831
|
+
lines.push(`Type: ${type} | Architecture: ${archPattern}`);
|
|
832
|
+
lines.push(`Stack: ${languages.map(l => `${l.name}(${l.percentage}%)`).join(', ')}`);
|
|
833
|
+
if (frameworks.length > 0)
|
|
834
|
+
lines.push(`Frameworks: ${frameworks.join(', ')}`);
|
|
835
|
+
lines.push(`Size: ${stats.totalFiles} files, ${stats.totalNodes} symbols, ${stats.totalEdges} relationships`);
|
|
836
|
+
lines.push('');
|
|
837
|
+
lines.push('Modules:');
|
|
838
|
+
for (const m of modules.slice(0, 10)) {
|
|
839
|
+
const deps = m.dependencies.length > 0 ? ` → ${m.dependencies.slice(0, 3).join(', ')}` : '';
|
|
840
|
+
lines.push(` ${m.name}/ (${m.fileCount} files) — ${m.purpose}${deps}`);
|
|
841
|
+
}
|
|
842
|
+
lines.push('');
|
|
843
|
+
if (entryPoints.length > 0) {
|
|
844
|
+
lines.push('Entry Points:');
|
|
845
|
+
for (const ep of entryPoints.slice(0, 5)) {
|
|
846
|
+
lines.push(` ${ep.file} (${ep.type})`);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return lines.join('\n');
|
|
850
|
+
}
|
|
851
|
+
// ── Module Deep Dive ────────────────────────────────────────
|
|
852
|
+
function buildModuleDeepDive(mod, graph, config) {
|
|
853
|
+
const indexedFiles = graph.getIndexedFiles();
|
|
854
|
+
const moduleFiles = indexedFiles.filter(f => {
|
|
855
|
+
const rel = relative(config.projectRoot, f).replace(/\\/g, '/');
|
|
856
|
+
return rel.startsWith(mod.path + '/') || (mod.path === '.' && !rel.includes('/'));
|
|
857
|
+
});
|
|
858
|
+
const allSymbols = [];
|
|
859
|
+
for (const f of moduleFiles.slice(0, 30)) {
|
|
860
|
+
for (const n of graph.getNodesForFile(f)) {
|
|
861
|
+
allSymbols.push({
|
|
862
|
+
name: n.qualifiedName || n.name,
|
|
863
|
+
type: n.type,
|
|
864
|
+
file: relative(config.projectRoot, n.filePath).replace(/\\/g, '/'),
|
|
865
|
+
exported: n.isExported || n.visibility === 'public',
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
module: mod.name,
|
|
871
|
+
files: moduleFiles.map(f => relative(config.projectRoot, f).replace(/\\/g, '/')),
|
|
872
|
+
symbols: allSymbols,
|
|
873
|
+
exportedSymbols: allSymbols.filter(s => s.exported),
|
|
874
|
+
internalSymbols: allSymbols.filter(s => !s.exported).length,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
//# sourceMappingURL=project-map-tool.js.map
|