@codexa/cli 8.5.0 → 8.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,621 +1,621 @@
1
- /**
2
- * Codexa Workflow v8.0 - Extração Automática de Patterns via Grepai
3
- *
4
- * Usa busca semântica (grepai search) para encontrar arquivos por intenção
5
- * e extrair patterns de código automaticamente.
6
- */
7
-
8
- import { execSync, spawnSync } from "child_process";
9
- import { readFileSync, existsSync } from "fs";
10
- import { extname, basename, dirname } from "path";
11
- import { getDb } from "../db/connection";
12
- import { initSchema } from "../db/schema";
13
-
14
- // ═══════════════════════════════════════════════════════════════
15
- // TIPOS
16
- // ═══════════════════════════════════════════════════════════════
17
-
18
- interface PatternQuery {
19
- scope: string;
20
- category: string;
21
- query: string;
22
- appliesTo: string; // glob pattern
23
- }
24
-
25
- interface ExtractedPattern {
26
- category: string;
27
- name: string;
28
- scope: string;
29
- appliesTo: string;
30
- structure: {
31
- commonImports: string[];
32
- commonExports: string[];
33
- conventions: string[];
34
- };
35
- template: string;
36
- examples: Array<{ path: string; relevance: number }>;
37
- antiPatterns: string[];
38
- confidence: number;
39
- extractedFrom: number;
40
- }
41
-
42
- interface GrepaiResult {
43
- path: string;
44
- score: number;
45
- content?: string;
46
- }
47
-
48
- // ═══════════════════════════════════════════════════════════════
49
- // QUERIES SEMÂNTICAS POR ESCOPO
50
- // ═══════════════════════════════════════════════════════════════
51
-
52
- const PATTERN_QUERIES: PatternQuery[] = [
53
- // Frontend
54
- { scope: "frontend", category: "component", query: "React page component with layout", appliesTo: "app/**/page.tsx" },
55
- { scope: "frontend", category: "component", query: "React client component with state", appliesTo: "components/**/*.tsx" },
56
- { scope: "frontend", category: "hook", query: "custom React hook with useState", appliesTo: "hooks/**/*.ts" },
57
- { scope: "frontend", category: "component", query: "React server component with async data", appliesTo: "app/**/*.tsx" },
58
-
59
- // Backend
60
- { scope: "backend", category: "route", query: "API route handler with request response", appliesTo: "app/api/**/*.ts" },
61
- { scope: "backend", category: "service", query: "service class with methods", appliesTo: "services/**/*.ts" },
62
- { scope: "backend", category: "action", query: "server action with use server", appliesTo: "actions/**/*.ts" },
63
-
64
- // Database
65
- { scope: "database", category: "schema", query: "database schema definition", appliesTo: "db/**/*.ts" },
66
- { scope: "database", category: "schema", query: "drizzle table schema", appliesTo: "schema/**/*.ts" },
67
-
68
- // Testing
69
- { scope: "testing", category: "test", query: "test file with describe and it", appliesTo: "**/*.test.ts" },
70
- { scope: "testing", category: "test", query: "vitest test with expect", appliesTo: "**/*.spec.ts" },
71
- ];
72
-
73
- // ═══════════════════════════════════════════════════════════════
74
- // VERIFICAR GREPAI
75
- // ═══════════════════════════════════════════════════════════════
76
-
77
- function isGrepaiAvailable(): boolean {
78
- try {
79
- const result = spawnSync("grepai", ["--version"], { encoding: "utf-8" });
80
- return result.status === 0;
81
- } catch {
82
- return false;
83
- }
84
- }
85
-
86
- // ═══════════════════════════════════════════════════════════════
87
- // BUSCA SEMÂNTICA
88
- // ═══════════════════════════════════════════════════════════════
89
-
90
- function searchWithGrepai(query: string, topK: number = 10): GrepaiResult[] {
91
- try {
92
- const result = spawnSync(
93
- "grepai",
94
- ["search", query, "--top", topK.toString(), "--format", "json"],
95
- { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
96
- );
97
-
98
- if (result.status !== 0) {
99
- console.warn(` [!] grepai search falhou: ${result.stderr}`);
100
- return [];
101
- }
102
-
103
- const output = result.stdout.trim();
104
- if (!output || output === "[]") return [];
105
-
106
- return JSON.parse(output) as GrepaiResult[];
107
- } catch (error: any) {
108
- console.warn(` [!] Erro no grepai: ${error.message}`);
109
- return [];
110
- }
111
- }
112
-
113
- // ═══════════════════════════════════════════════════════════════
114
- // ANÁLISE DE ARQUIVOS
115
- // ═══════════════════════════════════════════════════════════════
116
-
117
- interface FileAnalysis {
118
- path: string;
119
- imports: string[];
120
- exports: string[];
121
- hasDefaultExport: boolean;
122
- hasNamedExports: boolean;
123
- conventions: string[];
124
- size: number;
125
- }
126
-
127
- function analyzeFile(filePath: string): FileAnalysis | null {
128
- if (!existsSync(filePath)) return null;
129
-
130
- try {
131
- const content = readFileSync(filePath, "utf-8");
132
- const lines = content.split("\n");
133
-
134
- const imports: string[] = [];
135
- const exports: string[] = [];
136
- const conventions: string[] = [];
137
-
138
- let hasDefaultExport = false;
139
- let hasNamedExports = false;
140
-
141
- for (const line of lines) {
142
- const trimmed = line.trim();
143
-
144
- // Extrair imports
145
- const importMatch = trimmed.match(/^import\s+.*\s+from\s+['"]([^'"]+)['"]/);
146
- if (importMatch) {
147
- imports.push(importMatch[1]);
148
- }
149
-
150
- // Extrair exports
151
- if (trimmed.startsWith("export default")) {
152
- hasDefaultExport = true;
153
- exports.push("default");
154
- } else if (trimmed.startsWith("export ")) {
155
- hasNamedExports = true;
156
- const exportMatch = trimmed.match(/export\s+(?:const|function|class|interface|type)\s+(\w+)/);
157
- if (exportMatch) {
158
- exports.push(exportMatch[1]);
159
- }
160
- }
161
-
162
- // Detectar convenções
163
- if (trimmed.includes("'use client'") || trimmed.includes('"use client"')) {
164
- conventions.push("use client");
165
- }
166
- if (trimmed.includes("'use server'") || trimmed.includes('"use server"')) {
167
- conventions.push("use server");
168
- }
169
- if (trimmed.includes("async function") || trimmed.includes("async (")) {
170
- conventions.push("async");
171
- }
172
- }
173
-
174
- return {
175
- path: filePath,
176
- imports: [...new Set(imports)],
177
- exports: [...new Set(exports)],
178
- hasDefaultExport,
179
- hasNamedExports,
180
- conventions: [...new Set(conventions)],
181
- size: content.length,
182
- };
183
- } catch {
184
- return null;
185
- }
186
- }
187
-
188
- // ═══════════════════════════════════════════════════════════════
189
- // v8.5: EXTRAÇÃO DE UTILITIES (DRY enforcement)
190
- // ═══════════════════════════════════════════════════════════════
191
-
192
- export interface ExtractedUtility {
193
- name: string;
194
- type: "function" | "const" | "class" | "interface" | "type";
195
- signature?: string;
196
- }
197
-
198
- export function extractUtilitiesFromFile(filePath: string): ExtractedUtility[] {
199
- if (!existsSync(filePath)) return [];
200
- try {
201
- const content = readFileSync(filePath, "utf-8");
202
- const lines = content.split("\n");
203
- const utilities: ExtractedUtility[] = [];
204
-
205
- for (const line of lines) {
206
- const trimmed = line.trim();
207
-
208
- // export [async] function name(args): ReturnType
209
- const funcMatch = trimmed.match(
210
- /^export\s+(?:async\s+)?function\s+(\w+)\s*(\([^)]*\)(?:\s*:\s*[^{]+)?)?/
211
- );
212
- if (funcMatch) {
213
- utilities.push({
214
- name: funcMatch[1],
215
- type: "function",
216
- signature: funcMatch[2]?.trim() || undefined,
217
- });
218
- continue;
219
- }
220
-
221
- // export const name: Type = ...
222
- const constMatch = trimmed.match(
223
- /^export\s+const\s+(\w+)(?:\s*:\s*([^=]+))?\s*=/
224
- );
225
- if (constMatch) {
226
- utilities.push({
227
- name: constMatch[1],
228
- type: "const",
229
- signature: constMatch[2]?.trim() || undefined,
230
- });
231
- continue;
232
- }
233
-
234
- // export class Name
235
- const classMatch = trimmed.match(/^export\s+class\s+(\w+)/);
236
- if (classMatch) {
237
- utilities.push({ name: classMatch[1], type: "class" });
238
- continue;
239
- }
240
-
241
- // export interface Name
242
- const ifaceMatch = trimmed.match(/^export\s+interface\s+(\w+)/);
243
- if (ifaceMatch) {
244
- utilities.push({ name: ifaceMatch[1], type: "interface" });
245
- continue;
246
- }
247
-
248
- // export type Name
249
- const typeMatch = trimmed.match(/^export\s+type\s+(\w+)/);
250
- if (typeMatch) {
251
- utilities.push({ name: typeMatch[1], type: "type" });
252
- continue;
253
- }
254
- }
255
- return utilities;
256
- } catch {
257
- return [];
258
- }
259
- }
260
-
261
- export function inferScopeFromPath(filePath: string): string {
262
- const lower = filePath.toLowerCase().replace(/\\/g, "/");
263
- if (lower.includes("/app/api/") || lower.includes("/server/") ||
264
- lower.includes("/backend/") || lower.includes("/services/") ||
265
- lower.includes("/actions/")) return "backend";
266
- if (lower.includes("/components/") || lower.includes("/hooks/") ||
267
- lower.includes("/pages/") || lower.includes("/frontend/")) return "frontend";
268
- if (lower.includes("/app/") && !lower.includes("/api/")) return "frontend";
269
- if (lower.includes("/db/") || lower.includes("/schema/") ||
270
- lower.includes("/migration") || lower.includes("/database/")) return "database";
271
- if (lower.includes("/test") || lower.includes(".test.") ||
272
- lower.includes(".spec.")) return "testing";
273
- return "shared";
274
- }
275
-
276
- function findCommonElements<T>(arrays: T[][]): T[] {
277
- if (arrays.length === 0) return [];
278
- if (arrays.length === 1) return arrays[0];
279
-
280
- // Elementos que aparecem em pelo menos 50% dos arquivos
281
- const threshold = Math.ceil(arrays.length * 0.5);
282
- const counts = new Map<string, number>();
283
-
284
- for (const arr of arrays) {
285
- const seen = new Set<string>();
286
- for (const item of arr) {
287
- const key = String(item);
288
- if (!seen.has(key)) {
289
- seen.add(key);
290
- counts.set(key, (counts.get(key) || 0) + 1);
291
- }
292
- }
293
- }
294
-
295
- return [...counts.entries()]
296
- .filter(([_, count]) => count >= threshold)
297
- .map(([item]) => item as unknown as T);
298
- }
299
-
300
- // ═══════════════════════════════════════════════════════════════
301
- // GERAÇÃO DE TEMPLATE
302
- // ═══════════════════════════════════════════════════════════════
303
-
304
- function generateTemplate(analyses: FileAnalysis[], category: string): string {
305
- const commonImports = findCommonElements(analyses.map((a) => a.imports));
306
- const hasUseClient = analyses.some((a) => a.conventions.includes("use client"));
307
- const hasUseServer = analyses.some((a) => a.conventions.includes("use server"));
308
- const hasAsync = analyses.some((a) => a.conventions.includes("async"));
309
-
310
- let template = "";
311
-
312
- // Diretiva de cliente/servidor
313
- if (hasUseClient) {
314
- template += `"use client"\n\n`;
315
- } else if (hasUseServer) {
316
- template += `"use server"\n\n`;
317
- }
318
-
319
- // Imports comuns
320
- if (commonImports.length > 0) {
321
- for (const imp of commonImports.slice(0, 5)) {
322
- // Max 5 imports
323
- if (imp.startsWith("react") || imp.startsWith("next")) {
324
- template += `import { /* ... */ } from "${imp}"\n`;
325
- } else if (imp.startsWith("@/") || imp.startsWith("~/")) {
326
- template += `import { /* ... */ } from "${imp}"\n`;
327
- }
328
- }
329
- template += "\n";
330
- }
331
-
332
- // Estrutura baseada na categoria
333
- switch (category) {
334
- case "component":
335
- template += `interface {{ComponentName}}Props {\n // Props\n}\n\n`;
336
- template += hasAsync
337
- ? `export default async function {{ComponentName}}({ /* props */ }: {{ComponentName}}Props) {\n // Implementação\n return (\n <div>\n {/* JSX */}\n </div>\n )\n}`
338
- : `export default function {{ComponentName}}({ /* props */ }: {{ComponentName}}Props) {\n // Implementação\n return (\n <div>\n {/* JSX */}\n </div>\n )\n}`;
339
- break;
340
-
341
- case "hook":
342
- template += `export function use{{HookName}}() {\n // Estado e lógica\n \n return {\n // Valores e funções\n }\n}`;
343
- break;
344
-
345
- case "route":
346
- template += `import { NextRequest, NextResponse } from "next/server"\n\n`;
347
- template += `export async function GET(request: NextRequest) {\n // Implementação GET\n return NextResponse.json({ /* data */ })\n}\n\n`;
348
- template += `export async function POST(request: NextRequest) {\n // Implementação POST\n return NextResponse.json({ /* data */ })\n}`;
349
- break;
350
-
351
- case "service":
352
- template += `export class {{ServiceName}}Service {\n constructor() {\n // Inicialização\n }\n\n async execute() {\n // Lógica do serviço\n }\n}`;
353
- break;
354
-
355
- case "action":
356
- template += `"use server"\n\n`;
357
- template += `export async function {{actionName}}(formData: FormData) {\n // Validação\n \n // Lógica\n \n // Retorno\n}`;
358
- break;
359
-
360
- case "schema":
361
- template += `import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"\n\n`;
362
- template += `export const {{tableName}} = pgTable("{{table_name}}", {\n id: uuid("id").primaryKey().defaultRandom(),\n // Colunas\n createdAt: timestamp("created_at").defaultNow(),\n updatedAt: timestamp("updated_at").defaultNow(),\n})`;
363
- break;
364
-
365
- case "test":
366
- template += `import { describe, it, expect } from "vitest"\n\n`;
367
- template += `describe("{{TestSubject}}", () => {\n it("should do something", () => {\n // Arrange\n \n // Act\n \n // Assert\n expect(true).toBe(true)\n })\n})`;
368
- break;
369
-
370
- default:
371
- template += `// Template para ${category}\n`;
372
- }
373
-
374
- return template;
375
- }
376
-
377
- // ═══════════════════════════════════════════════════════════════
378
- // EXTRAÇÃO DE PATTERNS
379
- // ═══════════════════════════════════════════════════════════════
380
-
381
- function extractPatternFromFiles(
382
- query: PatternQuery,
383
- files: GrepaiResult[]
384
- ): ExtractedPattern | null {
385
- if (files.length < 2) {
386
- // Precisa de pelo menos 2 arquivos para identificar pattern
387
- return null;
388
- }
389
-
390
- // Analisar cada arquivo
391
- const analyses: FileAnalysis[] = [];
392
- for (const file of files) {
393
- const analysis = analyzeFile(file.path);
394
- if (analysis) {
395
- analyses.push(analysis);
396
- }
397
- }
398
-
399
- if (analyses.length < 2) return null;
400
-
401
- // Encontrar elementos comuns
402
- const commonImports = findCommonElements(analyses.map((a) => a.imports));
403
- const commonExports = findCommonElements(analyses.map((a) => a.exports));
404
- const commonConventions = findCommonElements(analyses.map((a) => a.conventions));
405
-
406
- // Gerar template
407
- const template = generateTemplate(analyses, query.category);
408
-
409
- // Calcular confiança baseada em quantos arquivos seguem o pattern
410
- const confidence = Math.min(analyses.length / 10, 1); // Max 1.0 com 10+ arquivos
411
-
412
- // Gerar nome único
413
- const name = `${query.scope}-${query.category}-pattern`;
414
-
415
- return {
416
- category: query.category,
417
- name: name,
418
- scope: query.scope,
419
- appliesTo: query.appliesTo,
420
- structure: {
421
- commonImports,
422
- commonExports,
423
- conventions: commonConventions,
424
- },
425
- template,
426
- examples: files.slice(0, 5).map((f) => ({
427
- path: f.path,
428
- relevance: f.score || 0.8,
429
- })),
430
- antiPatterns: [], // Será preenchido se detectarmos inconsistências
431
- confidence,
432
- extractedFrom: analyses.length,
433
- };
434
- }
435
-
436
- // ═══════════════════════════════════════════════════════════════
437
- // COMANDOS PÚBLICOS
438
- // ═══════════════════════════════════════════════════════════════
439
-
440
- export interface ExtractOptions {
441
- scope?: string;
442
- all?: boolean;
443
- json?: boolean;
444
- dryRun?: boolean;
445
- }
446
-
447
- export function patternsExtract(options: ExtractOptions): void {
448
- initSchema();
449
-
450
- // Verificar grepai
451
- if (!isGrepaiAvailable()) {
452
- console.error("\n[ERRO] grepai nao encontrado.");
453
- console.error("Instale com: go install github.com/your-org/grepai@latest");
454
- console.error("Ou configure no PATH.\n");
455
- process.exit(1);
456
- }
457
-
458
- const db = getDb();
459
- const now = new Date().toISOString();
460
-
461
- // Filtrar queries por escopo
462
- let queries = PATTERN_QUERIES;
463
- if (options.scope && !options.all) {
464
- queries = queries.filter((q) => q.scope === options.scope);
465
- }
466
-
467
- if (queries.length === 0) {
468
- console.error(`\n[ERRO] Escopo '${options.scope}' nao reconhecido.`);
469
- console.error("Escopos validos: frontend, backend, database, testing\n");
470
- process.exit(1);
471
- }
472
-
473
- console.log(`\n🔍 Extraindo patterns via grepai...`);
474
- console.log(` Queries: ${queries.length}`);
475
- console.log(` Escopo: ${options.scope || "todos"}\n`);
476
-
477
- const extracted: ExtractedPattern[] = [];
478
-
479
- for (const query of queries) {
480
- console.log(` [${query.scope}/${query.category}] "${query.query}"`);
481
-
482
- // Buscar arquivos via grepai
483
- const results = searchWithGrepai(query.query, 15);
484
-
485
- if (results.length === 0) {
486
- console.log(` → Nenhum arquivo encontrado`);
487
- continue;
488
- }
489
-
490
- console.log(` → ${results.length} arquivos encontrados`);
491
-
492
- // Extrair pattern
493
- const pattern = extractPatternFromFiles(query, results);
494
-
495
- if (pattern) {
496
- extracted.push(pattern);
497
- console.log(` ✓ Pattern extraido (confiança: ${(pattern.confidence * 100).toFixed(0)}%)`);
498
- } else {
499
- console.log(` → Arquivos insuficientes para pattern`);
500
- }
501
- }
502
-
503
- console.log(`\n${"─".repeat(50)}`);
504
- console.log(`Patterns extraidos: ${extracted.length}`);
505
-
506
- if (extracted.length === 0) {
507
- console.log("Nenhum pattern encontrado. Execute 'grepai index' primeiro.\n");
508
- return;
509
- }
510
-
511
- // Output JSON se solicitado
512
- if (options.json) {
513
- console.log(JSON.stringify({ patterns: extracted }, null, 2));
514
- return;
515
- }
516
-
517
- // Dry run - apenas mostrar
518
- if (options.dryRun) {
519
- console.log("\n[DRY RUN] Patterns que seriam salvos:");
520
- for (const p of extracted) {
521
- console.log(` - ${p.name} (${p.scope}/${p.category})`);
522
- console.log(` Exemplos: ${p.examples.length}`);
523
- console.log(` Confiança: ${(p.confidence * 100).toFixed(0)}%`);
524
- }
525
- console.log("\nUse sem --dry-run para salvar no banco.\n");
526
- return;
527
- }
528
-
529
- // Salvar no banco
530
- console.log("\nSalvando patterns no banco...");
531
-
532
- for (const pattern of extracted) {
533
- // Verificar se já existe
534
- const existing = db
535
- .query("SELECT id FROM implementation_patterns WHERE name = ?")
536
- .get(pattern.name);
537
-
538
- if (existing) {
539
- // Atualizar
540
- db.run(
541
- `UPDATE implementation_patterns SET
542
- structure = ?, template = ?, examples = ?,
543
- confidence = ?, extracted_from = ?, updated_at = ?
544
- WHERE name = ?`,
545
- [
546
- JSON.stringify(pattern.structure),
547
- pattern.template,
548
- JSON.stringify(pattern.examples),
549
- pattern.confidence,
550
- pattern.extractedFrom,
551
- now,
552
- pattern.name,
553
- ]
554
- );
555
- console.log(` ✓ Atualizado: ${pattern.name}`);
556
- } else {
557
- // Inserir novo
558
- db.run(
559
- `INSERT INTO implementation_patterns
560
- (category, name, scope, applies_to, structure, template, examples, anti_patterns, confidence, extracted_from, created_at)
561
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
562
- [
563
- pattern.category,
564
- pattern.name,
565
- pattern.scope,
566
- pattern.appliesTo,
567
- JSON.stringify(pattern.structure),
568
- pattern.template,
569
- JSON.stringify(pattern.examples),
570
- JSON.stringify(pattern.antiPatterns),
571
- pattern.confidence,
572
- pattern.extractedFrom,
573
- now,
574
- ]
575
- );
576
- console.log(` ✓ Criado: ${pattern.name}`);
577
- }
578
- }
579
-
580
- console.log(`\n${"─".repeat(50)}`);
581
- console.log(`✓ ${extracted.length} patterns salvos no banco.`);
582
- console.log(`Use 'discover patterns' para visualizar.`);
583
- console.log(`Use 'discover export-patterns' para gerar patterns.md\n`);
584
- }
585
-
586
- export function patternsAnalyze(filePath: string, json: boolean = false): void {
587
- if (!existsSync(filePath)) {
588
- console.error(`\n[ERRO] Arquivo nao encontrado: ${filePath}\n`);
589
- process.exit(1);
590
- }
591
-
592
- const analysis = analyzeFile(filePath);
593
-
594
- if (!analysis) {
595
- console.error(`\n[ERRO] Nao foi possivel analisar o arquivo.\n`);
596
- process.exit(1);
597
- }
598
-
599
- if (json) {
600
- console.log(JSON.stringify(analysis, null, 2));
601
- return;
602
- }
603
-
604
- console.log(`\n📄 Analise: ${filePath}`);
605
- console.log(`${"─".repeat(50)}`);
606
- console.log(`\nImports (${analysis.imports.length}):`);
607
- for (const imp of analysis.imports) {
608
- console.log(` - ${imp}`);
609
- }
610
- console.log(`\nExports (${analysis.exports.length}):`);
611
- for (const exp of analysis.exports) {
612
- console.log(` - ${exp}`);
613
- }
614
- console.log(`\nConvenções:`);
615
- console.log(` - Default export: ${analysis.hasDefaultExport ? "sim" : "não"}`);
616
- console.log(` - Named exports: ${analysis.hasNamedExports ? "sim" : "não"}`);
617
- if (analysis.conventions.length > 0) {
618
- console.log(` - Diretivas: ${analysis.conventions.join(", ")}`);
619
- }
620
- console.log(`\nTamanho: ${analysis.size} bytes\n`);
621
- }
1
+ /**
2
+ * Codexa Workflow v8.0 - Extração Automática de Patterns via Grepai
3
+ *
4
+ * Usa busca semântica (grepai search) para encontrar arquivos por intenção
5
+ * e extrair patterns de código automaticamente.
6
+ */
7
+
8
+ import { execSync, spawnSync } from "child_process";
9
+ import { readFileSync, existsSync } from "fs";
10
+ import { extname, basename, dirname } from "path";
11
+ import { getDb } from "../db/connection";
12
+ import { initSchema } from "../db/schema";
13
+
14
+ // ═══════════════════════════════════════════════════════════════
15
+ // TIPOS
16
+ // ═══════════════════════════════════════════════════════════════
17
+
18
+ interface PatternQuery {
19
+ scope: string;
20
+ category: string;
21
+ query: string;
22
+ appliesTo: string; // glob pattern
23
+ }
24
+
25
+ interface ExtractedPattern {
26
+ category: string;
27
+ name: string;
28
+ scope: string;
29
+ appliesTo: string;
30
+ structure: {
31
+ commonImports: string[];
32
+ commonExports: string[];
33
+ conventions: string[];
34
+ };
35
+ template: string;
36
+ examples: Array<{ path: string; relevance: number }>;
37
+ antiPatterns: string[];
38
+ confidence: number;
39
+ extractedFrom: number;
40
+ }
41
+
42
+ interface GrepaiResult {
43
+ path: string;
44
+ score: number;
45
+ content?: string;
46
+ }
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // QUERIES SEMÂNTICAS POR ESCOPO
50
+ // ═══════════════════════════════════════════════════════════════
51
+
52
+ const PATTERN_QUERIES: PatternQuery[] = [
53
+ // Frontend
54
+ { scope: "frontend", category: "component", query: "React page component with layout", appliesTo: "app/**/page.tsx" },
55
+ { scope: "frontend", category: "component", query: "React client component with state", appliesTo: "components/**/*.tsx" },
56
+ { scope: "frontend", category: "hook", query: "custom React hook with useState", appliesTo: "hooks/**/*.ts" },
57
+ { scope: "frontend", category: "component", query: "React server component with async data", appliesTo: "app/**/*.tsx" },
58
+
59
+ // Backend
60
+ { scope: "backend", category: "route", query: "API route handler with request response", appliesTo: "app/api/**/*.ts" },
61
+ { scope: "backend", category: "service", query: "service class with methods", appliesTo: "services/**/*.ts" },
62
+ { scope: "backend", category: "action", query: "server action with use server", appliesTo: "actions/**/*.ts" },
63
+
64
+ // Database
65
+ { scope: "database", category: "schema", query: "database schema definition", appliesTo: "db/**/*.ts" },
66
+ { scope: "database", category: "schema", query: "drizzle table schema", appliesTo: "schema/**/*.ts" },
67
+
68
+ // Testing
69
+ { scope: "testing", category: "test", query: "test file with describe and it", appliesTo: "**/*.test.ts" },
70
+ { scope: "testing", category: "test", query: "vitest test with expect", appliesTo: "**/*.spec.ts" },
71
+ ];
72
+
73
+ // ═══════════════════════════════════════════════════════════════
74
+ // VERIFICAR GREPAI
75
+ // ═══════════════════════════════════════════════════════════════
76
+
77
+ function isGrepaiAvailable(): boolean {
78
+ try {
79
+ const result = spawnSync("grepai", ["--version"], { encoding: "utf-8" });
80
+ return result.status === 0;
81
+ } catch {
82
+ return false;
83
+ }
84
+ }
85
+
86
+ // ═══════════════════════════════════════════════════════════════
87
+ // BUSCA SEMÂNTICA
88
+ // ═══════════════════════════════════════════════════════════════
89
+
90
+ function searchWithGrepai(query: string, topK: number = 10): GrepaiResult[] {
91
+ try {
92
+ const result = spawnSync(
93
+ "grepai",
94
+ ["search", query, "--top", topK.toString(), "--format", "json"],
95
+ { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }
96
+ );
97
+
98
+ if (result.status !== 0) {
99
+ console.warn(` [!] grepai search falhou: ${result.stderr}`);
100
+ return [];
101
+ }
102
+
103
+ const output = result.stdout.trim();
104
+ if (!output || output === "[]") return [];
105
+
106
+ return JSON.parse(output) as GrepaiResult[];
107
+ } catch (error: any) {
108
+ console.warn(` [!] Erro no grepai: ${error.message}`);
109
+ return [];
110
+ }
111
+ }
112
+
113
+ // ═══════════════════════════════════════════════════════════════
114
+ // ANÁLISE DE ARQUIVOS
115
+ // ═══════════════════════════════════════════════════════════════
116
+
117
+ interface FileAnalysis {
118
+ path: string;
119
+ imports: string[];
120
+ exports: string[];
121
+ hasDefaultExport: boolean;
122
+ hasNamedExports: boolean;
123
+ conventions: string[];
124
+ size: number;
125
+ }
126
+
127
+ function analyzeFile(filePath: string): FileAnalysis | null {
128
+ if (!existsSync(filePath)) return null;
129
+
130
+ try {
131
+ const content = readFileSync(filePath, "utf-8");
132
+ const lines = content.split("\n");
133
+
134
+ const imports: string[] = [];
135
+ const exports: string[] = [];
136
+ const conventions: string[] = [];
137
+
138
+ let hasDefaultExport = false;
139
+ let hasNamedExports = false;
140
+
141
+ for (const line of lines) {
142
+ const trimmed = line.trim();
143
+
144
+ // Extrair imports
145
+ const importMatch = trimmed.match(/^import\s+.*\s+from\s+['"]([^'"]+)['"]/);
146
+ if (importMatch) {
147
+ imports.push(importMatch[1]);
148
+ }
149
+
150
+ // Extrair exports
151
+ if (trimmed.startsWith("export default")) {
152
+ hasDefaultExport = true;
153
+ exports.push("default");
154
+ } else if (trimmed.startsWith("export ")) {
155
+ hasNamedExports = true;
156
+ const exportMatch = trimmed.match(/export\s+(?:const|function|class|interface|type)\s+(\w+)/);
157
+ if (exportMatch) {
158
+ exports.push(exportMatch[1]);
159
+ }
160
+ }
161
+
162
+ // Detectar convenções
163
+ if (trimmed.includes("'use client'") || trimmed.includes('"use client"')) {
164
+ conventions.push("use client");
165
+ }
166
+ if (trimmed.includes("'use server'") || trimmed.includes('"use server"')) {
167
+ conventions.push("use server");
168
+ }
169
+ if (trimmed.includes("async function") || trimmed.includes("async (")) {
170
+ conventions.push("async");
171
+ }
172
+ }
173
+
174
+ return {
175
+ path: filePath,
176
+ imports: [...new Set(imports)],
177
+ exports: [...new Set(exports)],
178
+ hasDefaultExport,
179
+ hasNamedExports,
180
+ conventions: [...new Set(conventions)],
181
+ size: content.length,
182
+ };
183
+ } catch {
184
+ return null;
185
+ }
186
+ }
187
+
188
+ // ═══════════════════════════════════════════════════════════════
189
+ // v8.5: EXTRAÇÃO DE UTILITIES (DRY enforcement)
190
+ // ═══════════════════════════════════════════════════════════════
191
+
192
+ export interface ExtractedUtility {
193
+ name: string;
194
+ type: "function" | "const" | "class" | "interface" | "type";
195
+ signature?: string;
196
+ }
197
+
198
+ export function extractUtilitiesFromFile(filePath: string): ExtractedUtility[] {
199
+ if (!existsSync(filePath)) return [];
200
+ try {
201
+ const content = readFileSync(filePath, "utf-8");
202
+ const lines = content.split("\n");
203
+ const utilities: ExtractedUtility[] = [];
204
+
205
+ for (const line of lines) {
206
+ const trimmed = line.trim();
207
+
208
+ // export [async] function name(args): ReturnType
209
+ const funcMatch = trimmed.match(
210
+ /^export\s+(?:async\s+)?function\s+(\w+)\s*(\([^)]*\)(?:\s*:\s*[^{]+)?)?/
211
+ );
212
+ if (funcMatch) {
213
+ utilities.push({
214
+ name: funcMatch[1],
215
+ type: "function",
216
+ signature: funcMatch[2]?.trim() || undefined,
217
+ });
218
+ continue;
219
+ }
220
+
221
+ // export const name: Type = ...
222
+ const constMatch = trimmed.match(
223
+ /^export\s+const\s+(\w+)(?:\s*:\s*([^=]+))?\s*=/
224
+ );
225
+ if (constMatch) {
226
+ utilities.push({
227
+ name: constMatch[1],
228
+ type: "const",
229
+ signature: constMatch[2]?.trim() || undefined,
230
+ });
231
+ continue;
232
+ }
233
+
234
+ // export class Name
235
+ const classMatch = trimmed.match(/^export\s+class\s+(\w+)/);
236
+ if (classMatch) {
237
+ utilities.push({ name: classMatch[1], type: "class" });
238
+ continue;
239
+ }
240
+
241
+ // export interface Name
242
+ const ifaceMatch = trimmed.match(/^export\s+interface\s+(\w+)/);
243
+ if (ifaceMatch) {
244
+ utilities.push({ name: ifaceMatch[1], type: "interface" });
245
+ continue;
246
+ }
247
+
248
+ // export type Name
249
+ const typeMatch = trimmed.match(/^export\s+type\s+(\w+)/);
250
+ if (typeMatch) {
251
+ utilities.push({ name: typeMatch[1], type: "type" });
252
+ continue;
253
+ }
254
+ }
255
+ return utilities;
256
+ } catch {
257
+ return [];
258
+ }
259
+ }
260
+
261
+ export function inferScopeFromPath(filePath: string): string {
262
+ const lower = filePath.toLowerCase().replace(/\\/g, "/");
263
+ if (lower.includes("/app/api/") || lower.includes("/server/") ||
264
+ lower.includes("/backend/") || lower.includes("/services/") ||
265
+ lower.includes("/actions/")) return "backend";
266
+ if (lower.includes("/components/") || lower.includes("/hooks/") ||
267
+ lower.includes("/pages/") || lower.includes("/frontend/")) return "frontend";
268
+ if (lower.includes("/app/") && !lower.includes("/api/")) return "frontend";
269
+ if (lower.includes("/db/") || lower.includes("/schema/") ||
270
+ lower.includes("/migration") || lower.includes("/database/")) return "database";
271
+ if (lower.includes("/test") || lower.includes(".test.") ||
272
+ lower.includes(".spec.")) return "testing";
273
+ return "shared";
274
+ }
275
+
276
+ function findCommonElements<T>(arrays: T[][]): T[] {
277
+ if (arrays.length === 0) return [];
278
+ if (arrays.length === 1) return arrays[0];
279
+
280
+ // Elementos que aparecem em pelo menos 50% dos arquivos
281
+ const threshold = Math.ceil(arrays.length * 0.5);
282
+ const counts = new Map<string, number>();
283
+
284
+ for (const arr of arrays) {
285
+ const seen = new Set<string>();
286
+ for (const item of arr) {
287
+ const key = String(item);
288
+ if (!seen.has(key)) {
289
+ seen.add(key);
290
+ counts.set(key, (counts.get(key) || 0) + 1);
291
+ }
292
+ }
293
+ }
294
+
295
+ return [...counts.entries()]
296
+ .filter(([_, count]) => count >= threshold)
297
+ .map(([item]) => item as unknown as T);
298
+ }
299
+
300
+ // ═══════════════════════════════════════════════════════════════
301
+ // GERAÇÃO DE TEMPLATE
302
+ // ═══════════════════════════════════════════════════════════════
303
+
304
+ function generateTemplate(analyses: FileAnalysis[], category: string): string {
305
+ const commonImports = findCommonElements(analyses.map((a) => a.imports));
306
+ const hasUseClient = analyses.some((a) => a.conventions.includes("use client"));
307
+ const hasUseServer = analyses.some((a) => a.conventions.includes("use server"));
308
+ const hasAsync = analyses.some((a) => a.conventions.includes("async"));
309
+
310
+ let template = "";
311
+
312
+ // Diretiva de cliente/servidor
313
+ if (hasUseClient) {
314
+ template += `"use client"\n\n`;
315
+ } else if (hasUseServer) {
316
+ template += `"use server"\n\n`;
317
+ }
318
+
319
+ // Imports comuns
320
+ if (commonImports.length > 0) {
321
+ for (const imp of commonImports.slice(0, 5)) {
322
+ // Max 5 imports
323
+ if (imp.startsWith("react") || imp.startsWith("next")) {
324
+ template += `import { /* ... */ } from "${imp}"\n`;
325
+ } else if (imp.startsWith("@/") || imp.startsWith("~/")) {
326
+ template += `import { /* ... */ } from "${imp}"\n`;
327
+ }
328
+ }
329
+ template += "\n";
330
+ }
331
+
332
+ // Estrutura baseada na categoria
333
+ switch (category) {
334
+ case "component":
335
+ template += `interface {{ComponentName}}Props {\n // Props\n}\n\n`;
336
+ template += hasAsync
337
+ ? `export default async function {{ComponentName}}({ /* props */ }: {{ComponentName}}Props) {\n // Implementação\n return (\n <div>\n {/* JSX */}\n </div>\n )\n}`
338
+ : `export default function {{ComponentName}}({ /* props */ }: {{ComponentName}}Props) {\n // Implementação\n return (\n <div>\n {/* JSX */}\n </div>\n )\n}`;
339
+ break;
340
+
341
+ case "hook":
342
+ template += `export function use{{HookName}}() {\n // Estado e lógica\n \n return {\n // Valores e funções\n }\n}`;
343
+ break;
344
+
345
+ case "route":
346
+ template += `import { NextRequest, NextResponse } from "next/server"\n\n`;
347
+ template += `export async function GET(request: NextRequest) {\n // Implementação GET\n return NextResponse.json({ /* data */ })\n}\n\n`;
348
+ template += `export async function POST(request: NextRequest) {\n // Implementação POST\n return NextResponse.json({ /* data */ })\n}`;
349
+ break;
350
+
351
+ case "service":
352
+ template += `export class {{ServiceName}}Service {\n constructor() {\n // Inicialização\n }\n\n async execute() {\n // Lógica do serviço\n }\n}`;
353
+ break;
354
+
355
+ case "action":
356
+ template += `"use server"\n\n`;
357
+ template += `export async function {{actionName}}(formData: FormData) {\n // Validação\n \n // Lógica\n \n // Retorno\n}`;
358
+ break;
359
+
360
+ case "schema":
361
+ template += `import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"\n\n`;
362
+ template += `export const {{tableName}} = pgTable("{{table_name}}", {\n id: uuid("id").primaryKey().defaultRandom(),\n // Colunas\n createdAt: timestamp("created_at").defaultNow(),\n updatedAt: timestamp("updated_at").defaultNow(),\n})`;
363
+ break;
364
+
365
+ case "test":
366
+ template += `import { describe, it, expect } from "vitest"\n\n`;
367
+ template += `describe("{{TestSubject}}", () => {\n it("should do something", () => {\n // Arrange\n \n // Act\n \n // Assert\n expect(true).toBe(true)\n })\n})`;
368
+ break;
369
+
370
+ default:
371
+ template += `// Template para ${category}\n`;
372
+ }
373
+
374
+ return template;
375
+ }
376
+
377
+ // ═══════════════════════════════════════════════════════════════
378
+ // EXTRAÇÃO DE PATTERNS
379
+ // ═══════════════════════════════════════════════════════════════
380
+
381
+ function extractPatternFromFiles(
382
+ query: PatternQuery,
383
+ files: GrepaiResult[]
384
+ ): ExtractedPattern | null {
385
+ if (files.length < 2) {
386
+ // Precisa de pelo menos 2 arquivos para identificar pattern
387
+ return null;
388
+ }
389
+
390
+ // Analisar cada arquivo
391
+ const analyses: FileAnalysis[] = [];
392
+ for (const file of files) {
393
+ const analysis = analyzeFile(file.path);
394
+ if (analysis) {
395
+ analyses.push(analysis);
396
+ }
397
+ }
398
+
399
+ if (analyses.length < 2) return null;
400
+
401
+ // Encontrar elementos comuns
402
+ const commonImports = findCommonElements(analyses.map((a) => a.imports));
403
+ const commonExports = findCommonElements(analyses.map((a) => a.exports));
404
+ const commonConventions = findCommonElements(analyses.map((a) => a.conventions));
405
+
406
+ // Gerar template
407
+ const template = generateTemplate(analyses, query.category);
408
+
409
+ // Calcular confiança baseada em quantos arquivos seguem o pattern
410
+ const confidence = Math.min(analyses.length / 10, 1); // Max 1.0 com 10+ arquivos
411
+
412
+ // Gerar nome único
413
+ const name = `${query.scope}-${query.category}-pattern`;
414
+
415
+ return {
416
+ category: query.category,
417
+ name: name,
418
+ scope: query.scope,
419
+ appliesTo: query.appliesTo,
420
+ structure: {
421
+ commonImports,
422
+ commonExports,
423
+ conventions: commonConventions,
424
+ },
425
+ template,
426
+ examples: files.slice(0, 5).map((f) => ({
427
+ path: f.path,
428
+ relevance: f.score || 0.8,
429
+ })),
430
+ antiPatterns: [], // Será preenchido se detectarmos inconsistências
431
+ confidence,
432
+ extractedFrom: analyses.length,
433
+ };
434
+ }
435
+
436
+ // ═══════════════════════════════════════════════════════════════
437
+ // COMANDOS PÚBLICOS
438
+ // ═══════════════════════════════════════════════════════════════
439
+
440
+ export interface ExtractOptions {
441
+ scope?: string;
442
+ all?: boolean;
443
+ json?: boolean;
444
+ dryRun?: boolean;
445
+ }
446
+
447
+ export function patternsExtract(options: ExtractOptions): void {
448
+ initSchema();
449
+
450
+ // Verificar grepai
451
+ if (!isGrepaiAvailable()) {
452
+ console.error("\n[ERRO] grepai nao encontrado.");
453
+ console.error("Instale com: go install github.com/your-org/grepai@latest");
454
+ console.error("Ou configure no PATH.\n");
455
+ process.exit(1);
456
+ }
457
+
458
+ const db = getDb();
459
+ const now = new Date().toISOString();
460
+
461
+ // Filtrar queries por escopo
462
+ let queries = PATTERN_QUERIES;
463
+ if (options.scope && !options.all) {
464
+ queries = queries.filter((q) => q.scope === options.scope);
465
+ }
466
+
467
+ if (queries.length === 0) {
468
+ console.error(`\n[ERRO] Escopo '${options.scope}' nao reconhecido.`);
469
+ console.error("Escopos validos: frontend, backend, database, testing\n");
470
+ process.exit(1);
471
+ }
472
+
473
+ console.log(`\n🔍 Extraindo patterns via grepai...`);
474
+ console.log(` Queries: ${queries.length}`);
475
+ console.log(` Escopo: ${options.scope || "todos"}\n`);
476
+
477
+ const extracted: ExtractedPattern[] = [];
478
+
479
+ for (const query of queries) {
480
+ console.log(` [${query.scope}/${query.category}] "${query.query}"`);
481
+
482
+ // Buscar arquivos via grepai
483
+ const results = searchWithGrepai(query.query, 15);
484
+
485
+ if (results.length === 0) {
486
+ console.log(` → Nenhum arquivo encontrado`);
487
+ continue;
488
+ }
489
+
490
+ console.log(` → ${results.length} arquivos encontrados`);
491
+
492
+ // Extrair pattern
493
+ const pattern = extractPatternFromFiles(query, results);
494
+
495
+ if (pattern) {
496
+ extracted.push(pattern);
497
+ console.log(` ✓ Pattern extraido (confiança: ${(pattern.confidence * 100).toFixed(0)}%)`);
498
+ } else {
499
+ console.log(` → Arquivos insuficientes para pattern`);
500
+ }
501
+ }
502
+
503
+ console.log(`\n${"─".repeat(50)}`);
504
+ console.log(`Patterns extraidos: ${extracted.length}`);
505
+
506
+ if (extracted.length === 0) {
507
+ console.log("Nenhum pattern encontrado. Execute 'grepai index' primeiro.\n");
508
+ return;
509
+ }
510
+
511
+ // Output JSON se solicitado
512
+ if (options.json) {
513
+ console.log(JSON.stringify({ patterns: extracted }, null, 2));
514
+ return;
515
+ }
516
+
517
+ // Dry run - apenas mostrar
518
+ if (options.dryRun) {
519
+ console.log("\n[DRY RUN] Patterns que seriam salvos:");
520
+ for (const p of extracted) {
521
+ console.log(` - ${p.name} (${p.scope}/${p.category})`);
522
+ console.log(` Exemplos: ${p.examples.length}`);
523
+ console.log(` Confiança: ${(p.confidence * 100).toFixed(0)}%`);
524
+ }
525
+ console.log("\nUse sem --dry-run para salvar no banco.\n");
526
+ return;
527
+ }
528
+
529
+ // Salvar no banco
530
+ console.log("\nSalvando patterns no banco...");
531
+
532
+ for (const pattern of extracted) {
533
+ // Verificar se já existe
534
+ const existing = db
535
+ .query("SELECT id FROM implementation_patterns WHERE name = ?")
536
+ .get(pattern.name);
537
+
538
+ if (existing) {
539
+ // Atualizar
540
+ db.run(
541
+ `UPDATE implementation_patterns SET
542
+ structure = ?, template = ?, examples = ?,
543
+ confidence = ?, extracted_from = ?, updated_at = ?
544
+ WHERE name = ?`,
545
+ [
546
+ JSON.stringify(pattern.structure),
547
+ pattern.template,
548
+ JSON.stringify(pattern.examples),
549
+ pattern.confidence,
550
+ pattern.extractedFrom,
551
+ now,
552
+ pattern.name,
553
+ ]
554
+ );
555
+ console.log(` ✓ Atualizado: ${pattern.name}`);
556
+ } else {
557
+ // Inserir novo
558
+ db.run(
559
+ `INSERT INTO implementation_patterns
560
+ (category, name, scope, applies_to, structure, template, examples, anti_patterns, confidence, extracted_from, created_at)
561
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
562
+ [
563
+ pattern.category,
564
+ pattern.name,
565
+ pattern.scope,
566
+ pattern.appliesTo,
567
+ JSON.stringify(pattern.structure),
568
+ pattern.template,
569
+ JSON.stringify(pattern.examples),
570
+ JSON.stringify(pattern.antiPatterns),
571
+ pattern.confidence,
572
+ pattern.extractedFrom,
573
+ now,
574
+ ]
575
+ );
576
+ console.log(` ✓ Criado: ${pattern.name}`);
577
+ }
578
+ }
579
+
580
+ console.log(`\n${"─".repeat(50)}`);
581
+ console.log(`✓ ${extracted.length} patterns salvos no banco.`);
582
+ console.log(`Use 'discover patterns' para visualizar.`);
583
+ console.log(`Use 'discover export-patterns' para gerar patterns.md\n`);
584
+ }
585
+
586
+ export function patternsAnalyze(filePath: string, json: boolean = false): void {
587
+ if (!existsSync(filePath)) {
588
+ console.error(`\n[ERRO] Arquivo nao encontrado: ${filePath}\n`);
589
+ process.exit(1);
590
+ }
591
+
592
+ const analysis = analyzeFile(filePath);
593
+
594
+ if (!analysis) {
595
+ console.error(`\n[ERRO] Nao foi possivel analisar o arquivo.\n`);
596
+ process.exit(1);
597
+ }
598
+
599
+ if (json) {
600
+ console.log(JSON.stringify(analysis, null, 2));
601
+ return;
602
+ }
603
+
604
+ console.log(`\n📄 Analise: ${filePath}`);
605
+ console.log(`${"─".repeat(50)}`);
606
+ console.log(`\nImports (${analysis.imports.length}):`);
607
+ for (const imp of analysis.imports) {
608
+ console.log(` - ${imp}`);
609
+ }
610
+ console.log(`\nExports (${analysis.exports.length}):`);
611
+ for (const exp of analysis.exports) {
612
+ console.log(` - ${exp}`);
613
+ }
614
+ console.log(`\nConvenções:`);
615
+ console.log(` - Default export: ${analysis.hasDefaultExport ? "sim" : "não"}`);
616
+ console.log(` - Named exports: ${analysis.hasNamedExports ? "sim" : "não"}`);
617
+ if (analysis.conventions.length > 0) {
618
+ console.log(` - Diretivas: ${analysis.conventions.join(", ")}`);
619
+ }
620
+ console.log(`\nTamanho: ${analysis.size} bytes\n`);
621
+ }