wogiflow 2.1.3 → 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.
@@ -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
 
@@ -254,6 +254,134 @@ function calculateHealthScore(scores) {
254
254
  };
255
255
  }
256
256
 
257
+ // ============================================================
258
+ // Pattern Promotion (Audit → Learning Pipeline)
259
+ // ============================================================
260
+
261
+ // Severity classification thresholds for audit patterns
262
+ const SYSTEMIC_THRESHOLD = 5; // 5+ files = HIGH severity / systemic
263
+ const MEDIUM_THRESHOLD = 3; // 3-4 files = MEDIUM severity
264
+
265
+ /**
266
+ * Process AI-clustered audit findings through the learning pipeline.
267
+ *
268
+ * For each clustered pattern:
269
+ * 1. Check if a rule already exists in decisions.md → ENFORCEMENT_GAP
270
+ * 2. Record/increment in feedback-patterns.md
271
+ * 3. Check promotion threshold → auto-promote if met
272
+ * 4. Check last-audit.json → detect RECURRING patterns
273
+ *
274
+ * @param {Object[]} clusters - AI-clustered patterns from audit
275
+ * @param {Object} [previousAudit] - Previous audit data for recurrence detection
276
+ * @returns {Object} Promotion results per pattern
277
+ */
278
+ function promoteAuditPatterns(clusters, previousAudit) {
279
+ const {
280
+ recordAuditPattern,
281
+ checkEnforcementGap,
282
+ promoteToDecisions,
283
+ syncToRulesDir,
284
+ mapAuditCategoryToLearnerCategory,
285
+ buildAuditRuleTemplate
286
+ } = require('./flow-standards-learner');
287
+
288
+ const previousPatternIds = new Set(
289
+ (previousAudit?.patterns || []).map(p => p.patternId)
290
+ );
291
+
292
+ const results = {
293
+ patterns: [],
294
+ summary: {
295
+ total: clusters.length,
296
+ promoted: 0,
297
+ promotionFailed: 0,
298
+ tracking: 0,
299
+ enforcementGaps: 0,
300
+ newPatterns: 0,
301
+ recurring: 0
302
+ }
303
+ };
304
+
305
+ for (const cluster of clusters) {
306
+ const patternResult = {
307
+ patternId: cluster.patternId,
308
+ category: cluster.category,
309
+ description: cluster.description,
310
+ instanceCount: cluster.instanceCount,
311
+ severity: cluster.severity || (cluster.instanceCount >= SYSTEMIC_THRESHOLD ? 'HIGH' : cluster.instanceCount >= MEDIUM_THRESHOLD ? 'MEDIUM' : 'LOW'),
312
+ isSystemic: cluster.isSystemic || cluster.instanceCount >= SYSTEMIC_THRESHOLD,
313
+ status: 'NEW',
314
+ count: 0,
315
+ rootCause: null,
316
+ recommendation: null
317
+ };
318
+
319
+ // Step 1: Check for enforcement gap
320
+ const gapCheck = checkEnforcementGap(cluster.patternId, cluster.description);
321
+ if (gapCheck.exists) {
322
+ patternResult.status = 'ENFORCEMENT_GAP';
323
+ patternResult.ruleLocation = gapCheck.section;
324
+ patternResult.ruleText = gapCheck.ruleText;
325
+ results.summary.enforcementGaps++;
326
+ } else {
327
+ // Step 2: Record/increment in feedback-patterns
328
+ const recordResult = recordAuditPattern(cluster);
329
+
330
+ if (recordResult.recorded) {
331
+ patternResult.count = recordResult.newCount;
332
+
333
+ // Step 3: Check promotion threshold
334
+ if (recordResult.shouldPromote) {
335
+ // Build a learning object for promotion (reuse learner functions)
336
+ const learning = {
337
+ canLearn: true,
338
+ violationType: cluster.category,
339
+ category: mapAuditCategoryToLearnerCategory(cluster.category),
340
+ patternName: cluster.description.slice(0, 100),
341
+ message: `${cluster.instanceCount} instances found (audit source, ${recordResult.newCount} occurrences)`,
342
+ ruleTemplate: buildAuditRuleTemplate(cluster)
343
+ };
344
+
345
+ const promoteResult = promoteToDecisions(learning, recordResult.newCount);
346
+ if (promoteResult.promoted) {
347
+ patternResult.status = 'PROMOTED';
348
+ results.summary.promoted++;
349
+
350
+ // Also sync to rules dir
351
+ const syncLearning = {
352
+ ...learning,
353
+ subcategory: cluster.patternId
354
+ };
355
+ syncToRulesDir(syncLearning);
356
+ } else {
357
+ // Distinguish "not yet at threshold" from "promotion failed"
358
+ patternResult.status = 'PROMOTION_FAILED';
359
+ patternResult.failureReason = promoteResult.reason || 'Unknown promotion failure';
360
+ results.summary.promotionFailed++;
361
+ }
362
+ } else {
363
+ patternResult.status = `TRACKING (${recordResult.newCount}/${recordResult.threshold})`;
364
+ results.summary.tracking++;
365
+ }
366
+ }
367
+ }
368
+
369
+ // Step 4: Check recurrence
370
+ if (previousPatternIds.has(cluster.patternId)) {
371
+ if (patternResult.status !== 'ENFORCEMENT_GAP' && patternResult.status !== 'PROMOTED') {
372
+ patternResult.status = `RECURRING — ${patternResult.status}`;
373
+ }
374
+ results.summary.recurring++;
375
+ } else if (patternResult.status === 'NEW' || patternResult.status.startsWith('TRACKING')) {
376
+ results.summary.newPatterns++;
377
+ }
378
+
379
+ results.patterns.push(patternResult);
380
+ }
381
+
382
+ return results;
383
+ }
384
+
257
385
  // ============================================================
