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.
- package/.workflow/specs/architecture.md.template +24 -0
- package/.workflow/specs/stack.md.template +33 -0
- package/.workflow/specs/testing.md.template +36 -0
- package/README.md +90 -1
- package/lib/unified-wizard.js +569 -30
- package/package.json +1 -1
- package/scripts/MEMORY-ARCHITECTURE.md +150 -0
- package/scripts/flow +20 -19
- package/scripts/flow-auto-context.js +97 -3
- package/scripts/flow-conflict-resolver.js +735 -0
- package/scripts/flow-context-gatherer.js +520 -0
- package/scripts/flow-context-monitor.js +148 -19
- package/scripts/flow-damage-control.js +5 -1
- package/scripts/flow-export-profile +168 -1
- package/scripts/flow-import-profile +257 -6
- package/scripts/flow-instruction-richness.js +182 -18
- package/scripts/flow-knowledge-router.js +2 -0
- package/scripts/flow-knowledge-sync.js +2 -0
- package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
- package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
- package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
- package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
- package/scripts/flow-memory-db.js +386 -1
- package/scripts/flow-memory-sync.js +2 -0
- package/scripts/flow-model-adapter.js +53 -29
- package/scripts/flow-model-router.js +246 -1
- package/scripts/flow-morning.js +94 -0
- package/scripts/flow-onboard +223 -10
- package/scripts/flow-orchestrate-validation.js +539 -0
- package/scripts/flow-orchestrate.js +16 -507
- package/scripts/flow-pattern-extractor.js +1265 -0
- package/scripts/flow-prompt-composer.js +222 -2
- package/scripts/flow-quality-guard.js +594 -0
- package/scripts/flow-section-index.js +713 -0
- package/scripts/flow-section-resolver.js +484 -0
- package/scripts/flow-session-end.js +188 -2
- package/scripts/flow-skill-create.js +19 -3
- package/scripts/flow-skill-matcher.js +122 -7
- package/scripts/flow-statusline-setup.js +218 -0
- package/scripts/flow-step-review.js +19 -0
- package/scripts/flow-tech-debt.js +734 -0
- package/scripts/flow-utils.js +2 -0
- package/scripts/hooks/core/long-input-gate.js +293 -0
- package/scripts/flow-parallel-detector.js +0 -399
- package/scripts/flow-parallel-dispatch.js +0 -987
- /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
|
+
};
|