wogiflow 1.0.11 → 1.0.13

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 (46) hide show
  1. package/.workflow/specs/architecture.md.template +24 -0
  2. package/.workflow/specs/stack.md.template +33 -0
  3. package/.workflow/specs/testing.md.template +36 -0
  4. package/README.md +90 -1
  5. package/lib/unified-wizard.js +569 -30
  6. package/package.json +1 -1
  7. package/scripts/MEMORY-ARCHITECTURE.md +150 -0
  8. package/scripts/flow +20 -19
  9. package/scripts/flow-auto-context.js +97 -3
  10. package/scripts/flow-conflict-resolver.js +735 -0
  11. package/scripts/flow-context-gatherer.js +520 -0
  12. package/scripts/flow-context-monitor.js +148 -19
  13. package/scripts/flow-damage-control.js +5 -1
  14. package/scripts/flow-export-profile +168 -1
  15. package/scripts/flow-import-profile +257 -6
  16. package/scripts/flow-instruction-richness.js +182 -18
  17. package/scripts/flow-knowledge-router.js +2 -0
  18. package/scripts/flow-knowledge-sync.js +2 -0
  19. package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
  20. package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
  21. package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
  22. package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
  23. package/scripts/flow-memory-db.js +386 -1
  24. package/scripts/flow-memory-sync.js +2 -0
  25. package/scripts/flow-model-adapter.js +53 -29
  26. package/scripts/flow-model-router.js +246 -1
  27. package/scripts/flow-morning.js +94 -0
  28. package/scripts/flow-onboard +223 -10
  29. package/scripts/flow-orchestrate-validation.js +539 -0
  30. package/scripts/flow-orchestrate.js +16 -507
  31. package/scripts/flow-pattern-extractor.js +1265 -0
  32. package/scripts/flow-prompt-composer.js +222 -2
  33. package/scripts/flow-quality-guard.js +594 -0
  34. package/scripts/flow-section-index.js +713 -0
  35. package/scripts/flow-section-resolver.js +484 -0
  36. package/scripts/flow-session-end.js +188 -2
  37. package/scripts/flow-skill-create.js +19 -3
  38. package/scripts/flow-skill-matcher.js +122 -7
  39. package/scripts/flow-statusline-setup.js +218 -0
  40. package/scripts/flow-step-review.js +19 -0
  41. package/scripts/flow-tech-debt.js +734 -0
  42. package/scripts/flow-utils.js +2 -0
  43. package/scripts/hooks/core/long-input-gate.js +293 -0
  44. package/scripts/flow-parallel-detector.js +0 -399
  45. package/scripts/flow-parallel-dispatch.js +0 -987
  46. /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
