wogiflow 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.
@@ -530,14 +530,36 @@ Display:
530
530
 
531
531
  Display: ` Data-fetching hooks... ✓ react-query, 80 useGet* hooks`
532
532
 
533
- 15. **Populate app-map.md from component data:**
534
- From the pattern extraction result, populate app-map.md with:
535
- - Detected UI components -> Components table
536
- - Detected pages/screens -> Screens table
537
- - Detected modals -> Modals table
538
- Include paths and patterns where detected.
539
-
540
- Display: ` app-map.md... ✓ Found 24 components/modules`
533
+ 15. **Run registry-manager scan (comprehensive registry population):**
534
+
535
+ **CRITICAL**: This step replaces manual AI-driven app-map population. The registry
536
+ manager runs ALL active scanners (components, functions, APIs, schemas, services)
537
+ with recursive directory traversal and glob-based discovery. This ensures no
538
+ subdirectory components, co-located hooks, or separated-export API functions are missed.
539
+
540
+ ```javascript
541
+ // Run the full registry scan — this handles recursion, glob patterns, and all export patterns
542
+ const { execSync } = require('child_process');
543
+ try {
544
+ execSync('node node_modules/wogiflow/scripts/flow-registry-manager.js scan', {
545
+ cwd: projectRoot,
546
+ stdio: 'inherit',
547
+ timeout: 60000
548
+ });
549
+ } catch (err) {
550
+ console.warn('Registry manager scan failed, falling back to individual scanners:', err.message);
551
+ // Individual scanners from steps 13-14 already ran as fallback
552
+ }
553
+ ```
554
+
555
+ The component scanner generates `app-map.md` from scan results (grouped by category/directory).
556
+ The function scanner discovers co-located hooks via glob patterns (`src/**/hooks`, etc.).
557
+ The API scanner handles all export patterns including separated `const + export default`.
558
+
559
+ Display: ` Registry scan... ✓ All registries populated (components, functions, APIs)`
560
+
561
+ **NOTE**: If the registry manager scan succeeds, it supersedes the individual scanner runs
562
+ from steps 13-14. The scanners are idempotent — running them twice just refreshes the same data.
541
563
 
542
564
  16. **Extract file templates:**
