@merlean/analyzer 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/bin/cli.js +47 -12
  2. package/lib/analyzer.js +858 -49
  3. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -23,7 +23,8 @@ function parseArgs() {
23
23
  path: null,
24
24
  name: null,
25
25
  backend: DEFAULT_BACKEND,
26
- output: null
26
+ output: null,
27
+ mergeWith: null // Existing siteId to merge into
27
28
  };
28
29
 
29
30
  for (let i = 0; i < args.length; i++) {
@@ -35,6 +36,8 @@ function parseArgs() {
35
36
  options.backend = args[++i];
36
37
  } else if (arg === '--output' || arg === '-o') {
37
38
  options.output = args[++i];
39
+ } else if (arg === '--merge-with' || arg === '-m') {
40
+ options.mergeWith = args[++i];
38
41
  } else if (arg === '--help' || arg === '-h') {
39
42
  printHelp();
40
43
  process.exit(0);
@@ -58,7 +61,8 @@ Arguments:
58
61
  <path> Path to codebase (default: current directory)
59
62
 
60
63
  Options:
61
- --name, -n <name> Site name (required)
64
+ --name, -n <name> Site name (required for new analysis)
65
+ --merge-with, -m <siteId> Merge into existing site map (appends routes/forms/actions)
62
66
  --backend, -b <url> Backend URL (default: ${DEFAULT_BACKEND})
63
67
  --output, -o <file> Save site map locally (optional)
64
68
  --help, -h Show this help
@@ -70,6 +74,9 @@ Examples:
70
74
  # Analyze specific path
71
75
  npx @merlean/analyzer ./my-app --name "My App"
72
76
 
77
+ # Merge another frontend into existing site map
78
+ npx @merlean/analyzer ./admin-panel --merge-with site_abc123
79
+
73
80
  # Use custom backend (for local dev)
74
81
  npx @merlean/analyzer --name "My App" --backend http://localhost:3004
75
82
  `);
@@ -81,8 +88,8 @@ async function main() {
81
88
  const options = parseArgs();
82
89
 
83
90
  // Validate required args
84
- if (!options.name) {
85
- console.error('❌ Error: --name is required');
91
+ if (!options.name && !options.mergeWith) {
92
+ console.error('❌ Error: --name is required (or use --merge-with to merge into existing site)');
86
93
  console.log(' Run with --help for usage');
87
94
  process.exit(1);
88
95
  }
@@ -96,22 +103,41 @@ async function main() {
96
103
  }
97
104
 
98
105
  console.log(`📁 Scanning: ${codebasePath}`);
99
- console.log(`📛 Site name: ${options.name}`);
106
+ if (options.mergeWith) {
107
+ console.log(`🔗 Merging into: ${options.mergeWith}`);
108
+ if (options.name) {
109
+ console.log(`📛 Site name: ${options.name} (will update existing)`);
110
+ }
111
+ } else {
112
+ console.log(`📛 Site name: ${options.name}`);
113
+ }
100
114
 
101
115
  try {
102
116
  // Scan codebase locally
103
- const fileContents = await scanCodebase(codebasePath);
117
+ const scanResult = await scanCodebase(codebasePath);
118
+ // Handle both old format (array) and new format (object with files + preExtractedRoutes)
119
+ const fileContents = Array.isArray(scanResult) ? scanResult : scanResult.files;
120
+ const preExtractedRoutes = Array.isArray(scanResult) ? [] : (scanResult.preExtractedRoutes || []);
104
121
 
105
122
  console.log(`\n📤 Uploading to backend for analysis...`);
106
123
 
124
+ // Build request body
125
+ const requestBody = {
126
+ siteName: options.name,
127
+ files: fileContents,
128
+ preExtractedRoutes // Include convention-based routes
129
+ };
130
+
131
+ // Add merge parameter if specified
132
+ if (options.mergeWith) {
133
+ requestBody.mergeWithSiteId = options.mergeWith;
134
+ }
135
+
107
136
  // Send to backend for LLM analysis
108
137
  const response = await fetch(`${options.backend}/api/analyze`, {
109
138
  method: 'POST',
110
139
  headers: { 'Content-Type': 'application/json' },
111
- body: JSON.stringify({
112
- siteName: options.name,
113
- files: fileContents
114
- })
140
+ body: JSON.stringify(requestBody)
115
141
  });
116
142
 
117
143
  if (!response.ok) {
@@ -121,13 +147,22 @@ async function main() {
121
147
 
122
148
  const result = await response.json();
123
149
 
124
- console.log('\n✅ Analysis complete!');
125
- console.log('\n📊 Summary:');
150
+ if (options.mergeWith) {
151
+ console.log('\n Merge complete!');
152
+ console.log('\n📊 Updated Summary:');
153
+ } else {
154
+ console.log('\n✅ Analysis complete!');
155
+ console.log('\n📊 Summary:');
156
+ }
126
157
  console.log(` Site ID: ${result.siteId}`);
127
158
  console.log(` Framework: ${result.framework || 'Unknown'}`);
128
159
  console.log(` Routes: ${result.routes?.length || 0}`);
129
160
  console.log(` Forms: ${result.forms?.length || 0}`);
130
161
  console.log(` Actions: ${result.actions?.length || 0}`);
162
+
163
+ if (result.merged) {
164
+ console.log(`\n 📎 Merged from: ${result.merged.sourcesCount} source(s)`);
165
+ }
131
166
 
132
167
  // Save locally if requested
133
168
  if (options.output) {
package/lib/analyzer.js CHANGED
@@ -1,25 +1,48 @@
1
1
  /**
2
- * Framework-Agnostic Codebase Scanner
2
+ * Language & Framework Agnostic Codebase Scanner
3
3
  *
4
- * Scans ANY codebase (frontend or backend) to extract API patterns:
4
+ * Scans ANY codebase (frontend or backend, any language) to extract API patterns:
5
+ *
6
+ * JavaScript/TypeScript:
5
7
  * - Express/Fastify/Koa/Hapi route definitions
6
8
  * - NestJS decorators (@Get, @Post, @Body, @Query, etc.)
7
9
  * - Frontend HTTP calls (fetch, axios, HttpClient, etc.)
8
- * - Swagger/OpenAPI annotations
9
- * - Request body schemas and query parameters
10
- * - TypeScript interfaces/DTOs
11
- * - Validation schemas (Zod, Joi, class-validator)
10
+ *
11
+ * PHP:
12
+ * - CodeIgniter routes ($routes->get(), $routes->post())
13
+ * - Laravel routes (Route::get(), Route::post())
14
+ * - Symfony annotations (@Route)
15
+ *
16
+ * Python:
17
+ * - Flask routes (@app.route)
18
+ * - FastAPI routes (@app.get, @router.post)
19
+ * - Django URLs (path(), url())
20
+ *
21
+ * Ruby:
22
+ * - Rails routes (get, post, resources)
23
+ * - Sinatra routes (get '/path')
24
+ *
25
+ * Go:
26
+ * - Gin routes (r.GET, r.POST)
27
+ * - Echo routes (e.GET, e.POST)
28
+ * - Chi routes (r.Get, r.Post)
29
+ *
30
+ * Java/Kotlin:
31
+ * - Spring Boot (@GetMapping, @PostMapping, @RequestMapping)
32
+ *
33
+ * Plus: Swagger/OpenAPI annotations, request body schemas, DTOs
12
34
  *
13
35
  * The goal is to understand what APIs exist, their parameters,
14
- * and body structures - regardless of framework or code style.
36
+ * and body structures - regardless of framework, language, or code style.
15
37
  */
16
38
 
17
39
  const fs = require('fs');
18
40
  const path = require('path');
19
41
  const { glob } = require('glob');
20
42
 
21
- // File patterns to scan (be inclusive)
43
+ // File patterns to scan - include ALL common backend/frontend languages
22
44
  const FILE_PATTERNS = [
45
+ // JavaScript/TypeScript
23
46
  '**/*.js',
24
47
  '**/*.jsx',
25
48
  '**/*.ts',
@@ -27,7 +50,22 @@ const FILE_PATTERNS = [
27
50
  '**/*.vue',
28
51
  '**/*.svelte',
29
52
  '**/*.mjs',
30
- '**/*.cjs'
53
+ '**/*.cjs',
54
+ // PHP
55
+ '**/*.php',
56
+ // Python
57
+ '**/*.py',
58
+ // Ruby
59
+ '**/*.rb',
60
+ // Go
61
+ '**/*.go',
62
+ // Java/Kotlin
63
+ '**/*.java',
64
+ '**/*.kt',
65
+ // C#
66
+ '**/*.cs',
67
+ // Rust
68
+ '**/*.rs'
31
69
  ];
32
70
 
33
71
  // Only ignore truly irrelevant directories
@@ -51,47 +89,74 @@ const IGNORE_PATTERNS = [
51
89
  '**/__mocks__/**'
52
90
  ];
53
91
 
54
- // Files that are highly likely to contain API definitions
92
+ // Files that are highly likely to contain API definitions (any language)
55
93
  const HIGH_PRIORITY_PATTERNS = [
56
- /routes?\.ts$/i,
57
- /routes?\.js$/i,
58
- /router\.ts$/i,
59
- /router\.js$/i,
60
- /controller\.ts$/i,
61
- /controller\.js$/i,
62
- /\.controller\.ts$/i,
63
- /\.controller\.js$/i,
64
- /service\.ts$/i,
65
- /service\.js$/i,
66
- /\.service\.ts$/i,
67
- /\.service\.js$/i,
68
- /api\.ts$/i,
69
- /api\.js$/i,
70
- /endpoints?\.ts$/i,
71
- /endpoints?\.js$/i,
72
- /http\.ts$/i,
73
- /http\.js$/i,
74
- /client\.ts$/i,
75
- /client\.js$/i,
94
+ // Generic route/controller patterns (any extension)
95
+ /routes?\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
96
+ /router\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
97
+ /controller\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
98
+ /\.controller\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
99
+ /Controller\.(php|java|kt|cs)$/, // PascalCase controllers (PHP, Java, C#)
100
+ /service\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
101
+ /\.service\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
102
+ /api\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
103
+ /endpoints?\.(ts|js|php|py|rb|go|java|kt|cs)$/i,
104
+ /http\.(ts|js|php|py|rb|go)$/i,
105
+ /client\.(ts|js|php|py|rb|go)$/i,
106
+
107
+ // TypeScript/JavaScript specific
76
108
  /dto\.ts$/i,
77
109
  /\.dto\.ts$/i,
78
110
  /interfaces?\.ts$/i,
79
111
  /types?\.ts$/i,
80
112
  /schema\.ts$/i,
81
113
  /schemas?\.ts$/i,
82
- /validation\.ts$/i
114
+ /validation\.ts$/i,
115
+
116
+ // PHP specific (CodeIgniter, Laravel, Symfony)
117
+ /Routes\.php$/i,
118
+ /web\.php$/i, // Laravel web routes
119
+ /api\.php$/i, // Laravel API routes
120
+ /Config\/Routes\.php$/i, // CodeIgniter routes
121
+ // CodeIgniter convention: application/controllers/**/*.php are ALL route controllers
122
+ /application\/controllers\/.*\.php$/i,
123
+ /app\/Controllers\/.*\.php$/i, // CodeIgniter 4
124
+ /app\/Http\/Controllers\/.*\.php$/i, // Laravel controllers
125
+ // Libraries and models that define data structures
126
+ /application\/libraries\/.*\.php$/i,
127
+ /application\/models\/.*\.php$/i,
128
+ /app\/Models\/.*\.php$/i,
129
+ /app\/Services\/.*\.php$/i,
130
+
131
+ // Python specific (Flask, FastAPI, Django)
132
+ /views?\.py$/i,
133
+ /urls\.py$/i, // Django URLs
134
+ /routers?\.py$/i, // FastAPI routers
135
+
136
+ // Ruby specific (Rails)
137
+ /routes\.rb$/i,
138
+ /_controller\.rb$/i,
139
+
140
+ // Go specific
141
+ /handlers?\.go$/i,
142
+ /routes?\.go$/i,
143
+
144
+ // Java/Kotlin specific (Spring Boot)
145
+ /Controller\.java$/,
146
+ /Controller\.kt$/,
147
+ /RestController\.java$/,
148
+ /RestController\.kt$/
83
149
  ];
84
150
 
85
151
  // Medium priority - might contain API patterns
86
152
  const MEDIUM_PRIORITY_PATTERNS = [
87
- /index\.ts$/i,
88
- /index\.js$/i,
89
- /app\.ts$/i,
90
- /app\.js$/i,
91
- /main\.ts$/i,
92
- /main\.js$/i,
93
- /server\.ts$/i,
94
- /server\.js$/i
153
+ /index\.(ts|js|php|py|rb|go)$/i,
154
+ /app\.(ts|js|php|py|rb|go)$/i,
155
+ /main\.(ts|js|php|py|rb|go|java|kt)$/i,
156
+ /server\.(ts|js|php|py|rb|go)$/i,
157
+ /Application\.(java|kt)$/, // Spring Boot main class
158
+ /bootstrap\.php$/i, // PHP bootstrap files
159
+ /kernel\.php$/i, // Laravel/Symfony kernel
95
160
  ];
96
161
 
97
162
  /**
@@ -116,15 +181,32 @@ async function scanCodebase(codebasePath) {
116
181
 
117
182
  // Analyze high priority files first (routes, controllers, services, DTOs)
118
183
  // Then medium priority, then scan others for API patterns
184
+ // Increased limits to ensure all controllers are scanned
119
185
  const filesToAnalyze = [
120
- ...highPriority,
121
- ...mediumPriority.slice(0, 20),
122
- ...other.slice(0, 50)
186
+ ...highPriority, // ALL high priority files (controllers, routes, etc.)
187
+ ...mediumPriority.slice(0, 30),
188
+ ...other.slice(0, 75)
123
189
  ];
124
190
 
125
191
  console.log(` Analyzing ${filesToAnalyze.length} files for API patterns...`);
126
192
 
193
+ // PHASE 1: Pre-scan all files to discover JSON structures
194
+ // This ensures structures found in one file can be used for others
195
+ console.log(` Phase 1: Discovering JSON structures...`);
196
+ for (const file of filesToAnalyze) {
197
+ try {
198
+ const content = fs.readFileSync(file, 'utf-8');
199
+ // Look for structure-defining patterns (json_decode + array access)
200
+ preDiscoverStructures(content);
201
+ } catch (error) {
202
+ // Skip files that can't be read
203
+ }
204
+ }
205
+ console.log(` Discovered ${discoveredStructures.size} reusable structures`);
206
+
207
+ // PHASE 2: Extract routes and apply discovered structures
127
208
  const fileContents = [];
209
+ const preExtractedRoutes = []; // Routes extracted directly from conventions
128
210
  let filesWithPatterns = 0;
129
211
 
130
212
  for (const file of filesToAnalyze) {
@@ -132,6 +214,12 @@ async function scanCodebase(codebasePath) {
132
214
  const content = fs.readFileSync(file, 'utf-8');
133
215
  const relativePath = path.relative(codebasePath, file);
134
216
 
217
+ // Pre-extract routes from convention-based frameworks (CodeIgniter, etc.)
218
+ const conventionRoutes = extractConventionRoutes(content, relativePath);
219
+ if (conventionRoutes.length > 0) {
220
+ preExtractedRoutes.push(...conventionRoutes);
221
+ }
222
+
135
223
  // Extract API patterns from the file
136
224
  const extracted = extractApiPatterns(content, relativePath, file);
137
225
 
@@ -148,8 +236,537 @@ async function scanCodebase(codebasePath) {
148
236
  }
149
237
 
150
238
  console.log(` Found API patterns in ${filesWithPatterns} files`);
239
+ if (preExtractedRoutes.length > 0) {
240
+ console.log(` Pre-extracted ${preExtractedRoutes.length} convention-based routes`);
241
+ }
151
242
 
152
- return fileContents;
243
+ return { files: fileContents, preExtractedRoutes };
244
+ }
245
+
246
+ /**
247
+ * Extract routes from convention-based frameworks
248
+ * Returns array of {method, path, description, bodySchema?} objects
249
+ */
250
+ function extractConventionRoutes(content, filePath) {
251
+ const routes = [];
252
+
253
+ // CodeIgniter: application/controllers/api/v2/Settings.php -> /api/v2/settings
254
+ const basePath = extractRouteFromControllerPath(filePath);
255
+ if (!basePath) return routes;
256
+
257
+ // Split content into lines for method body extraction
258
+ const lines = content.split('\n');
259
+
260
+ // Extract CodeIgniter REST controller methods: index_get, index_post, items_get, etc.
261
+ const methodRegex = /public\s+function\s+(\w+)_(get|post|put|patch|delete)\s*\(([^)]*)\)/gi;
262
+ let match;
263
+
264
+ while ((match = methodRegex.exec(content)) !== null) {
265
+ const action = match[1]; // e.g., 'index', 'items', 'user'
266
+ const httpMethod = match[2].toUpperCase(); // e.g., 'GET', 'POST'
267
+ const params = match[3]; // e.g., '$id = null'
268
+ const matchIndex = match.index;
269
+
270
+ // Build the route path
271
+ let path = basePath;
272
+ if (action !== 'index') {
273
+ path += '/' + action.toLowerCase().replace(/_/g, '/');
274
+ }
275
+
276
+ // Add path parameters from function arguments
277
+ if (params) {
278
+ const paramMatches = params.match(/\$(\w+)(?:\s*=\s*[^,)]+)?/g);
279
+ if (paramMatches) {
280
+ paramMatches.forEach(p => {
281
+ const paramName = p.match(/\$(\w+)/)[1];
282
+ // Only add as path param if it's not optional (no default value) or looks like an ID
283
+ if (!p.includes('=') || paramName.toLowerCase().includes('id')) {
284
+ path += '/:' + paramName;
285
+ }
286
+ });
287
+ }
288
+ }
289
+
290
+ // Extract method body to analyze for body schema (for POST/PUT/PATCH)
291
+ let bodySchema = null;
292
+ if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
293
+ bodySchema = extractBodySchemaFromMethod(content, matchIndex);
294
+ }
295
+
296
+ const route = {
297
+ method: httpMethod,
298
+ path,
299
+ description: `${action}_${match[2]}() method in ${filePath.split('/').pop()}`
300
+ };
301
+
302
+ if (bodySchema && Object.keys(bodySchema).length > 0) {
303
+ route.bodySchema = bodySchema;
304
+ }
305
+
306
+ routes.push(route);
307
+ }
308
+
309
+ return routes;
310
+ }
311
+
312
+ // Global cache for discovered complex structures (shared across file analysis)
313
+ const discoveredStructures = new Map();
314
+
315
+ /**
316
+ * Pre-scan a file to discover JSON structure patterns
317
+ * Looks for json_decode followed by array access patterns
318
+ */
319
+ function preDiscoverStructures(content) {
320
+ // Find patterns like: $actions = json_decode($actions, true);
321
+ const jsonDecodeRegex = /\$(\w+)\s*=\s*json_decode\s*\(\s*\$\1/gi;
322
+ let match;
323
+
324
+ while ((match = jsonDecodeRegex.exec(content)) !== null) {
325
+ const varName = match[1];
326
+
327
+ // Extract the structure used for this variable
328
+ const structure = extractNestedStructure(content, varName);
329
+
330
+ // If we found a meaningful structure with examples, cache it
331
+ if (structure._example && Object.keys(structure._example).length > 0) {
332
+ // Determine field name from common patterns
333
+ // e.g., $actions = $this->post('actions'); $actions = json_decode($actions);
334
+ const fieldRegex = new RegExp(
335
+ `\\$${varName}\\s*=\\s*\\$this\\s*->\\s*(?:post|put|patch|get)\\s*\\(\\s*['"]([\\w]+)['"]`,
336
+ 'i'
337
+ );
338
+ const fieldMatch = content.match(fieldRegex);
339
+ if (fieldMatch) {
340
+ const fieldName = fieldMatch[1];
341
+ discoveredStructures.set(fieldName, structure);
342
+ } else {
343
+ // Use variable name as field name
344
+ discoveredStructures.set(varName, structure);
345
+ }
346
+ }
347
+ }
348
+
349
+ // Also look for library/model calls that reveal structure
350
+ // e.g., ->edit($update["settings"]), ->edit($update["integrations"])
351
+ const modelEditRegex = /(\w+_model|lib_\w+)\s*->\s*edit\s*\(\s*\$(\w+)\s*\[\s*["'](\w+)["']\s*\]/gi;
352
+ const discoveredSections = new Set();
353
+
354
+ while ((match = modelEditRegex.exec(content)) !== null) {
355
+ const section = match[3]; // e.g., 'settings', 'integrations', 'assignments'
356
+ discoveredSections.add(section);
357
+ }
358
+
359
+ // Store discovered sections globally so they can be applied later
360
+ if (discoveredSections.size > 0) {
361
+ const existingSections = discoveredStructures.get('_sections') || new Set();
362
+ for (const section of discoveredSections) {
363
+ existingSections.add(section);
364
+ }
365
+ discoveredStructures.set('_sections', existingSections);
366
+ }
367
+
368
+ // Also look for common field names with library calls
369
+ // e.g., $this->lib_settings->edit($actions) suggests 'actions' has a structure
370
+ const libCallRegex = /lib_settings\s*->\s*edit\s*\(\s*\$(\w+)/gi;
371
+ while ((match = libCallRegex.exec(content)) !== null) {
372
+ const varName = match[1];
373
+ // If we already discovered structure for 'actions', this confirms it
374
+ if (!discoveredStructures.has(varName)) {
375
+ // Mark it as a known complex field even if we don't have the structure
376
+ const existing = discoveredStructures.get('actions');
377
+ if (existing) {
378
+ discoveredStructures.set(varName, existing);
379
+ }
380
+ }
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Extract body schema from a CodeIgniter controller method
386
+ * Looks for $this->post('field'), $this->input->post('field'), $this->put('field'), etc.
387
+ * Also analyzes nested array access patterns after json_decode
388
+ */
389
+ function extractBodySchemaFromMethod(content, methodStartIndex, fullFileContent = null) {
390
+ const bodyFields = {};
391
+
392
+ // Find the method body (from method start to next public function or end of class)
393
+ const methodContent = content.slice(methodStartIndex, methodStartIndex + 8000); // Increased limit
394
+ const endMatch = methodContent.match(/\n\s*(?:public|private|protected)\s+function\s+/);
395
+ const methodBody = endMatch ? methodContent.slice(0, endMatch.index) : methodContent;
396
+
397
+ // Use full file content for structure analysis if available
398
+ const analysisContent = fullFileContent || content;
399
+
400
+ // CodeIgniter REST: $this->post('field'), $this->put('field'), $this->patch('field')
401
+ const postRegex = /\$(\w+)\s*=\s*\$this\s*->\s*(post|put|patch|get)\s*\(\s*['"](\w+)['"]/gi;
402
+ let match;
403
+ while ((match = postRegex.exec(methodBody)) !== null) {
404
+ const varName = match[1];
405
+ const fieldName = match[3];
406
+
407
+ // Check if this is a complex field that likely contains JSON structure
408
+ const isComplexField = isLikelyJsonField(fieldName);
409
+
410
+ // Check if this var is later json_decoded OR is a known complex field
411
+ const jsonDecodeCheck = new RegExp(`\\$${varName}\\s*=\\s*json_decode\\s*\\(\\s*\\$${varName}`, 'i');
412
+ const hasJsonDecode = jsonDecodeCheck.test(methodBody);
413
+
414
+ if (hasJsonDecode || isComplexField) {
415
+ // Analyze nested structure for this variable
416
+ let nestedSchema = extractNestedStructure(analysisContent, varName);
417
+
418
+ // If we found structure, cache it for reuse
419
+ if (Object.keys(nestedSchema).length > 0 && !nestedSchema._example) {
420
+ // Has keys but no example - partial structure
421
+ } else if (Object.keys(nestedSchema).length > 0) {
422
+ discoveredStructures.set(fieldName, nestedSchema);
423
+ }
424
+
425
+ // If we didn't find structure locally, check cache
426
+ if (Object.keys(nestedSchema).length === 0 || !nestedSchema._example) {
427
+ const cachedStructure = discoveredStructures.get(fieldName);
428
+ if (cachedStructure) {
429
+ nestedSchema = cachedStructure;
430
+ }
431
+ }
432
+
433
+ if (Object.keys(nestedSchema).length > 0) {
434
+ bodyFields[fieldName] = nestedSchema;
435
+ } else {
436
+ bodyFields[fieldName] = 'object (JSON structure)';
437
+ }
438
+ } else {
439
+ bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
440
+ }
441
+ }
442
+
443
+ // Simple post without assignment
444
+ const simplePostRegex = /\$this\s*->\s*(post|put|patch|get)\s*\(\s*['"](\w+)['"]/gi;
445
+ while ((match = simplePostRegex.exec(methodBody)) !== null) {
446
+ const fieldName = match[2];
447
+ if (!bodyFields[fieldName]) {
448
+ // Check if this is a known complex field
449
+ if (isLikelyJsonField(fieldName)) {
450
+ const cachedStructure = discoveredStructures.get(fieldName);
451
+ if (cachedStructure) {
452
+ bodyFields[fieldName] = cachedStructure;
453
+ } else {
454
+ bodyFields[fieldName] = 'object (JSON structure)';
455
+ }
456
+ } else {
457
+ bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
458
+ }
459
+ }
460
+ }
461
+
462
+ // CodeIgniter 3: $this->input->post('field')
463
+ const inputPostRegex = /\$this\s*->\s*input\s*->\s*(post|get|put)\s*\(\s*['"](\w+)['"]/gi;
464
+ while ((match = inputPostRegex.exec(methodBody)) !== null) {
465
+ const fieldName = match[2];
466
+ if (!bodyFields[fieldName]) {
467
+ if (isLikelyJsonField(fieldName)) {
468
+ const cachedStructure = discoveredStructures.get(fieldName);
469
+ bodyFields[fieldName] = cachedStructure || 'object (JSON structure)';
470
+ } else {
471
+ bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
472
+ }
473
+ }
474
+ }
475
+
476
+ // PHP $_POST['field'], $_GET['field'], $_REQUEST['field']
477
+ const globalPostRegex = /\$_(POST|GET|REQUEST|PUT)\s*\[\s*['"](\w+)['"]\s*\]/gi;
478
+ while ((match = globalPostRegex.exec(methodBody)) !== null) {
479
+ const fieldName = match[2];
480
+ if (!bodyFields[fieldName]) {
481
+ bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
482
+ }
483
+ }
484
+
485
+ // Laravel/PHP: $request->input('field'), $request->get('field')
486
+ const requestRegex = /\$request\s*->\s*(input|get|post|all)\s*\(\s*['"](\w+)['"]/gi;
487
+ while ((match = requestRegex.exec(methodBody)) !== null) {
488
+ const fieldName = match[2];
489
+ if (!bodyFields[fieldName]) {
490
+ if (isLikelyJsonField(fieldName)) {
491
+ const cachedStructure = discoveredStructures.get(fieldName);
492
+ bodyFields[fieldName] = cachedStructure || 'object (JSON structure)';
493
+ } else {
494
+ bodyFields[fieldName] = inferFieldType(fieldName, methodBody);
495
+ }
496
+ }
497
+ }
498
+
499
+ return bodyFields;
500
+ }
501
+
502
+ /**
503
+ * Check if a field name likely contains JSON/object data
504
+ */
505
+ function isLikelyJsonField(fieldName) {
506
+ const jsonFieldNames = [
507
+ 'actions', 'data', 'body', 'payload', 'params', 'options', 'settings',
508
+ 'config', 'meta', 'metadata', 'attributes', 'properties', 'fields',
509
+ 'items', 'records', 'entries', 'values', 'content', 'json'
510
+ ];
511
+ return jsonFieldNames.includes(fieldName.toLowerCase());
512
+ }
513
+
514
+ /**
515
+ * Extract nested structure from array access patterns
516
+ * e.g., $actions['settings']['updates'] -> { settings: { updates: [] } }
517
+ */
518
+ function extractNestedStructure(methodBody, varName) {
519
+ const structure = {};
520
+ const itemFields = new Set();
521
+
522
+ // Match direct access patterns like $varName['key1']['key2']
523
+ const arrayAccessRegex = new RegExp(
524
+ `\\$${varName}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`,
525
+ 'gi'
526
+ );
527
+
528
+ let match;
529
+ while ((match = arrayAccessRegex.exec(methodBody)) !== null) {
530
+ const key1 = match[1];
531
+ const key2 = match[2];
532
+
533
+ if (!structure[key1]) structure[key1] = {};
534
+ structure[key1][key2] = []; // Arrays like updates, inserts, deletes
535
+ }
536
+
537
+ // Look for foreach patterns to find nested iterators
538
+ // Pattern: foreach ($varName as $key => &$iterVar) { ... foreach ($iterVar['subkey'] as &$item) }
539
+ const foreachRegex = new RegExp(
540
+ `foreach\\s*\\(\\s*\\$${varName}\\s+as\\s+(?:\\$\\w+\\s*=>\\s*)?[&]?\\$(\\w+)\\s*\\)`,
541
+ 'gi'
542
+ );
543
+
544
+ while ((match = foreachRegex.exec(methodBody)) !== null) {
545
+ const iterVar = match[1];
546
+
547
+ // Find nested foreach on iterator's array property
548
+ // e.g., foreach ($settings['updates'] as &$setting)
549
+ const nestedForeachRegex = new RegExp(
550
+ `foreach\\s*\\(\\s*\\$${iterVar}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]\\s+as\\s+(?:\\$\\w+\\s*=>\\s*)?[&]?\\$(\\w+)\\s*\\)`,
551
+ 'gi'
552
+ );
553
+
554
+ let nestedMatch;
555
+ while ((nestedMatch = nestedForeachRegex.exec(methodBody)) !== null) {
556
+ const arrayKey = nestedMatch[1]; // e.g., 'updates'
557
+ const itemVar = nestedMatch[2]; // e.g., 'setting'
558
+
559
+ // Find all field accesses on the item variable
560
+ const itemAccessRegex = new RegExp(`\\$${itemVar}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`, 'gi');
561
+ let itemMatch;
562
+ while ((itemMatch = itemAccessRegex.exec(methodBody)) !== null) {
563
+ itemFields.add(itemMatch[1]);
564
+ }
565
+ }
566
+ }
567
+
568
+ // Also look for direct item field accesses (common variable names)
569
+ const commonItemVars = ['setting', 'item', 'update', 'insert', 'delete', 'row', 'entry', 'data', 'integration', 'assignment'];
570
+ for (const itemVar of commonItemVars) {
571
+ const itemAccessRegex = new RegExp(`\\$${itemVar}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`, 'gi');
572
+ let itemMatch;
573
+ while ((itemMatch = itemAccessRegex.exec(methodBody)) !== null) {
574
+ itemFields.add(itemMatch[1]);
575
+ }
576
+ }
577
+
578
+ // Look for common section names that might be used alongside 'settings'
579
+ // Pattern: $actions['sectionName'] or $actions["sectionName"]
580
+ const sectionRegex = new RegExp(`\\$${varName}\\s*\\[\\s*['"]([\\w]+)['"]\\s*\\]`, 'gi');
581
+ const sections = new Set();
582
+ let sectionMatch;
583
+ while ((sectionMatch = sectionRegex.exec(methodBody)) !== null) {
584
+ sections.add(sectionMatch[1]);
585
+ }
586
+
587
+ // Common section names in settings/actions payloads
588
+ const commonSections = ['settings', 'assignments', 'integrations', 'notifications', 'permissions'];
589
+ for (const section of commonSections) {
590
+ // Check if this section is referenced anywhere in the codebase
591
+ if (methodBody.includes(`['${section}']`) || methodBody.includes(`["${section}"]`)) {
592
+ sections.add(section);
593
+ }
594
+ }
595
+
596
+ // Add sections discovered during pre-scan phase
597
+ const globalSections = discoveredStructures.get('_sections');
598
+ if (globalSections) {
599
+ for (const section of globalSections) {
600
+ // Filter out false positives (id, id_user, etc. are not sections)
601
+ if (!section.startsWith('id') && section.length > 2) {
602
+ sections.add(section);
603
+ }
604
+ }
605
+ }
606
+
607
+ // Build item schema from collected fields
608
+ const itemSchema = {};
609
+ for (const field of itemFields) {
610
+ itemSchema[field] = inferFieldType(field, methodBody);
611
+ }
612
+
613
+ // Ensure common fields are included
614
+ const commonFields = ['key', 'value', 'id_facility', 'type', 'category', 'id_file', 'integrator'];
615
+ for (const field of commonFields) {
616
+ if (!itemSchema[field]) {
617
+ // Check if field is referenced in content
618
+ if (methodBody.includes(`['${field}']`) || methodBody.includes(`["${field}"]`)) {
619
+ itemSchema[field] = inferFieldType(field, methodBody);
620
+ }
621
+ }
622
+ }
623
+
624
+ // Build structure for all discovered sections
625
+ if (sections.size > 0 && Object.keys(itemSchema).length > 0) {
626
+ for (const section of sections) {
627
+ if (!structure[section]) {
628
+ structure[section] = {};
629
+ }
630
+ // Add updates/inserts/deletes for each section
631
+ for (const action of ['updates', 'inserts', 'deletes']) {
632
+ structure[section][action] = `array of { ${Object.entries(itemSchema).map(([k, v]) => `${k}: ${v}`).join(', ')} }`;
633
+ }
634
+ }
635
+ } else if (Object.keys(itemSchema).length > 0) {
636
+ // Apply item schema to existing structure
637
+ for (const [key, value] of Object.entries(structure)) {
638
+ if (typeof value === 'object' && !key.startsWith('_')) {
639
+ for (const [subKey, subValue] of Object.entries(value)) {
640
+ if (Array.isArray(subValue) || ['updates', 'inserts', 'deletes', 'items'].includes(subKey)) {
641
+ structure[key][subKey] = `array of { ${Object.entries(itemSchema).map(([k, v]) => `${k}: ${v}`).join(', ')} }`;
642
+ }
643
+ }
644
+ }
645
+ }
646
+ }
647
+
648
+ // Generate a complete example
649
+ if (Object.keys(structure).length > 0 && Object.keys(itemSchema).length > 0) {
650
+ const exampleItem = {};
651
+ for (const [field, type] of Object.entries(itemSchema)) {
652
+ exampleItem[field] = getExampleValue(field, type);
653
+ }
654
+
655
+ // Example for integration item (with integrator field)
656
+ const integrationExampleItem = { ...exampleItem, integrator: 'mews' };
657
+
658
+ structure._example = {};
659
+ for (const [key, value] of Object.entries(structure)) {
660
+ if (key.startsWith('_')) continue;
661
+ structure._example[key] = {};
662
+ for (const [subKey] of Object.entries(value)) {
663
+ if (['updates', 'inserts', 'deletes'].includes(subKey)) {
664
+ if (key === 'integrations') {
665
+ structure._example[key][subKey] = subKey === 'updates' ? [integrationExampleItem] : [];
666
+ } else {
667
+ structure._example[key][subKey] = subKey === 'updates' ? [exampleItem] : [];
668
+ }
669
+ }
670
+ }
671
+ }
672
+ }
673
+
674
+ return structure;
675
+ }
676
+
677
+ /**
678
+ * Generate an example JSON body based on extracted structure
679
+ */
680
+ function generateExample(structure, itemFields) {
681
+ const example = {};
682
+
683
+ for (const [key, value] of Object.entries(structure)) {
684
+ if (key.startsWith('_')) continue;
685
+
686
+ if (typeof value === 'object' && value !== null) {
687
+ // Nested object
688
+ if (value._arrayOf) {
689
+ // Array of items
690
+ const itemExample = {};
691
+ for (const [itemKey, itemType] of Object.entries(value._arrayOf)) {
692
+ itemExample[itemKey] = getExampleValue(itemKey, itemType);
693
+ }
694
+ example[key] = [itemExample];
695
+ } else {
696
+ example[key] = generateExample(value, itemFields);
697
+ }
698
+ } else {
699
+ example[key] = getExampleValue(key, value);
700
+ }
701
+ }
702
+
703
+ return example;
704
+ }
705
+
706
+ /**
707
+ * Get example value for a field based on its name and type
708
+ */
709
+ function getExampleValue(fieldName, fieldType) {
710
+ const nameLower = fieldName.toLowerCase();
711
+
712
+ if (nameLower === 'key') return 'hotel_title';
713
+ if (nameLower === 'value') return 'My Hotel Name';
714
+ if (nameLower === 'id_facility' || nameLower === 'id_file') return '-1';
715
+ if (nameLower === 'type') return 'text';
716
+ if (nameLower === 'category') return 'main';
717
+ if (nameLower === 'id') return 1;
718
+ if (nameLower.includes('email')) return 'user@example.com';
719
+ if (fieldType === 'integer' || fieldType === 'number') return 1;
720
+ if (fieldType === 'boolean') return true;
721
+ if (fieldType === 'array') return [];
722
+
723
+ return 'string_value';
724
+ }
725
+
726
+ /**
727
+ * Infer field type from field name and context
728
+ */
729
+ function inferFieldType(fieldName, context) {
730
+ const nameLower = fieldName.toLowerCase();
731
+
732
+ // ID fields
733
+ if (nameLower.startsWith('id_') || nameLower.endsWith('_id') || nameLower === 'id') {
734
+ return 'integer';
735
+ }
736
+
737
+ // Boolean-ish fields
738
+ if (nameLower.startsWith('is_') || nameLower.startsWith('has_') ||
739
+ nameLower.includes('enabled') || nameLower.includes('active') ||
740
+ nameLower.includes('flag') || nameLower === 'status') {
741
+ return 'boolean';
742
+ }
743
+
744
+ // Date/time fields
745
+ if (nameLower.includes('date') || nameLower.includes('time') ||
746
+ nameLower.includes('_at') || nameLower === 'created' || nameLower === 'updated') {
747
+ return 'datetime';
748
+ }
749
+
750
+ // Numeric fields
751
+ if (nameLower.includes('count') || nameLower.includes('amount') ||
752
+ nameLower.includes('price') || nameLower.includes('quantity') ||
753
+ nameLower.includes('total') || nameLower.includes('number')) {
754
+ return 'number';
755
+ }
756
+
757
+ // Email
758
+ if (nameLower.includes('email')) {
759
+ return 'email';
760
+ }
761
+
762
+ // Array/JSON fields
763
+ if (nameLower.includes('items') || nameLower.includes('list') ||
764
+ nameLower.includes('array') || nameLower === 'data' || nameLower === 'actions') {
765
+ return 'array|object';
766
+ }
767
+
768
+ // Default to string
769
+ return 'string';
153
770
  }
154
771
 
155
772
  /**
@@ -306,6 +923,78 @@ function extractRequestPatterns(content) {
306
923
  return patterns;
307
924
  }
308
925
 
926
+ /**
927
+ * Extract CodeIgniter REST controller methods and map to routes
928
+ * e.g., index_get() -> GET /path, index_post() -> POST /path
929
+ * e.g., items_get() -> GET /path/items, user_get($id) -> GET /path/user/:id
930
+ */
931
+ function extractCodeIgniterMethods(content, basePath) {
932
+ const routes = [];
933
+
934
+ // Match public function methodname_httpverb($params)
935
+ // CodeIgniter REST convention: {action}_{method}
936
+ const methodRegex = /public\s+function\s+(\w+)_(get|post|put|patch|delete)\s*\(([^)]*)\)/gi;
937
+ let match;
938
+
939
+ while ((match = methodRegex.exec(content)) !== null) {
940
+ const action = match[1]; // e.g., 'index', 'items', 'user'
941
+ const method = match[2].toUpperCase(); // e.g., 'GET', 'POST'
942
+ const params = match[3]; // e.g., '$id = null'
943
+
944
+ // Build the route path
945
+ let path = basePath;
946
+ if (action !== 'index') {
947
+ path += '/' + action.toLowerCase().replace(/_/g, '/');
948
+ }
949
+
950
+ // Add path parameters from function arguments
951
+ if (params) {
952
+ const paramNames = params.match(/\$(\w+)/g);
953
+ if (paramNames) {
954
+ paramNames.forEach(p => {
955
+ const paramName = p.replace('$', '');
956
+ if (!paramName.includes('=')) { // Not optional
957
+ path += '/:' + paramName;
958
+ }
959
+ });
960
+ }
961
+ }
962
+
963
+ routes.push(`// ${method} ${path} - from ${match[1]}_${match[2]}()`);
964
+ }
965
+
966
+ return routes;
967
+ }
968
+
969
+ /**
970
+ * Extract route path from CodeIgniter/Laravel controller file path
971
+ * e.g., application/controllers/api/v2/Settings.php -> /api/v2/settings
972
+ */
973
+ function extractRouteFromControllerPath(filePath) {
974
+ // CodeIgniter 3: application/controllers/api/v2/Settings.php -> /api/v2/settings
975
+ let match = filePath.match(/application\/controllers\/(.+)\.php$/i);
976
+ if (match) {
977
+ const route = '/' + match[1].toLowerCase().replace(/\/index$/, '');
978
+ return route;
979
+ }
980
+
981
+ // CodeIgniter 4: app/Controllers/Api/V2/Settings.php -> /api/v2/settings
982
+ match = filePath.match(/app\/Controllers\/(.+)\.php$/i);
983
+ if (match) {
984
+ const route = '/' + match[1].toLowerCase().replace(/\/index$/, '');
985
+ return route;
986
+ }
987
+
988
+ // Laravel: app/Http/Controllers/Api/V2/SettingsController.php -> /api/v2/settings
989
+ match = filePath.match(/app\/Http\/Controllers\/(.+)Controller\.php$/i);
990
+ if (match) {
991
+ const route = '/' + match[1].toLowerCase().replace(/\/index$/, '');
992
+ return route;
993
+ }
994
+
995
+ return null;
996
+ }
997
+
309
998
  /**
310
999
  * Extract API patterns from file content - framework agnostic
311
1000
  */
@@ -316,7 +1005,22 @@ function extractApiPatterns(content, filePath, absolutePath) {
316
1005
 
317
1006
  // Check if this is a route/controller file - if so, include more context
318
1007
  const isRouteFile = HIGH_PRIORITY_PATTERNS.some(p => p.test(filePath));
319
- const isDtoFile = /dto|interface|types?|schema/i.test(filePath);
1008
+ // DTO/Model/Entity files across languages
1009
+ const isDtoFile = /dto|interface|types?|schema|model|entity|request|response|form/i.test(filePath);
1010
+
1011
+ // For CodeIgniter/Laravel controllers, extract the route from file path
1012
+ const conventionRoute = extractRouteFromControllerPath(filePath);
1013
+ if (conventionRoute) {
1014
+ // Extract HTTP methods from CodeIgniter-style method names
1015
+ const ciMethods = extractCodeIgniterMethods(content, conventionRoute);
1016
+ if (ciMethods.length > 0) {
1017
+ extractedBlocks.push(`// CONVENTION-BASED ROUTES FROM CONTROLLER:\n${ciMethods.join('\n')}`);
1018
+ hasApiPatterns = true;
1019
+ } else {
1020
+ extractedBlocks.push(`// CONVENTION-BASED ROUTE: ${conventionRoute}\n// Controller file path maps to this API endpoint`);
1021
+ hasApiPatterns = true;
1022
+ }
1023
+ }
320
1024
 
321
1025
  // ============================================
322
1026
  // PATTERN 0: Swagger/OpenAPI documentation
@@ -346,7 +1050,7 @@ function extractApiPatterns(content, filePath, absolutePath) {
346
1050
  }
347
1051
 
348
1052
  // ============================================
349
- // PATTERN 3: Express/Koa/Fastify Router definitions
1053
+ // PATTERN 3: Express/Koa/Fastify Router definitions (JavaScript/TypeScript)
350
1054
  // ============================================
351
1055
  const routerPatterns = [
352
1056
  // Express Router: router.get('/path', handler)
@@ -358,12 +1062,92 @@ function extractApiPatterns(content, filePath, absolutePath) {
358
1062
  ];
359
1063
 
360
1064
  // ============================================
361
- // PATTERN 4: NestJS/Decorators
1065
+ // PATTERN 4: NestJS/Decorators (JavaScript/TypeScript)
362
1066
  // ============================================
363
1067
  const decoratorPatterns = [
364
1068
  /@(Get|Post|Put|Patch|Delete|All)\s*\(\s*['"`]?([^'"`\)]*)/gi,
365
1069
  /@Controller\s*\(\s*['"`]([^'"`]+)/gi,
366
1070
  ];
1071
+
1072
+ // ============================================
1073
+ // PATTERN PHP: CodeIgniter, Laravel, Symfony routes
1074
+ // ============================================
1075
+ const phpPatterns = [
1076
+ // CodeIgniter 4: $routes->get('path', 'Controller::method')
1077
+ /\$routes\s*->\s*(get|post|put|patch|delete|add|match|cli)\s*\(\s*['"]([^'"]+)['"]/gi,
1078
+ // CodeIgniter 4: $routes->group()
1079
+ /\$routes\s*->\s*group\s*\(\s*['"]([^'"]+)['"]/gi,
1080
+ // CodeIgniter 3: $route['path'] = 'controller/method'
1081
+ /\$route\s*\[\s*['"]([^'"]+)['"]\s*\]/gi,
1082
+ // Laravel: Route::get('path', ...)
1083
+ /Route\s*::\s*(get|post|put|patch|delete|any|match|resource|apiResource)\s*\(\s*['"]([^'"]+)['"]/gi,
1084
+ // Laravel route groups
1085
+ /Route\s*::\s*(prefix|group|middleware)\s*\(\s*['"]([^'"]+)['"]/gi,
1086
+ // Symfony annotations: @Route("/path")
1087
+ /@Route\s*\(\s*['"]([^'"]+)['"]/gi,
1088
+ // Symfony PHP 8 attributes: #[Route('/path')]
1089
+ /#\[Route\s*\(\s*['"]([^'"]+)['"]/gi,
1090
+ // PHP method definitions in controllers (for convention-based routing)
1091
+ /public\s+function\s+(\w+)\s*\([^)]*\)/gi,
1092
+ // PHP class definitions (to get controller names)
1093
+ /class\s+(\w+)\s+extends\s+(\w*Controller|CI_Controller|BaseController|ResourceController)/gi,
1094
+ ];
1095
+
1096
+ // ============================================
1097
+ // PATTERN Python: Flask, FastAPI, Django
1098
+ // ============================================
1099
+ const pythonPatterns = [
1100
+ // Flask: @app.route('/path')
1101
+ /@app\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
1102
+ // Flask Blueprint: @blueprint.route('/path')
1103
+ /@\w+\.(route|get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi,
1104
+ // FastAPI: @app.get('/path'), @router.post('/path')
1105
+ /@(app|router)\.(get|post|put|patch|delete|api_route)\s*\(\s*['"]([^'"]+)['"]/gi,
1106
+ // Django: path('route/', view)
1107
+ /path\s*\(\s*['"]([^'"]+)['"]/gi,
1108
+ // Django: url(r'^route/$', view)
1109
+ /url\s*\(\s*r?['"]([^'"]+)['"]/gi,
1110
+ ];
1111
+
1112
+ // ============================================
1113
+ // PATTERN Ruby: Rails, Sinatra
1114
+ // ============================================
1115
+ const rubyPatterns = [
1116
+ // Rails: get '/path', post '/path', resources :items
1117
+ /^\s*(get|post|put|patch|delete|resources|resource|root|match)\s+['":]/gim,
1118
+ // Rails namespace/scope
1119
+ /(namespace|scope)\s+[:'"](\w+)/gi,
1120
+ // Sinatra: get '/path' do
1121
+ /^\s*(get|post|put|patch|delete)\s+['"]([^'"]+)['"]\s+do/gim,
1122
+ ];
1123
+
1124
+ // ============================================
1125
+ // PATTERN Go: Gin, Echo, Chi, net/http
1126
+ // ============================================
1127
+ const goPatterns = [
1128
+ // Gin: r.GET("/path", handler), router.POST("/path", handler)
1129
+ /\.(GET|POST|PUT|PATCH|DELETE|Handle|Any|Group)\s*\(\s*["']([^"']+)["']/gi,
1130
+ // Echo: e.GET("/path", handler)
1131
+ /e\.(GET|POST|PUT|PATCH|DELETE|Any|Group)\s*\(\s*["']([^"']+)["']/gi,
1132
+ // Chi: r.Get("/path", handler), r.Route("/path", ...)
1133
+ /r\.(Get|Post|Put|Patch|Delete|Route|Group|Mount)\s*\(\s*["']([^"']+)["']/gi,
1134
+ // net/http: http.HandleFunc("/path", handler)
1135
+ /http\.(HandleFunc|Handle)\s*\(\s*["']([^"']+)["']/gi,
1136
+ // Gorilla mux: r.HandleFunc("/path", handler).Methods("GET")
1137
+ /HandleFunc\s*\(\s*["']([^"']+)["']/gi,
1138
+ ];
1139
+
1140
+ // ============================================
1141
+ // PATTERN Java/Kotlin: Spring Boot
1142
+ // ============================================
1143
+ const javaPatterns = [
1144
+ // Spring: @GetMapping("/path"), @PostMapping("/path")
1145
+ /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?["']?([^"'\)]+)/gi,
1146
+ // Spring: @RequestMapping(method = RequestMethod.GET)
1147
+ /@RequestMapping\s*\([^)]*method\s*=\s*RequestMethod\.(GET|POST|PUT|PATCH|DELETE)/gi,
1148
+ // JAX-RS: @GET, @POST, @Path("/path")
1149
+ /@(GET|POST|PUT|PATCH|DELETE|Path)\s*(?:\(\s*["']([^"']+)["']\s*\))?/gi,
1150
+ ];
367
1151
 
368
1152
  // ============================================
369
1153
  // PATTERN 5: Frontend HTTP calls
@@ -406,10 +1190,15 @@ function extractApiPatterns(content, filePath, absolutePath) {
406
1190
  /method:\s*['"`](GET|POST|PUT|PATCH|DELETE)['"`]/gi,
407
1191
  ];
408
1192
 
409
- // Combine all patterns for line scanning
1193
+ // Combine all patterns for line scanning (all languages)
410
1194
  const allPatterns = [
411
1195
  ...routerPatterns,
412
1196
  ...decoratorPatterns,
1197
+ ...phpPatterns,
1198
+ ...pythonPatterns,
1199
+ ...rubyPatterns,
1200
+ ...goPatterns,
1201
+ ...javaPatterns,
413
1202
  ...httpCallPatterns,
414
1203
  ...urlPatterns,
415
1204
  ...methodUrlPatterns
@@ -495,8 +1284,28 @@ function extractApiPatterns(content, filePath, absolutePath) {
495
1284
  }
496
1285
  }
497
1286
 
498
- // Also check for common API keywords
499
- const hasKeyword = /\.get\(|\.post\(|\.put\(|\.patch\(|\.delete\(|fetch\(|axios|\/api\/|endpoint|@Get|@Post|@Put|@Delete/i.test(line);
1287
+ // Also check for common API keywords (all languages)
1288
+ const hasKeyword = new RegExp([
1289
+ // JavaScript/TypeScript
1290
+ '\\.get\\(', '\\.post\\(', '\\.put\\(', '\\.patch\\(', '\\.delete\\(',
1291
+ 'fetch\\(', 'axios', '\\/api\\/', 'endpoint',
1292
+ '@Get', '@Post', '@Put', '@Delete',
1293
+ // PHP (CodeIgniter, Laravel)
1294
+ '\\$routes\\s*->', 'Route\\s*::', '@Route',
1295
+ '\\$this->input->', '\\$this->request->', // CodeIgniter input
1296
+ '\\$request->', '\\$_POST', '\\$_GET', '\\$_PUT', // PHP request data
1297
+ 'public\\s+function\\s+\\w+', // PHP public methods (potential endpoints)
1298
+ 'extends\\s+\\w*Controller', // PHP controller classes
1299
+ // Python (Flask, FastAPI, Django)
1300
+ '@app\\.route', '@app\\.get', '@app\\.post', '@router\\.',
1301
+ 'path\\s*\\(', 'url\\s*\\(',
1302
+ // Ruby (Rails)
1303
+ 'resources\\s+:', 'get\\s+[\'"]/', 'post\\s+[\'"]/',
1304
+ // Go (Gin, Echo, Chi)
1305
+ '\\.GET\\(', '\\.POST\\(', 'HandleFunc\\(',
1306
+ // Java/Kotlin (Spring)
1307
+ '@GetMapping', '@PostMapping', '@RequestMapping', '@Path'
1308
+ ].join('|'), 'i').test(line);
500
1309
 
501
1310
  if (hasPattern || hasKeyword) {
502
1311
  hasApiPatterns = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merlean/analyzer",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "AI Bot codebase analyzer - generates site maps for AI assistant integration",
5
5
  "keywords": ["ai", "bot", "analyzer", "claude", "anthropic", "widget"],
6
6
  "author": "zmaren",