@@ -0,0 +1,1265 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Wogi Flow - Pattern Extraction Engine
5
+ *
6
+ * Scans codebases to extract patterns across 4 categories:
7
+ * - Code patterns (naming, error handling, imports)
8
+ * - API patterns (endpoints, responses, validation)
9
+ * - Component patterns (props, hooks, state)
10
+ * - Architecture patterns (file org, modules, layers)
11
+ *
12
+ * Detects conflicts between old and new code patterns,
13
+ * provides recommendations based on frequency/recency/best practices.
14
+ *
15
+ * Usage:
16
+ * flow pattern-extract [options]
17
+ * node scripts/flow-pattern-extractor.js [options]
18
+ *
19
+ * Options:
20
+ * --output <file> Output file (default: stdout)
21
+ * --format <format> Output format: json, markdown, decisions (default: json)
22
+ * --categories <cats> Categories: code,api,component,architecture (default: all)
23
+ * --framework <name> Framework: auto, react, nestjs, python (default: auto)
24
+ * --with-conflicts Include conflict analysis
25
+ * --resolve-conflicts Interactive conflict resolution (uses flow-conflict-resolver)
26
+ * --analysis-mode <mode> Git analysis: balanced (default), deep
27
+ * --max-files <n> Max files to scan (default: 1000)
28
+ * --json JSON output for scripting
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const { execSync, execFileSync } = require('child_process');
34
+ const crypto = require('crypto');
35
+
36
+ // ============================================================================
37
+ // Constants
38
+ // ============================================================================
39
+
40
+ const DEFAULT_MAX_FILES = 1000;
41
+ const DEFAULT_ANALYSIS_MODE = 'balanced';
42
+
43
+ // Pattern detection thresholds
44
+ const MIN_PATTERN_FREQUENCY = 0.05; // At least 5% of files must use pattern
45
+ const CONFLICT_THRESHOLD = 0.10; // Patterns with >10% each are conflicting
46
+
47
+ // Scoring weights for recommendations
48
+ const SCORING_WEIGHTS = {
49
+ frequency: 0.30,
50
+ recency: 0.30,
51
+ bestPractice: 0.25,
52
+ consistency: 0.15
53
+ };
54
+
55
+ // File patterns to scan by language
56
+ const FILE_PATTERNS = {
57
+ javascript: ['**/*.js', '**/*.jsx', '**/*.mjs'],
58
+ typescript: ['**/*.ts', '**/*.tsx'],
59
+ python: ['**/*.py'],
60
+ go: ['**/*.go'],
61
+ rust: ['**/*.rs'],
62
+ java: ['**/*.java']
63
+ };
64
+
65
+ // Ignore patterns
66
+ const IGNORE_PATTERNS = [
67
+ 'node_modules/**',
68
+ 'dist/**',
69
+ 'build/**',
70
+ '.git/**',
71
+ 'coverage/**',
72
+ '*.min.js',
73
+ '*.bundle.js',
74
+ '__pycache__/**',
75
+ '.venv/**',
76
+ 'vendor/**'
77
+ ];
78
+
79
+ // Colors for CLI output
80
+ const c = {
81
+ reset: '\x1b[0m',
82
+ dim: '\x1b[2m',
83
+ bold: '\x1b[1m',
84
+ red: '\x1b[31m',
85
+ green: '\x1b[32m',
86
+ yellow: '\x1b[33m',
87
+ blue: '\x1b[34m',
88
+ cyan: '\x1b[36m'
89
+ };
90
+
91
+ // ============================================================================
92
+ // Utility Functions
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Get project root directory
97
+ */
98
+ function getProjectRoot() {
99
+ try {
100
+ return execSync('git rev-parse --show-toplevel', {
101
+ encoding: 'utf-8',
102
+ stdio: ['pipe', 'pipe', 'pipe']
103
+ }).trim();
104
+ } catch {
105
+ return process.cwd();
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Generate unique pattern ID
111
+ */
112
+ function generatePatternId() {
113
+ return 'pat-' + crypto.randomBytes(4).toString('hex');
114
+ }
115
+
116
+ /**
117
+ * Glob files with ignore patterns
118
+ */
119
+ function globFiles(projectRoot, patterns, ignorePatterns = IGNORE_PATTERNS) {
120
+ const results = [];
121
+
122
+ function walkDir(dir, baseDir) {
123
+ try {
124
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
125
+
126
+ for (const entry of entries) {
127
+ const fullPath = path.join(dir, entry.name);
128
+ const relativePath = path.relative(baseDir, fullPath);
129
+
130
+ // Check ignore patterns
131
+ const shouldIgnore = ignorePatterns.some(pattern => {
132
+ if (pattern.endsWith('/**')) {
133
+ const dirPattern = pattern.slice(0, -3);
134
+ return relativePath.startsWith(dirPattern) || entry.name === dirPattern;
135
+ }
136
+ return entry.name === pattern || relativePath === pattern;
137
+ });
138
+
139
+ if (shouldIgnore) continue;
140
+
141
+ if (entry.isDirectory()) {
142
+ walkDir(fullPath, baseDir);
143
+ } else if (entry.isFile()) {
144
+ // Check if matches any pattern
145
+ const matches = patterns.some(pattern => {
146
+ if (pattern.startsWith('**/*.')) {
147
+ const ext = pattern.slice(4);
148
+ return entry.name.endsWith(ext);
149
+ }
150
+ return entry.name === pattern;
151
+ });
152
+
153
+ if (matches) {
154
+ results.push(relativePath);
155
+ }
156
+ }
157
+ }
158
+ } catch {
159
+ // Skip directories we can't read
160
+ }
161
+ }
162
+
163
+ walkDir(projectRoot, projectRoot);
164
+ return results;
165
+ }
166
+
167
+ /**
168
+ * Detect project framework
169
+ */
170
+ function detectFramework(projectRoot) {
171
+ const packageJsonPath = path.join(projectRoot, 'package.json');
172
+
173
+ if (fs.existsSync(packageJsonPath)) {
174
+ try {
175
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
176
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
177
+
178
+ // Check for frameworks
179
+ if (deps['next']) return 'nextjs';
180
+ if (deps['@nestjs/core']) return 'nestjs';
181
+ if (deps['react']) return 'react';
182
+ if (deps['vue']) return 'vue';
183
+ if (deps['@angular/core']) return 'angular';
184
+ if (deps['express']) return 'express';
185
+ if (deps['fastify']) return 'fastify';
186
+
187
+ // Check for TypeScript
188
+ if (fs.existsSync(path.join(projectRoot, 'tsconfig.json'))) {
189
+ return 'typescript';
190
+ }
191
+
192
+ return 'javascript';
193
+ } catch {
194
+ return 'javascript';
195
+ }
196
+ }
197
+
198
+ // Check for Python
199
+ if (fs.existsSync(path.join(projectRoot, 'requirements.txt')) ||
200
+ fs.existsSync(path.join(projectRoot, 'setup.py')) ||
201
+ fs.existsSync(path.join(projectRoot, 'pyproject.toml'))) {
202
+
203
+ // Check for frameworks
204
+ try {
205
+ const reqPath = path.join(projectRoot, 'requirements.txt');
206
+ if (fs.existsSync(reqPath)) {
207
+ const reqs = fs.readFileSync(reqPath, 'utf-8');
208
+ if (reqs.includes('fastapi')) return 'fastapi';
209
+ if (reqs.includes('django')) return 'django';
210
+ if (reqs.includes('flask')) return 'flask';
211
+ }
212
+ } catch {
213
+ // Ignore
214
+ }
215
+ return 'python';
216
+ }
217
+
218
+ // Check for Go
219
+ if (fs.existsSync(path.join(projectRoot, 'go.mod'))) {
220
+ return 'go';
221
+ }
222
+
223
+ // Check for Rust
224
+ if (fs.existsSync(path.join(projectRoot, 'Cargo.toml'))) {
225
+ return 'rust';
226
+ }
227
+
228
+ return 'unknown';
229
+ }
230
+
231
+ /**
232
+ * Get git blame date for a file line
233
+ */
234
+ function _getGitBlameDate(projectRoot, filePath, lineNumber) {
235
+ try {
236
+ // Validate lineNumber to prevent command injection
237
+ const lineNum = parseInt(lineNumber, 10);
238
+ if (isNaN(lineNum) || lineNum < 1 || lineNum > 1000000) {
239
+ return null;
240
+ }
241
+
242
+ const fullPath = path.join(projectRoot, filePath);
243
+ // Use execFileSync with array arguments to prevent shell injection
244
+ const output = execFileSync('git', [
245
+ 'blame',
246
+ '-L', `${lineNum},${lineNum}`,
247
+ '--porcelain',
248
+ fullPath
249
+ ], {
250
+ encoding: 'utf-8',
251
+ cwd: projectRoot,
252
+ stdio: ['pipe', 'pipe', 'pipe']
253
+ });
254
+
255
+ const timestampMatch = output.match(/^author-time (\d+)/m);
256
+ if (timestampMatch) {
257
+ return new Date(parseInt(timestampMatch[1]) * 1000);
258
+ }
259
+ } catch {
260
+ // Git blame failed (file not tracked, invalid line, etc.)
261
+ }
262
+ return null;
263
+ }
264
+
265
+ /**
266
+ * Get file modification time
267
+ */
268
+ function getFileMtime(projectRoot, filePath) {
269
+ try {
270
+ const fullPath = path.join(projectRoot, filePath);
271
+ const stats = fs.statSync(fullPath);
272
+ return stats.mtime;
273
+ } catch {
274
+ return new Date(0);
275
+ }
276
+ }
277
+
278
+ // ============================================================================
279
+ // Pattern Data Structures
280
+ // ============================================================================
281
+
282
+ /**
283
+ * Create a pattern object
284
+ */
285
+ function createPattern(category, subcategory, name, options = {}) {
286
+ return {
287
+ id: generatePatternId(),
288
+ category,
289
+ subcategory,
290
+ name,
291
+ description: options.description || '',
292
+ examples: options.examples || [],
293
+ frequency: options.frequency || 0,
294
+ files: options.files || [],
295
+ firstSeen: options.firstSeen || null,
296
+ lastSeen: options.lastSeen || null,
297
+ confidence: options.confidence || 0,
298
+ source: options.source || 'detected'
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Create a conflict object
304
+ */
305
+ function createConflict(patternA, patternB, options = {}) {
306
+ return {
307
+ id: 'conf-' + crypto.randomBytes(4).toString('hex'),
308
+ category: patternA.category,
309
+ subcategory: patternA.subcategory,
310
+ description: options.description || `Conflicting ${patternA.subcategory} patterns`,
311
+ patternA: {
312
+ pattern: patternA,
313
+ occurrences: patternA.frequency,
314
+ newestOccurrence: patternA.lastSeen,
315
+ files: patternA.files.slice(0, 5)
316
+ },
317
+ patternB: {
318
+ pattern: patternB,
319
+ occurrences: patternB.frequency,
320
+ newestOccurrence: patternB.lastSeen,
321
+ files: patternB.files.slice(0, 5)
322
+ },
323
+ recommendation: options.recommendation || null,
324
+ recommendationReason: options.recommendationReason || '',
325
+ resolution: null
326
+ };
327
+ }
328
+
329
+ // ============================================================================
330
+ // Code Pattern Extractors
331
+ // ============================================================================
332
+
333
+ /**
334
+ * Extract code-level patterns: naming, error handling, imports
335
+ */
336
+ function extractCodePatterns(projectRoot, files, _options = {}) {
337
+ const patterns = {
338
+ 'naming.files': {},
339
+ 'naming.functions': {},
340
+ 'naming.variables': {},
341
+ 'error-handling.catch-variable': {},
342
+ 'error-handling.style': {},
343
+ 'imports.style': {}
344
+ };
345
+
346
+ for (const file of files) {
347
+ const fullPath = path.join(projectRoot, file);
348
+ let content;
349
+
350
+ try {
351
+ content = fs.readFileSync(fullPath, 'utf-8');
352
+ } catch {
353
+ continue;
354
+ }
355
+
356
+ const basename = path.basename(file);
357
+ const ext = path.extname(file);
358
+
359
+ // File naming patterns
360
+ if (/^[a-z][a-z0-9-]*\.[a-z]+$/.test(basename)) {
361
+ addPatternOccurrence(patterns['naming.files'], 'kebab-case', file, projectRoot);
362
+ } else if (/^[A-Z][a-zA-Z0-9]*\.[a-z]+$/.test(basename)) {
363
+ addPatternOccurrence(patterns['naming.files'], 'PascalCase', file, projectRoot);
364
+ } else if (/^[a-z][a-zA-Z0-9]*\.[a-z]+$/.test(basename)) {
365
+ addPatternOccurrence(patterns['naming.files'], 'camelCase', file, projectRoot);
366
+ } else if (/^[a-z][a-z0-9_]*\.[a-z]+$/.test(basename)) {
367
+ addPatternOccurrence(patterns['naming.files'], 'snake_case', file, projectRoot);
368
+ }
369
+
370
+ // Function naming patterns (JS/TS)
371
+ if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
372
+ const funcMatches = content.matchAll(/function\s+([a-zA-Z_][a-zA-Z0-9_]*)/g);
373
+ for (const match of funcMatches) {
374
+ const funcName = match[1];
375
+ if (/^[a-z][a-zA-Z0-9]*$/.test(funcName)) {
376
+ addPatternOccurrence(patterns['naming.functions'], 'camelCase', file, projectRoot);
377
+ } else if (/^[a-z][a-z0-9_]*$/.test(funcName)) {
378
+ addPatternOccurrence(patterns['naming.functions'], 'snake_case', file, projectRoot);
379
+ }
380
+ }
381
+
382
+ // Arrow function naming
383
+ const arrowMatches = content.matchAll(/const\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(?:async\s*)?\(/g);
384
+ for (const match of arrowMatches) {
385
+ const funcName = match[1];
386
+ if (/^[a-z][a-zA-Z0-9]*$/.test(funcName)) {
387
+ addPatternOccurrence(patterns['naming.functions'], 'camelCase', file, projectRoot);
388
+ }
389
+ }
390
+
391
+ // Error handling - catch variable naming
392
+ const catchMatches = content.matchAll(/catch\s*\(\s*(\w+)\s*\)/g);
393
+ for (const match of catchMatches) {
394
+ const errorVar = match[1];
395
+ if (errorVar === 'err') {
396
+ addPatternOccurrence(patterns['error-handling.catch-variable'], 'err', file, projectRoot);
397
+ } else if (errorVar === 'e') {
398
+ addPatternOccurrence(patterns['error-handling.catch-variable'], 'e', file, projectRoot);
399
+ } else if (errorVar === 'error') {
400
+ addPatternOccurrence(patterns['error-handling.catch-variable'], 'error', file, projectRoot);
401
+ }
402
+ }
403
+
404
+ // Import style - absolute vs relative
405
+ const importMatches = content.matchAll(/import\s+.*\s+from\s+['"]([^'"]+)['"]/g);
406
+ for (const match of importMatches) {
407
+ const importPath = match[1];
408
+ if (importPath.startsWith('.') || importPath.startsWith('..')) {
409
+ addPatternOccurrence(patterns['imports.style'], 'relative', file, projectRoot);
410
+ } else if (importPath.startsWith('@/') || importPath.startsWith('~/')) {
411
+ addPatternOccurrence(patterns['imports.style'], 'absolute-alias', file, projectRoot);
412
+ } else if (!importPath.includes('/') || importPath.startsWith('@')) {
413
+ // Package import, skip
414
+ } else {
415
+ addPatternOccurrence(patterns['imports.style'], 'absolute', file, projectRoot);
416
+ }
417
+ }
418
+ }
419
+
420
+ // Python-specific patterns
421
+ if (ext === '.py') {
422
+ // Function naming
423
+ const pyFuncMatches = content.matchAll(/def\s+([a-zA-Z_][a-zA-Z0-9_]*)/g);
424
+ for (const match of pyFuncMatches) {
425
+ const funcName = match[1];
426
+ if (/^[a-z][a-z0-9_]*$/.test(funcName)) {
427
+ addPatternOccurrence(patterns['naming.functions'], 'snake_case', file, projectRoot);
428
+ } else if (/^[a-z][a-zA-Z0-9]*$/.test(funcName)) {
429
+ addPatternOccurrence(patterns['naming.functions'], 'camelCase', file, projectRoot);
430
+ }
431
+ }
432
+
433
+ // Exception variable
434
+ const exceptMatches = content.matchAll(/except\s+\w+\s+as\s+(\w+)/g);
435
+ for (const match of exceptMatches) {
436
+ const errorVar = match[1];
437
+ if (errorVar === 'e') {
438
+ addPatternOccurrence(patterns['error-handling.catch-variable'], 'e', file, projectRoot);
439
+ } else if (errorVar === 'err') {
440
+ addPatternOccurrence(patterns['error-handling.catch-variable'], 'err', file, projectRoot);
441
+ } else if (errorVar === 'error') {
442
+ addPatternOccurrence(patterns['error-handling.catch-variable'], 'error', file, projectRoot);
443
+ }
444
+ }
445
+ }
446
+ }
447
+
448
+ return aggregatePatterns(patterns, 'code', files.length);
449
+ }
450
+
451
+ /**
452
+ * Add a pattern occurrence
453
+ */
454
+ function addPatternOccurrence(patternMap, patternName, file, projectRoot) {
455
+ if (!patternMap[patternName]) {
456
+ patternMap[patternName] = {
457
+ files: [],
458
+ mtime: null
459
+ };
460
+ }
461
+
462
+ patternMap[patternName].files.push(file);
463
+
464
+ // Track most recent
465
+ const mtime = getFileMtime(projectRoot, file);
466
+ if (!patternMap[patternName].mtime || mtime > patternMap[patternName].mtime) {
467
+ patternMap[patternName].mtime = mtime;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Aggregate pattern occurrences into Pattern objects
473
+ */
474
+ function aggregatePatterns(patternMap, category, totalFiles) {
475
+ const results = [];
476
+
477
+ for (const [subcategory, patterns] of Object.entries(patternMap)) {
478
+ for (const [name, data] of Object.entries(patterns)) {
479
+ const frequency = data.files.length;
480
+ const frequencyRatio = frequency / totalFiles;
481
+
482
+ // Skip patterns with too few occurrences
483
+ if (frequencyRatio < MIN_PATTERN_FREQUENCY) continue;
484
+
485
+ results.push(createPattern(category, subcategory, name, {
486
+ description: getPatternDescription(subcategory, name),
487
+ frequency: frequency,
488
+ files: data.files.slice(0, 10), // Keep first 10 examples
489
+ lastSeen: data.mtime,
490
+ confidence: Math.min(frequencyRatio * 2, 1) // Higher frequency = higher confidence
491
+ }));
492
+ }
493
+ }
494
+
495
+ return results;
496
+ }
497
+
498
+ /**
499
+ * Get human-readable pattern description
500
+ */
501
+ function getPatternDescription(subcategory, name) {
502
+ const descriptions = {
503
+ 'naming.files': {
504
+ 'kebab-case': 'File names use kebab-case (e.g., my-component.tsx)',
505
+ 'PascalCase': 'File names use PascalCase (e.g., MyComponent.tsx)',
506
+ 'camelCase': 'File names use camelCase (e.g., myComponent.tsx)',
507
+ 'snake_case': 'File names use snake_case (e.g., my_component.tsx)'
508
+ },
509
+ 'naming.functions': {
510
+ 'camelCase': 'Functions use camelCase naming',
511
+ 'snake_case': 'Functions use snake_case naming',
512
+ 'PascalCase': 'Functions use PascalCase naming'
513
+ },
514
+ 'error-handling.catch-variable': {
515
+ 'err': 'Catch blocks use "err" as error variable',
516
+ 'e': 'Catch blocks use "e" as error variable',
517
+ 'error': 'Catch blocks use "error" as error variable'
518
+ },
519
+ 'imports.style': {
520
+ 'relative': 'Imports use relative paths (./file)',
521
+ 'absolute': 'Imports use absolute paths',
522
+ 'absolute-alias': 'Imports use path aliases (@/ or ~/)'
523
+ }
524
+ };
525
+
526
+ return descriptions[subcategory]?.[name] || `${subcategory}: ${name}`;
527
+ }
528
+
529
+ // ============================================================================
530
+ // API Pattern Extractors
531
+ // ============================================================================
532
+
533
+ /**
534
+ * Extract API patterns: endpoints, responses, validation
535
+ */
536
+ function extractApiPatterns(projectRoot, files, options = {}) {
537
+ const patterns = {
538
+ 'api.naming': {},
539
+ 'api.response-format': {},
540
+ 'api.error-format': {}
541
+ };
542
+
543
+ const framework = options.framework || 'unknown';
544
+
545
+ for (const file of files) {
546
+ const fullPath = path.join(projectRoot, file);
547
+ let content;
548
+
549
+ try {
550
+ content = fs.readFileSync(fullPath, 'utf-8');
551
+ } catch {
552
+ continue;
553
+ }
554
+
555
+ // NestJS patterns
556
+ if (framework === 'nestjs' || content.includes('@Controller')) {
557
+ // Route naming
558
+ const routeMatches = content.matchAll(/@(Get|Post|Put|Delete|Patch)\(['"]([^'"]*)['"]\)/g);
559
+ for (const match of routeMatches) {
560
+ const route = match[2];
561
+ if (route.includes('-')) {
562
+ addPatternOccurrence(patterns['api.naming'], 'kebab-case-routes', file, projectRoot);
563
+ } else if (route.includes('_')) {
564
+ addPatternOccurrence(patterns['api.naming'], 'snake_case-routes', file, projectRoot);
565
+ } else if (/[A-Z]/.test(route)) {
566
+ addPatternOccurrence(patterns['api.naming'], 'camelCase-routes', file, projectRoot);
567
+ }
568
+ }
569
+ }
570
+
571
+ // Express patterns
572
+ if (content.includes('express') || content.includes('app.get') || content.includes('router.')) {
573
+ const routeMatches = content.matchAll(/\.(get|post|put|delete|patch)\(['"]([^'"]*)['"]/gi);
574
+ for (const match of routeMatches) {
575
+ const route = match[2];
576
+ if (route.includes('-')) {
577
+ addPatternOccurrence(patterns['api.naming'], 'kebab-case-routes', file, projectRoot);
578
+ } else if (route.includes('_')) {
579
+ addPatternOccurrence(patterns['api.naming'], 'snake_case-routes', file, projectRoot);
580
+ }
581
+ }
582
+ }
583
+
584
+ // Response patterns
585
+ if (content.includes('res.json') || content.includes('res.send')) {
586
+ // Check for wrapped responses
587
+ if (content.includes('success:') || content.includes('"success"')) {
588
+ addPatternOccurrence(patterns['api.response-format'], 'wrapped-response', file, projectRoot);
589
+ }
590
+ if (content.includes('data:') || content.includes('"data"')) {
591
+ addPatternOccurrence(patterns['api.response-format'], 'data-wrapper', file, projectRoot);
592
+ }
593
+ }
594
+
595
+ // FastAPI patterns (Python)
596
+ if (content.includes('@app.') || content.includes('@router.')) {
597
+ const routeMatches = content.matchAll(/@(?:app|router)\.(get|post|put|delete|patch)\(["']([^"']*)/gi);
598
+ for (const match of routeMatches) {
599
+ const route = match[2];
600
+ if (route.includes('-')) {
601
+ addPatternOccurrence(patterns['api.naming'], 'kebab-case-routes', file, projectRoot);
602
+ } else if (route.includes('_')) {
603
+ addPatternOccurrence(patterns['api.naming'], 'snake_case-routes', file, projectRoot);
604
+ }
605
+ }
606
+ }
607
+ }
608
+
609
+ return aggregatePatterns(patterns, 'api', files.length);
610
+ }
611
+
612
+ // ============================================================================
613
+ // Component Pattern Extractors
614
+ // ============================================================================
615
+
616
+ /**
617
+ * Extract component patterns: props, hooks, state
618
+ */
619
+ function extractComponentPatterns(projectRoot, files, _options = {}) {
620
+ const patterns = {
621
+ 'component.style': {},
622
+ 'component.props': {},
623
+ 'component.hooks': {},
624
+ 'component.state': {}
625
+ };
626
+
627
+ for (const file of files) {
628
+ const ext = path.extname(file);
629
+ if (!['.jsx', '.tsx'].includes(ext)) continue;
630
+
631
+ const fullPath = path.join(projectRoot, file);
632
+ let content;
633
+
634
+ try {
635
+ content = fs.readFileSync(fullPath, 'utf-8');
636
+ } catch {
637
+ continue;
638
+ }
639
+
640
+ // Component style: functional vs class
641
+ if (content.includes('extends React.Component') || content.includes('extends Component')) {
642
+ addPatternOccurrence(patterns['component.style'], 'class-component', file, projectRoot);
643
+ }
644
+ if (content.includes('function ') && (content.includes('return (') || content.includes('return <'))) {
645
+ addPatternOccurrence(patterns['component.style'], 'functional-component', file, projectRoot);
646
+ }
647
+ if (content.includes('const ') && content.includes(' = (') && content.includes('return (')) {
648
+ addPatternOccurrence(patterns['component.style'], 'arrow-function-component', file, projectRoot);
649
+ }
650
+
651
+ // Props patterns
652
+ if (content.includes('interface') && content.includes('Props')) {
653
+ addPatternOccurrence(patterns['component.props'], 'typescript-interface', file, projectRoot);
654
+ }
655
+ if (content.includes('PropTypes')) {
656
+ addPatternOccurrence(patterns['component.props'], 'prop-types', file, projectRoot);
657
+ }
658
+ if (content.includes('type ') && content.includes('Props =')) {
659
+ addPatternOccurrence(patterns['component.props'], 'typescript-type', file, projectRoot);
660
+ }
661
+
662
+ // Hooks patterns
663
+ if (content.includes('useState')) {
664
+ addPatternOccurrence(patterns['component.state'], 'useState', file, projectRoot);
665
+ }
666
+ if (content.includes('useReducer')) {
667
+ addPatternOccurrence(patterns['component.state'], 'useReducer', file, projectRoot);
668
+ }
669
+
670
+ // Custom hooks
671
+ const hookMatches = content.matchAll(/function\s+(use[A-Z][a-zA-Z]*)/g);
672
+ for (const _match of hookMatches) {
673
+ addPatternOccurrence(patterns['component.hooks'], 'custom-hooks', file, projectRoot);
674
+ }
675
+ const arrowHookMatches = content.matchAll(/const\s+(use[A-Z][a-zA-Z]*)\s*=/g);
676
+ for (const _match of arrowHookMatches) {
677
+ addPatternOccurrence(patterns['component.hooks'], 'custom-hooks', file, projectRoot);
678
+ }
679
+ }
680
+
681
+ return aggregatePatterns(patterns, 'component', files.length);
682
+ }
683
+
684
+ // ============================================================================
685
+ // Architecture Pattern Extractors
686
+ // ============================================================================
687
+
688
+ /**
689
+ * Extract architecture patterns: file org, modules, layers
690
+ */
691
+ function extractArchitecturePatterns(projectRoot, files, _options = {}) {
692
+ const patterns = {
693
+ 'architecture.layers': {},
694
+ 'architecture.modules': {},
695
+ 'architecture.file-structure': {}
696
+ };
697
+
698
+ // Analyze directory structure
699
+ const directories = new Set();
700
+ for (const file of files) {
701
+ const dir = path.dirname(file);
702
+ directories.add(dir);
703
+
704
+ // Check for layered architecture
705
+ if (dir.includes('controller') || dir.includes('controllers')) {
706
+ addPatternOccurrence(patterns['architecture.layers'], 'controller-layer', file, projectRoot);
707
+ }
708
+ if (dir.includes('service') || dir.includes('services')) {
709
+ addPatternOccurrence(patterns['architecture.layers'], 'service-layer', file, projectRoot);
710
+ }
711
+ if (dir.includes('repository') || dir.includes('repositories')) {
712
+ addPatternOccurrence(patterns['architecture.layers'], 'repository-layer', file, projectRoot);
713
+ }
714
+ if (dir.includes('model') || dir.includes('models') || dir.includes('entity') || dir.includes('entities')) {
715
+ addPatternOccurrence(patterns['architecture.layers'], 'model-layer', file, projectRoot);
716
+ }
717
+
718
+ // Check for module structure
719
+ if (dir.includes('modules/') || dir.includes('features/')) {
720
+ addPatternOccurrence(patterns['architecture.modules'], 'feature-modules', file, projectRoot);
721
+ }
722
+ if (dir.includes('shared/') || dir.includes('common/')) {
723
+ addPatternOccurrence(patterns['architecture.modules'], 'shared-modules', file, projectRoot);
724
+ }
725
+
726
+ // File structure patterns
727
+ const basename = path.basename(file);
728
+ if (basename.includes('.controller.')) {
729
+ addPatternOccurrence(patterns['architecture.file-structure'], 'suffix-naming', file, projectRoot);
730
+ }
731
+ if (basename.includes('.service.')) {
732
+ addPatternOccurrence(patterns['architecture.file-structure'], 'suffix-naming', file, projectRoot);
733
+ }
734
+ }
735
+
736
+ // Check for src structure
737
+ if (directories.has('src') || Array.from(directories).some(d => d.startsWith('src/'))) {
738
+ patterns['architecture.file-structure']['src-root'] = {
739
+ files: files.filter(f => f.startsWith('src/')).slice(0, 10),
740
+ mtime: new Date()
741
+ };
742
+ }
743
+
744
+ return aggregatePatterns(patterns, 'architecture', files.length);
745
+ }
746
+
747
+ // ============================================================================
748
+ // Conflict Detection
749
+ // ============================================================================
750
+
751
+ /**
752
+ * Detect conflicting patterns
753
+ */
754
+ function detectConflicts(patterns) {
755
+ const conflicts = [];
756
+
757
+ // Group patterns by subcategory
758
+ const bySubcategory = {};
759
+ for (const pattern of patterns) {
760
+ const key = pattern.subcategory;
761
+ if (!bySubcategory[key]) {
762
+ bySubcategory[key] = [];
763
+ }
764
+ bySubcategory[key].push(pattern);
765
+ }
766
+
767
+ // Check each subcategory for conflicts
768
+ for (const [subcategory, subcatPatterns] of Object.entries(bySubcategory)) {
769
+ if (subcatPatterns.length < 2) continue;
770
+
771
+ // Sort by frequency descending
772
+ subcatPatterns.sort((a, b) => b.frequency - a.frequency);
773
+
774
+ // Check for significant alternatives
775
+ const total = subcatPatterns.reduce((sum, p) => sum + p.frequency, 0);
776
+
777
+ for (let i = 0; i < subcatPatterns.length - 1; i++) {
778
+ for (let j = i + 1; j < subcatPatterns.length; j++) {
779
+ const ratioA = subcatPatterns[i].frequency / total;
780
+ const ratioB = subcatPatterns[j].frequency / total;
781
+
782
+ // Both patterns must have significant usage to be a conflict
783
+ if (ratioA >= CONFLICT_THRESHOLD && ratioB >= CONFLICT_THRESHOLD) {
784
+ const conflict = createConflict(subcatPatterns[i], subcatPatterns[j], {
785
+ description: `Conflicting ${subcategory.replace('.', ' ')}: ${subcatPatterns[i].name} vs ${subcatPatterns[j].name}`
786
+ });
787
+
788
+ // Add recommendation
789
+ const rec = scoreRecommendation(subcatPatterns[i], subcatPatterns[j]);
790
+ conflict.recommendation = rec.recommendation;
791
+ conflict.recommendationReason = rec.reason;
792
+
793
+ conflicts.push(conflict);
794
+ }
795
+ }
796
+ }
797
+ }
798
+
799
+ return conflicts;
800
+ }
801
+
802
+ /**
803
+ * Score patterns to determine recommendation
804
+ */
805
+ function scoreRecommendation(patternA, patternB) {
806
+ const scoreA = {
807
+ frequency: patternA.frequency,
808
+ recency: patternA.lastSeen ? patternA.lastSeen.getTime() : 0,
809
+ total: 0
810
+ };
811
+
812
+ const scoreB = {
813
+ frequency: patternB.frequency,
814
+ recency: patternB.lastSeen ? patternB.lastSeen.getTime() : 0,
815
+ total: 0
816
+ };
817
+
818
+ // Normalize scores
819
+ const totalFreq = scoreA.frequency + scoreB.frequency;
820
+ scoreA.frequencyNorm = scoreA.frequency / totalFreq;
821
+ scoreB.frequencyNorm = scoreB.frequency / totalFreq;
822
+
823
+ const maxRecency = Math.max(scoreA.recency, scoreB.recency);
824
+ scoreA.recencyNorm = maxRecency > 0 ? scoreA.recency / maxRecency : 0.5;
825
+ scoreB.recencyNorm = maxRecency > 0 ? scoreB.recency / maxRecency : 0.5;
826
+
827
+ // Calculate weighted scores
828
+ scoreA.total = scoreA.frequencyNorm * SCORING_WEIGHTS.frequency +
829
+ scoreA.recencyNorm * SCORING_WEIGHTS.recency;
830
+ scoreB.total = scoreB.frequencyNorm * SCORING_WEIGHTS.frequency +
831
+ scoreB.recencyNorm * SCORING_WEIGHTS.recency;
832
+
833
+ const reasons = [];
834
+
835
+ if (scoreA.total >= scoreB.total) {
836
+ if (scoreA.frequencyNorm > scoreB.frequencyNorm) {
837
+ reasons.push(`More frequent (${Math.round(scoreA.frequencyNorm * 100)}% vs ${Math.round(scoreB.frequencyNorm * 100)}%)`);
838
+ }
839
+ if (scoreA.recencyNorm > scoreB.recencyNorm) {
840
+ reasons.push('More recent usage');
841
+ }
842
+ return {
843
+ recommendation: 'A',
844
+ scoreA: Math.round(scoreA.total * 100),
845
+ scoreB: Math.round(scoreB.total * 100),
846
+ reason: reasons.join(', ') || 'Higher overall score'
847
+ };
848
+ } else {
849
+ if (scoreB.frequencyNorm > scoreA.frequencyNorm) {
850
+ reasons.push(`More frequent (${Math.round(scoreB.frequencyNorm * 100)}% vs ${Math.round(scoreA.frequencyNorm * 100)}%)`);
851
+ }
852
+ if (scoreB.recencyNorm > scoreA.recencyNorm) {
853
+ reasons.push('More recent usage');
854
+ }
855
+ return {
856
+ recommendation: 'B',
857
+ scoreA: Math.round(scoreA.total * 100),
858
+ scoreB: Math.round(scoreB.total * 100),
859
+ reason: reasons.join(', ') || 'Higher overall score'
860
+ };
861
+ }
862
+ }
863
+
864
+ /**
865
+ * Generate recommendations from patterns
866
+ */
867
+ function generateRecommendations(patterns, _conflicts) {
868
+ const recommendations = [];
869
+
870
+ // Group by subcategory
871
+ const bySubcategory = {};
872
+ for (const pattern of patterns) {
873
+ const key = pattern.subcategory;
874
+ if (!bySubcategory[key]) {
875
+ bySubcategory[key] = [];
876
+ }
877
+ bySubcategory[key].push(pattern);
878
+ }
879
+
880
+ // Generate recommendation for each subcategory
881
+ for (const [subcategory, subcatPatterns] of Object.entries(bySubcategory)) {
882
+ // Sort by frequency
883
+ subcatPatterns.sort((a, b) => b.frequency - a.frequency);
884
+
885
+ // Top pattern is recommended
886
+ const top = subcatPatterns[0];
887
+ const total = subcatPatterns.reduce((sum, p) => sum + p.frequency, 0);
888
+ const percentage = Math.round((top.frequency / total) * 100);
889
+
890
+ recommendations.push({
891
+ subcategory,
892
+ pattern: top,
893
+ score: percentage,
894
+ reasoning: `Used in ${percentage}% of relevant files (${top.frequency} occurrences)`,
895
+ alternatives: subcatPatterns.slice(1).map(p => ({
896
+ name: p.name,
897
+ frequency: p.frequency,
898
+ percentage: Math.round((p.frequency / total) * 100)
899
+ }))
900
+ });
901
+ }
902
+
903
+ return recommendations;
904
+ }
905
+
906
+ // ============================================================================
907
+ // Output Formatters
908
+ // ============================================================================
909
+
910
+ /**
911
+ * Format extraction result as JSON
912
+ */
913
+ function formatAsJson(result) {
914
+ return JSON.stringify(result, null, 2);
915
+ }
916
+
917
+ /**
918
+ * Format extraction result as markdown (decisions.md compatible)
919
+ */
920
+ function formatAsDecisions(result) {
921
+ let md = `# Extracted Patterns\n\n`;
922
+ md += `Generated: ${new Date().toISOString().split('T')[0]}\n`;
923
+ md += `Framework: ${result.meta.framework}\n`;
924
+ md += `Files scanned: ${result.meta.filesScanned}\n\n`;
925
+
926
+ // Group recommendations by category
927
+ const byCategory = {};
928
+ for (const rec of result.recommendations) {
929
+ const cat = rec.pattern.category;
930
+ if (!byCategory[cat]) {
931
+ byCategory[cat] = [];
932
+ }
933
+ byCategory[cat].push(rec);
934
+ }
935
+
936
+ for (const [category, recs] of Object.entries(byCategory)) {
937
+ md += `## ${capitalize(category)} Patterns\n\n`;
938
+
939
+ for (const rec of recs) {
940
+ md += `### ${formatSubcategory(rec.subcategory)}\n\n`;
941
+ md += `**Recommended**: ${rec.pattern.name}\n`;
942
+ md += `**Usage**: ${rec.score}% (${rec.pattern.frequency} files)\n`;
943
+ md += `**Description**: ${rec.pattern.description}\n\n`;
944
+
945
+ if (rec.alternatives.length > 0) {
946
+ md += `*Alternatives found*:\n`;
947
+ for (const alt of rec.alternatives) {
948
+ md += `- ${alt.name}: ${alt.percentage}% (${alt.frequency} files)\n`;
949
+ }
950
+ md += '\n';
951
+ }
952
+ }
953
+ }
954
+
955
+ // Add conflicts section if any
956
+ if (result.conflicts.length > 0) {
957
+ md += `## Conflicts Detected\n\n`;
958
+ md += `The following patterns have significant usage of multiple approaches:\n\n`;
959
+
960
+ for (const conflict of result.conflicts) {
961
+ md += `### ${conflict.description}\n\n`;
962
+ md += `- **Option A**: ${conflict.patternA.pattern.name} (${conflict.patternA.occurrences} files)\n`;
963
+ md += `- **Option B**: ${conflict.patternB.pattern.name} (${conflict.patternB.occurrences} files)\n`;
964
+ md += `- **Recommendation**: ${conflict.recommendation} - ${conflict.recommendationReason}\n\n`;
965
+ }
966
+ }
967
+
968
+ return md;
969
+ }
970
+
971
+ function capitalize(str) {
972
+ return str.charAt(0).toUpperCase() + str.slice(1);
973
+ }
974
+
975
+ function formatSubcategory(subcategory) {
976
+ return subcategory
977
+ .split('.')
978
+ .map(part => part.split('-').map(capitalize).join(' '))
979
+ .join(' - ');
980
+ }
981
+
982
+ // ============================================================================
983
+ // Main Extraction Function
984
+ // ============================================================================
985
+
986
+ /**
987
+ * Main pattern extraction entry point
988
+ */
989
+ async function extractPatterns(projectRoot, options = {}) {
990
+ const {
991
+ categories = ['code', 'api', 'component', 'architecture'],
992
+ includeConflicts = true,
993
+ includeRecommendations = true,
994
+ maxFiles = DEFAULT_MAX_FILES,
995
+ framework: frameworkOption = 'auto',
996
+ analysisMode = DEFAULT_ANALYSIS_MODE
997
+ } = options;
998
+
999
+ const startTime = Date.now();
1000
+
1001
+ // Detect framework
1002
+ const framework = frameworkOption === 'auto'
1003
+ ? detectFramework(projectRoot)
1004
+ : frameworkOption;
1005
+
1006
+ // Determine file patterns based on framework
1007
+ let filePatterns = [...FILE_PATTERNS.javascript, ...FILE_PATTERNS.typescript];
1008
+ if (['python', 'fastapi', 'django', 'flask'].includes(framework)) {
1009
+ filePatterns = FILE_PATTERNS.python;
1010
+ } else if (framework === 'go') {
1011
+ filePatterns = FILE_PATTERNS.go;
1012
+ } else if (framework === 'rust') {
1013
+ filePatterns = FILE_PATTERNS.rust;
1014
+ }
1015
+
1016
+ // Get files to scan
1017
+ let files = globFiles(projectRoot, filePatterns);
1018
+
1019
+ // Limit files if needed
1020
+ if (files.length > maxFiles) {
1021
+ console.error(`${c.yellow}Warning: Limiting scan to ${maxFiles} files (found ${files.length})${c.reset}`);
1022
+ files = files.slice(0, maxFiles);
1023
+ }
1024
+
1025
+ // Extract patterns by category
1026
+ const allPatterns = [];
1027
+
1028
+ if (categories.includes('code')) {
1029
+ const codePatterns = extractCodePatterns(projectRoot, files, { framework });
1030
+ allPatterns.push(...codePatterns);
1031
+ }
1032
+
1033
+ if (categories.includes('api')) {
1034
+ const apiPatterns = extractApiPatterns(projectRoot, files, { framework });
1035
+ allPatterns.push(...apiPatterns);
1036
+ }
1037
+
1038
+ if (categories.includes('component')) {
1039
+ const componentPatterns = extractComponentPatterns(projectRoot, files, { framework });
1040
+ allPatterns.push(...componentPatterns);
1041
+ }
1042
+
1043
+ if (categories.includes('architecture')) {
1044
+ const archPatterns = extractArchitecturePatterns(projectRoot, files, { framework });
1045
+ allPatterns.push(...archPatterns);
1046
+ }
1047
+
1048
+ // Detect conflicts
1049
+ const conflicts = includeConflicts ? detectConflicts(allPatterns) : [];
1050
+
1051
+ // Generate recommendations
1052
+ const recommendations = includeRecommendations
1053
+ ? generateRecommendations(allPatterns, conflicts)
1054
+ : [];
1055
+
1056
+ const elapsed = Date.now() - startTime;
1057
+
1058
+ return {
1059
+ meta: {
1060
+ extractedAt: new Date().toISOString(),
1061
+ projectRoot,
1062
+ framework,
1063
+ filesScanned: files.length,
1064
+ scanDurationMs: elapsed,
1065
+ analysisMode
1066
+ },
1067
+ patterns: {
1068
+ code: allPatterns.filter(p => p.category === 'code'),
1069
+ api: allPatterns.filter(p => p.category === 'api'),
1070
+ component: allPatterns.filter(p => p.category === 'component'),
1071
+ architecture: allPatterns.filter(p => p.category === 'architecture')
1072
+ },
1073
+ conflicts,
1074
+ recommendations
1075
+ };
1076
+ }
1077
+
1078
+ // ============================================================================
1079
+ // CLI Interface
1080
+ // ============================================================================
1081
+
1082
+ function parseArgs(args) {
1083
+ const options = {
1084
+ output: null,
1085
+ format: 'json',
1086
+ categories: ['code', 'api', 'component', 'architecture'],
1087
+ framework: 'auto',
1088
+ withConflicts: true,
1089
+ resolveConflicts: false,
1090
+ analysisMode: 'balanced',
1091
+ maxFiles: DEFAULT_MAX_FILES,
1092
+ json: false,
1093
+ help: false,
1094
+ project: null // Project folder to scan (default: current)
1095
+ };
1096
+
1097
+ for (let i = 0; i < args.length; i++) {
1098
+ const arg = args[i];
1099
+
1100
+ switch (arg) {
1101
+ case '--output':
1102
+ case '-o':
1103
+ options.output = args[++i];
1104
+ break;
1105
+ case '--format':
1106
+ case '-f':
1107
+ options.format = args[++i];
1108
+ break;
1109
+ case '--categories':
1110
+ options.categories = args[++i].split(',');
1111
+ break;
1112
+ case '--framework':
1113
+ options.framework = args[++i];
1114
+ break;
1115
+ case '--with-conflicts':
1116
+ options.withConflicts = true;
1117
+ break;
1118
+ case '--no-conflicts':
1119
+ options.withConflicts = false;
1120
+ break;
1121
+ case '--resolve-conflicts':
1122
+ options.resolveConflicts = true;
1123
+ break;
1124
+ case '--analysis-mode':
1125
+ options.analysisMode = args[++i];
1126
+ break;
1127
+ case '--max-files':
1128
+ options.maxFiles = parseInt(args[++i], 10);
1129
+ break;
1130
+ case '--json':
1131
+ options.json = true;
1132
+ options.format = 'json';
1133
+ break;
1134
+ case '--project':
1135
+ case '-p':
1136
+ options.project = args[++i];
1137
+ break;
1138
+ case '--help':
1139
+ case '-h':
1140
+ options.help = true;
1141
+ break;
1142
+ }
1143
+ }
1144
+
1145
+ return options;
1146
+ }
1147
+
1148
+ function showHelp() {
1149
+ console.log(`
1150
+ ${c.bold}Wogi Flow - Pattern Extraction Engine${c.reset}
1151
+
1152
+ ${c.cyan}Usage:${c.reset}
1153
+ flow pattern-extract [options]
1154
+ node scripts/flow-pattern-extractor.js [options]
1155
+
1156
+ ${c.cyan}Options:${c.reset}
1157
+ --output, -o <file> Output file (default: stdout)
1158
+ --format, -f <format> Output format: json, markdown, decisions (default: json)
1159
+ --project, -p <folder> Project folder to scan (default: current directory)
1160
+ --categories <cats> Categories: code,api,component,architecture (default: all)
1161
+ --framework <name> Framework: auto, react, nestjs, python (default: auto)
1162
+ --with-conflicts Include conflict analysis (default)
1163
+ --no-conflicts Skip conflict analysis
1164
+ --resolve-conflicts Interactive conflict resolution
1165
+ --analysis-mode <mode> Git analysis: balanced (default), deep
1166
+ --max-files <n> Max files to scan (default: 1000)
1167
+ --json JSON output for scripting
1168
+ --help, -h Show this help
1169
+
1170
+ ${c.cyan}Examples:${c.reset}
1171
+ flow pattern-extract # Basic extraction
1172
+ flow pattern-extract --project /path/to/other # Scan different project
1173
+ flow pattern-extract --format decisions # Output as decisions.md format
1174
+ flow pattern-extract --framework react # Force React framework detection
1175
+ flow pattern-extract --no-conflicts --json # JSON without conflicts
1176
+ `);
1177
+ }
1178
+
1179
+ async function main() {
1180
+ const args = process.argv.slice(2);
1181
+ const options = parseArgs(args);
1182
+
1183
+ if (options.help) {
1184
+ showHelp();
1185
+ process.exit(0);
1186
+ }
1187
+
1188
+ // Use specified project folder or default to current directory
1189
+ const projectRoot = options.project
1190
+ ? path.resolve(options.project)
1191
+ : getProjectRoot();
1192
+
1193
+ console.error(`${c.cyan}Scanning project...${c.reset}`);
1194
+ console.error(` Root: ${projectRoot}`);
1195
+
1196
+ try {
1197
+ const result = await extractPatterns(projectRoot, {
1198
+ categories: options.categories,
1199
+ includeConflicts: options.withConflicts,
1200
+ includeRecommendations: true,
1201
+ maxFiles: options.maxFiles,
1202
+ framework: options.framework,
1203
+ analysisMode: options.analysisMode
1204
+ });
1205
+
1206
+ console.error(` Framework: ${result.meta.framework}`);
1207
+ console.error(` Files: ${result.meta.filesScanned}`);
1208
+ console.error(` Patterns: ${Object.values(result.patterns).flat().length}`);
1209
+ console.error(` Conflicts: ${result.conflicts.length}`);
1210
+ console.error(` Duration: ${result.meta.scanDurationMs}ms`);
1211
+ console.error('');
1212
+
1213
+ // Format output
1214
+ let output;
1215
+ if (options.format === 'json' || options.json) {
1216
+ output = formatAsJson(result);
1217
+ } else if (options.format === 'markdown' || options.format === 'decisions') {
1218
+ output = formatAsDecisions(result);
1219
+ } else {
1220
+ output = formatAsJson(result);
1221
+ }
1222
+
1223
+ // Write output
1224
+ if (options.output) {
1225
+ fs.writeFileSync(options.output, output);
1226
+ console.error(`${c.green}✓ Output written to ${options.output}${c.reset}`);
1227
+ } else {
1228
+ console.log(output);
1229
+ }
1230
+
1231
+ // Handle interactive conflict resolution
1232
+ if (options.resolveConflicts && result.conflicts.length > 0) {
1233
+ console.error(`\n${c.yellow}Conflict resolution requested but not yet implemented.${c.reset}`);
1234
+ console.error(`Run: node scripts/flow-conflict-resolver.js --input <patterns.json>`);
1235
+ }
1236
+
1237
+ } catch (err) {
1238
+ console.error(`${c.red}Error: ${err.message}${c.reset}`);
1239
+ process.exit(1);
1240
+ }
1241
+ }
1242
+
1243
+ // Run if executed directly
1244
+ if (require.main === module) {
1245
+ main();
1246
+ }
1247
+
1248
+ // Export for use as module
1249
+ module.exports = {
1250
+ extractPatterns,
1251
+ detectFramework,
1252
+ detectConflicts,
1253
+ generateRecommendations,
1254
+ formatAsJson,
1255
+ formatAsDecisions,
1256
+ // Individual extractors
1257
+ extractCodePatterns,
1258
+ extractApiPatterns,
1259
+ extractComponentPatterns,
1260
+ extractArchitecturePatterns,
1261
+ // Utilities
1262
+ globFiles,
1263
+ createPattern,
1264
+ createConflict
1265
+ };