543
565
  ```javascript
@@ -100,7 +100,7 @@
100
100
  "displayName": "Claude Sonnet 4.6",
101
101
  "contextWindow": 200000,
102
102
  "contextWindowBeta": 1000000,
103
- "maxOutputTokens": 128000,
103
+ "maxOutputTokens": 64000,
104
104
  "costTier": "standard",
105
105
  "pricing": {
106
106
  "inputPer1kTokens": 0.003,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "AI-powered development workflow management system with multi-model support",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -45,6 +45,13 @@ const DEFAULT_CONFIG = {
45
45
  'src/mutations'
46
46
  ],
47
47
 
48
+ // Glob patterns discover co-located API files (any project structure)
49
+ globPatterns: [
50
+ 'src/**/queries',
51
+ 'src/**/mutations',
52
+ 'src/**/api'
53
+ ],
54
+
48
55
  filePatterns: ['**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'],
49
56
 
50
57
  excludePatterns: [
@@ -75,6 +82,7 @@ class APIScanner extends BaseScanner {
75
82
  super({
76
83
  configKey: 'apiRegistry',
77
84
  directories: DEFAULT_CONFIG.directories,
85
+ globPatterns: DEFAULT_CONFIG.globPatterns,
78
86
  filePatterns: DEFAULT_CONFIG.filePatterns,
79
87
  excludePatterns: DEFAULT_CONFIG.excludePatterns,
80
88
  ...config
@@ -131,36 +139,27 @@ class APIScanner extends BaseScanner {
131
139
  }
132
140
 
133
141
  /**
134
- * Parse file with Babel AST
142
+ * Parse file with Babel AST using shared two-pass approach from BaseScanner.
143
+ * Handles every JS/TS export pattern by design.
135
144
  */
136
145
  parseWithBabel(content, filePath, service) {
137
- try {
138
- const ast = this.parser.parse(content, {
139
- sourceType: 'module',
140
- plugins: ['typescript', 'jsx', 'decorators-legacy']
141
- });
142
-
143
- this.traverse(ast, {
144
- ExportNamedDeclaration: (nodePath) => {
145
- const declaration = nodePath.node.declaration;
146
- if (!declaration) return;
147
-
148
- if (declaration.type === 'FunctionDeclaration' && declaration.id) {
149
- this.extractAPIFunction(declaration, filePath, service, content);
150
- } else if (declaration.type === 'VariableDeclaration') {
151
- for (const decl of declaration.declarations) {
152
- if (decl.init &&
153
- (decl.init.type === 'ArrowFunctionExpression' ||
154
- decl.init.type === 'FunctionExpression')) {
155
- this.extractAPIFunctionFromVariable(decl, filePath, service, content);
156
- }
157
- }
158
- }
159
- }
160
- });
161
- } catch (err) {
162
- // Fall back to regex if babel fails
146
+ const result = this.collectExportedDeclarations(content);
147
+ if (!result) {
163
148
  this.parseWithRegex(content, filePath, service);
149
+ return;
150
+ }
151
+
152
+ const { declarations, exported } = result;
153
+
154
+ for (const [name, _exportInfo] of exported) {
155
+ const declInfo = declarations.get(name);
156
+ if (!declInfo) continue;
157
+
158
+ if (declInfo.kind === 'func') {
159
+ this.extractAPIFunction(declInfo.node, filePath, service, content);
160
+ } else {
161
+ this.extractAPIFunctionFromVariable(declInfo.node, filePath, service, content);
162
+ }
164
163
  }
165
164
  }
166
165
 
@@ -181,7 +180,7 @@ class APIScanner extends BaseScanner {
181
180
  name,
182
181
  params,
183
182
  method: isAPIFunction.method || this.inferMethodFromName(name),
184
- endpoint: isAPIFunction.endpoint,
183
+ endpoint: isAPIFunction.endpoint || 'dynamic',
185
184
  description: jsdoc.description || '',
186
185
  file: filePath,
187
186
  service,
@@ -210,7 +209,7 @@ class APIScanner extends BaseScanner {
210
209
  name,
211
210
  params,
212
211
  method: isAPIFunction.method || this.inferMethodFromName(name),
213
- endpoint: isAPIFunction.endpoint,
212
+ endpoint: isAPIFunction.endpoint || 'dynamic',
214
213
  description: jsdoc.description || '',
215
214
  file: filePath,
216
215
  service,
@@ -228,13 +227,15 @@ class APIScanner extends BaseScanner {
228
227
  /^(post|create|add|save|submit)/i,
229
228
  /^(put|update|modify|patch)/i,
230
229
  /^(delete|remove|destroy)/i,
230
+ /^use(Get|Fetch|Load|Create|Update|Delete|Post|Put|Patch|Remove|Query|Mutation)/,
231
231
  /(api|endpoint|request|mutation|query)$/i
232
232
  ];
233
233
 
234
234
  const nameMatches = apiNamePatterns.some(p => p.test(name));
235
235
 
236
- // Check function body for HTTP calls
237
- const funcBody = content.substring(startPos, endPos);
236
+ // Check function body for HTTP calls — use full body including nested scopes
237
+ // Find the matching closing brace to capture nested arrow functions
238
+ const funcBody = this._extractFullBody(content, startPos, endPos);
238
239
  const httpPatterns = [
239
240
  /fetch\s*\(/,
240
241
  /axios\./,
@@ -247,7 +248,9 @@ class APIScanner extends BaseScanner {
247
248
  /apiClient/i,
248
249
  /useSWR/,
249
250
  /useQuery/,
250
- /useMutation/
251
+ /useMutation/,
252
+ /useInfiniteQuery/,
253
+ /useSuspenseQuery/
251
254
  ];
252
255
 
253
256
  const bodyMatches = httpPatterns.some(p => p.test(funcBody));
@@ -280,6 +283,68 @@ class APIScanner extends BaseScanner {
280
283
  };
281
284
  }
282
285
 
286
+ /**
287
+ * Extract the full function body content, including nested scopes.
288
+ * Falls back to substring if brace matching fails.
289
+ * @param {string} content - Full file content
290
+ * @param {number} startPos - Start position of the function
291
+ * @param {number} endPos - End position from AST (may be too narrow)
292
+ * @returns {string} Function body content
293
+ */
294
+ _extractFullBody(content, startPos, endPos) {
295
+ const MAX_SCAN = 50000; // Cap at 50KB to avoid stalls on large files
296
+ const braceStart = content.indexOf('{', startPos);
297
+ if (braceStart === -1 || braceStart > endPos + 100) {
298
+ return content.substring(startPos, Math.min(endPos + 500, content.length));
299
+ }
300
+
301
+ // Match braces with string/comment awareness
302
+ let depth = 0;
303
+ let i = braceStart;
304
+ const limit = Math.min(content.length, braceStart + MAX_SCAN);
305
+ while (i < limit) {
306
+ const ch = content[i];
307
+
308
+ // Skip single-line comments
309
+ if (ch === '/' && content[i + 1] === '/') {
310
+ i = content.indexOf('\n', i + 2);
311
+ if (i === -1) break;
312
+ i++;
313
+ continue;
314
+ }
315
+ // Skip multi-line comments
316
+ if (ch === '/' && content[i + 1] === '*') {
317
+ i = content.indexOf('*/', i + 2);
318
+ if (i === -1) break;
319
+ i += 2;
320
+ continue;
321
+ }
322
+ // Skip string literals
323
+ if (ch === "'" || ch === '"' || ch === '`') {
324
+ i++;
325
+ while (i < limit) {
326
+ if (content[i] === '\\') { i += 2; continue; }
327
+ if (content[i] === ch) break;
328
+ i++;
329
+ }
330
+ i++;
331
+ continue;
332
+ }
333
+
334
+ if (ch === '{') depth++;
335
+ else if (ch === '}') {
336
+ depth--;
337
+ if (depth === 0) {
338
+ return content.substring(startPos, i + 1);
339
+ }
340
+ }
341
+ i++;
342
+ }
343
+
344
+ // Fallback to AST bounds with buffer
345
+ return content.substring(startPos, Math.min(endPos + 200, content.length));
346
+ }
347
+
283
348
  /**
284
349
  * Infer HTTP method from function name
285
350
  */
