musubi-sdd 3.10.0 → 5.1.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.
Files changed (44) hide show
  1. package/README.md +24 -19
  2. package/package.json +1 -1
  3. package/src/agents/agent-loop.js +532 -0
  4. package/src/agents/agentic/code-generator.js +767 -0
  5. package/src/agents/agentic/code-reviewer.js +698 -0
  6. package/src/agents/agentic/index.js +43 -0
  7. package/src/agents/function-tool.js +432 -0
  8. package/src/agents/index.js +45 -0
  9. package/src/agents/schema-generator.js +514 -0
  10. package/src/analyzers/ast-extractor.js +870 -0
  11. package/src/analyzers/context-optimizer.js +681 -0
  12. package/src/analyzers/repository-map.js +692 -0
  13. package/src/integrations/index.js +7 -1
  14. package/src/integrations/mcp/index.js +175 -0
  15. package/src/integrations/mcp/mcp-context-provider.js +472 -0
  16. package/src/integrations/mcp/mcp-discovery.js +436 -0
  17. package/src/integrations/mcp/mcp-tool-registry.js +467 -0
  18. package/src/integrations/mcp-connector.js +818 -0
  19. package/src/integrations/tool-discovery.js +589 -0
  20. package/src/managers/index.js +7 -0
  21. package/src/managers/skill-tools.js +565 -0
  22. package/src/monitoring/cost-tracker.js +7 -0
  23. package/src/monitoring/incident-manager.js +10 -0
  24. package/src/monitoring/observability.js +10 -0
  25. package/src/monitoring/quality-dashboard.js +491 -0
  26. package/src/monitoring/release-manager.js +10 -0
  27. package/src/orchestration/agent-skill-binding.js +655 -0
  28. package/src/orchestration/error-handler.js +827 -0
  29. package/src/orchestration/index.js +235 -1
  30. package/src/orchestration/mcp-tool-adapters.js +896 -0
  31. package/src/orchestration/reasoning/index.js +58 -0
  32. package/src/orchestration/reasoning/planning-engine.js +831 -0
  33. package/src/orchestration/reasoning/reasoning-engine.js +710 -0
  34. package/src/orchestration/reasoning/self-correction.js +751 -0
  35. package/src/orchestration/skill-executor.js +665 -0
  36. package/src/orchestration/skill-registry.js +650 -0
  37. package/src/orchestration/workflow-examples.js +1072 -0
  38. package/src/orchestration/workflow-executor.js +779 -0
  39. package/src/phase4-integration.js +248 -0
  40. package/src/phase5-integration.js +402 -0
  41. package/src/steering/steering-auto-update.js +572 -0
  42. package/src/steering/steering-validator.js +547 -0
  43. package/src/templates/template-constraints.js +646 -0
  44. package/src/validators/advanced-validation.js +580 -0