258
386
  // CLI Interface
259
387
  // ============================================================
@@ -307,6 +435,30 @@ function main() {
307
435
  break;
308
436
  }
309
437
 
438
+ case 'promote': {
439
+ // Process AI-clustered findings through the learning pipeline
440
+ // Usage: node scripts/flow-audit.js promote '<clusters-json>'
441
+ const clustersArg = process.argv[3];
442
+ if (!clustersArg) {
443
+ console.error('Usage: flow-audit.js promote \'[{patternId, category, description, instanceCount, instances}]\'');
444
+ process.exit(1);
445
+ }
446
+
447
+ const clusters = safeJsonParseString(clustersArg, null);
448
+ if (!Array.isArray(clusters)) {
449
+ console.error('Invalid JSON clusters argument — expected an array');
450
+ process.exit(1);
451
+ }
452
+
453
+ // Load previous audit for recurrence detection
454
+ const lastAuditPath = path.join(PATHS.state, 'last-audit.json');
455
+ const previousAudit = safeJsonParse(lastAuditPath, {});
456
+
457
+ const promoteResults = promoteAuditPatterns(clusters, previousAudit);
458
+ console.log(JSON.stringify(promoteResults, null, 2));
459
+ break;
460
+ }
461
+
310
462
  default: {
311
463
  console.log(`
312
464
  Wogi Flow - Project Audit Helpers
@@ -319,9 +471,13 @@ Commands:
319
471
  outdated Run npm outdated (structured JSON output)
320
472
  audit Run npm audit (structured JSON output)
321
473
  score Calculate weighted health score from agent grades
474
+ promote Process AI-clustered findings through learning pipeline
322
475
 
323
476
  Score usage:
324
477
  node scripts/flow-audit.js score '{"architecture":"B+","dependencies":"A-"}'
478
+
479
+ Promote usage:
480
+ node scripts/flow-audit.js promote '[{"patternId":"missing-error-handling","category":"security","description":"...","instanceCount":7}]'
325
481
  `);
326
482
  break;
327
483
  }
@@ -333,7 +489,8 @@ module.exports = {
333
489
  findTodos,
334
490
  getOutdatedDeps,
335
491
  getAuditResults,
336
- calculateHealthScore
492
+ calculateHealthScore,
493
+ promoteAuditPatterns
337
494
  };
338
495
 
339
496
  if (require.main === module) {
@@ -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