@@ -294,56 +359,55 @@ class APIScanner extends BaseScanner {
294
359
  }
295
360
 
296
361
  /**
297
- * Parse with regex (fallback)
362
+ * Parse with regex (fallback, two-pass approach)
298
363
  */
299
364
  parseWithRegex(content, filePath, service) {
300
- // Match exported async functions
301
- const funcRegex = /export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
365
+ // Two-pass regex: find declarations, find exports, intersect
302
366
  let match;
303
367
 
304
- while ((match = funcRegex.exec(content)) !== null) {
305
- const [fullMatch, isAsync, name, paramsStr] = match;
368
+ // Pass 1: Find all function-like declarations
369
+ const declarations = new Map(); // name -> { line, paramsStr }
306
370
 
307
- // Check if it looks like an API function
308
- if (!this.isLikelyAPIFunctionFromName(name) && !this.hasHTTPCall(content, match.index)) {
309
- continue;
310
- }
311
-
312
- const jsdoc = this.extractJSDocBefore(content, match.index);
313
-
314
- this.addClientFunction({
315
- name,
316
- params: this.parseParamsFromString(paramsStr),
317
- method: this.inferMethodFromName(name),
318
- endpoint: null,
319
- description: jsdoc,
320
- file: filePath,
321
- service,
322
- line: this.getLineNumber(content, match.index)
371
+ const funcRegex = /(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
372
+ while ((match = funcRegex.exec(content)) !== null) {
373
+ declarations.set(match[1], {
374
+ line: this.getLineNumber(content, match.index),
375
+ paramsStr: match[2],
376
+ pos: match.index
323
377
  });
324
378
  }
325
379
 
326
- // Match exported const arrow functions
327
- const arrowRegex = /export\s+const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/g;
328
-
380
+ const arrowRegex = /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/g;
329
381
  while ((match = arrowRegex.exec(content)) !== null) {
330
- const [fullMatch, name] = match;
382
+ if (!declarations.has(match[1])) {
383
+ declarations.set(match[1], {
384
+ line: this.getLineNumber(content, match.index),
385
+ paramsStr: '',
386
+ pos: match.index
387
+ });
388
+ }
389
+ }
390
+
391
+ // Pass 2: Use shared export-collection from BaseScanner
392
+ const exported = this.collectExportedNamesRegex(content);
331
393
 
332
- if (!this.isLikelyAPIFunctionFromName(name) && !this.hasHTTPCall(content, match.index)) {
394
+ // Intersect: register exported declarations that look like API functions
395
+ for (const [name, info] of declarations) {
396
+ if (!exported.has(name)) continue;
397
+ if (!this.isLikelyAPIFunctionFromName(name) && !this.hasHTTPCall(content, info.pos)) {
333
398
  continue;
334
399
  }
335
400
 
336
- const jsdoc = this.extractJSDocBefore(content, match.index);
337
-
401
+ const jsdoc = this.extractJSDocBefore(content, info.pos);
338
402
  this.addClientFunction({
339
403
  name,
340
- params: [],
404
+ params: info.paramsStr ? this.parseParamsFromString(info.paramsStr) : [],
341
405
  method: this.inferMethodFromName(name),
342
- endpoint: null,
406
+ endpoint: 'dynamic',
343
407
  description: jsdoc,
344
408
  file: filePath,
345
409
  service,
346
- line: this.getLineNumber(content, match.index)
410
+ line: info.line
347
411
  });
348
412
  }
349
413
  }
@@ -353,6 +417,7 @@ class APIScanner extends BaseScanner {
353
417
  */
354
418
  isLikelyAPIFunctionFromName(name) {
355
419
  return /^(get|fetch|load|post|create|put|update|patch|delete|remove|query|mutation)/i.test(name) ||
420
+ /^use(Get|Fetch|Load|Create|Update|Delete|Post|Put|Patch|Remove|Query|Mutation)/i.test(name) ||
356
421
  /(api|endpoint|request)$/i.test(name);
357
422
  }
358
423
 
@@ -38,6 +38,7 @@ const DEFAULT_CONFIG = {
38
38
  'src/utils',
39
39
  'src/lib',
40
40
  'src/helpers',
41
+ 'src/services',
41
42
  'utils',
42
43
  'lib',
43
44
  'helpers',
@@ -45,6 +46,13 @@ const DEFAULT_CONFIG = {
45
46
  'shared'
46
47
  ],
47
48
 
49
+ // Glob patterns discover co-located directories (any project structure)
50
+ globPatterns: [
51
+ 'src/**/hooks',
52
+ 'src/**/helpers',
53
+ 'src/**/utils'
54
+ ],
55
+
48
56
  filePatterns: ['**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'],
49
57
 
50
58
  excludePatterns: [
@@ -70,6 +78,7 @@ class FunctionScanner extends BaseScanner {
70
78
  super({
71
79
  configKey: 'functionRegistry',
72
80
  directories: DEFAULT_CONFIG.directories,
81
+ globPatterns: DEFAULT_CONFIG.globPatterns,
73
82
  filePatterns: DEFAULT_CONFIG.filePatterns,
74
83
  excludePatterns: DEFAULT_CONFIG.excludePatterns,
75
84
  ...config
@@ -113,42 +122,27 @@ class FunctionScanner extends BaseScanner {
113
122
  }
114
123
 
115
124
  /**
116
- * Parse file with Babel AST
125
+ * Parse file with Babel AST using shared two-pass approach from BaseScanner.
126
+ * Handles every JS/TS export pattern by design — no whack-a-mole.
117
127
  */
118
128
  parseWithBabel(content, filePath, category) {
119
- try {
120
- const ast = this.parser.parse(content, {
121
- sourceType: 'module',
122
- plugins: ['typescript', 'jsx', 'decorators-legacy']
123
- });
124
-
125
- this.traverse(ast, {
126
- ExportNamedDeclaration: (nodePath) => {
127
- const declaration = nodePath.node.declaration;
128
- if (!declaration) return;
129
-
130
- if (declaration.type === 'FunctionDeclaration' && declaration.id) {
131
- this.extractFunction(declaration, filePath, category, content);
132
- } else if (declaration.type === 'VariableDeclaration') {
133
- for (const decl of declaration.declarations) {
134
- if (decl.init &&
135
- (decl.init.type === 'ArrowFunctionExpression' ||
136
- decl.init.type === 'FunctionExpression')) {
137
- this.extractFunctionFromVariable(decl, filePath, category, content);
138
- }
139
- }
140
- }
141
- },
142
- ExportDefaultDeclaration: (nodePath) => {
143
- const declaration = nodePath.node.declaration;
144
- if (declaration.type === 'FunctionDeclaration' && declaration.id) {
145
- this.extractFunction(declaration, filePath, category, content, true);
146
- }
147
- }
148
- });
149
- } catch (err) {
150
- // Fall back to regex if babel fails
129
+ const result = this.collectExportedDeclarations(content);
130
+ if (!result) {
151
131
  this.parseWithRegex(content, filePath, category);
132
+ return;
133
+ }
134
+
135
+ const { declarations, exported } = result;
136
+
137
+ for (const [name, exportInfo] of exported) {
138
+ const declInfo = declarations.get(name);
139
+ if (!declInfo) continue;
140
+
141
+ if (declInfo.kind === 'func') {
142
+ this.extractFunction(declInfo.node, filePath, category, content, exportInfo.isDefault);
143
+ } else {
144
+ this.extractFunctionFromVariable(declInfo.node, filePath, category, content);
145
+ }
152
146
  }
153
147
  }
154
148
 
@@ -223,44 +217,52 @@ class FunctionScanner extends BaseScanner {
223
217
  * Parse file with regex (fallback)
224
218
  */
225
219
  parseWithRegex(content, filePath, category) {
226
- // Match exported function declarations
227
- const functionRegex = /export\s+(async\s+)?function\s+(\w+)\s*(<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\s{]+))?\s*\{/g;
220
+ // Pass 1: Find all function-like declarations
221
+ const declarations = new Map();
228
222
  let match;
229
223
 
224
+ const functionRegex = /(?:export\s+(?:default\s+)?)?(?:(async)\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\s{]+))?\s*\{/g;
230
225
  while ((match = functionRegex.exec(content)) !== null) {
231
- const [, isAsync, name, generics, paramsStr, returnType] = match;
232
- const params = this.parseParamsFromString(paramsStr);
233
- const jsdoc = this.extractJSDocBefore(content, match.index);
234
-
235
- this.addFunction({
236
- name,
237
- params,
226
+ const [, isAsync, name, paramsStr, returnType] = match;
227
+ declarations.set(name, {
228
+ line: this.getLineNumber(content, match.index),
229
+ params: this.parseParamsFromString(paramsStr),
238
230
  returnType: returnType || (isAsync ? 'Promise<any>' : null),
239
- description: jsdoc,
240
- file: filePath,
241
- category,
242
- isDefault: false,
243
- line: this.getLineNumber(content, match.index)
231
+ jsdoc: this.extractJSDocBefore(content, match.index)
244
232
  });
245
233
  }
246
234
 
247
- // Match exported const arrow functions
248
- const arrowRegex = /export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*([^\s=>]+))?\s*=>/g;
249
-
235
+ // Const arrow functions capture params group (finding-005 fix)
236
+ const arrowRegex = /(?:export\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*([^\s=>]+))?\s*=>/g;
250
237
  while ((match = arrowRegex.exec(content)) !== null) {
251
- const [, name, returnType] = match;
252
- const jsdoc = this.extractJSDocBefore(content, match.index);
253
-
254
- this.addFunction({
255
- name,
256
- params: [],
257
- returnType,
258
- description: jsdoc,
259
- file: filePath,
260
- category,
261
- isDefault: false,
262
- line: this.getLineNumber(content, match.index)
263
- });
238
+ const [, name, paramsStr, returnType] = match;
239
+ if (!declarations.has(name)) {
240
+ declarations.set(name, {
241
+ line: this.getLineNumber(content, match.index),
242
+ params: paramsStr ? this.parseParamsFromString(paramsStr) : [],
243
+ returnType,
244
+ jsdoc: this.extractJSDocBefore(content, match.index)
245
+ });
246
+ }
247
+ }
248
+
249
+ // Pass 2: Use shared export-collection from BaseScanner
250
+ const exported = this.collectExportedNamesRegex(content);
251
+
252
+ // Intersect: register all exported declarations
253
+ for (const [name, info] of declarations) {
254
+ if (exported.has(name)) {
255
+ this.addFunction({
256
+ name,
257
+ params: info.params,
258
+ returnType: info.returnType,
259
+ description: info.jsdoc,
260
+ file: filePath,
261
+ category,
262
+ isDefault: false,
263
+ line: info.line
264
+ });
265
+ }
264
266
  }
265
267
  }
266
268
 
@@ -92,7 +92,7 @@ const IGNORE_PATTERNS = [
92
92
  ];
93
93
 
94
94
  // Colors for CLI output
95
- const { colors: c } = require('./flow-output');
95
+ const { colors: c, getTodayDate } = require('./flow-output');
96
96
 
97
97
  // ============================================================================
98
98
  // Utility Functions