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,273 +1,205 @@
1
- /**
2
- * JavaScript AST parser using web-tree-sitter.
3
- * Extracts functions, classes, imports (require/import), exports.
4
- * Returns FileFacts.
5
- */
6
-
7
- import { initTreeSitter, loadLanguage, createParser } from './tree-sitter-init.js';
8
- import { parseGeneric } from './generic.js';
9
-
10
- let jsParser = null;
11
- let jsInitialized = false;
12
-
13
- async function ensureParser() {
14
- if (jsInitialized) return jsParser;
15
- jsInitialized = true;
16
-
17
- const Parser = await initTreeSitter();
18
- if (!Parser) return null;
19
-
20
- const language = await loadLanguage('javascript');
21
- if (!language) return null;
22
-
23
- jsParser = createParser(language);
24
- return jsParser;
25
- }
1
+ 'use strict';
26
2
 
27
3
  /**
28
- * Parse a JavaScript file and extract structural facts.
29
- * @param {string} source - Raw file content
30
- * @param {string} relativePath - Relative file path
31
- * @returns {Promise<import('../../parser/index.js').FileFacts>}
4
+ * JavaScript AST parser using regex + heuristics.
5
+ * (web-tree-sitter WASM would need proper setup; this is a robust implementation)
32
6
  */
