wogiflow 2.2.0 → 2.3.1

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 (29) hide show
  1. package/.claude/commands/wogi-epics.md +16 -0
  2. package/.claude/commands/wogi-onboard.md +30 -8
  3. package/.claude/commands/wogi-start.md +2 -0
  4. package/.workflow/models/registry.json +1 -1
  5. package/.workflow/templates/claude-md.hbs +25 -0
  6. package/package.json +1 -1
  7. package/scripts/flow-api-index.js +128 -63
  8. package/scripts/flow-function-index.js +65 -63
  9. package/scripts/flow-pattern-extractor.js +1 -1
  10. package/scripts/flow-scanner-base.js +200 -7
  11. package/scripts/flow-skill-generator.js +1 -0
  12. package/scripts/flow-template-extractor.js +1 -1
  13. package/scripts/registries/component-registry.js +141 -4
  14. package/.claude/rules/_internal/README.md +0 -64
  15. package/.claude/rules/_internal/document-structure.md +0 -77
  16. package/.claude/rules/_internal/dual-repo-management.md +0 -174
  17. package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
  18. package/.claude/rules/_internal/github-releases.md +0 -71
  19. package/.claude/rules/_internal/model-management.md +0 -35
  20. package/.claude/rules/_internal/self-maintenance.md +0 -87
  21. package/.claude/rules/architecture/component-reuse.md +0 -38
  22. package/.claude/rules/code-style/naming-conventions.md +0 -52
  23. package/.claude/rules/operations/git-workflows.md +0 -92
  24. package/.claude/rules/operations/scratch-directory.md +0 -54
  25. package/.claude/rules/security/security-patterns.md +0 -176
  26. package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
  27. package/.workflow/specs/architecture.md.template +0 -24
  28. package/.workflow/specs/stack.md.template +0 -33
  29. package/.workflow/specs/testing.md.template +0 -36
@@ -148,6 +148,22 @@ This means after creating an epic with stories, you don't need to manually start
148
148
 
149
149
  **To disable**: Set `config.bulkOrchestrator.enabled: false`
150
150
 
151
+ ## Anti-Deferral Rule (MANDATORY)
152
+
153
+ **When creating an epic from user input, EVERY item the user provided MUST become a tracked story.**
154
+
155
+ You must NEVER:
156
+ - Create stories for items 1-5 and silently skip items 6-9 because you judged them as "enhancements"
157
+ - Label items as "deferred" or "long-term" and exclude them from the epic
158
+ - Apply your own priority filter to decide which items deserve tasks
159
+
160
+ You MAY:
161
+ - Assign different priorities (P0/P1/P2/P3) to stories — but ALL items get stories
162
+ - Suggest an execution order — but ALL items are tracked in the epic
163
+ - Ask the user "Should I defer items 6-9?" — explicit user consent is the ONLY valid reason to exclude items
164
+
165
+ **If the user provides 9 items, the epic MUST contain 9 stories (or items grouped into stories where every item appears as an acceptance criterion). Verify this with a reconciliation count before proceeding.**
166
+
151
167
  ## Tips
152
168
 
153
169
  - **Start with epics for major features** - Break down into stories before implementation
@@ -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
@@ -192,6 +192,8 @@ Item Reconciliation:
192
192
 
193
193
  **Skip when**: Input has only 1-2 items, or is a task ID reference.
194
194
 
195
+ **ANTI-DEFERRAL ENFORCEMENT**: After reconciliation, verify ALL items became tasks/criteria. If you find yourself writing "deferred", "skipped", or "not created" for ANY item — STOP. You are violating the anti-deferral rule. The user provided these items for a reason. Create tasks for ALL of them. You may suggest priority ordering (P0-P3), but you must NEVER autonomously filter items out. A large ready queue is correct behavior. A filtered queue is data loss that breaks the user's trust.
196
+
195
197
  ### Step 1.3: Explore Phase (MANDATORY Multi-Agent Research)
196
198
 
197
199
  **For L2+ tasks. Research is MANDATORY** — do NOT skip even if you think you know the answer.
@@ -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,
@@ -191,6 +191,31 @@ cat .workflow/state/decisions.md # Project rules
191
191
 
192
192
  **These apply to ALL implementation work:**
193
193
 
194
+ ### Anti-Deferral Rule (MANDATORY — ZERO TOLERANCE)
195
+
196
+ **You MUST NEVER autonomously defer, skip, deprioritize, or drop items from the user's input.**
197
+
198
+ If the user provides N items, ALL N must become tracked work items. No exceptions. No judgment calls about what's "important" vs. "enhancement" vs. "long-term."
199
+
200
+ **Anti-Deferral Checklist** — If ANY of these thoughts cross your mind, you are about to drop items:
201
+ - "Items 6-9 are enhancements, I'll focus on the fixes first" → WRONG. Create tasks for ALL items.
202
+ - "This one was labeled 'long-term' by the team" → WRONG. Track it. The user decides when to execute, not you.
203
+ - "I'll defer these as lower priority" → WRONG. You may SUGGEST a priority order, but every item must be a tracked task.
204
+ - "The ready queue would be too large" → WRONG. A large queue is correct. A filtered queue is data loss.
205
+ - "I already created the important ones" → WRONG. Important is not your call. Create ALL of them.
206
+
207
+ **What you MAY do:**
208
+ - Suggest a priority order (P0/P1/P2/P3) — but ALL items get tasks regardless of priority
209
+ - Group related items into stories — but every item must appear as a criterion in at least one story
210
+ - Ask the user to confirm scope — but do NOT preemptively filter
211
+
212
+ **What you must NEVER do:**
213
+ - Silently drop items because you judged them as "enhancements" or "nice-to-haves"
214
+ - Create tasks for only a subset of items without explicit user approval to defer the rest
215
+ - Use words like "deferred", "skipped", or "not created" for items the user provided
216
+
217
+ **This rule applies everywhere**: `/wogi-start`, `/wogi-story`, `/wogi-epics`, `/wogi-extract-review`, and any other command that converts user input into tracked work.
218
+
194
219
  ### Task ID Format (MANDATORY)
195
220
 
196
221
  All task IDs MUST be generated by `generateTaskId()` from `wogiflow/scripts/flow-utils.js`. **Never manually type a task ID.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wogiflow",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
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