@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.
- package/commands/architect.ts +760 -896
- package/commands/check.ts +131 -131
- package/commands/clear.ts +170 -174
- package/commands/decide.ts +249 -249
- package/commands/discover.ts +82 -10
- package/commands/knowledge.ts +361 -361
- package/commands/patterns.ts +621 -621
- package/commands/plan.ts +376 -376
- package/commands/product.ts +626 -628
- package/commands/research.ts +754 -754
- package/commands/review.ts +463 -463
- package/commands/standards.ts +200 -223
- package/commands/task.ts +2 -2
- package/commands/utils.ts +1021 -1021
- package/db/connection.ts +32 -32
- package/db/schema.ts +719 -788
- package/detectors/loader.ts +0 -12
- package/gates/standards-validator.ts +204 -204
- package/gates/validator.ts +441 -441
- package/package.json +43 -43
- package/protocol/process-return.ts +450 -450
- package/protocol/subagent-protocol.ts +401 -411
- package/workflow.ts +0 -18
package/commands/patterns.ts
CHANGED
|
@@ -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
|
+
}
|