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
@@ -1,250 +1,16 @@
1
- /**
2
- * TypeScript AST parser using web-tree-sitter.
3
- * Extends JS parser with TypeScript grammar and type annotations.
4
- */
5
-
6
- import { initTreeSitter, loadLanguage, createParser } from './tree-sitter-init.js';
7
- import { parseGeneric } from './generic.js';
8
-
9
- let tsParser = null;
10
- let tsInitialized = false;
11
-
12
- async function ensureParser() {
13
- if (tsInitialized) return tsParser;
14
- tsInitialized = true;
15
-
16
- const Parser = await initTreeSitter();
17
- if (!Parser) return null;
18
-
19
- const language = await loadLanguage('typescript');
20
- if (!language) return null;
21
-
22
- tsParser = createParser(language);
23
- return tsParser;
24
- }
1
+ 'use strict';
25
2
 
26
3
  /**
27
- * Parse a TypeScript file and extract structural facts.
28
- * @param {string} source - Raw file content
29
- * @param {string} relativePath - Relative file path
30
- * @returns {Promise<import('../../parser/index.js').FileFacts>}
4
+ * TypeScript parser extends JS parser, handles type annotations.
31
5
  */
32
- export async function parseTypeScript(source, relativePath) {
33
- const parser = await ensureParser();
34
-
35
- if (!parser) {
36
- return parseGeneric(source, relativePath, 'typescript');
37
- }
38
-
39
- const tree = parser.parse(source);
40
- const rootNode = tree.rootNode;
41
-
42
- const functions = [];
43
- const classes = [];
44
- const imports = [];
45
- const exports = [];
46
- const lines = source.split('\n');
47
- const linesOfCode = lines.filter((l) => l.trim().length > 0).length;
48
-
49
- walkNode(rootNode, { functions, classes, imports, exports, source, lines });
50
-
51
- return {
52
- relativePath,
53
- language: 'typescript',
54
- linesOfCode,
55
- parserType: 'tree-sitter',
56
- functions,
57
- classes,
58
- imports,
59
- exports: exports.map((e) => e.name || e),
60
- callsTo: [],
61
- calledBy: [],
62
- hash: '',
63
- };
64
- }
65
-
66
- function walkNode(node, ctx) {
67
- switch (node.type) {
68
- case 'function_declaration':
69
- extractFunction(node, ctx, false);
70
- break;
71
- case 'export_statement':
72
- handleExport(node, ctx);
73
- break;
74
- case 'import_statement':
75
- extractImport(node, ctx);
76
- break;
77
- case 'class_declaration':
78
- extractClass(node, ctx);
79
- break;
80
- case 'lexical_declaration':
81
- case 'variable_declaration':
82
- extractVariableDeclaration(node, ctx);
83
- break;
84
- case 'interface_declaration':
85
- case 'type_alias_declaration':
86
- // Track type exports
87
- break;
88
- }
89
-
90
- for (let i = 0; i < node.childCount; i++) {
91
- walkNode(node.child(i), ctx);
92
- }
93
- }
94
-
95
- function extractFunction(node, ctx, isExported) {
96
- const nameNode = node.childForFieldName('name');
97
- const paramsNode = node.childForFieldName('parameters');
98
- const returnTypeNode = node.childForFieldName('return_type');
99
- const isAsync = node.text.startsWith('async');
6
+ const { parse: parseJS } = require('./javascript');
100
7
 
101
- if (nameNode) {
102
- ctx.functions.push({
103
- name: nameNode.text,
104
- params: extractTypedParams(paramsNode),
105
- returnType: returnTypeNode ? returnTypeNode.text.replace(/^:\s*/, '') : null,
106
- isExported,
107
- isAsync,
108
- lineStart: node.startPosition.row + 1,
109
- lineEnd: node.endPosition.row + 1,
110
- calls: [],
111
- });
112
- }
8
+ function parse(sourceText, relativePath) {
9
+ const result = parseJS(sourceText, relativePath);
10
+ result.language = 'typescript';
11
+ // TypeScript annotations are partially handled in the JS parser already
12
+ // (the regex catches `: Type` patterns in params and return types)
13
+ return result;
113
14
  }
114
15
 
115
- function extractClass(node, ctx) {
116
- const nameNode = node.childForFieldName('name');
117
- const bodyNode = node.childForFieldName('body');
118
-
119
- let superclass = null;
120
- for (let i = 0; i < node.childCount; i++) {
121
- const child = node.child(i);
122
- if (child.type === 'class_heritage') {
123
- const extendsClause = child.text;
124
- const match = extendsClause.match(/extends\s+(\w+)/);
125
- if (match) superclass = match[1];
126
- }
127
- }
128
-
129
- if (nameNode) {
130
- const methods = [];
131
- if (bodyNode) {
132
- for (let i = 0; i < bodyNode.childCount; i++) {
133
- const child = bodyNode.child(i);
134
- if (child.type === 'method_definition' || child.type === 'public_field_definition') {
135
- const methodName = child.childForFieldName('name');
136
- if (methodName) methods.push(methodName.text);
137
- }
138
- }
139
- }
140
-
141
- ctx.classes.push({
142
- name: nameNode.text,
143
- methods,
144
- extends: superclass,
145
- });
146
- }
147
- }
148
-
149
- function extractImport(node, ctx) {
150
- const sourceNode = node.childForFieldName('source');
151
- if (!sourceNode) return;
152
-
153
- const module = sourceNode.text.replace(/['"]/g, '');
154
- const specifiers = [];
155
-
156
- for (let i = 0; i < node.childCount; i++) {
157
- const child = node.child(i);
158
- if (child.type === 'import_clause') {
159
- for (let j = 0; j < child.childCount; j++) {
160
- const clauseChild = child.child(j);
161
- if (clauseChild.type === 'identifier') {
162
- specifiers.push(clauseChild.text);
163
- } else if (clauseChild.type === 'named_imports') {
164
- for (let k = 0; k < clauseChild.childCount; k++) {
165
- const importSpec = clauseChild.child(k);
166
- if (importSpec.type === 'import_specifier') {
167
- const name = importSpec.childForFieldName('name');
168
- if (name) specifiers.push(name.text);
169
- }
170
- }
171
- }
172
- }
173
- }
174
- }
175
-
176
- ctx.imports.push({ module, specifiers });
177
- }
178
-
179
- function handleExport(node, ctx) {
180
- for (let i = 0; i < node.childCount; i++) {
181
- const child = node.child(i);
182
- if (child.type === 'function_declaration') {
183
- extractFunction(child, ctx, true);
184
- const nameNode = child.childForFieldName('name');
185
- if (nameNode) ctx.exports.push({ name: nameNode.text });
186
- } else if (child.type === 'class_declaration') {
187
- extractClass(child, ctx);
188
- const nameNode = child.childForFieldName('name');
189
- if (nameNode) ctx.exports.push({ name: nameNode.text });
190
- } else if (child.type === 'lexical_declaration') {
191
- extractVariableDeclaration(child, ctx);
192
- for (let j = 0; j < child.childCount; j++) {
193
- const decl = child.child(j);
194
- if (decl.type === 'variable_declarator') {
195
- const nameNode = decl.childForFieldName('name');
196
- if (nameNode) ctx.exports.push({ name: nameNode.text });
197
- }
198
- }
199
- } else if (child.type === 'interface_declaration' || child.type === 'type_alias_declaration') {
200
- const nameNode = child.childForFieldName('name');
201
- if (nameNode) ctx.exports.push({ name: nameNode.text });
202
- }
203
- }
204
- }
205
-
206
- function extractVariableDeclaration(node, ctx) {
207
- for (let i = 0; i < node.childCount; i++) {
208
- const child = node.child(i);
209
- if (child.type === 'variable_declarator') {
210
- const nameNode = child.childForFieldName('name');
211
- const valueNode = child.childForFieldName('value');
212
- if (nameNode && valueNode) {
213
- if (valueNode.type === 'arrow_function' || valueNode.type === 'function') {
214
- const paramsNode = valueNode.childForFieldName('parameters');
215
- const returnTypeNode = valueNode.childForFieldName('return_type');
216
- const isAsync = valueNode.text.startsWith('async');
217
- ctx.functions.push({
218
- name: nameNode.text,
219
- params: extractTypedParams(paramsNode),
220
- returnType: returnTypeNode ? returnTypeNode.text.replace(/^:\s*/, '') : null,
221
- isExported: false,
222
- isAsync,
223
- lineStart: node.startPosition.row + 1,
224
- lineEnd: node.endPosition.row + 1,
225
- calls: [],
226
- });
227
- }
228
- }
229
- }
230
- }
231
- }
232
-
233
- function extractTypedParams(paramsNode) {
234
- if (!paramsNode) return [];
235
- const params = [];
236
- for (let i = 0; i < paramsNode.childCount; i++) {
237
- const child = paramsNode.child(i);
238
- if (child.type === 'required_parameter' || child.type === 'optional_parameter') {
239
- const nameNode = child.childForFieldName('pattern') || child.childForFieldName('name');
240
- const typeNode = child.childForFieldName('type');
241
- params.push({
242
- name: nameNode ? nameNode.text : child.text,
243
- type: typeNode ? typeNode.text.replace(/^:\s*/, '') : null,
244
- });
245
- } else if (child.type === 'identifier') {
246
- params.push({ name: child.text, type: null });
247
- }
248
- }
249
- return params;
250
- }
16
+ module.exports = { parse };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const MAX_BODY_LINES = 60;
4
+
5
+ /**
6
+ * Extract body snippet from source text given line start/end positions.
7
+ * Returns null for simple functions (complexityScore <= 3).
8
+ * @param {string} sourceText
9
+ * @param {number} lineStart 1-indexed
10
+ * @param {number} lineEnd 1-indexed
11
+ * @param {number} complexityScore
12
+ * @returns {{ bodySnippet: string|null, bodySnippetTruncated: boolean }}
13
+ */
14
+ function extractBodySnippet(sourceText, lineStart, lineEnd, complexityScore) {
15
+ if (complexityScore <= 3) {
16
+ return { bodySnippet: null, bodySnippetTruncated: false };
17
+ }
18
+
19
+ if (!sourceText || lineStart == null || lineEnd == null) {
20
+ return { bodySnippet: null, bodySnippetTruncated: false };
21
+ }
22
+
23
+ const lines = sourceText.split('\n');
24
+ const startIdx = Math.max(0, (lineStart || 1) - 1);
25
+ const endIdx = Math.min(lines.length, lineEnd || lines.length);
26
+ const bodyLines = lines.slice(startIdx, endIdx);
27
+
28
+ let truncated = false;
29
+ let snippet;
30
+ if (bodyLines.length > MAX_BODY_LINES) {
31
+ snippet = bodyLines.slice(0, MAX_BODY_LINES).join('\n');
32
+ truncated = true;
33
+ } else {
34
+ snippet = bodyLines.join('\n');
35
+ }
36
+
37
+ return { bodySnippet: snippet, bodySnippetTruncated: truncated };
38
+ }
39
+
40
+ module.exports = { extractBodySnippet };
@@ -1,86 +1,69 @@
1
- /**
2
- * Call graph resolver.
3
- * Post-processes all FileFacts:
4
- * - Resolves relative import paths to file paths
5
- * - Populates calledBy arrays on referenced files
6
- */
7
-
8
- import { dirname, join, normalize } from 'node:path';
1
+ 'use strict';
9
2
 
10
3
  /**
11
- * Resolve call graph across all file facts.
12
- * @param {import('./index.js').FileFacts[]} allFacts
13
- * @returns {import('./index.js').FileFacts[]} Facts with callsTo/calledBy populated
4
+ * Build cross-file call graph by resolving import paths.
14
5
  */
15
- export function resolveCallGraph(allFacts) {
16
- // Build a map of relative paths for lookup
17
- const pathMap = new Map();
6
+ function buildCallGraph(allFacts) {
7
+ const fileMap = new Map();
18
8
  for (const facts of allFacts) {
19
- pathMap.set(normalize(facts.relativePath), facts);
20
- // Also index without extension for resolution
21
- const withoutExt = facts.relativePath.replace(/\.\w+$/, '');
22
- pathMap.set(normalize(withoutExt), facts);
9
+ fileMap.set(facts.relativePath, facts);
23
10
  }
24
11
 
25
- // Resolve imports to file paths
26
12
  for (const facts of allFacts) {
27
- const callsTo = new Set();
13
+ facts.callsTo = [];
14
+ facts.calledBy = [];
15
+ }
28
16
 
29
- for (const imp of facts.imports) {
30
- // Skip external modules (no ./ or ../ prefix and no file extension)
31
- if (!imp.module.startsWith('.') && !imp.module.startsWith('/')) {
32
- continue;
17
+ for (const facts of allFacts) {
18
+ for (const imp of (facts.imports || [])) {
19
+ const resolved = resolveImport(imp.module, facts.relativePath, fileMap);
20
+ if (resolved && !facts.callsTo.includes(resolved)) {
21
+ facts.callsTo.push(resolved);
22
+ const target = fileMap.get(resolved);
23
+ if (target && !target.calledBy.includes(facts.relativePath)) {
24
+ target.calledBy.push(facts.relativePath);
25
+ }
33
26
  }
27
+ }
34
28
 
35
- // Resolve relative import
36
- const importerDir = dirname(facts.relativePath);
37
- const resolved = normalize(join(importerDir, imp.module));
38
-
39
- // Try exact match and common extensions
40
- const candidates = [
41
- resolved,
42
- resolved + '.js',
43
- resolved + '.ts',
44
- resolved + '.tsx',
45
- resolved + '.jsx',
46
- resolved + '.mjs',
47
- resolved + '.py',
48
- resolved + '.java',
49
- resolved + '.go',
50
- join(resolved, 'index.js'),
51
- join(resolved, 'index.ts'),
52
- ];
53
-
54
- for (const candidate of candidates) {
55
- const normalizedCandidate = normalize(candidate);
56
- if (pathMap.has(normalizedCandidate)) {
57
- callsTo.add(pathMap.get(normalizedCandidate).relativePath);
58
- break;
29
+ // Laravel: resolve controller references from route laravelContext
30
+ if (facts.laravelContext && facts.laravelContext.type === 'route_file') {
31
+ for (const route of (facts.laravelContext.routes || [])) {
32
+ if (route.controller) {
33
+ // Try to find a matching controller file
34
+ for (const [relPath] of fileMap) {
35
+ if (relPath.includes(route.controller)) {
36
+ if (!facts.callsTo.includes(relPath)) facts.callsTo.push(relPath);
37
+ const target = fileMap.get(relPath);
38
+ if (target && !target.calledBy.includes(facts.relativePath)) {
39
+ target.calledBy.push(facts.relativePath);
40
+ }
41
+ }
42
+ }
59
43
  }
60
44
  }
61
45
  }
62
-
63
- facts.callsTo = [...callsTo];
64
46
  }
65
47
 
66
- // Build calledBy from callsTo
67
- const calledByMap = new Map();
68
- for (const facts of allFacts) {
69
- calledByMap.set(facts.relativePath, new Set());
70
- }
48
+ return allFacts;
49
+ }
71
50
 
72
- for (const facts of allFacts) {
73
- for (const target of facts.callsTo) {
74
- if (calledByMap.has(target)) {
75
- calledByMap.get(target).add(facts.relativePath);
76
- }
77
- }
78
- }
51
+ function resolveImport(modulePath, fromPath, fileMap) {
52
+ if (!modulePath.startsWith('.')) return null;
79
53
 
80
- for (const facts of allFacts) {
81
- const callers = calledByMap.get(facts.relativePath);
82
- facts.calledBy = callers ? [...callers] : [];
83
- }
54
+ const fromDir = fromPath.includes('/') ? fromPath.split('/').slice(0, -1).join('/') : '';
55
+ const candidates = [
56
+ `${fromDir}/${modulePath}`,
57
+ `${fromDir}/${modulePath}.js`,
58
+ `${fromDir}/${modulePath}.ts`,
59
+ `${fromDir}/${modulePath}/index.js`,
60
+ `${fromDir}/${modulePath}/index.ts`,
61
+ ].map(p => p.replace(/^\//, '').replace(/\/\//g, '/'));
84
62
 
85
- return allFacts;
63
+ for (const candidate of candidates) {
64
+ if (fileMap.has(candidate)) return candidate;
65
+ }
66
+ return null;
86
67
  }
68
+
69
+ module.exports = { buildCallGraph };
@@ -0,0 +1,59 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Complexity scorer — given function/method facts + body text,
5
+ * computes a complexityScore and classifies it.
6
+ */
7
+
8
+ const DOMAIN_PATTERNS = require('./pattern-detector');
9
+
10
+ /**
11
+ * Score a function based on its body text using heuristics.
12
+ * @param {string} bodyText raw source of the function body
13
+ * @returns {{ complexityScore: number, complexityClass: string, detectedPatterns: string[] }}
14
+ */
15
+ function scoreComplexity(bodyText) {
16
+ if (!bodyText) {
17
+ return { complexityScore: 0, complexityClass: 'simple', detectedPatterns: [] };
18
+ }
19
+
20
+ let score = 0;
21
+
22
+ // Binary/ternary operators
23
+ const arithmeticOps = (bodyText.match(/[\+\-\*\/\%\*\*\?\?]/g) || []).length;
24
+ score += Math.min(arithmeticOps, 5); // cap to avoid noise
25
+
26
+ // Conditional branches
27
+ const ifCount = (bodyText.match(/\bif\s*\(/g) || []).length;
28
+ const elseIfCount = (bodyText.match(/\belse\s+if\s*\(/g) || []).length;
29
+ const switchCase = (bodyText.match(/\bcase\b/g) || []).length;
30
+ const ternary = (bodyText.match(/\?[^:]/g) || []).length;
31
+ score += ifCount + elseIfCount + switchCase + ternary;
32
+
33
+ // Loop constructs
34
+ const forCount = (bodyText.match(/\bfor\s*\(/g) || []).length;
35
+ const foreachCount = (bodyText.match(/\bforeach\s*\(/g) || []).length;
36
+ const whileCount = (bodyText.match(/\bwhile\s*\(/g) || []).length;
37
+ const doCount = (bodyText.match(/\bdo\s*\{/g) || []).length;
38
+ score += forCount + foreachCount + whileCount + doCount;
39
+
40
+ // Nesting depth beyond level 1
41
+ let depth = 0;
42
+ let maxDepth = 0;
43
+ for (const ch of bodyText) {
44
+ if (ch === '{') { depth++; if (depth > maxDepth) maxDepth = depth; }
45
+ else if (ch === '}') { depth--; }
46
+ }
47
+ if (maxDepth > 1) score += maxDepth - 1;
48
+
49
+ // Domain patterns
50
+ const detectedPatterns = DOMAIN_PATTERNS.detectPatterns(bodyText);
51
+ score += detectedPatterns.length * 2;
52
+
53
+ const complexityClass =
54
+ score <= 3 ? 'simple' : score <= 8 ? 'moderate' : 'complex';
55
+
56
+ return { complexityScore: score, complexityClass, detectedPatterns };
57
+ }
58
+
59
+ module.exports = { scoreComplexity };
@@ -1,106 +1,81 @@
1
- /**
2
- * Language-aware parser dispatcher.
3
- * Maps language string to correct AST parser.
4
- * Falls back to generic.js on unsupported language or Tree-sitter parse failure.
5
- */
1
+ 'use strict';
6
2
 
7
- import { readFileSync } from 'node:fs';
8
- import { parseJavaScript } from './ast/javascript.js';
9
- import { parseTypeScript } from './ast/typescript.js';
10
- import { parsePython } from './ast/python.js';
11
- import { parseJava } from './ast/java.js';
12
- import { parseGo } from './ast/go.js';
13
- import { parseGeneric } from './ast/generic.js';
14
- import { logger } from '../utils/logger.js';
3
+ const path = require('path');
4
+ const { readFileSync } = require('fs');
5
+ const logger = require('../utils/logger');
15
6
 
16
- /**
17
- * @typedef {object} FileFacts
18
- * @property {string} relativePath
19
- * @property {string} language
20
- * @property {number} linesOfCode
21
- * @property {string} parserType - "tree-sitter" or "generic"
22
- * @property {Array} functions
23
- * @property {Array} classes
24
- * @property {Array} imports
25
- * @property {string[]} exports
26
- * @property {string[]} callsTo
27
- * @property {string[]} calledBy
28
- * @property {string} hash
29
- */
30
-
31
- const LANGUAGE_PARSERS = {
32
- javascript: parseJavaScript,
33
- typescript: parseTypeScript,
34
- python: parsePython,
35
- java: parseJava,
36
- go: parseGo,
7
+ const PARSERS = {
8
+ javascript: () => require('./ast/javascript'),
9
+ typescript: () => require('./ast/typescript'),
10
+ python: () => require('./ast/python'),
11
+ java: () => require('./ast/java'),
12
+ go: () => require('./ast/go'),
13
+ php: () => require('./ast/php'),
37
14
  };
38
15
 
39
16
  /**
40
- * Parse a single file and return FileFacts.
41
- * @param {import('../crawler/manifest.js').FileManifest} fileManifest
42
- * @param {string} targetDir - Base directory
43
- * @returns {Promise<FileFacts>}
17
+ * Dispatch parsing to the appropriate language parser.
18
+ * @param {import('../crawler/index').FileManifest} fileManifest
19
+ * @param {Object} meta project metadata (includes framework)
20
+ * @returns {Object} FileFacts
44
21
  */
45
- export async function parseFile(fileManifest, targetDir) {
46
- const { relativePath, absolutePath, language, hash } = fileManifest;
22
+ function parseFile(fileManifest, meta) {
23
+ const { absolutePath, relativePath, language } = fileManifest;
47
24
 
48
- let source;
25
+ let sourceText;
49
26
  try {
50
- source = readFileSync(absolutePath, 'utf-8');
51
- } catch (err) {
52
- logger.warn(`Cannot read ${relativePath}: ${err.message}`);
53
- return createEmptyFacts(relativePath, language, hash);
27
+ sourceText = readFileSync(absolutePath, 'utf8');
28
+ } catch (e) {
29
+ logger.warn(`Could not read file: ${relativePath}`);
30
+ return null;
54
31
  }
55
32
 
56
- const parser = LANGUAGE_PARSERS[language];
57
-
58
- if (!parser) {
59
- logger.debug(`No specialized parser for "${language}", using generic: ${relativePath}`);
60
- const facts = parseGeneric(source, relativePath, language);
61
- facts.hash = hash;
62
- return facts;
33
+ let facts;
34
+ if (language && PARSERS[language]) {
35
+ try {
36
+ const parser = PARSERS[language]();
37
+ facts = parser.parse(sourceText, relativePath);
38
+ } catch (e) {
39
+ logger.warn(`AST parse failed for ${relativePath}: ${e.message}. Falling back to generic.`);
40
+ facts = require('./ast/generic').parse(sourceText, relativePath, language);
41
+ }
42
+ } else {
43
+ facts = require('./ast/generic').parse(sourceText, relativePath, language);
63
44
  }
64
45
 
65
- try {
66
- const facts = await parser(source, relativePath);
67
- facts.hash = hash;
68
- return facts;
69
- } catch (err) {
70
- logger.warn(`Parser error for ${relativePath}: ${err.message}. Falling back to generic.`);
71
- const facts = parseGeneric(source, relativePath, language);
72
- facts.hash = hash;
73
- return facts;
46
+ if (!facts) return null;
47
+
48
+ facts.hash = fileManifest.hash;
49
+
50
+ // Laravel enrichment
51
+ if (language === 'php' && meta && meta.framework === 'laravel') {
52
+ const { enrich } = require('./ast/laravel/index');
53
+ facts = enrich(sourceText, facts);
74
54
  }
55
+
56
+ return facts;
75
57
  }
76
58
 
77
59
  /**
78
- * Parse all files in the manifest.
79
- * @param {import('../crawler/manifest.js').FileManifest[]} manifest
80
- * @param {string} targetDir
81
- * @returns {Promise<FileFacts[]>}
60
+ * Parse all files and assemble the PKG.
61
+ * @param {import('../crawler/index').FileManifest[]} files
62
+ * @param {Object} meta
63
+ * @param {Object} config
64
+ * @returns {Object} PKG
82
65
  */
83
- export async function parseAll(manifest, targetDir) {
84
- const results = [];
85
- for (const file of manifest) {
86
- const facts = await parseFile(file, targetDir);
87
- results.push(facts);
66
+ async function parseFiles(files, meta, config) {
67
+ const allFacts = [];
68
+
69
+ for (const file of files) {
70
+ const facts = parseFile(file, meta);
71
+ if (facts) allFacts.push(facts);
88
72
  }
89
- return results;
90
- }
91
73
 
92
- function createEmptyFacts(relativePath, language, hash) {
93
- return {
94
- relativePath,
95
- language,
96
- linesOfCode: 0,
97
- parserType: 'none',
98
- functions: [],
99
- classes: [],
100
- imports: [],
101
- exports: [],
102
- callsTo: [],
103
- calledBy: [],
104
- hash,
105
- };
74
+ const { buildCallGraph } = require('./call-graph');
75
+ const factsWithGraph = buildCallGraph(allFacts);
76
+
77
+ const { buildPKG } = require('./pkg-builder');
78
+ return buildPKG(factsWithGraph, meta);
106
79
  }
80
+
81
+ module.exports = { parseFile, parseFiles };