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,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
+ };