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.
- package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
- package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
- package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
- package/.agent/skills/openspec-explore/SKILL.md +290 -0
- package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
- package/.agent/skills/openspec-new-change/SKILL.md +74 -0
- package/.agent/skills/openspec-onboard/SKILL.md +529 -0
- package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
- package/.agent/workflows/opsx-apply.md +149 -0
- package/.agent/workflows/opsx-archive.md +154 -0
- package/.agent/workflows/opsx-bulk-archive.md +239 -0
- package/.agent/workflows/opsx-continue.md +111 -0
- package/.agent/workflows/opsx-explore.md +171 -0
- package/.agent/workflows/opsx-ff.md +91 -0
- package/.agent/workflows/opsx-new.md +66 -0
- package/.agent/workflows/opsx-onboard.md +522 -0
- package/.agent/workflows/opsx-sync.md +131 -0
- package/.agent/workflows/opsx-verify.md +161 -0
- package/.github/prompts/opsx-apply.prompt.md +149 -0
- package/.github/prompts/opsx-archive.prompt.md +154 -0
- package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
- package/.github/prompts/opsx-continue.prompt.md +111 -0
- package/.github/prompts/opsx-explore.prompt.md +171 -0
- package/.github/prompts/opsx-ff.prompt.md +91 -0
- package/.github/prompts/opsx-new.prompt.md +66 -0
- package/.github/prompts/opsx-onboard.prompt.md +522 -0
- package/.github/prompts/opsx-sync.prompt.md +131 -0
- package/.github/prompts/opsx-verify.prompt.md +161 -0
- package/.github/skills/openspec-apply-change/SKILL.md +156 -0
- package/.github/skills/openspec-archive-change/SKILL.md +114 -0
- package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.github/skills/openspec-continue-change/SKILL.md +118 -0
- package/.github/skills/openspec-explore/SKILL.md +290 -0
- package/.github/skills/openspec-ff-change/SKILL.md +101 -0
- package/.github/skills/openspec-new-change/SKILL.md +74 -0
- package/.github/skills/openspec-onboard/SKILL.md +529 -0
- package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.github/skills/openspec-verify-change/SKILL.md +168 -0
- package/.legacyverignore.example +43 -0
- package/.legacyverrc +7 -0
- package/.opencode/command/opsx-apply.md +149 -0
- package/.opencode/command/opsx-archive.md +154 -0
- package/.opencode/command/opsx-bulk-archive.md +239 -0
- package/.opencode/command/opsx-continue.md +111 -0
- package/.opencode/command/opsx-explore.md +171 -0
- package/.opencode/command/opsx-ff.md +91 -0
- package/.opencode/command/opsx-new.md +66 -0
- package/.opencode/command/opsx-onboard.md +522 -0
- package/.opencode/command/opsx-sync.md +131 -0
- package/.opencode/command/opsx-verify.md +161 -0
- package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
- package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
- package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
- package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
- package/.opencode/skills/openspec-explore/SKILL.md +290 -0
- package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
- package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
- package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
- package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
- package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
- package/LICENSE +1 -1
- package/README.md +128 -83
- package/bin/legacyver.js +48 -25
- package/legacyver-docs/SUMMARY.md +3 -0
- package/legacyver-docs/components.md +57 -0
- package/legacyver-docs/index.md +15 -0
- package/nul +2 -0
- package/package.json +23 -25
- package/src/cache/hash.js +9 -10
- package/src/cache/index.js +43 -65
- package/src/cli/commands/analyze.js +212 -190
- package/src/cli/commands/cache.js +15 -35
- package/src/cli/commands/init.js +63 -107
- package/src/cli/commands/providers.js +56 -81
- package/src/cli/commands/version.js +7 -10
- package/src/cli/ui.js +58 -77
- package/src/crawler/filters.js +41 -40
- package/src/crawler/index.js +52 -36
- package/src/crawler/manifest.js +31 -43
- package/src/crawler/walk.js +32 -38
- package/src/llm/chunker.js +34 -56
- package/src/llm/cost-estimator.js +68 -51
- package/src/llm/free-model.js +67 -0
- package/src/llm/index.js +22 -43
- package/src/llm/prompts.js +45 -33
- package/src/llm/providers/gemini.js +94 -0
- package/src/llm/providers/groq.js +55 -40
- package/src/llm/providers/ollama.js +38 -65
- package/src/llm/providers/openrouter.js +67 -0
- package/src/llm/queue.js +59 -88
- package/src/llm/re-prompter.js +41 -0
- package/src/llm/validator.js +72 -0
- package/src/parser/ast/generic.js +45 -222
- package/src/parser/ast/go.js +86 -205
- package/src/parser/ast/java.js +76 -146
- package/src/parser/ast/javascript.js +173 -241
- package/src/parser/ast/laravel/blade.js +56 -0
- package/src/parser/ast/laravel/classifier.js +30 -0
- package/src/parser/ast/laravel/controller.js +35 -0
- package/src/parser/ast/laravel/index.js +54 -0
- package/src/parser/ast/laravel/model.js +41 -0
- package/src/parser/ast/laravel/provider.js +28 -0
- package/src/parser/ast/laravel/routes.js +45 -0
- package/src/parser/ast/php.js +129 -0
- package/src/parser/ast/python.js +76 -199
- package/src/parser/ast/typescript.js +10 -244
- package/src/parser/body-extractor.js +40 -0
- package/src/parser/call-graph.js +50 -67
- package/src/parser/complexity-scorer.js +59 -0
- package/src/parser/index.js +61 -86
- package/src/parser/pattern-detector.js +71 -0
- package/src/parser/pkg-builder.js +36 -83
- package/src/renderer/html.js +63 -135
- package/src/renderer/index.js +23 -35
- package/src/renderer/json.js +17 -35
- package/src/renderer/markdown.js +83 -117
- package/src/utils/config.js +52 -53
- package/src/utils/errors.js +26 -41
- package/src/utils/logger.js +32 -53
- package/src/cli/flags.js +0 -87
- package/src/llm/providers/anthropic.js +0 -57
- package/src/llm/providers/google.js +0 -65
- package/src/llm/providers/openai.js +0 -52
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
*
|
|
20
|
-
* @param {
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {
|
|
23
|
-
* @param {
|
|
24
|
-
* @
|
|
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
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
},
|
|
62
|
+
if (callbacks.onProgress) callbacks.onProgress();
|
|
63
|
+
})
|
|
64
|
+
);
|
|
94
65
|
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
178
|
-
}
|
|
53
|
+
const exports = functions.filter(f => f.isExported).map(f => f.name);
|
|
179
54
|
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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 };
|