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.
@@ -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