legacyver 2.1.0 → 2.1.2

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 (126) hide show
  1. package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.agent/skills/openspec-explore/SKILL.md +290 -0
  6. package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.agent/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.agent/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/.agent/workflows/opsx-apply.md +149 -0
  12. package/.agent/workflows/opsx-archive.md +154 -0
  13. package/.agent/workflows/opsx-bulk-archive.md +239 -0
  14. package/.agent/workflows/opsx-continue.md +111 -0
  15. package/.agent/workflows/opsx-explore.md +171 -0
  16. package/.agent/workflows/opsx-ff.md +91 -0
  17. package/.agent/workflows/opsx-new.md +66 -0
  18. package/.agent/workflows/opsx-onboard.md +522 -0
  19. package/.agent/workflows/opsx-sync.md +131 -0
  20. package/.agent/workflows/opsx-verify.md +161 -0
  21. package/.github/prompts/opsx-apply.prompt.md +149 -0
  22. package/.github/prompts/opsx-archive.prompt.md +154 -0
  23. package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
  24. package/.github/prompts/opsx-continue.prompt.md +111 -0
  25. package/.github/prompts/opsx-explore.prompt.md +171 -0
  26. package/.github/prompts/opsx-ff.prompt.md +91 -0
  27. package/.github/prompts/opsx-new.prompt.md +66 -0
  28. package/.github/prompts/opsx-onboard.prompt.md +522 -0
  29. package/.github/prompts/opsx-sync.prompt.md +131 -0
  30. package/.github/prompts/opsx-verify.prompt.md +161 -0
  31. package/.github/skills/openspec-apply-change/SKILL.md +156 -0
  32. package/.github/skills/openspec-archive-change/SKILL.md +114 -0
  33. package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  34. package/.github/skills/openspec-continue-change/SKILL.md +118 -0
  35. package/.github/skills/openspec-explore/SKILL.md +290 -0
  36. package/.github/skills/openspec-ff-change/SKILL.md +101 -0
  37. package/.github/skills/openspec-new-change/SKILL.md +74 -0
  38. package/.github/skills/openspec-onboard/SKILL.md +529 -0
  39. package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
  40. package/.github/skills/openspec-verify-change/SKILL.md +168 -0
  41. package/.legacyverignore.example +43 -0
  42. package/.legacyverrc +7 -0
  43. package/.opencode/command/opsx-apply.md +149 -0
  44. package/.opencode/command/opsx-archive.md +154 -0
  45. package/.opencode/command/opsx-bulk-archive.md +239 -0
  46. package/.opencode/command/opsx-continue.md +111 -0
  47. package/.opencode/command/opsx-explore.md +171 -0
  48. package/.opencode/command/opsx-ff.md +91 -0
  49. package/.opencode/command/opsx-new.md +66 -0
  50. package/.opencode/command/opsx-onboard.md +522 -0
  51. package/.opencode/command/opsx-sync.md +131 -0
  52. package/.opencode/command/opsx-verify.md +161 -0
  53. package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
  54. package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
  55. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  56. package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
  57. package/.opencode/skills/openspec-explore/SKILL.md +290 -0
  58. package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
  59. package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
  60. package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
  61. package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
  62. package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
  63. package/LICENSE +1 -1
  64. package/README.md +128 -83
  65. package/bin/legacyver.js +48 -25
  66. package/legacyver-docs/SUMMARY.md +3 -0
  67. package/legacyver-docs/components.md +57 -0
  68. package/legacyver-docs/index.md +15 -0
  69. package/nul +2 -0
  70. package/package.json +23 -25
  71. package/src/cache/hash.js +9 -10
  72. package/src/cache/index.js +43 -65
  73. package/src/cli/commands/analyze.js +212 -190
  74. package/src/cli/commands/cache.js +15 -35
  75. package/src/cli/commands/init.js +63 -107
  76. package/src/cli/commands/providers.js +56 -81
  77. package/src/cli/commands/version.js +7 -10
  78. package/src/cli/ui.js +58 -77
  79. package/src/crawler/filters.js +41 -40
  80. package/src/crawler/index.js +52 -36
  81. package/src/crawler/manifest.js +31 -43
  82. package/src/crawler/walk.js +32 -38
  83. package/src/llm/chunker.js +34 -56
  84. package/src/llm/cost-estimator.js +68 -51
  85. package/src/llm/free-model.js +67 -0
  86. package/src/llm/index.js +22 -43
  87. package/src/llm/prompts.js +45 -33
  88. package/src/llm/providers/gemini.js +94 -0
  89. package/src/llm/providers/groq.js +55 -40
  90. package/src/llm/providers/ollama.js +38 -65
  91. package/src/llm/providers/openrouter.js +67 -0
  92. package/src/llm/queue.js +59 -88
  93. package/src/llm/re-prompter.js +41 -0
  94. package/src/llm/validator.js +72 -0
  95. package/src/parser/ast/generic.js +45 -222
  96. package/src/parser/ast/go.js +86 -205
  97. package/src/parser/ast/java.js +76 -146
  98. package/src/parser/ast/javascript.js +173 -241
  99. package/src/parser/ast/laravel/blade.js +56 -0
  100. package/src/parser/ast/laravel/classifier.js +30 -0
  101. package/src/parser/ast/laravel/controller.js +35 -0
  102. package/src/parser/ast/laravel/index.js +54 -0
  103. package/src/parser/ast/laravel/model.js +41 -0
  104. package/src/parser/ast/laravel/provider.js +28 -0
  105. package/src/parser/ast/laravel/routes.js +45 -0
  106. package/src/parser/ast/php.js +129 -0
  107. package/src/parser/ast/python.js +76 -199
  108. package/src/parser/ast/typescript.js +10 -244
  109. package/src/parser/body-extractor.js +40 -0
  110. package/src/parser/call-graph.js +50 -67
  111. package/src/parser/complexity-scorer.js +59 -0
  112. package/src/parser/index.js +61 -86
  113. package/src/parser/pattern-detector.js +71 -0
  114. package/src/parser/pkg-builder.js +36 -83
  115. package/src/renderer/html.js +63 -135
  116. package/src/renderer/index.js +23 -35
  117. package/src/renderer/json.js +17 -35
  118. package/src/renderer/markdown.js +83 -117
  119. package/src/utils/config.js +52 -53
  120. package/src/utils/errors.js +26 -41
  121. package/src/utils/logger.js +32 -53
  122. package/src/cli/flags.js +0 -87
  123. package/src/llm/providers/anthropic.js +0 -57
  124. package/src/llm/providers/google.js +0 -65
  125. package/src/llm/providers/openai.js +0 -52
  126. package/src/parser/ast/tree-sitter-init.js +0 -80
