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,870 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST Extractor
|
|
3
|
+
*
|
|
4
|
+
* Extracts Abstract Syntax Tree information from source code files.
|
|
5
|
+
* Provides structured analysis of code structure, symbols, and relationships.
|
|
6
|
+
*
|
|
7
|
+
* Part of MUSUBI v5.0.0 - Codebase Intelligence
|
|
8
|
+
*
|
|
9
|
+
* @module analyzers/ast-extractor
|
|
10
|
+
* @version 1.0.0
|
|
11
|
+
*
|
|
12
|
+
* @traceability
|
|
13
|
+
* - Requirement: REQ-P4-002 (AST Extraction and Analysis)
|
|
14
|
+
* - Design: docs/design/tdd-musubi-v5.0.0.md#2.2
|
|
15
|
+
* - Test: tests/analyzers/ast-extractor.test.js
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { EventEmitter } = require('events');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} SymbolInfo
|
|
24
|
+
* @property {string} name - Symbol name
|
|
25
|
+
* @property {string} type - Symbol type (function, class, variable, etc.)
|
|
26
|
+
* @property {number} line - Line number
|
|
27
|
+
* @property {number} column - Column number
|
|
28
|
+
* @property {string} [docstring] - Documentation string
|
|
29
|
+
* @property {string[]} [params] - Function parameters
|
|
30
|
+
* @property {string} [returnType] - Return type
|
|
31
|
+
* @property {string} [visibility] - public, private, protected
|
|
32
|
+
* @property {boolean} [isExported] - Whether exported
|
|
33
|
+
* @property {boolean} [isAsync] - Whether async
|
|
34
|
+
* @property {string[]} [decorators] - Applied decorators
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {Object} ImportInfo
|
|
39
|
+
* @property {string} source - Import source
|
|
40
|
+
* @property {string[]} names - Imported names
|
|
41
|
+
* @property {boolean} isDefault - Whether default import
|
|
42
|
+
* @property {boolean} isNamespace - Whether namespace import
|
|
43
|
+
* @property {number} line - Line number
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} FileAST
|
|
48
|
+
* @property {string} path - File path
|
|
49
|
+
* @property {string} language - Detected language
|
|
50
|
+
* @property {SymbolInfo[]} symbols - Extracted symbols
|
|
51
|
+
* @property {ImportInfo[]} imports - Import statements
|
|
52
|
+
* @property {string[]} exports - Exported names
|
|
53
|
+
* @property {Object} structure - Hierarchical structure
|
|
54
|
+
* @property {Object} metadata - Additional metadata
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Language-specific patterns
|
|
59
|
+
*/
|
|
60
|
+
const PATTERNS = {
|
|
61
|
+
javascript: {
|
|
62
|
+
// Function declarations (removed ^ to match anywhere in line)
|
|
63
|
+
functionDecl: /(?:^|\n)\s*(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(([^)]*)\)/g,
|
|
64
|
+
// Arrow functions with const/let/var
|
|
65
|
+
arrowFunc: /(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/g,
|
|
66
|
+
// Class declarations
|
|
67
|
+
classDecl: /(?:^|\n)\s*(?:export\s+)?class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s+extends\s+([a-zA-Z_$][a-zA-Z0-9_$]*))?/g,
|
|
68
|
+
// Class methods
|
|
69
|
+
methodDecl: /\n\s+(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g,
|
|
70
|
+
// Variable declarations
|
|
71
|
+
constDecl: /(?:export\s+)?(?:const|let|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g,
|
|
72
|
+
// ES6 imports
|
|
73
|
+
importStmt: /import\s+(?:(\{[^}]+\})|([a-zA-Z_$][a-zA-Z0-9_$]*)(?:\s*,\s*(\{[^}]+\}))?|\*\s+as\s+([a-zA-Z_$][a-zA-Z0-9_$]*))\s+from\s+['"](.*?)['"]/g,
|
|
74
|
+
// CommonJS require
|
|
75
|
+
requireStmt: /(?:const|let|var)\s+(?:(\{[^}]+\})|([a-zA-Z_$][a-zA-Z0-9_$]*))\s*=\s*require\s*\(\s*['"](.*?)['"]\s*\)/g,
|
|
76
|
+
// Exports - capture function/class/const names after export
|
|
77
|
+
exportStmt: /export\s+(?:default\s+)?(?:(const|let|var|function|class|async\s+function)\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)?/g,
|
|
78
|
+
namedExport: /export\s*\{([^}]+)\}/gm,
|
|
79
|
+
// JSDoc comments
|
|
80
|
+
jsdoc: /\/\*\*\s*([\s\S]*?)\s*\*\//g,
|
|
81
|
+
// Single-line comments
|
|
82
|
+
comment: /\/\/\s*(.+)$/g
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
typescript: {
|
|
86
|
+
// Extends JavaScript patterns
|
|
87
|
+
// Interface declarations
|
|
88
|
+
interfaceDecl: /(?:^|\n)\s*(?:export\s+)?interface\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:<[^>]+>)?(?:\s+extends\s+([^{]+))?/g,
|
|
89
|
+
// Type aliases
|
|
90
|
+
typeDecl: /(?:^|\n)\s*(?:export\s+)?type\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:<[^>]+>)?\s*=/g,
|
|
91
|
+
// Enum declarations
|
|
92
|
+
enumDecl: /(?:^|\n)\s*(?:export\s+)?(?:const\s+)?enum\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g,
|
|
93
|
+
// Function with types
|
|
94
|
+
typedFunc: /(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)(?:<[^>]+>)?\s*\(([^)]*)\)\s*:\s*([^{]+)/g
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
python: {
|
|
98
|
+
// Function definitions
|
|
99
|
+
functionDef: /(?:^|\n)(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]*)\)(?:\s*->\s*([^\s:]+))?/g,
|
|
100
|
+
// Class definitions
|
|
101
|
+
classDef: /(?:^|\n)class\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\s*\(([^)]*)\))?/g,
|
|
102
|
+
// Method definitions (inside class)
|
|
103
|
+
methodDef: /\n\s+(?:async\s+)?def\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(self(?:,\s*)?([^)]*)\)/g,
|
|
104
|
+
// Decorators
|
|
105
|
+
decorator: /(?:^|\n)@([a-zA-Z_][a-zA-Z0-9_.]*(?:\([^)]*\))?)/g,
|
|
106
|
+
// Imports
|
|
107
|
+
importFrom: /(?:^|\n)from\s+([^\s]+)\s+import\s+(.+)/g,
|
|
108
|
+
importModule: /(?:^|\n)import\s+([^\s]+)(?:\s+as\s+([^\s]+))?/g,
|
|
109
|
+
// Docstrings
|
|
110
|
+
docstring: /"""([\s\S]*?)"""|'''([\s\S]*?)'''/g,
|
|
111
|
+
// Type hints
|
|
112
|
+
typeHint: /:\s*([a-zA-Z_][a-zA-Z0-9_\[\],\s]*)/g
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* AST Extractor class
|
|
118
|
+
* @extends EventEmitter
|
|
119
|
+
*/
|
|
120
|
+
class ASTExtractor extends EventEmitter {
|
|
121
|
+
/**
|
|
122
|
+
* Create AST extractor
|
|
123
|
+
* @param {Object} options - Configuration options
|
|
124
|
+
* @param {string[]} [options.supportedLanguages] - Languages to support
|
|
125
|
+
* @param {boolean} [options.includeDocstrings=true] - Include documentation
|
|
126
|
+
* @param {boolean} [options.extractComments=false] - Extract inline comments
|
|
127
|
+
*/
|
|
128
|
+
constructor(options = {}) {
|
|
129
|
+
super();
|
|
130
|
+
this.supportedLanguages = options.supportedLanguages || ['javascript', 'typescript', 'python'];
|
|
131
|
+
this.includeDocstrings = options.includeDocstrings ?? true;
|
|
132
|
+
this.extractComments = options.extractComments ?? false;
|
|
133
|
+
|
|
134
|
+
// Results cache
|
|
135
|
+
this.cache = new Map();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extract AST from file
|
|
140
|
+
* @param {string} filePath - File path
|
|
141
|
+
* @returns {Promise<FileAST>}
|
|
142
|
+
*/
|
|
143
|
+
async extractFromFile(filePath) {
|
|
144
|
+
const content = await fs.promises.readFile(filePath, 'utf-8');
|
|
145
|
+
const language = this.detectLanguage(filePath);
|
|
146
|
+
|
|
147
|
+
if (!this.supportedLanguages.includes(language)) {
|
|
148
|
+
return {
|
|
149
|
+
path: filePath,
|
|
150
|
+
language,
|
|
151
|
+
symbols: [],
|
|
152
|
+
imports: [],
|
|
153
|
+
exports: [],
|
|
154
|
+
structure: {},
|
|
155
|
+
metadata: { supported: false }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this.extract(content, language, filePath);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract AST from content
|
|
164
|
+
* @param {string} content - Source code content
|
|
165
|
+
* @param {string} language - Programming language
|
|
166
|
+
* @param {string} [filePath='<source>'] - Optional file path
|
|
167
|
+
* @returns {FileAST}
|
|
168
|
+
*/
|
|
169
|
+
extract(content, language, filePath = '<source>') {
|
|
170
|
+
this.emit('extract:start', { filePath, language });
|
|
171
|
+
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
const symbols = [];
|
|
174
|
+
const imports = [];
|
|
175
|
+
const exports = [];
|
|
176
|
+
const structure = {
|
|
177
|
+
classes: [],
|
|
178
|
+
functions: [],
|
|
179
|
+
variables: []
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
switch (language) {
|
|
184
|
+
case 'javascript':
|
|
185
|
+
case 'typescript':
|
|
186
|
+
this.extractJavaScript(content, lines, symbols, imports, exports, structure);
|
|
187
|
+
if (language === 'typescript') {
|
|
188
|
+
this.extractTypeScript(content, lines, symbols, structure);
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case 'python':
|
|
192
|
+
this.extractPython(content, lines, symbols, imports, exports, structure);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = {
|
|
197
|
+
path: filePath,
|
|
198
|
+
language,
|
|
199
|
+
symbols,
|
|
200
|
+
imports,
|
|
201
|
+
exports,
|
|
202
|
+
structure,
|
|
203
|
+
metadata: {
|
|
204
|
+
lineCount: lines.length,
|
|
205
|
+
symbolCount: symbols.length,
|
|
206
|
+
extractedAt: new Date().toISOString()
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
this.emit('extract:complete', result);
|
|
211
|
+
return result;
|
|
212
|
+
|
|
213
|
+
} catch (error) {
|
|
214
|
+
this.emit('extract:error', { filePath, error });
|
|
215
|
+
return {
|
|
216
|
+
path: filePath,
|
|
217
|
+
language,
|
|
218
|
+
symbols: [],
|
|
219
|
+
imports: [],
|
|
220
|
+
exports: [],
|
|
221
|
+
structure: {},
|
|
222
|
+
metadata: { error: error.message }
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extract JavaScript/TypeScript patterns
|
|
229
|
+
* @private
|
|
230
|
+
*/
|
|
231
|
+
extractJavaScript(content, lines, symbols, imports, exports, structure) {
|
|
232
|
+
const patterns = PATTERNS.javascript;
|
|
233
|
+
|
|
234
|
+
// Extract docstrings for association
|
|
235
|
+
const docstrings = this.extractDocstrings(content, 'javascript');
|
|
236
|
+
|
|
237
|
+
// Functions
|
|
238
|
+
let match;
|
|
239
|
+
const funcPattern = new RegExp(patterns.functionDecl.source, 'g');
|
|
240
|
+
while ((match = funcPattern.exec(content)) !== null) {
|
|
241
|
+
const line = this.getLineNumber(content, match.index);
|
|
242
|
+
const isExported = match[0].includes('export ');
|
|
243
|
+
const isAsync = match[0].includes('async');
|
|
244
|
+
const params = this.parseParams(match[2]);
|
|
245
|
+
const doc = this.findNearestDocstring(docstrings, line);
|
|
246
|
+
|
|
247
|
+
const symbol = {
|
|
248
|
+
name: match[1],
|
|
249
|
+
type: 'function',
|
|
250
|
+
line,
|
|
251
|
+
column: match.index - content.lastIndexOf('\n', match.index) - 1,
|
|
252
|
+
params,
|
|
253
|
+
isExported,
|
|
254
|
+
isAsync,
|
|
255
|
+
docstring: doc
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
symbols.push(symbol);
|
|
259
|
+
structure.functions.push(symbol.name);
|
|
260
|
+
|
|
261
|
+
if (isExported) {
|
|
262
|
+
exports.push(match[1]);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Arrow functions
|
|
267
|
+
const arrowPattern = new RegExp(patterns.arrowFunc.source, 'g');
|
|
268
|
+
while ((match = arrowPattern.exec(content)) !== null) {
|
|
269
|
+
const line = this.getLineNumber(content, match.index);
|
|
270
|
+
const isExported = match[0].includes('export ');
|
|
271
|
+
const isAsync = match[0].includes('async');
|
|
272
|
+
const doc = this.findNearestDocstring(docstrings, line);
|
|
273
|
+
|
|
274
|
+
const symbol = {
|
|
275
|
+
name: match[1],
|
|
276
|
+
type: 'function',
|
|
277
|
+
line,
|
|
278
|
+
isExported,
|
|
279
|
+
isAsync,
|
|
280
|
+
docstring: doc
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
symbols.push(symbol);
|
|
284
|
+
structure.functions.push(symbol.name);
|
|
285
|
+
|
|
286
|
+
if (isExported) {
|
|
287
|
+
exports.push(match[1]);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Classes
|
|
292
|
+
const classPattern = new RegExp(patterns.classDecl.source, 'g');
|
|
293
|
+
while ((match = classPattern.exec(content)) !== null) {
|
|
294
|
+
const line = this.getLineNumber(content, match.index);
|
|
295
|
+
const isExported = match[0].includes('export ');
|
|
296
|
+
const doc = this.findNearestDocstring(docstrings, line);
|
|
297
|
+
|
|
298
|
+
const classSymbol = {
|
|
299
|
+
name: match[1],
|
|
300
|
+
type: 'class',
|
|
301
|
+
line,
|
|
302
|
+
isExported,
|
|
303
|
+
extends: match[2] || null,
|
|
304
|
+
docstring: doc,
|
|
305
|
+
methods: []
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Extract class methods
|
|
309
|
+
const classEndIndex = this.findClassEnd(content, match.index);
|
|
310
|
+
const classContent = content.slice(match.index, classEndIndex);
|
|
311
|
+
const methodPattern = new RegExp(patterns.methodDecl.source, 'gm');
|
|
312
|
+
let methodMatch;
|
|
313
|
+
|
|
314
|
+
while ((methodMatch = methodPattern.exec(classContent)) !== null) {
|
|
315
|
+
classSymbol.methods.push(methodMatch[1]);
|
|
316
|
+
|
|
317
|
+
symbols.push({
|
|
318
|
+
name: `${match[1]}.${methodMatch[1]}`,
|
|
319
|
+
type: 'method',
|
|
320
|
+
line: line + this.getLineNumber(classContent, methodMatch.index) - 1,
|
|
321
|
+
parentClass: match[1]
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
symbols.push(classSymbol);
|
|
326
|
+
structure.classes.push({
|
|
327
|
+
name: classSymbol.name,
|
|
328
|
+
extends: classSymbol.extends,
|
|
329
|
+
methods: classSymbol.methods
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
if (isExported) {
|
|
333
|
+
exports.push(match[1]);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Imports (ES6)
|
|
338
|
+
const importPattern = new RegExp(patterns.importStmt.source, 'gm');
|
|
339
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
340
|
+
const line = this.getLineNumber(content, match.index);
|
|
341
|
+
const source = match[5];
|
|
342
|
+
let names = [];
|
|
343
|
+
let isDefault = false;
|
|
344
|
+
let isNamespace = false;
|
|
345
|
+
|
|
346
|
+
if (match[1]) {
|
|
347
|
+
// Named imports { a, b }
|
|
348
|
+
names = match[1].replace(/[{}]/g, '').split(',').map(n => n.trim());
|
|
349
|
+
}
|
|
350
|
+
if (match[2]) {
|
|
351
|
+
// Default import
|
|
352
|
+
names.push(match[2]);
|
|
353
|
+
isDefault = true;
|
|
354
|
+
}
|
|
355
|
+
if (match[3]) {
|
|
356
|
+
// Additional named imports after default
|
|
357
|
+
names.push(...match[3].replace(/[{}]/g, '').split(',').map(n => n.trim()));
|
|
358
|
+
}
|
|
359
|
+
if (match[4]) {
|
|
360
|
+
// Namespace import * as X
|
|
361
|
+
names.push(match[4]);
|
|
362
|
+
isNamespace = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
imports.push({ source, names, isDefault, isNamespace, line });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// CommonJS require
|
|
369
|
+
const requirePattern = new RegExp(patterns.requireStmt.source, 'gm');
|
|
370
|
+
while ((match = requirePattern.exec(content)) !== null) {
|
|
371
|
+
const line = this.getLineNumber(content, match.index);
|
|
372
|
+
const source = match[3];
|
|
373
|
+
let names = [];
|
|
374
|
+
|
|
375
|
+
if (match[1]) {
|
|
376
|
+
// Destructured require
|
|
377
|
+
names = match[1].replace(/[{}]/g, '').split(',').map(n => n.trim());
|
|
378
|
+
}
|
|
379
|
+
if (match[2]) {
|
|
380
|
+
// Simple require
|
|
381
|
+
names.push(match[2]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
imports.push({ source, names, isDefault: !!match[2], isNamespace: false, line });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Named exports: export { a, b }
|
|
388
|
+
const namedExportPattern = new RegExp(patterns.namedExport.source, 'gm');
|
|
389
|
+
while ((match = namedExportPattern.exec(content)) !== null) {
|
|
390
|
+
const names = match[1].split(',').map(n => n.trim().split(' as ')[0].trim());
|
|
391
|
+
exports.push(...names);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Direct exports: export const/let/var/function/class name
|
|
395
|
+
const exportStmtPattern = new RegExp(patterns.exportStmt.source, 'gm');
|
|
396
|
+
while ((match = exportStmtPattern.exec(content)) !== null) {
|
|
397
|
+
// match[2] is the exported name (e.g., VALUE, helper, Service)
|
|
398
|
+
if (match[2] && !exports.includes(match[2])) {
|
|
399
|
+
exports.push(match[2]);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Extract TypeScript-specific patterns
|
|
406
|
+
* @private
|
|
407
|
+
*/
|
|
408
|
+
extractTypeScript(content, lines, symbols, structure) {
|
|
409
|
+
const patterns = PATTERNS.typescript;
|
|
410
|
+
let match;
|
|
411
|
+
|
|
412
|
+
// Interfaces
|
|
413
|
+
const interfacePattern = new RegExp(patterns.interfaceDecl.source, 'gm');
|
|
414
|
+
while ((match = interfacePattern.exec(content)) !== null) {
|
|
415
|
+
const line = this.getLineNumber(content, match.index);
|
|
416
|
+
const isExported = match[0].includes('export');
|
|
417
|
+
|
|
418
|
+
symbols.push({
|
|
419
|
+
name: match[1],
|
|
420
|
+
type: 'interface',
|
|
421
|
+
line,
|
|
422
|
+
isExported,
|
|
423
|
+
extends: match[2] ? match[2].split(',').map(s => s.trim()) : []
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Type aliases
|
|
428
|
+
const typePattern = new RegExp(patterns.typeDecl.source, 'gm');
|
|
429
|
+
while ((match = typePattern.exec(content)) !== null) {
|
|
430
|
+
const line = this.getLineNumber(content, match.index);
|
|
431
|
+
const isExported = match[0].includes('export');
|
|
432
|
+
|
|
433
|
+
symbols.push({
|
|
434
|
+
name: match[1],
|
|
435
|
+
type: 'type',
|
|
436
|
+
line,
|
|
437
|
+
isExported
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Enums
|
|
442
|
+
const enumPattern = new RegExp(patterns.enumDecl.source, 'gm');
|
|
443
|
+
while ((match = enumPattern.exec(content)) !== null) {
|
|
444
|
+
const line = this.getLineNumber(content, match.index);
|
|
445
|
+
const isExported = match[0].includes('export');
|
|
446
|
+
|
|
447
|
+
symbols.push({
|
|
448
|
+
name: match[1],
|
|
449
|
+
type: 'enum',
|
|
450
|
+
line,
|
|
451
|
+
isExported
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Extract Python patterns
|
|
458
|
+
* @private
|
|
459
|
+
*/
|
|
460
|
+
extractPython(content, lines, symbols, imports, exports, structure) {
|
|
461
|
+
const patterns = PATTERNS.python;
|
|
462
|
+
let match;
|
|
463
|
+
|
|
464
|
+
// Collect decorators for association
|
|
465
|
+
const decorators = [];
|
|
466
|
+
const decoratorPattern = new RegExp(patterns.decorator.source, 'gm');
|
|
467
|
+
while ((match = decoratorPattern.exec(content)) !== null) {
|
|
468
|
+
const line = this.getLineNumber(content, match.index);
|
|
469
|
+
decorators.push({ name: match[1], line });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Extract docstrings
|
|
473
|
+
const docstrings = this.extractDocstrings(content, 'python');
|
|
474
|
+
|
|
475
|
+
// Functions
|
|
476
|
+
const funcPattern = new RegExp(patterns.functionDef.source, 'gm');
|
|
477
|
+
while ((match = funcPattern.exec(content)) !== null) {
|
|
478
|
+
const line = this.getLineNumber(content, match.index);
|
|
479
|
+
const isAsync = match[0].includes('async');
|
|
480
|
+
const params = this.parseParams(match[2]);
|
|
481
|
+
const returnType = match[3] || null;
|
|
482
|
+
const doc = this.findNearestDocstring(docstrings, line);
|
|
483
|
+
const funcDecorators = decorators
|
|
484
|
+
.filter(d => d.line === line - 1)
|
|
485
|
+
.map(d => d.name);
|
|
486
|
+
|
|
487
|
+
const symbol = {
|
|
488
|
+
name: match[1],
|
|
489
|
+
type: 'function',
|
|
490
|
+
line,
|
|
491
|
+
params,
|
|
492
|
+
returnType,
|
|
493
|
+
isAsync,
|
|
494
|
+
docstring: doc,
|
|
495
|
+
decorators: funcDecorators,
|
|
496
|
+
visibility: match[1].startsWith('_') ? 'private' : 'public'
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
symbols.push(symbol);
|
|
500
|
+
structure.functions.push(symbol.name);
|
|
501
|
+
|
|
502
|
+
// Python uses __all__ for explicit exports, but we mark non-underscore as potentially exported
|
|
503
|
+
if (!match[1].startsWith('_')) {
|
|
504
|
+
exports.push(match[1]);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Classes
|
|
509
|
+
const classPattern = new RegExp(patterns.classDef.source, 'gm');
|
|
510
|
+
while ((match = classPattern.exec(content)) !== null) {
|
|
511
|
+
const line = this.getLineNumber(content, match.index);
|
|
512
|
+
const doc = this.findNearestDocstring(docstrings, line);
|
|
513
|
+
const baseClasses = match[2] ? match[2].split(',').map(s => s.trim()) : [];
|
|
514
|
+
const classDecorators = decorators
|
|
515
|
+
.filter(d => d.line === line - 1)
|
|
516
|
+
.map(d => d.name);
|
|
517
|
+
|
|
518
|
+
const classSymbol = {
|
|
519
|
+
name: match[1],
|
|
520
|
+
type: 'class',
|
|
521
|
+
line,
|
|
522
|
+
extends: baseClasses,
|
|
523
|
+
docstring: doc,
|
|
524
|
+
decorators: classDecorators,
|
|
525
|
+
methods: [],
|
|
526
|
+
visibility: match[1].startsWith('_') ? 'private' : 'public'
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// Extract class methods
|
|
530
|
+
const classEndIndex = this.findPythonClassEnd(content, lines, match.index);
|
|
531
|
+
const classContent = content.slice(match.index, classEndIndex);
|
|
532
|
+
const methodPattern = new RegExp(patterns.methodDef.source, 'gm');
|
|
533
|
+
let methodMatch;
|
|
534
|
+
|
|
535
|
+
while ((methodMatch = methodPattern.exec(classContent)) !== null) {
|
|
536
|
+
const methodName = methodMatch[1];
|
|
537
|
+
classSymbol.methods.push(methodName);
|
|
538
|
+
|
|
539
|
+
symbols.push({
|
|
540
|
+
name: `${match[1]}.${methodName}`,
|
|
541
|
+
type: 'method',
|
|
542
|
+
line: line + this.getLineNumber(classContent, methodMatch.index) - 1,
|
|
543
|
+
parentClass: match[1],
|
|
544
|
+
visibility: methodName.startsWith('_') ? 'private' : 'public'
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
symbols.push(classSymbol);
|
|
549
|
+
structure.classes.push({
|
|
550
|
+
name: classSymbol.name,
|
|
551
|
+
extends: classSymbol.extends,
|
|
552
|
+
methods: classSymbol.methods
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
if (!match[1].startsWith('_')) {
|
|
556
|
+
exports.push(match[1]);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Imports
|
|
561
|
+
const importFromPattern = new RegExp(patterns.importFrom.source, 'gm');
|
|
562
|
+
while ((match = importFromPattern.exec(content)) !== null) {
|
|
563
|
+
const line = this.getLineNumber(content, match.index);
|
|
564
|
+
const source = match[1];
|
|
565
|
+
const names = match[2].split(',').map(n => {
|
|
566
|
+
const parts = n.trim().split(' as ');
|
|
567
|
+
return parts[0].trim();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
imports.push({ source, names, isDefault: false, isNamespace: false, line });
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const importModulePattern = new RegExp(patterns.importModule.source, 'gm');
|
|
574
|
+
while ((match = importModulePattern.exec(content)) !== null) {
|
|
575
|
+
const line = this.getLineNumber(content, match.index);
|
|
576
|
+
const source = match[1];
|
|
577
|
+
const alias = match[2] || match[1];
|
|
578
|
+
|
|
579
|
+
imports.push({ source, names: [alias], isDefault: true, isNamespace: false, line });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Extract docstrings from content
|
|
585
|
+
* @private
|
|
586
|
+
*/
|
|
587
|
+
extractDocstrings(content, language) {
|
|
588
|
+
const docstrings = [];
|
|
589
|
+
let match;
|
|
590
|
+
|
|
591
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
592
|
+
const pattern = PATTERNS.javascript.jsdoc;
|
|
593
|
+
const jsdocPattern = new RegExp(pattern.source, 'gm');
|
|
594
|
+
while ((match = jsdocPattern.exec(content)) !== null) {
|
|
595
|
+
const line = this.getLineNumber(content, match.index);
|
|
596
|
+
const text = match[1]
|
|
597
|
+
.replace(/^\s*\*\s?/gm, '')
|
|
598
|
+
.trim();
|
|
599
|
+
docstrings.push({ line, text });
|
|
600
|
+
}
|
|
601
|
+
} else if (language === 'python') {
|
|
602
|
+
const pattern = PATTERNS.python.docstring;
|
|
603
|
+
const docPattern = new RegExp(pattern.source, 'gm');
|
|
604
|
+
while ((match = docPattern.exec(content)) !== null) {
|
|
605
|
+
const line = this.getLineNumber(content, match.index);
|
|
606
|
+
const text = (match[1] || match[2]).trim();
|
|
607
|
+
docstrings.push({ line, text });
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return docstrings;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Find nearest docstring before a line
|
|
616
|
+
* @private
|
|
617
|
+
*/
|
|
618
|
+
findNearestDocstring(docstrings, line) {
|
|
619
|
+
if (!this.includeDocstrings) return null;
|
|
620
|
+
|
|
621
|
+
// Look for docstring within 10 lines before or 2 lines after
|
|
622
|
+
// JSDoc/docstrings can span multiple lines, so we need a wider range
|
|
623
|
+
const candidates = docstrings.filter(d =>
|
|
624
|
+
d.line >= line - 10 && d.line <= line + 2
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
if (candidates.length === 0) return null;
|
|
628
|
+
|
|
629
|
+
// Return the closest one that comes before the target line
|
|
630
|
+
const before = candidates.filter(d => d.line <= line);
|
|
631
|
+
if (before.length > 0) {
|
|
632
|
+
// Sort by line descending to get the closest before
|
|
633
|
+
before.sort((a, b) => b.line - a.line);
|
|
634
|
+
return before[0].text;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Fall back to any candidate
|
|
638
|
+
candidates.sort((a, b) => Math.abs(a.line - line) - Math.abs(b.line - line));
|
|
639
|
+
return candidates[0].text;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get line number from character index
|
|
644
|
+
* @private
|
|
645
|
+
*/
|
|
646
|
+
getLineNumber(content, index) {
|
|
647
|
+
return content.slice(0, index).split('\n').length;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Parse function parameters
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
parseParams(paramsStr) {
|
|
655
|
+
if (!paramsStr || !paramsStr.trim()) return [];
|
|
656
|
+
|
|
657
|
+
return paramsStr
|
|
658
|
+
.split(',')
|
|
659
|
+
.map(p => p.trim())
|
|
660
|
+
.filter(p => p)
|
|
661
|
+
.map(p => {
|
|
662
|
+
// Remove default values and type annotations for basic param name
|
|
663
|
+
const name = p.split('=')[0].split(':')[0].trim();
|
|
664
|
+
return name;
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Find end of JavaScript class
|
|
670
|
+
* @private
|
|
671
|
+
*/
|
|
672
|
+
findClassEnd(content, startIndex) {
|
|
673
|
+
let depth = 0;
|
|
674
|
+
let inClass = false;
|
|
675
|
+
|
|
676
|
+
for (let i = startIndex; i < content.length; i++) {
|
|
677
|
+
if (content[i] === '{') {
|
|
678
|
+
depth++;
|
|
679
|
+
inClass = true;
|
|
680
|
+
} else if (content[i] === '}') {
|
|
681
|
+
depth--;
|
|
682
|
+
if (inClass && depth === 0) {
|
|
683
|
+
return i + 1;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return content.length;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Find end of Python class (by indentation)
|
|
693
|
+
* @private
|
|
694
|
+
*/
|
|
695
|
+
findPythonClassEnd(content, lines, startIndex) {
|
|
696
|
+
const startLine = this.getLineNumber(content, startIndex);
|
|
697
|
+
const classLine = lines[startLine - 1];
|
|
698
|
+
const classIndent = classLine.match(/^(\s*)/)[1].length;
|
|
699
|
+
|
|
700
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
701
|
+
const line = lines[i];
|
|
702
|
+
if (line.trim() === '') continue;
|
|
703
|
+
|
|
704
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
705
|
+
if (indent <= classIndent && i > startLine) {
|
|
706
|
+
// Found line with same or less indentation
|
|
707
|
+
let charIndex = 0;
|
|
708
|
+
for (let j = 0; j < i; j++) {
|
|
709
|
+
charIndex += lines[j].length + 1;
|
|
710
|
+
}
|
|
711
|
+
return charIndex;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return content.length;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Detect language from file path
|
|
720
|
+
* @param {string} filePath - File path
|
|
721
|
+
* @returns {string}
|
|
722
|
+
*/
|
|
723
|
+
detectLanguage(filePath) {
|
|
724
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
725
|
+
const langMap = {
|
|
726
|
+
'.js': 'javascript',
|
|
727
|
+
'.mjs': 'javascript',
|
|
728
|
+
'.cjs': 'javascript',
|
|
729
|
+
'.jsx': 'javascript',
|
|
730
|
+
'.ts': 'typescript',
|
|
731
|
+
'.tsx': 'typescript',
|
|
732
|
+
'.py': 'python'
|
|
733
|
+
};
|
|
734
|
+
return langMap[ext] || 'unknown';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Generate symbol summary for LLM context
|
|
739
|
+
* @param {FileAST} ast - Parsed AST
|
|
740
|
+
* @returns {string}
|
|
741
|
+
*/
|
|
742
|
+
toSummary(ast) {
|
|
743
|
+
let summary = `# ${ast.path}\n\n`;
|
|
744
|
+
summary += `Language: ${ast.language}\n`;
|
|
745
|
+
summary += `Lines: ${ast.metadata.lineCount}\n`;
|
|
746
|
+
summary += `Symbols: ${ast.metadata.symbolCount}\n\n`;
|
|
747
|
+
|
|
748
|
+
// Imports
|
|
749
|
+
if (ast.imports.length > 0) {
|
|
750
|
+
summary += `## Dependencies\n\n`;
|
|
751
|
+
for (const imp of ast.imports) {
|
|
752
|
+
summary += `- ${imp.source}: ${imp.names.join(', ')}\n`;
|
|
753
|
+
}
|
|
754
|
+
summary += '\n';
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Classes
|
|
758
|
+
const classes = ast.symbols.filter(s => s.type === 'class');
|
|
759
|
+
if (classes.length > 0) {
|
|
760
|
+
summary += `## Classes\n\n`;
|
|
761
|
+
for (const cls of classes) {
|
|
762
|
+
summary += `### ${cls.name}`;
|
|
763
|
+
if (cls.extends) {
|
|
764
|
+
summary += ` extends ${Array.isArray(cls.extends) ? cls.extends.join(', ') : cls.extends}`;
|
|
765
|
+
}
|
|
766
|
+
summary += '\n';
|
|
767
|
+
|
|
768
|
+
if (cls.docstring) {
|
|
769
|
+
summary += `${cls.docstring}\n`;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (cls.methods && cls.methods.length > 0) {
|
|
773
|
+
summary += `Methods: ${cls.methods.join(', ')}\n`;
|
|
774
|
+
}
|
|
775
|
+
summary += '\n';
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Functions
|
|
780
|
+
const functions = ast.symbols.filter(s => s.type === 'function');
|
|
781
|
+
if (functions.length > 0) {
|
|
782
|
+
summary += `## Functions\n\n`;
|
|
783
|
+
for (const func of functions) {
|
|
784
|
+
summary += `- \`${func.name}(${(func.params || []).join(', ')})\``;
|
|
785
|
+
if (func.isAsync) summary += ' (async)';
|
|
786
|
+
if (func.isExported) summary += ' [exported]';
|
|
787
|
+
summary += '\n';
|
|
788
|
+
|
|
789
|
+
if (func.docstring) {
|
|
790
|
+
summary += ` ${func.docstring.split('\n')[0]}\n`;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
summary += '\n';
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Exports
|
|
797
|
+
if (ast.exports.length > 0) {
|
|
798
|
+
summary += `## Exports\n\n`;
|
|
799
|
+
summary += ast.exports.join(', ') + '\n';
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return summary;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Get cache key
|
|
807
|
+
* @param {string} filePath - File path
|
|
808
|
+
* @param {number} mtime - Modification time
|
|
809
|
+
* @returns {string}
|
|
810
|
+
*/
|
|
811
|
+
getCacheKey(filePath, mtime) {
|
|
812
|
+
return `ast:${filePath}:${mtime}`;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Get from cache
|
|
817
|
+
* @param {string} filePath - File path
|
|
818
|
+
* @param {number} mtime - Modification time
|
|
819
|
+
* @returns {FileAST|null}
|
|
820
|
+
*/
|
|
821
|
+
getFromCache(filePath, mtime) {
|
|
822
|
+
const key = this.getCacheKey(filePath, mtime);
|
|
823
|
+
return this.cache.get(key) || null;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Add to cache
|
|
828
|
+
* @param {string} filePath - File path
|
|
829
|
+
* @param {number} mtime - Modification time
|
|
830
|
+
* @param {FileAST} ast - AST result
|
|
831
|
+
*/
|
|
832
|
+
addToCache(filePath, mtime, ast) {
|
|
833
|
+
const key = this.getCacheKey(filePath, mtime);
|
|
834
|
+
this.cache.set(key, ast);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Clear cache
|
|
839
|
+
*/
|
|
840
|
+
clearCache() {
|
|
841
|
+
this.cache.clear();
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Create AST extractor
|
|
847
|
+
* @param {Object} options - Options
|
|
848
|
+
* @returns {ASTExtractor}
|
|
849
|
+
*/
|
|
850
|
+
function createASTExtractor(options = {}) {
|
|
851
|
+
return new ASTExtractor(options);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Extract AST from file
|
|
856
|
+
* @param {string} filePath - File path
|
|
857
|
+
* @param {Object} options - Options
|
|
858
|
+
* @returns {Promise<FileAST>}
|
|
859
|
+
*/
|
|
860
|
+
async function extractAST(filePath, options = {}) {
|
|
861
|
+
const extractor = createASTExtractor(options);
|
|
862
|
+
return extractor.extractFromFile(filePath);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
module.exports = {
|
|
866
|
+
ASTExtractor,
|
|
867
|
+
createASTExtractor,
|
|
868
|
+
extractAST,
|
|
869
|
+
PATTERNS
|
|
870
|
+
};
|