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.
- package/.claude/commands/wogi-epics.md +16 -0
- package/.claude/commands/wogi-onboard.md +30 -8
- package/.claude/commands/wogi-start.md +2 -0
- package/.workflow/models/registry.json +1 -1
- package/.workflow/templates/claude-md.hbs +25 -0
- package/package.json +1 -1
- package/scripts/flow-api-index.js +128 -63
- package/scripts/flow-function-index.js +65 -63
- package/scripts/flow-pattern-extractor.js +1 -1
- package/scripts/flow-scanner-base.js +200 -7
- package/scripts/flow-skill-generator.js +1 -0
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/registries/component-registry.js +141 -4
- package/.claude/rules/_internal/README.md +0 -64
- package/.claude/rules/_internal/document-structure.md +0 -77
- package/.claude/rules/_internal/dual-repo-management.md +0 -174
- package/.claude/rules/_internal/feature-refactoring-cleanup.md +0 -87
- package/.claude/rules/_internal/github-releases.md +0 -71
- package/.claude/rules/_internal/model-management.md +0 -35
- package/.claude/rules/_internal/self-maintenance.md +0 -87
- package/.claude/rules/architecture/component-reuse.md +0 -38
- package/.claude/rules/code-style/naming-conventions.md +0 -52
- package/.claude/rules/operations/git-workflows.md +0 -92
- package/.claude/rules/operations/scratch-directory.md +0 -54
- package/.claude/rules/security/security-patterns.md +0 -176
- package/.claude/skills/figma-analyzer/knowledge/learnings.md +0 -11
- package/.workflow/specs/architecture.md.template +0 -24
- package/.workflow/specs/stack.md.template +0 -33
- 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. **
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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.
|
|
@@ -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
|
@@ -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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
305
|
-
|
|
368
|
+
// Pass 1: Find all function-like declarations
|
|
369
|
+
const declarations = new Map(); // name -> { line, paramsStr }
|
|
306
370
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
406
|
+
endpoint: 'dynamic',
|
|
343
407
|
description: jsdoc,
|
|
344
408
|
file: filePath,
|
|
345
409
|
service,
|
|
346
|
-
line:
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
227
|
-
const
|
|
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,
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
248
|
-
const arrowRegex = /export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\([^)]
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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
|