package/src/llm/queue.js CHANGED
@@ -1,99 +1,70 @@
1
- /**
2
- * LLM request queue with concurrency control and retry logic.
3
- * Uses p-limit for concurrency and p-retry for retries.
4
- */
1
+ 'use strict';
5
2
 
6
- import pLimit from 'p-limit';
7
- import pRetry from 'p-retry';
8
- import { RateLimitError } from '../utils/errors.js';
9
- import { logger } from '../utils/logger.js';
10
-
11
- /**
12
- * @typedef {object} DocFragment
13
- * @property {string} relativePath
14
- * @property {string} content - Generated markdown documentation
15
- * @property {object} tokensUsed - { input, output }
16
- */
3
+ const pLimit = require('p-limit');
4
+ const pRetry = require('p-retry');
5
+ const logger = require('../utils/logger');
17
6
 
18
7
  /**
19
- * Create a processing queue for LLM requests.
20
- * @param {object} engine - LLM engine with `complete(request)` method
21
- * @param {object} options
22
- * @param {number} [options.concurrency=5] - Max concurrent requests
23
- * @param {function} [options.onProgress] - Called after each request completes
24
- * @param {function} [options.onError] - Called when a request fails after retries
25
- * @returns {object} Queue with processAll method
8
+ * Process chunks through the LLM provider with concurrency and retry.
9
+ * @param {Array} chunks
10
+ * @param {Object} provider
11
+ * @param {Object} config
12
+ * @param {{ onProgress, onError }} callbacks
13
+ * @returns {Promise<Array>} DocFragment[]
26
14
  */
