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.
- package/README.md +24 -19
- package/package.json +1 -1
- package/src/agents/agent-loop.js +532 -0
- package/src/agents/agentic/code-generator.js +767 -0
- package/src/agents/agentic/code-reviewer.js +698 -0
- package/src/agents/agentic/index.js +43 -0
- package/src/agents/function-tool.js +432 -0
- package/src/agents/index.js +45 -0
- package/src/agents/schema-generator.js +514 -0
- package/src/analyzers/ast-extractor.js +870 -0
- package/src/analyzers/context-optimizer.js +681 -0
- package/src/analyzers/repository-map.js +692 -0
- package/src/integrations/index.js +7 -1
- package/src/integrations/mcp/index.js +175 -0
- package/src/integrations/mcp/mcp-context-provider.js +472 -0
- package/src/integrations/mcp/mcp-discovery.js +436 -0
- package/src/integrations/mcp/mcp-tool-registry.js +467 -0
- package/src/integrations/mcp-connector.js +818 -0
- package/src/integrations/tool-discovery.js +589 -0
- package/src/managers/index.js +7 -0
- package/src/managers/skill-tools.js +565 -0
- package/src/monitoring/cost-tracker.js +7 -0
- package/src/monitoring/incident-manager.js +10 -0
- package/src/monitoring/observability.js +10 -0
- package/src/monitoring/quality-dashboard.js +491 -0
- package/src/monitoring/release-manager.js +10 -0
- package/src/orchestration/agent-skill-binding.js +655 -0
- package/src/orchestration/error-handler.js +827 -0
- package/src/orchestration/index.js +235 -1
- package/src/orchestration/mcp-tool-adapters.js +896 -0
- package/src/orchestration/reasoning/index.js +58 -0
- package/src/orchestration/reasoning/planning-engine.js +831 -0
- package/src/orchestration/reasoning/reasoning-engine.js +710 -0
- package/src/orchestration/reasoning/self-correction.js +751 -0
- package/src/orchestration/skill-executor.js +665 -0
- package/src/orchestration/skill-registry.js +650 -0
- package/src/orchestration/workflow-examples.js +1072 -0
- package/src/orchestration/workflow-executor.js +779 -0
- package/src/phase4-integration.js +248 -0
- package/src/phase5-integration.js +402 -0
- package/src/steering/steering-auto-update.js +572 -0
- package/src/steering/steering-validator.js +547 -0
- package/src/templates/template-constraints.js +646 -0
- 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
|
};
|