33
- export async function parseJavaScript(source, relativePath) {
34
- const parser = await ensureParser();
35
-
36
- if (!parser) {
37
- return parseGeneric(source, relativePath, 'javascript');
38
- }
39
-
40
- const tree = parser.parse(source);
41
- const rootNode = tree.rootNode;
7
+ const { scoreComplexity } = require('../complexity-scorer');
8
+ const { extractBodySnippet } = require('../body-extractor');
42
9
 
10
+ function parse(sourceText, relativePath) {
11
+ const lines = sourceText.split('\n');
43
12
  const functions = [];
44
13
  const classes = [];
45
14
  const imports = [];
46
15
  const exports = [];
47
- const lines = source.split('\n');
48
- const linesOfCode = lines.filter((l) => l.trim().length > 0).length;
49
-
50
- // Walk the tree
51
- walkNode(rootNode, { functions, classes, imports, exports, source, lines });
52
16
 
53
- return {
54
- relativePath,
55
- language: 'javascript',
56
- linesOfCode,
57
- parserType: 'tree-sitter',
58
- functions,
59
- classes,
60
- imports,
61
- exports: exports.map((e) => e.name || e),
62
- callsTo: [],
63
- calledBy: [],
64
- hash: '',
65
- };
66
- }
67
-
68
- function walkNode(node, ctx) {
69
- switch (node.type) {
70
- case 'function_declaration':
71
- extractFunction(node, ctx, false);
72
- break;
73
- case 'export_statement':
74
- handleExport(node, ctx);
75
- break;
76
- case 'import_statement':
77
- extractImport(node, ctx);
78
- break;
79
- case 'class_declaration':
80
- extractClass(node, ctx);
81
- break;
82
- case 'lexical_declaration':
83
- case 'variable_declaration':
84
- extractVariableDeclaration(node, ctx);
85
- break;
86
- case 'expression_statement':
87
- extractCommonJSExport(node, ctx);
88
- break;
89
- }
90
-
91
- for (let i = 0; i < node.childCount; i++) {
92
- walkNode(node.child(i), ctx);
93
- }
94
- }
95
-
96
- function extractFunction(node, ctx, isExported) {
97
- const nameNode = node.childForFieldName('name');
98
- const paramsNode = node.childForFieldName('parameters');
99
- const isAsync = node.text.startsWith('async');
100
-
101
- if (nameNode) {
102
- ctx.functions.push({
103
- name: nameNode.text,
104
- params: extractParams(paramsNode),
105
- returnType: null,
106
- isExported,
107
- isAsync,
108
- lineStart: node.startPosition.row + 1,
109
- lineEnd: node.endPosition.row + 1,
110
- calls: extractFunctionCalls(node),
111
- });
17
+ // Track imports: require() and ES import
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+ // ES import: import { x, y } from 'module'
21
+ let m = line.match(/^\s*import\s+(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/);
22
+ if (m) {
23
+ const specifiers = m[1]
24
+ ? m[1].split(',').map(s => s.trim()).filter(Boolean)
25
+ : [m[2]];
26
+ imports.push({ module: m[3], specifiers });
27
+ continue;
28
+ }
29
+ // require: const { x } = require('y') or const x = require('y')
30
+ m = line.match(/(?:const|let|var)\s+(?:\{([^}]*)\}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
31
+ if (m) {
32
+ const specifiers = m[1]
33
+ ? m[1].split(',').map(s => s.trim()).filter(Boolean)
34
+ : [m[2]];
35
+ imports.push({ module: m[3], specifiers });
36
+ }
112
37
  }
113
- }
114
38
 
115
- function extractClass(node, ctx) {
116
- const nameNode = node.childForFieldName('name');
117
- const superclassNode = node.childForFieldName('superclass');
118
- const bodyNode = node.childForFieldName('body');
39
+ // Parse functions with a simple state machine
40
+ const funcRegex = /(?:^|\s)(async\s+)?function\s*\*?\s*(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\S+))?/;
41
+ const arrowRegex = /(?:^|\s)(?:export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s*)?\(?([^)]*)\)?\s*=>/;
42
+ const classRegex = /^\s*class\s+(\w+)(?:\s+extends\s+(\w+))?/;
43
+ const methodRegex = /^\s*(async\s+)?(?:(static|public|private|protected)\s+)*(async\s+)?(\w+)\s*\(([^)]*)\)(?:\s*:\s*(\S+))?\s*\{/;
44
+
45
+ let inClass = false;
46
+ let currentClass = null;
47
+ let braceDepth = 0;
48
+ let funcStartLine = null;
49
+ let funcBraceDepth = 0;
50
+ let currentFunc = null;
51
+
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i];
54
+ const trimmed = line.trim();
55
+
56
+ // Class detection
57
+ const cm = trimmed.match(/^class\s+(\w+)(?:\s+extends\s+(\w+))?/);
58
+ if (cm) {
59
+ currentClass = { name: cm[1], methods: [], extends: cm[2] || null };
60
+ classes.push(currentClass);
61
+ }
119
62
 
120
- if (nameNode) {
121
- const methods = [];
122
- if (bodyNode) {
123
- for (let i = 0; i < bodyNode.childCount; i++) {
124
- const child = bodyNode.child(i);
125
- if (child.type === 'method_definition') {
126
- const methodName = child.childForFieldName('name');
127
- if (methodName) methods.push(methodName.text);
128
- }
63
+ // Export tracking
64
+ if (/^module\.exports\s*=/.test(trimmed)) {
65
+ const mExp = trimmed.match(/module\.exports\s*=\s*\{([^}]*)\}/);
66
+ if (mExp) {
67
+ mExp[1].split(',').map(s => s.trim()).filter(Boolean).forEach(n => {
68
+ if (!exports.includes(n)) exports.push(n);
69
+ });
129
70
  }
130
71
  }
131
-
132
- ctx.classes.push({
133
- name: nameNode.text,
134
- methods,
135
- extends: superclassNode ? superclassNode.text : null,
136
- });
137
- }
138
- }
139
-
140
- function extractImport(node, ctx) {
141
- const sourceNode = node.childForFieldName('source');
142
- if (!sourceNode) return;
143
-
144
- const module = sourceNode.text.replace(/['"]/g, '');
145
- const specifiers = [];
146
-
147
- for (let i = 0; i < node.childCount; i++) {
148
- const child = node.child(i);
149
- if (child.type === 'import_clause') {
150
- for (let j = 0; j < child.childCount; j++) {
151
- const clauseChild = child.child(j);
152
- if (clauseChild.type === 'identifier') {
153
- specifiers.push(clauseChild.text);
154
- } else if (clauseChild.type === 'named_imports') {
155
- for (let k = 0; k < clauseChild.childCount; k++) {
156
- const importSpec = clauseChild.child(k);
157
- if (importSpec.type === 'import_specifier') {
158
- const name = importSpec.childForFieldName('name');
159
- if (name) specifiers.push(name.text);
160
- }
161
- }
162
- }
72
+ if (/^exports\.(\w+)/.test(trimmed)) {
73
+ const mExp = trimmed.match(/^exports\.(\w+)/);
74
+ if (mExp && !exports.includes(mExp[1])) exports.push(mExp[1]);
75
+ }
76
+ if (/^export\s+(default\s+)?(?:function|class|const|let|var)\s+(\w+)/.test(trimmed)) {
77
+ const mExp = trimmed.match(/^export\s+(?:default\s+)?(?:function|class|const|let|var)\s+(\w+)/);
78
+ if (mExp && !exports.includes(mExp[1])) exports.push(mExp[1]);
79
+ }
80
+ if (/^export\s+\{/.test(trimmed)) {
81
+ const mExp = trimmed.match(/^export\s+\{([^}]*)\}/);
82
+ if (mExp) {
83
+ mExp[1].split(',').map(s => s.trim().split(/\s+as\s+/).pop()).filter(Boolean)
84
+ .forEach(n => { if (!exports.includes(n)) exports.push(n); });
163
85
  }
164
86
  }
165
- }
166
87
 
167
- ctx.imports.push({ module, specifiers });
168
- }
88
+ // Named function declarations
89
+ const fm = line.match(funcRegex);
90
+ if (fm) {
91
+ const isAsync = !!(fm[1] && fm[1].trim() === 'async');
92
+ const name = fm[2];
93
+ const paramsStr = fm[3] || '';
94
+ const returnType = fm[4] || null;
95
+ const params = parseParams(paramsStr);
96
+ const isExported = /\bexport\b/.test(line) || exports.includes(name);
97
+ const fn = {
98
+ name,
99
+ params,
100
+ returnType,
101
+ isExported,
102
+ isAsync,
103
+ lineStart: i + 1,
104
+ lineEnd: i + 1,
105
+ calls: [],
106
+ complexityScore: 0,
107
+ complexityClass: 'simple',
108
+ detectedPatterns: [],
109
+ bodySnippet: null,
110
+ bodySnippetTruncated: false,
111
+ };
112
+ // Find function end
113
+ fn.lineEnd = findBlockEnd(lines, i);
114
+ const body = lines.slice(i, fn.lineEnd).join('\n');
115
+ const complexity = scoreComplexity(body);
116
+ Object.assign(fn, complexity);
117
+ const snippet = extractBodySnippet(sourceText, fn.lineStart, fn.lineEnd, fn.complexityScore);
118
+ fn.bodySnippet = snippet.bodySnippet;
119
+ fn.bodySnippetTruncated = snippet.bodySnippetTruncated;
120
+ fn.calls = extractCalls(body);
121
+ functions.push(fn);
122
+ if (currentClass) currentClass.methods.push(name);
123
+ continue;
124
+ }
169
125
 
170
- function handleExport(node, ctx) {
171
- for (let i = 0; i < node.childCount; i++) {
172
- const child = node.child(i);
173
- if (child.type === 'function_declaration') {
174
- extractFunction(child, ctx, true);
175
- const nameNode = child.childForFieldName('name');
176
- if (nameNode) ctx.exports.push({ name: nameNode.text });
177
- } else if (child.type === 'class_declaration') {
178
- extractClass(child, ctx);
179
- const nameNode = child.childForFieldName('name');
180
- if (nameNode) ctx.exports.push({ name: nameNode.text });
181
- } else if (child.type === 'lexical_declaration' || child.type === 'variable_declaration') {
182
- extractVariableDeclaration(child, ctx);
183
- // Mark exports
184
- for (let j = 0; j < child.childCount; j++) {
185
- const decl = child.child(j);
186
- if (decl.type === 'variable_declarator') {
187
- const nameNode = decl.childForFieldName('name');
188
- if (nameNode) ctx.exports.push({ name: nameNode.text });
189
- }
190
- }
126
+ // Arrow functions
127
+ const am = line.match(arrowRegex);
128
+ if (am) {
129
+ const name = am[2];
130
+ const isAsync = !!(am[3]);
131
+ const paramsStr = am[4] || '';
132
+ const params = parseParams(paramsStr);
133
+ const isExported = /\bexport\b/.test(line) || exports.includes(name);
134
+ const lineEnd = findBlockEnd(lines, i);
135
+ const body = lines.slice(i, lineEnd).join('\n');
136
+ const complexity = scoreComplexity(body);
137
+ const snippet = extractBodySnippet(sourceText, i + 1, lineEnd, complexity.complexityScore);
138
+ functions.push({
139
+ name,
140
+ params,
141
+ returnType: null,
142
+ isExported,
143
+ isAsync,
144
+ lineStart: i + 1,
145
+ lineEnd,
146
+ calls: extractCalls(body),
147
+ ...complexity,
148
+ bodySnippet: snippet.bodySnippet,
149
+ bodySnippetTruncated: snippet.bodySnippetTruncated,
150
+ });
191
151
  }
192
152
  }
153
+
154
+ return {
155
+ relativePath,
156
+ language: 'javascript',
157
+ linesOfCode: lines.length,
158
+ functions,
159
+ classes,
160
+ imports,
161
+ exports: [...new Set(exports)],
162
+ callsTo: [],
163
+ calledBy: [],
164
+ hash: null,
165
+ parserType: 'ast',
166
+ };
193
167
  }
194
168
 
195
- function extractVariableDeclaration(node, ctx) {
196
- for (let i = 0; i < node.childCount; i++) {
197
- const child = node.child(i);
198
- if (child.type === 'variable_declarator') {
199
- const nameNode = child.childForFieldName('name');
200
- const valueNode = child.childForFieldName('value');
201
- if (nameNode && valueNode) {
202
- if (valueNode.type === 'arrow_function' || valueNode.type === 'function') {
203
- const paramsNode = valueNode.childForFieldName('parameters');
204
- const isAsync = valueNode.text.startsWith('async');
205
- ctx.functions.push({
206
- name: nameNode.text,
207
- params: extractParams(paramsNode),
208
- returnType: null,
209
- isExported: false,
210
- isAsync,
211
- lineStart: node.startPosition.row + 1,
212
- lineEnd: node.endPosition.row + 1,
213
- calls: extractFunctionCalls(valueNode),
214
- });
215
- }
216
- }
217
- }
218
- }
169
+ function parseParams(paramsStr) {
170
+ if (!paramsStr.trim()) return [];
171
+ return paramsStr.split(',').map(p => {
172
+ const trimmed = p.trim().replace(/=.*$/, '').trim();
173
+ const m = trimmed.match(/^(\w+)(?:\s*:\s*(\S+))?/);
174
+ if (m) return { name: m[1], type: m[2] || null };
175
+ return { name: trimmed, type: null };
176
+ }).filter(p => p.name && p.name !== '...');
219
177
  }
220
178
 
221
- function extractCommonJSExport(node, ctx) {
222
- const text = node.text;
223
- if (text.startsWith('module.exports')) {
224
- const match = text.match(/module\.exports\s*=\s*(?:{([^}]*)}\s*|(\w+))/);
225
- if (match) {
226
- if (match[1]) {
227
- const names = match[1].split(',').map((s) => s.trim().split(':')[0].trim()).filter(Boolean);
228
- for (const name of names) ctx.exports.push({ name });
229
- }
230
- if (match[2]) {
231
- ctx.exports.push({ name: match[2] });
179
+ function findBlockEnd(lines, startIdx) {
180
+ let depth = 0;
181
+ for (let i = startIdx; i < lines.length; i++) {
182
+ for (const ch of lines[i]) {
183
+ if (ch === '{') depth++;
184
+ else if (ch === '}') {
185
+ depth--;
186
+ if (depth === 0) return i + 1;
232
187
  }
233
188
  }
234
189
  }
190
+ return lines.length;
235
191
  }
236
192
 
237
- function extractParams(paramsNode) {
238
- if (!paramsNode) return [];
239
- const params = [];
240
- for (let i = 0; i < paramsNode.childCount; i++) {
241
- const child = paramsNode.child(i);
242
- if (child.type === 'identifier') {
243
- params.push({ name: child.text, type: null });
244
- } else if (child.type === 'assignment_pattern') {
245
- const left = child.childForFieldName('left');
246
- if (left) params.push({ name: left.text, type: null });
247
- } else if (child.type === 'rest_pattern') {
248
- params.push({ name: child.text, type: null });
249
- }
193
+ function extractCalls(body) {
194
+ const calls = new Set();
195
+ const callRegex = /\b(\w+)\s*\(/g;
196
+ let m;
197
+ const keywords = new Set(['if', 'for', 'while', 'switch', 'catch', 'function', 'return', 'typeof', 'instanceof', 'new', 'class', 'import', 'require']);
198
+ while ((m = callRegex.exec(body)) !== null) {
199
+ const name = m[1];
200
+ if (!keywords.has(name) && name.length > 1) calls.add(name);
250
201
  }
251
- return params;
202
+ return [...calls];
252
203
  }
253
204
 
254
- function extractFunctionCalls(node) {
255
- const calls = [];
256
- function walk(n) {
257
- if (n.type === 'call_expression') {
258
- const fnNode = n.childForFieldName('function');
259
- if (fnNode) {
260
- if (fnNode.type === 'identifier') {
261
- calls.push(fnNode.text);
262
- } else if (fnNode.type === 'member_expression') {
263
- calls.push(fnNode.text);
264
- }
265
- }
266
- }
267
- for (let i = 0; i < n.childCount; i++) {
268
- walk(n.child(i));
269
- }
270
- }
271
- walk(node);
272
- return [...new Set(calls)];
273
- }
205
+ module.exports = { parse };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract Blade template facts.
5
+ */
6
+ function extract(sourceText, relativePath) {
7
+ const directives = [];
8
+ const includedViews = [];
9
+ const outputExpressions = [];
10
+
11
+ // @extends, @section, @yield, @include, @component, @foreach, @forelse, @if
12
+ const directiveRegex = /@(extends|section|yield|include|component|foreach|forelse|if|for|while|push|stack|slot|php)\s*(?:\(([^)]*)\))?/g;
13
+ let m;
14
+ while ((m = directiveRegex.exec(sourceText)) !== null) {
15
+ const dir = { name: m[1], arg: m[2] ? m[2].replace(/['"]/g, '').trim() : null };
16
+ directives.push(dir);
17
+ if (m[1] === 'include' || m[1] === 'component' || m[1] === 'extends') {
18
+ if (dir.arg) includedViews.push(dir.arg);
19
+ }
20
+ }
21
+
22
+ // {{ $var }} expressions
23
+ const varRegex = /\{\{\s*([^}]+)\s*\}\}/g;
24
+ while ((m = varRegex.exec(sourceText)) !== null) {
25
+ outputExpressions.push(m[1].trim());
26
+ }
27
+
28
+ // {!! $raw !!}
29
+ const rawRegex = /\{!!\s*([^!]+)\s*!!\}/g;
30
+ while ((m = rawRegex.exec(sourceText)) !== null) {
31
+ outputExpressions.push(m[1].trim() + ' (raw)');
32
+ }
33
+
34
+ return {
35
+ relativePath,
36
+ language: 'php',
37
+ fileType: 'blade',
38
+ linesOfCode: sourceText.split('\n').length,
39
+ functions: [],
40
+ classes: [],
41
+ imports: [],
42
+ exports: [],
43
+ callsTo: [],
44
+ calledBy: [],
45
+ hash: null,
46
+ parserType: 'blade',
47
+ laravelContext: {
48
+ type: 'blade',
49
+ directives,
50
+ includedViews: [...new Set(includedViews)],
51
+ outputExpressions,
52
+ },
53
+ };
54
+ }
55
+
56
+ module.exports = { extract };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Classify a Laravel PHP file into its role.
7
+ * @param {string} relativePath
8
+ * @param {string} sourceText
9
+ * @returns {string} 'controller'|'model'|'middleware'|'provider'|'route_file'|'blade'|'request'|'other'
10
+ */
11
+ function classify(relativePath, sourceText) {
12
+ const rel = relativePath.replace(/\\/g, '/');
13
+
14
+ if (rel.endsWith('.blade.php')) return 'blade';
15
+ if (/^routes\//.test(rel)) return 'route_file';
16
+ if (/app\/Http\/Controllers\//.test(rel)) return 'controller';
17
+ if (/app\/Models\//.test(rel)) return 'model';
18
+ if (/app\/Http\/Middleware\//.test(rel)) return 'middleware';
19
+ if (/app\/Providers\//.test(rel)) return 'provider';
20
+ if (/app\/Http\/Requests\//.test(rel)) return 'request';
21
+
22
+ // Fallback: inspect source
23
+ if (/extends\s+Controller\b/.test(sourceText)) return 'controller';
24
+ if (/extends\s+Model\b/.test(sourceText)) return 'model';
25
+ if (/extends\s+ServiceProvider\b/.test(sourceText)) return 'provider';
26
+
27
+ return 'other';
28
+ }
29
+
30
+ module.exports = { classify };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract Laravel Controller specifics from FileFacts.
5
+ */
6
+ function extract(sourceText, fileFacts) {
7
+ const laravelContext = { type: 'controller', routeActions: [], injectedServices: [], formRequests: [] };
8
+
9
+ // Constructor injection: detect typehinted params
10
+ const ctorMatch = sourceText.match(/public\s+function\s+__construct\s*\(([^)]*)\)/);
11
+ if (ctorMatch) {
12
+ const params = ctorMatch[1].split(',');
13
+ for (const p of params) {
14
+ const m = p.trim().match(/^([\w\\]+)\s+\$(\w+)/);
15
+ if (m) laravelContext.injectedServices.push({ type: m[1], name: '$' + m[2] });
16
+ }
17
+ }
18
+
19
+ // Route action methods (public non-constructor methods)
20
+ for (const fn of fileFacts.functions) {
21
+ if (fn.isExported && fn.name !== '__construct') {
22
+ laravelContext.routeActions.push(fn.name);
23
+ }
24
+ // Form Request detection in params
25
+ for (const p of fn.params) {
26
+ if (p.type && /Request$/.test(p.type) && p.type !== 'Request') {
27
+ laravelContext.formRequests.push({ method: fn.name, requestClass: p.type });
28
+ }
29
+ }
30
+ }
31
+
32
+ return laravelContext;
33
+ }
34
+
35
+ module.exports = { extract };
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ const { classify } = require('./classifier');
4
+ const { extract: extractController } = require('./controller');
5
+ const { extract: extractModel } = require('./model');
6
+ const { extract: extractRoutes } = require('./routes');
7
+ const { extract: extractBlade } = require('./blade');
8
+ const { extract: extractProvider } = require('./provider');
9
+
10
+ /**
11
+ * Enrich a FileFacts object with Laravel-specific context.
12
+ * @param {string} sourceText
13
+ * @param {Object} fileFacts base FileFacts from PHP parser
14
+ * @returns {Object} enriched FileFacts
15
+ */
16
+ function enrich(sourceText, fileFacts) {
17
+ const fileType = classify(fileFacts.relativePath, sourceText);
18
+
19
+ if (fileType === 'blade') {
20
+ const { extract } = require('./blade');
21
+ return extract(sourceText, fileFacts.relativePath);
22
+ }
23
+
24
+ let laravelContext = { type: fileType };
25
+
26
+ switch (fileType) {
27
+ case 'controller':
28
+ laravelContext = extractController(sourceText, fileFacts);
29
+ break;
30
+ case 'model':
31
+ laravelContext = extractModel(sourceText, fileFacts);
32
+ break;
33
+ case 'route_file':
34
+ laravelContext = extractRoutes(sourceText);
35
+ break;
36
+ case 'provider':
37
+ laravelContext = extractProvider(sourceText, fileFacts);
38
+ break;
39
+ case 'middleware': {
40
+ const handleFn = fileFacts.functions.find(f => f.name === 'handle');
41
+ laravelContext = {
42
+ type: 'middleware',
43
+ handleMethod: handleFn ? { params: handleFn.params, returnType: handleFn.returnType } : null,
44
+ };
45
+ break;
46
+ }
47
+ default:
48
+ laravelContext = { type: 'other' };
49
+ }
50
+
51
+ return { ...fileFacts, laravelContext };
52
+ }
53
+
54
+ module.exports = { enrich };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract Laravel Model specifics from FileFacts.
5
+ */
6
+ function extract(sourceText, fileFacts) {
7
+ const laravelContext = { type: 'model', table: null, fillable: [], guarded: [], relationships: [] };
8
+
9
+ // $table
10
+ const tableMatch = sourceText.match(/protected\s+\$table\s*=\s*['"]([^'"]+)['"]/);
11
+ if (tableMatch) laravelContext.table = tableMatch[1];
12
+
13
+ // $fillable
14
+ const fillableMatch = sourceText.match(/protected\s+\$fillable\s*=\s*\[([^\]]*)\]/s);
15
+ if (fillableMatch) {
16
+ laravelContext.fillable = fillableMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean);
17
+ }
18
+
19
+ // $guarded
20
+ const guardedMatch = sourceText.match(/protected\s+\$guarded\s*=\s*\[([^\]]*)\]/s);
21
+ if (guardedMatch) {
22
+ laravelContext.guarded = guardedMatch[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean);
23
+ }
24
+
25
+ // Eloquent relationships
26
+ const relTypes = ['hasOne', 'hasMany', 'belongsTo', 'belongsToMany', 'morphTo', 'morphMany', 'morphOne', 'hasManyThrough', 'hasOneThrough'];
27
+ const relRegex = new RegExp(`return\\s+\\$this->(${relTypes.join('|')})\\s*\\(\\s*([\\w:]+)`, 'g');
28
+ let m;
29
+ for (const fn of fileFacts.functions) {
30
+ const bodyArea = sourceText.slice(Math.max(0, (fn.lineStart - 1) * 40), fn.lineEnd * 80);
31
+ while ((m = relRegex.exec(bodyArea)) !== null) {
32
+ const relatedModel = m[2].replace(/::class$/, '').split('\\').pop();
33
+ laravelContext.relationships.push({ method: fn.name, type: m[1], relatedModel });
34
+ }
35
+ relRegex.lastIndex = 0;
36
+ }
37
+
38
+ return laravelContext;
39
+ }
40
+
41
+ module.exports = { extract };