27
- export function createQueue(engine, options = {}) {
28
- const {
29
- concurrency = 5,
30
- onProgress = () => {},
31
- onError = () => {},
32
- } = options;
33
-
34
- const limit = pLimit(Math.max(1, parseInt(concurrency, 10) || 5));
35
- const failures = [];
15
+ async function createQueue(chunks, provider, config, callbacks = {}) {
16
+ const concurrency = config.concurrency || 3;
17
+ const limit = pLimit(concurrency);
18
+ const fragments = [];
36
19
 
37
- return {
38
- /**
39
- * Process all chunks through the LLM engine.
40
- * @param {Array} chunks - LLM request chunks
41
- * @returns {Promise<DocFragment[]>}
42
- */
43
- async processAll(chunks) {
44
- const results = await Promise.allSettled(
45
- chunks.map((chunk) =>
46
- limit(async () => {
20
+ const tasks = chunks.map((chunk) =>
21
+ limit(async () => {
22
+ try {
23
+ const result = await pRetry(
24
+ async () => {
47
25
  try {
48
- const result = await pRetry(
49
- async () => {
50
- const response = await engine.complete({
51
- systemPrompt: chunk.systemPrompt,
52
- userMessage: chunk.userMessage,
53
- });
54
- return {
55
- relativePath: chunk.relativePath,
56
- content: response.content,
57
- tokensUsed: response.tokensUsed,
58
- };
59
- },
60
- {
61
- retries: 3,
62
- minTimeout: 1000,
63
- factor: 2,
64
- onFailedAttempt: (error) => {
65
- if (error instanceof RateLimitError) {
66
- logger.debug(
67
- `Rate limited for ${chunk.relativePath}, retrying (attempt ${error.attemptNumber}/4)...`
68
- );
69
- } else {
70
- logger.debug(
71
- `Request failed for ${chunk.relativePath}: ${error.message} (attempt ${error.attemptNumber}/4)`
72
- );
73
- }
74
- },
75
- }
76
- );
77
-
78
- onProgress();
79
- return result;
26
+ return await provider.complete(chunk);
80
27
  } catch (err) {
81
- onError(err, chunk);
82
- failures.push({ file: chunk.relativePath, error: err.message });
83
- onProgress();
84
- return null;
28
+ // If it's a rate-limit error, wait for the retry-after period
29
+ // before letting p-retry schedule the next attempt.
30
+ if (err.code === 'RATE_LIMIT' && err.retryAfter) {
31
+ await new Promise((r) => setTimeout(r, err.retryAfter));
32
+ }
33
+ throw err;
85
34
  }
86
- })
87
- )
88
- );
35
+ },
36
+ {
37
+ retries: 3,
38
+ minTimeout: config.isFreeModel ? 8000 : 1000,
39
+ factor: 3,
40
+ onFailedAttempt: (error) => {
41
+ logger.warn(`Retry ${error.attemptNumber}/3 for ${chunk.relativePath}: ${error.message}`);
42
+ },
43
+ }
44
+ );
45
+ fragments.push({
46
+ relativePath: chunk.relativePath,
47
+ content: result.content,
48
+ tokensUsed: result.tokensUsed,
49
+ _qualityWarnings: [],
50
+ });
51
+ } catch (e) {
52
+ logger.error(`Failed to generate docs for ${chunk.relativePath}: ${e.message}`);
53
+ if (callbacks.onError) callbacks.onError(e, chunk);
54
+ fragments.push({
55
+ relativePath: chunk.relativePath,
56
+ content: `<!-- Documentation generation failed: ${e.message} -->`,
57
+ tokensUsed: { input: 0, output: 0 },
58
+ _qualityWarnings: [`Generation failed: ${e.message}`],
59
+ });
60
+ }
89
61
 
90
- return results
91
- .map((r) => (r.status === 'fulfilled' ? r.value : null))
92
- .filter(Boolean);
93
- },
62
+ if (callbacks.onProgress) callbacks.onProgress();
63
+ })
64
+ );
94
65
 
95
- getFailures() {
96
- return failures;
97
- },
98
- };
66
+ await Promise.all(tasks);
67
+ return fragments;
99
68
  }
69
+
70
+ module.exports = { createQueue };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const { SYSTEM_PROMPT } = require('./prompts');
4
+ const logger = require('../utils/logger');
5
+
6
+ /**
7
+ * Re-prompt the LLM for missing exports.
8
+ * @param {Object} fragment
9
+ * @param {Object} fileFacts
10
+ * @param {Object} provider
11
+ * @param {Object} config
12
+ * @returns {Promise<Object|null>} updated fragment or null
13
+ */
14
+ async function reprompt(fragment, fileFacts, provider, config) {
15
+ const missing = (fileFacts.exports || []).filter(sym => sym && !fragment.content.includes(sym));
16
+
17
+ if (missing.length === 0) return null;
18
+
19
+ const followUpMessage =
20
+ `The following exported symbols were not documented in your previous response. ` +
21
+ `Please document each one based only on the FileFacts provided:\n\n` +
22
+ missing.map(s => `- ${s}`).join('\n');
23
+
24
+ try {
25
+ const result = await provider.complete({
26
+ systemPrompt: SYSTEM_PROMPT,
27
+ userMessage: followUpMessage,
28
+ relativePath: fragment.relativePath,
29
+ });
30
+ logger.info(`Re-prompted for ${fragment.relativePath}: ${missing.length} missing exports covered.`);
31
+ return {
32
+ ...fragment,
33
+ content: fragment.content + '\n\n' + result.content,
34
+ };
35
+ } catch (e) {
36
+ logger.warn(`Re-prompt failed for ${fragment.relativePath}: ${e.message}`);
37
+ return null;
38
+ }
39
+ }
40
+
41
+ module.exports = { reprompt };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ const logger = require('../utils/logger');
4
+
5
+ /**
6
+ * Post-generation quality validator.
7
+ *
8
+ * Two checks:
9
+ * 1. Hallucination check: identifiers in LLM output not found in FileFacts or raw source
10
+ * 2. Completeness check: exported symbols not mentioned in LLM output
11
+ */
12
+
13
+ /**
14
+ * @param {Object} fragment { relativePath, content }
15
+ * @param {Object} fileFacts
16
+ * @returns {{ hallucinations: string[], missingExports: string[], passed: boolean }}
17
+ */
18
+ function validateFragment(fragment, fileFacts) {
19
+ const hallucinations = [];
20
+ const missingExports = [];
21
+
22
+ if (!fileFacts) return { hallucinations, missingExports, passed: true };
23
+
24
+ const outputText = fragment.content || '';
25
+
26
+ // ── Hallucination check ──────────────────────────────────────────────────
27
+ // Collect all known identifiers from FileFacts
28
+ const knownIdentifiers = new Set();
29
+ for (const fn of (fileFacts.functions || [])) {
30
+ knownIdentifiers.add(fn.name);
31
+ for (const p of (fn.params || [])) knownIdentifiers.add(p.name && p.name.replace(/^\$/, ''));
32
+ }
33
+ for (const cls of (fileFacts.classes || [])) {
34
+ knownIdentifiers.add(cls.name);
35
+ for (const m of (cls.methods || [])) knownIdentifiers.add(m);
36
+ }
37
+ for (const imp of (fileFacts.imports || [])) {
38
+ for (const s of (imp.specifiers || [])) knownIdentifiers.add(s);
39
+ }
40
+ for (const exp of (fileFacts.exports || [])) knownIdentifiers.add(exp);
41
+
42
+ // Common words to skip (not identifiers)
43
+ const stopWords = new Set(['The', 'This', 'File', 'Function', 'Class', 'Method', 'Returns', 'Return', 'Parameter', 'Param', 'Import', 'Export', 'Overview', 'Usage', 'Example', 'Dependencies', 'Async', 'Static', 'Public', 'Private', 'Protected', 'Boolean', 'String', 'Number', 'Object', 'Array', 'Void', 'Null', 'Undefined', 'True', 'False', 'Error', 'Promise', 'Request', 'Response', 'Node', 'JavaScript', 'TypeScript', 'PHP', 'Python', 'Laravel', 'Express', 'Route', 'Controller', 'Model', 'Service', 'Repository', 'Middleware', 'Provider', 'Summary']);
44
+
45
+ const capitalizedIdentifiers = outputText.match(/\b([A-Z][a-zA-Z]{2,})\b/g) || [];
46
+ for (const identifier of capitalizedIdentifiers) {
47
+ if (stopWords.has(identifier)) continue;
48
+ if (!knownIdentifiers.has(identifier)) {
49
+ // Check raw source — it's embedded in the user message so we approximate
50
+ // by checking against function/class names only
51
+ const isInFacts = [...knownIdentifiers].some(k => k && k.toLowerCase() === identifier.toLowerCase());
52
+ if (!isInFacts) {
53
+ logger.warn(`Possible hallucination in ${fragment.relativePath}: identifier '${identifier}' not found in source facts`);
54
+ hallucinations.push(`Suspected hallucination: '${identifier}'`);
55
+ }
56
+ }
57
+ }
58
+
59
+ // ── Completeness check ───────────────────────────────────────────────────
60
+ const exportedSymbols = fileFacts.exports || [];
61
+ for (const symbol of exportedSymbols) {
62
+ if (symbol && !outputText.includes(symbol)) {
63
+ logger.warn(`Incomplete doc in ${fragment.relativePath}: exported symbol '${symbol}' not documented`);
64
+ missingExports.push(symbol);
65
+ }
66
+ }
67
+
68
+ const passed = hallucinations.length === 0 && missingExports.length === 0;
69
+ return { hallucinations, missingExports, passed };
70
+ }
71
+
72
+ module.exports = { validateFragment };
@@ -1,249 +1,72 @@
1
- /**
2
- * Generic regex-based fallback parser.
3
- * Extracts function-like patterns, import-like lines.
4
- * Returns partial FileFacts with parserType: "generic".
5
- */
1
+ 'use strict';
6
2
 
7
3
  /**
8
- * @param {string} source - Raw file content
9
- * @param {string} relativePath - Relative file path
10
- * @param {string} language - Detected language
11
- * @returns {import('../index.js').FileFacts}
4
+ * Generic regex-based fallback parser for unsupported languages.
12
5
  */
13
- export function parseGeneric(source, relativePath, language) {
14
- const lines = source.split('\n');
15
- const linesOfCode = lines.filter((l) => l.trim().length > 0).length;
16
-
17
- const functions = extractFunctions(source, language);
18
- const classes = extractClasses(source, language);
19
- const imports = extractImports(source, language);
20
- const exports = extractExports(source, language);
6
+ const logger = require('../../utils/logger');
21
7
 
22
- return {
23
- relativePath,
24
- language,
25
- linesOfCode,
26
- parserType: 'generic',
27
- functions,
28
- classes,
29
- imports,
30
- exports,
31
- callsTo: [],
32
- calledBy: [],
33
- hash: '',
34
- };
35
- }
36
-
37
- function extractFunctions(source, language) {
8
+ function parse(sourceText, relativePath, language) {
9
+ const lines = sourceText.split('\n');
38
10
  const functions = [];
39
- const lines = source.split('\n');
40
-
41
- // Patterns for different languages
42
- const patterns = [
43
- // JS/TS: function name(params)
44
- /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
45
- // JS/TS: const name = (params) =>
46
- /(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*=>/,
47
- // JS/TS: name(params) { (class method)
48
- /^\s+(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*\{/,
49
- // Python: def name(params):
50
- /def\s+(\w+)\s*\(([^)]*)\)\s*(?:->.*)?:/,
51
- // Java/Go: type name(params)
52
- /(?:public|private|protected|static|\s)*\s+\w+\s+(\w+)\s*\(([^)]*)\)/,
53
- // Go: func name(params)
54
- /func\s+(?:\([^)]*\)\s+)?(\w+)\s*\(([^)]*)\)/,
55
- ];
11
+ const imports = [];
12
+ const classes = [];
56
13
 
57
14
  for (let i = 0; i < lines.length; i++) {
58
15
  const line = lines[i];
59
- for (const pattern of patterns) {
60
- const match = line.match(pattern);
61
- if (match) {
62
- const name = match[1];
63
- // Skip common false positives
64
- if (['if', 'for', 'while', 'switch', 'catch', 'return'].includes(name)) continue;
65
-
66
- const params = parseParamString(match[2]);
67
- const isExported = /^export\s/.test(line.trim()) || /^module\.exports/.test(line.trim());
68
- const isAsync = /async\s/.test(line);
69
16
 
17
+ // Function-like patterns (very rough)
18
+ let m = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(?.*?\)?\s*=>|\bdef\s+(\w+)|(?:public|private|protected|static)?\s+function\s+(\w+))/);
19
+ if (m) {
20
+ const name = m[1] || m[2] || m[3] || m[4];
21
+ if (name) {
70
22
  functions.push({
71
23
  name,
72
- params,
24
+ params: [],
73
25
  returnType: null,
74
- isExported,
75
- isAsync,
26
+ isExported: /\bexport\b/.test(line),
27
+ isAsync: /\basync\b/.test(line),
76
28
  lineStart: i + 1,
77
- lineEnd: findBlockEnd(lines, i),
29
+ lineEnd: i + 1,
78
30
  calls: [],
31
+ complexityScore: null,
32
+ complexityClass: null,
33
+ detectedPatterns: [],
34
+ bodySnippet: null,
35
+ bodySnippetTruncated: false,
79
36
  });
80
- break;
81
37
  }
82
38
  }
83
- }
84
-
85
- return functions;
86
- }
87
-
88
- function extractClasses(source, language) {
89
- const classes = [];
90
- const classPattern = /class\s+(\w+)(?:\s+extends\s+(\w+))?/g;
91
39
 
92
- let match;
93
- while ((match = classPattern.exec(source)) !== null) {
94
- const name = match[1];
95
- const extendsName = match[2] || null;
96
-
97
- // Extract method names from the class body (simplified)
98
- const methods = [];
99
- const lines = source.substring(match.index).split('\n');
100
- const methodPattern = /^\s+(?:async\s+)?(\w+)\s*\(/;
101
- for (const line of lines.slice(1)) {
102
- const m = line.match(methodPattern);
103
- if (m && !['constructor', 'if', 'for', 'while'].includes(m[1])) {
104
- methods.push(m[1]);
105
- }
106
- if (/^}/.test(line.trim())) break;
40
+ // Import-like lines
41
+ m = line.match(/(?:import|require|from|#include|using)\s+['"]?([^'";\s]+)['"]?/);
42
+ if (m) {
43
+ imports.push({ module: m[1], specifiers: [] });
107
44
  }
108
45
 
109
- classes.push({ name, methods, extends: extendsName });
110
- }
111
-
112
- return classes;
113
- }
114
-
115
- function extractImports(source, language) {
116
- const imports = [];
117
- const lines = source.split('\n');
118
-
119
- for (const line of lines) {
120
- // ES imports
121
- const esMatch = line.match(/import\s+(?:{([^}]*)}\s+from\s+)?(?:(\w+)\s+from\s+)?['"]([^'"]+)['"]/);
122
- if (esMatch) {
123
- const specifiers = [];
124
- if (esMatch[1]) {
125
- specifiers.push(...esMatch[1].split(',').map((s) => s.trim().split(' as ')[0].trim()).filter(Boolean));
126
- }
127
- if (esMatch[2]) {
128
- specifiers.push(esMatch[2]);
129
- }
130
- imports.push({ module: esMatch[3], specifiers });
131
- continue;
132
- }
133
-
134
- // CommonJS require
135
- const cjsMatch = line.match(/(?:const|let|var)\s+(?:{([^}]*)}\s*=|(\w+)\s*=)\s*require\(['"]([^'"]+)['"]\)/);
136
- if (cjsMatch) {
137
- const specifiers = [];
138
- if (cjsMatch[1]) {
139
- specifiers.push(...cjsMatch[1].split(',').map((s) => s.trim()).filter(Boolean));
140
- }
141
- if (cjsMatch[2]) {
142
- specifiers.push(cjsMatch[2]);
143
- }
144
- imports.push({ module: cjsMatch[3], specifiers });
145
- continue;
146
- }
147
-
148
- // Python imports
149
- const pyFromMatch = line.match(/from\s+(\S+)\s+import\s+(.+)/);
150
- if (pyFromMatch) {
151
- const specifiers = pyFromMatch[2].split(',').map((s) => s.trim().split(' as ')[0].trim()).filter(Boolean);
152
- imports.push({ module: pyFromMatch[1], specifiers });
153
- continue;
154
- }
155
-
156
- const pyImportMatch = line.match(/^import\s+(\S+)(?:\s+as\s+\w+)?$/);
157
- if (pyImportMatch) {
158
- imports.push({ module: pyImportMatch[1], specifiers: [pyImportMatch[1]] });
159
- continue;
160
- }
161
-
162
- // Go imports
163
- const goMatch = line.match(/^\s*"([^"]+)"/);
164
- if (language === 'go' && goMatch) {
165
- imports.push({ module: goMatch[1], specifiers: [] });
166
- continue;
167
- }
168
-
169
- // Java imports
170
- const javaMatch = line.match(/import\s+([\w.]+(?:\.\*)?)\s*;/);
171
- if (javaMatch) {
172
- const parts = javaMatch[1].split('.');
173
- imports.push({ module: javaMatch[1], specifiers: [parts[parts.length - 1]] });
46
+ // Class definitions
47
+ m = line.match(/\bclass\s+(\w+)/);
48
+ if (m) {
49
+ classes.push({ name: m[1], methods: [], extends: null });
174
50
  }
175
51
  }
176
52
 
177
- return imports;
178
- }
53
+ const exports = functions.filter(f => f.isExported).map(f => f.name);
179
54
 
180
- function extractExports(source, language) {
181
- const exports = [];
182
- const lines = source.split('\n');
55
+ logger.warn(`No AST parser for ${language || 'unknown'}, using generic fallback: ${relativePath}`);
183
56
 
184
- for (const line of lines) {
185
- // ES named export
186
- const esMatch = line.match(/export\s+(?:const|let|var|function|class|async function)\s+(\w+)/);
187
- if (esMatch) {
188
- exports.push(esMatch[1]);
189
- continue;
190
- }
191
-
192
- // ES default export
193
- if (/export\s+default\s/.test(line)) {
194
- const defMatch = line.match(/export\s+default\s+(?:class|function)?\s*(\w+)/);
195
- if (defMatch) exports.push(defMatch[1]);
196
- continue;
197
- }
198
-
199
- // CommonJS
200
- const cjsMatch = line.match(/module\.exports\s*=\s*(?:{([^}]*)}\s*|(\w+))/);
201
- if (cjsMatch) {
202
- if (cjsMatch[1]) {
203
- exports.push(...cjsMatch[1].split(',').map((s) => s.trim().split(':')[0].trim()).filter(Boolean));
204
- }
205
- if (cjsMatch[2]) {
206
- exports.push(cjsMatch[2]);
207
- }
208
- }
209
- }
210
-
211
- return exports;
212
- }
213
-
214
- function parseParamString(paramStr) {
215
- if (!paramStr || !paramStr.trim()) return [];
216
- return paramStr.split(',').map((p) => {
217
- const parts = p.trim().split(/[:\s]+/);
218
- return {
219
- name: parts[0].replace(/[=?].*/, ''),
220
- type: parts.length > 1 ? parts.slice(1).join(' ') : null,
221
- };
222
- }).filter((p) => p.name && p.name !== '');
57
+ return {
58
+ relativePath,
59
+ language: language || 'unknown',
60
+ linesOfCode: lines.length,
61
+ functions,
62
+ classes,
63
+ imports,
64
+ exports,
65
+ callsTo: [],
66
+ calledBy: [],
67
+ hash: null,
68
+ parserType: 'generic',
69
+ };
223
70
  }
224
71
 
225
- function findBlockEnd(lines, startLine) {
226
- let depth = 0;
227
- let started = false;
228
- for (let i = startLine; i < lines.length; i++) {
229
- for (const ch of lines[i]) {
230
- if (ch === '{' || ch === ':') {
231
- if (ch === '{') { depth++; started = true; }
232
- else if (ch === ':' && !started && i === startLine) { started = true; depth = 1; }
233
- }
234
- if (ch === '}' && started) {
235
- depth--;
236
- if (depth <= 0) return i + 1;
237
- }
238
- }
239
- // Python: detect end by indentation
240
- if (started && depth === 0 && i > startLine) {
241
- const indent = lines[i].match(/^\s*/)[0].length;
242
- const startIndent = lines[startLine].match(/^\s*/)[0].length;
243
- if (indent <= startIndent && lines[i].trim() !== '') {
244
- return i;
245
- }
246
- }
247
- }
248
- return Math.min(startLine + 50, lines.length);
249
- }
72
+ module.exports = { parse };