@@ -0,0 +1,692 @@
1
+ /**
2
+ * Repository Map Generator
3
+ *
4
+ * Generates a comprehensive map of the repository structure for LLM context.
5
+ * Implements efficient file scanning, caching, and incremental updates.
6
+ *
7
+ * Part of MUSUBI v5.0.0 - Codebase Intelligence
8
+ *
9
+ * @module analyzers/repository-map
10
+ * @version 1.0.0
11
+ *
12
+ * @traceability
13
+ * - Requirement: REQ-P4-001 (Repository Map Generation)
14
+ * - Design: docs/design/tdd-musubi-v5.0.0.md#2.1
15
+ * - Test: tests/analyzers/repository-map.test.js
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { EventEmitter } = require('events');
21
+
22
+ /**
23
+ * @typedef {Object} FileInfo
24
+ * @property {string} path - Relative file path
25
+ * @property {string} type - File type (file/directory)
26
+ * @property {string} extension - File extension
27
+ * @property {number} size - File size in bytes
28
+ * @property {number} mtime - Modification timestamp
29
+ * @property {string} language - Detected programming language
30
+ * @property {boolean} isEntry - Whether this is an entry point
31
+ * @property {string[]} exports - Exported symbols (for modules)
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} RepositoryStats
36
+ * @property {number} totalFiles - Total number of files
37
+ * @property {number} totalDirs - Total number of directories
38
+ * @property {number} totalSize - Total size in bytes
39
+ * @property {Object<string, number>} byLanguage - File count by language
40
+ * @property {Object<string, number>} byExtension - File count by extension
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} RepositoryMap
45
+ * @property {string} root - Repository root path
46
+ * @property {Date} generatedAt - Generation timestamp
47
+ * @property {FileInfo[]} files - All files in repository
48
+ * @property {RepositoryStats} stats - Repository statistics
49
+ * @property {Object} structure - Tree structure
50
+ * @property {string[]} entryPoints - Detected entry points
51
+ */
52
+
53
+ /**
54
+ * Language detection mappings
55
+ */
56
+ const LANGUAGE_MAP = {
57
+ '.js': 'javascript',
58
+ '.mjs': 'javascript',
59
+ '.cjs': 'javascript',
60
+ '.jsx': 'javascript',
61
+ '.ts': 'typescript',
62
+ '.tsx': 'typescript',
63
+ '.py': 'python',
64
+ '.rb': 'ruby',
65
+ '.go': 'go',
66
+ '.rs': 'rust',
67
+ '.java': 'java',
68
+ '.kt': 'kotlin',
69
+ '.swift': 'swift',
70
+ '.cs': 'csharp',
71
+ '.cpp': 'cpp',
72
+ '.c': 'c',
73
+ '.h': 'c',
74
+ '.hpp': 'cpp',
75
+ '.php': 'php',
76
+ '.sh': 'shell',
77
+ '.bash': 'shell',
78
+ '.zsh': 'shell',
79
+ '.md': 'markdown',
80
+ '.json': 'json',
81
+ '.yaml': 'yaml',
82
+ '.yml': 'yaml',
83
+ '.toml': 'toml',
84
+ '.xml': 'xml',
85
+ '.html': 'html',
86
+ '.css': 'css',
87
+ '.scss': 'scss',
88
+ '.sass': 'sass',
89
+ '.less': 'less',
90
+ '.sql': 'sql',
91
+ '.graphql': 'graphql',
92
+ '.gql': 'graphql',
93
+ '.dockerfile': 'dockerfile',
94
+ '.vue': 'vue',
95
+ '.svelte': 'svelte'
96
+ };
97
+
98
+ /**
99
+ * Entry point patterns
100
+ */
101
+ const ENTRY_PATTERNS = [
102
+ /^index\.(js|ts|mjs|cjs)$/,
103
+ /^main\.(js|ts|py|go|rs)$/,
104
+ /^app\.(js|ts|py)$/,
105
+ /^server\.(js|ts)$/,
106
+ /^cli\.(js|ts)$/,
107
+ /^package\.json$/,
108
+ /^Cargo\.toml$/,
109
+ /^go\.mod$/,
110
+ /^setup\.py$/,
111
+ /^pyproject\.toml$/,
112
+ /^Gemfile$/,
113
+ /^pom\.xml$/,
114
+ /^build\.gradle(\.kts)?$/
115
+ ];
116
+
117
+ /**
118
+ * Default ignore patterns
119
+ */
120
+ const DEFAULT_IGNORE_PATTERNS = [
121
+ 'node_modules',
122
+ '.git',
123
+ '.svn',
124
+ '.hg',
125
+ '__pycache__',
126
+ '.pytest_cache',
127
+ '.mypy_cache',
128
+ 'venv',
129
+ '.venv',
130
+ 'env',
131
+ '.env',
132
+ 'dist',
133
+ 'build',
134
+ 'out',
135
+ 'target',
136
+ 'coverage',
137
+ '.nyc_output',
138
+ '.next',
139
+ '.nuxt',
140
+ '.cache',
141
+ 'vendor',
142
+ 'Pods',
143
+ '.idea',
144
+ '.vscode',
145
+ '.DS_Store',
146
+ 'Thumbs.db',
147
+ '*.log',
148
+ '*.lock',
149
+ 'package-lock.json',
150
+ 'yarn.lock',
151
+ 'pnpm-lock.yaml',
152
+ 'Cargo.lock',
153
+ 'Gemfile.lock',
154
+ 'poetry.lock'
155
+ ];
156
+
157
+ /**
158
+ * Repository Map Generator
159
+ * @extends EventEmitter
160
+ */
161
+ class RepositoryMap extends EventEmitter {
162
+ /**
163
+ * Create repository map generator
164
+ * @param {Object} options - Configuration options
165
+ * @param {string} options.rootPath - Repository root path
166
+ * @param {string[]} [options.ignorePatterns] - Patterns to ignore
167
+ * @param {number} [options.maxDepth=20] - Maximum directory depth
168
+ * @param {number} [options.maxFiles=10000] - Maximum files to scan
169
+ * @param {boolean} [options.includeContent=false] - Include file content
170
+ * @param {number} [options.contentMaxSize=10000] - Max content size to include
171
+ */
172
+ constructor(options = {}) {
173
+ super();
174
+ this.rootPath = options.rootPath || process.cwd();
175
+ this.ignorePatterns = [
176
+ ...DEFAULT_IGNORE_PATTERNS,
177
+ ...(options.ignorePatterns || [])
178
+ ];
179
+ this.maxDepth = options.maxDepth ?? 20;
180
+ this.maxFiles = options.maxFiles ?? 10000;
181
+ this.includeContent = options.includeContent ?? false;
182
+ this.contentMaxSize = options.contentMaxSize ?? 10000;
183
+
184
+ // Cache
185
+ this.cache = new Map();
186
+ this.lastScanTime = null;
187
+
188
+ // Statistics
189
+ this.stats = {
190
+ totalFiles: 0,
191
+ totalDirs: 0,
192
+ totalSize: 0,
193
+ byLanguage: {},
194
+ byExtension: {}
195
+ };
196
+
197
+ // Results
198
+ this.files = [];
199
+ this.structure = {};
200
+ this.entryPoints = [];
201
+ }
202
+
203
+ /**
204
+ * Generate repository map
205
+ * @returns {Promise<RepositoryMap>}
206
+ */
207
+ async generate() {
208
+ this.emit('scan:start', { rootPath: this.rootPath });
209
+
210
+ // Reset state
211
+ this.files = [];
212
+ this.structure = {};
213
+ this.entryPoints = [];
214
+ this.stats = {
215
+ totalFiles: 0,
216
+ totalDirs: 0,
217
+ totalSize: 0,
218
+ byLanguage: {},
219
+ byExtension: {}
220
+ };
221
+
222
+ try {
223
+ await this.scanDirectory(this.rootPath, '', 0);
224
+ this.lastScanTime = new Date();
225
+
226
+ const result = {
227
+ root: this.rootPath,
228
+ generatedAt: this.lastScanTime,
229
+ files: this.files,
230
+ stats: this.stats,
231
+ structure: this.structure,
232
+ entryPoints: this.entryPoints
233
+ };
234
+
235
+ this.emit('scan:complete', result);
236
+ return result;
237
+
238
+ } catch (error) {
239
+ this.emit('scan:error', error);
240
+ throw error;
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Scan a directory recursively
246
+ * @param {string} dirPath - Absolute directory path
247
+ * @param {string} relativePath - Relative path from root
248
+ * @param {number} depth - Current depth
249
+ * @private
250
+ */
251
+ async scanDirectory(dirPath, relativePath, depth) {
252
+ if (depth > this.maxDepth) return;
253
+ if (this.files.length >= this.maxFiles) return;
254
+
255
+ let entries;
256
+ try {
257
+ entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
258
+ } catch (error) {
259
+ this.emit('scan:dirError', { path: relativePath, error });
260
+ return;
261
+ }
262
+
263
+ for (const entry of entries) {
264
+ if (this.files.length >= this.maxFiles) break;
265
+ if (this.shouldIgnore(entry.name)) continue;
266
+
267
+ const entryRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
268
+ const entryAbsPath = path.join(dirPath, entry.name);
269
+
270
+ if (entry.isDirectory()) {
271
+ this.stats.totalDirs++;
272
+ this.setStructureNode(entryRelPath, { type: 'directory', children: {} });
273
+ await this.scanDirectory(entryAbsPath, entryRelPath, depth + 1);
274
+ } else if (entry.isFile()) {
275
+ await this.processFile(entryAbsPath, entryRelPath);
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Process a single file
282
+ * @param {string} absPath - Absolute file path
283
+ * @param {string} relPath - Relative file path
284
+ * @private
285
+ */
286
+ async processFile(absPath, relPath) {
287
+ try {
288
+ const stat = await fs.promises.stat(absPath);
289
+ const ext = path.extname(relPath).toLowerCase();
290
+ const basename = path.basename(relPath);
291
+ const language = LANGUAGE_MAP[ext] || 'unknown';
292
+
293
+ // Detect entry points
294
+ const isEntry = ENTRY_PATTERNS.some(pattern => pattern.test(basename));
295
+ if (isEntry) {
296
+ this.entryPoints.push(relPath);
297
+ }
298
+
299
+ // File info
300
+ const fileInfo = {
301
+ path: relPath,
302
+ type: 'file',
303
+ extension: ext,
304
+ size: stat.size,
305
+ mtime: stat.mtime.getTime(),
306
+ language,
307
+ isEntry,
308
+ exports: []
309
+ };
310
+
311
+ // Extract exports for JS/TS files
312
+ if (['javascript', 'typescript'].includes(language) && stat.size < 100000) {
313
+ const exports = await this.extractExports(absPath);
314
+ fileInfo.exports = exports;
315
+ }
316
+
317
+ // Optionally include content
318
+ if (this.includeContent && stat.size <= this.contentMaxSize) {
319
+ try {
320
+ const content = await fs.promises.readFile(absPath, 'utf-8');
321
+ fileInfo.content = content;
322
+ } catch {
323
+ // Binary or unreadable file
324
+ }
325
+ }
326
+
327
+ // Update statistics
328
+ this.stats.totalFiles++;
329
+ this.stats.totalSize += stat.size;
330
+ this.stats.byLanguage[language] = (this.stats.byLanguage[language] || 0) + 1;
331
+ this.stats.byExtension[ext] = (this.stats.byExtension[ext] || 0) + 1;
332
+
333
+ // Add to results
334
+ this.files.push(fileInfo);
335
+ this.setStructureNode(relPath, { type: 'file', language, size: stat.size });
336
+
337
+ this.emit('file:processed', fileInfo);
338
+
339
+ } catch (error) {
340
+ this.emit('file:error', { path: relPath, error });
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Extract exported symbols from JS/TS file
346
+ * @param {string} filePath - File path
347
+ * @returns {Promise<string[]>}
348
+ * @private
349
+ */
350
+ async extractExports(filePath) {
351
+ try {
352
+ const content = await fs.promises.readFile(filePath, 'utf-8');
353
+ const exports = [];
354
+
355
+ // CommonJS exports
356
+ const cjsMatch = content.match(/module\.exports\s*=\s*\{([^}]+)\}/);
357
+ if (cjsMatch) {
358
+ const props = cjsMatch[1].match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g);
359
+ if (props) exports.push(...props);
360
+ }
361
+
362
+ // Named exports
363
+ const namedExports = content.matchAll(/export\s+(?:const|let|var|function|class|async function)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g);
364
+ for (const match of namedExports) {
365
+ exports.push(match[1]);
366
+ }
367
+
368
+ // Export statements
369
+ const exportStmts = content.matchAll(/export\s*\{\s*([^}]+)\s*\}/g);
370
+ for (const match of exportStmts) {
371
+ const names = match[1].match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g);
372
+ if (names) exports.push(...names);
373
+ }
374
+
375
+ // Default export
376
+ if (/export\s+default/.test(content)) {
377
+ exports.push('default');
378
+ }
379
+
380
+ return [...new Set(exports)];
381
+
382
+ } catch {
383
+ return [];
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Set a node in the structure tree
389
+ * @param {string} relPath - Relative path
390
+ * @param {Object} value - Node value
391
+ * @private
392
+ */
393
+ setStructureNode(relPath, value) {
394
+ const parts = relPath.split('/');
395
+ let current = this.structure;
396
+
397
+ for (let i = 0; i < parts.length - 1; i++) {
398
+ if (!current[parts[i]]) {
399
+ current[parts[i]] = { type: 'directory', children: {} };
400
+ }
401
+ current = current[parts[i]].children || current[parts[i]];
402
+ }
403
+
404
+ const lastName = parts[parts.length - 1];
405
+ if (value.type === 'directory') {
406
+ if (!current[lastName]) {
407
+ current[lastName] = value;
408
+ }
409
+ } else {
410
+ current[lastName] = value;
411
+ }
412
+ }
413
+
414
+ /**
415
+ * Check if a path should be ignored
416
+ * @param {string} name - File/directory name
417
+ * @returns {boolean}
418
+ * @private
419
+ */
420
+ shouldIgnore(name) {
421
+ return this.ignorePatterns.some(pattern => {
422
+ if (pattern.includes('*')) {
423
+ const regex = new RegExp(pattern.replace(/\./g, '\\.').replace(/\*/g, '.*'));
424
+ return regex.test(name);
425
+ }
426
+ return name === pattern;
427
+ });
428
+ }
429
+
430
+ /**
431
+ * Get files by language
432
+ * @param {string} language - Programming language
433
+ * @returns {FileInfo[]}
434
+ */
435
+ getFilesByLanguage(language) {
436
+ return this.files.filter(f => f.language === language);
437
+ }
438
+
439
+ /**
440
+ * Get files by extension
441
+ * @param {string} extension - File extension (with dot)
442
+ * @returns {FileInfo[]}
443
+ */
444
+ getFilesByExtension(extension) {
445
+ return this.files.filter(f => f.extension === extension);
446
+ }
447
+
448
+ /**
449
+ * Get files in directory
450
+ * @param {string} dirPath - Relative directory path
451
+ * @returns {FileInfo[]}
452
+ */
453
+ getFilesInDirectory(dirPath) {
454
+ const prefix = dirPath.endsWith('/') ? dirPath : `${dirPath}/`;
455
+ return this.files.filter(f => f.path.startsWith(prefix));
456
+ }
457
+
458
+ /**
459
+ * Search files by pattern
460
+ * @param {string|RegExp} pattern - Search pattern
461
+ * @returns {FileInfo[]}
462
+ */
463
+ searchFiles(pattern) {
464
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
465
+ return this.files.filter(f => regex.test(f.path));
466
+ }
467
+
468
+ /**
469
+ * Generate tree view string
470
+ * @param {Object} [node] - Starting node
471
+ * @param {string} [prefix=''] - Prefix for indentation
472
+ * @param {number} [maxDepth=5] - Maximum depth
473
+ * @returns {string}
474
+ */
475
+ toTreeString(node = this.structure, prefix = '', maxDepth = 5) {
476
+ if (maxDepth <= 0) return prefix + '...\n';
477
+
478
+ let result = '';
479
+ const entries = Object.entries(node);
480
+
481
+ for (let i = 0; i < entries.length; i++) {
482
+ const [name, info] = entries[i];
483
+ const isLast = i === entries.length - 1;
484
+ const connector = isLast ? '└── ' : '├── ';
485
+ const childPrefix = isLast ? ' ' : '│ ';
486
+
487
+ result += `${prefix}${connector}${name}`;
488
+
489
+ if (info.type === 'file') {
490
+ result += ` (${info.language})\n`;
491
+ } else {
492
+ result += '/\n';
493
+ if (info.children) {
494
+ result += this.toTreeString(info.children, prefix + childPrefix, maxDepth - 1);
495
+ }
496
+ }
497
+ }
498
+
499
+ return result;
500
+ }
501
+
502
+ /**
503
+ * Generate LLM-optimized context
504
+ * @param {Object} options - Context options
505
+ * @param {number} [options.maxTokens=4000] - Maximum token estimate
506
+ * @param {string[]} [options.focusPaths] - Paths to focus on
507
+ * @param {string[]} [options.languages] - Languages to include
508
+ * @returns {string}
509
+ */
510
+ toLLMContext(options = {}) {
511
+ const { maxTokens = 4000, focusPaths = [], languages = [] } = options;
512
+
513
+ let context = `# Repository Map\n\n`;
514
+ context += `**Root**: ${this.rootPath}\n`;
515
+ context += `**Generated**: ${this.lastScanTime?.toISOString() || 'N/A'}\n\n`;
516
+
517
+ // Statistics
518
+ context += `## Statistics\n\n`;
519
+ context += `- Total Files: ${this.stats.totalFiles}\n`;
520
+ context += `- Total Directories: ${this.stats.totalDirs}\n`;
521
+ context += `- Total Size: ${this.formatBytes(this.stats.totalSize)}\n\n`;
522
+
523
+ // Language breakdown
524
+ context += `### Languages\n\n`;
525
+ const langEntries = Object.entries(this.stats.byLanguage)
526
+ .sort((a, b) => b[1] - a[1])
527
+ .slice(0, 10);
528
+ for (const [lang, count] of langEntries) {
529
+ context += `- ${lang}: ${count} files\n`;
530
+ }
531
+ context += '\n';
532
+
533
+ // Entry points
534
+ if (this.entryPoints.length > 0) {
535
+ context += `## Entry Points\n\n`;
536
+ for (const entry of this.entryPoints.slice(0, 10)) {
537
+ context += `- \`${entry}\`\n`;
538
+ }
539
+ context += '\n';
540
+ }
541
+
542
+ // Structure (token-limited)
543
+ context += `## Structure\n\n\`\`\`\n`;
544
+ const treeStr = this.toTreeString(this.structure, '', 4);
545
+ const truncatedTree = this.truncateToTokens(treeStr, Math.floor(maxTokens * 0.6));
546
+ context += truncatedTree;
547
+ context += `\`\`\`\n\n`;
548
+
549
+ // Key files
550
+ let keyFiles = this.files;
551
+
552
+ // Filter by focus paths
553
+ if (focusPaths.length > 0) {
554
+ keyFiles = keyFiles.filter(f =>
555
+ focusPaths.some(fp => f.path.startsWith(fp) || f.path.includes(fp))
556
+ );
557
+ }
558
+
559
+ // Filter by languages
560
+ if (languages.length > 0) {
561
+ keyFiles = keyFiles.filter(f => languages.includes(f.language));
562
+ }
563
+
564
+ // Sort by importance (entry points first, then by size)
565
+ keyFiles.sort((a, b) => {
566
+ if (a.isEntry && !b.isEntry) return -1;
567
+ if (!a.isEntry && b.isEntry) return 1;
568
+ return b.exports.length - a.exports.length;
569
+ });
570
+
571
+ // Add key files with exports
572
+ context += `## Key Modules\n\n`;
573
+ for (const file of keyFiles.slice(0, 20)) {
574
+ if (file.exports.length > 0) {
575
+ context += `### ${file.path}\n`;
576
+ context += `- Language: ${file.language}\n`;
577
+ context += `- Exports: ${file.exports.slice(0, 10).join(', ')}\n`;
578
+ if (file.exports.length > 10) {
579
+ context += ` (and ${file.exports.length - 10} more...)\n`;
580
+ }
581
+ context += '\n';
582
+ }
583
+ }
584
+
585
+ return context;
586
+ }
587
+
588
+ /**
589
+ * Format bytes to human readable
590
+ * @param {number} bytes - Size in bytes
591
+ * @returns {string}
592
+ * @private
593
+ */
594
+ formatBytes(bytes) {
595
+ if (bytes === 0) return '0 B';
596
+ const k = 1024;
597
+ const sizes = ['B', 'KB', 'MB', 'GB'];
598
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
599
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
600
+ }
601
+
602
+ /**
603
+ * Truncate string to approximate token count
604
+ * @param {string} str - Input string
605
+ * @param {number} maxTokens - Maximum tokens (approximate)
606
+ * @returns {string}
607
+ * @private
608
+ */
609
+ truncateToTokens(str, maxTokens) {
610
+ // Rough estimate: 1 token ≈ 4 characters
611
+ const maxChars = maxTokens * 4;
612
+ if (str.length <= maxChars) return str;
613
+ return str.slice(0, maxChars) + '\n... (truncated)\n';
614
+ }
615
+
616
+ /**
617
+ * Export to JSON
618
+ * @returns {Object}
619
+ */
620
+ toJSON() {
621
+ return {
622
+ root: this.rootPath,
623
+ generatedAt: this.lastScanTime?.toISOString(),
624
+ stats: this.stats,
625
+ files: this.files,
626
+ entryPoints: this.entryPoints,
627
+ structure: this.structure
628
+ };
629
+ }
630
+
631
+ /**
632
+ * Import from JSON
633
+ * @param {Object} data - JSON data
634
+ */
635
+ fromJSON(data) {
636
+ this.rootPath = data.root;
637
+ this.lastScanTime = new Date(data.generatedAt);
638
+ this.stats = data.stats;
639
+ this.files = data.files;
640
+ this.entryPoints = data.entryPoints;
641
+ this.structure = data.structure;
642
+ }
643
+
644
+ /**
645
+ * Get cache key for incremental updates
646
+ * @returns {string}
647
+ */
648
+ getCacheKey() {
649
+ return `repomap:${this.rootPath}`;
650
+ }
651
+
652
+ /**
653
+ * Check if file has changed since last scan
654
+ * @param {string} filePath - Relative file path
655
+ * @param {number} mtime - Current modification time
656
+ * @returns {boolean}
657
+ */
658
+ hasFileChanged(filePath, mtime) {
659
+ const cached = this.cache.get(filePath);
660
+ if (!cached) return true;
661
+ return cached.mtime !== mtime;
662
+ }
663
+ }
664
+
665
+ /**
666
+ * Create a repository map generator
667
+ * @param {Object} options - Options
668
+ * @returns {RepositoryMap}
669
+ */
670
+ function createRepositoryMap(options = {}) {
671
+ return new RepositoryMap(options);
672
+ }
673
+
674
+ /**
675
+ * Generate repository map for a path
676
+ * @param {string} rootPath - Repository root
677
+ * @param {Object} options - Options
678
+ * @returns {Promise<Object>}
679
+ */
680
+ async function generateRepositoryMap(rootPath, options = {}) {
681
+ const mapper = createRepositoryMap({ rootPath, ...options });
682
+ return mapper.generate();
683
+ }
684
+
685
+ module.exports = {
686
+ RepositoryMap,
687
+ createRepositoryMap,
688
+ generateRepositoryMap,
689
+ LANGUAGE_MAP,
690
+ ENTRY_PATTERNS,
691
+ DEFAULT_IGNORE_PATTERNS
692
+ };
@@ -7,6 +7,8 @@ const platforms = require('./platforms');
7
7
  const cicd = require('./cicd');
8
8
  const documentation = require('./documentation');
9
9
  const examples = require('./examples');
10
+ const mcpConnector = require('./mcp-connector');
11
+ const toolDiscovery = require('./tool-discovery');
10
12
 
11
13
  module.exports = {
12
14
  // Multi-Platform Support
@@ -19,5 +21,9 @@ module.exports = {
19
21
  ...documentation,
20
22
 
21
23
  // Example Projects & Launch
22
- ...examples
24
+ ...examples,
25
+
26
+ // MCP Integration (Sprint 3.4)
27
+ ...mcpConnector,
28
+ ...toolDiscovery
23
29
  };