@codexa/cli 9.0.2 ā 9.0.4
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.test.ts +531 -0
- package/commands/architect.ts +75 -17
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -1
- package/commands/decide.ts +37 -49
- package/commands/discover.ts +136 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +192 -102
- package/commands/patterns.test.ts +169 -0
- package/commands/patterns.ts +6 -13
- package/commands/plan.test.ts +73 -0
- package/commands/plan.ts +18 -66
- package/commands/product.ts +8 -17
- package/commands/research.ts +4 -3
- package/commands/review.ts +190 -28
- package/commands/spec-resolver.test.ts +119 -0
- package/commands/spec-resolver.ts +90 -0
- package/commands/standards.ts +7 -15
- package/commands/sync.ts +89 -0
- package/commands/task.ts +72 -167
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -706
- package/db/schema.test.ts +760 -0
- package/db/schema.ts +284 -130
- package/gates/validator.test.ts +675 -0
- package/gates/validator.ts +112 -27
- package/package.json +3 -1
- package/protocol/process-return.ts +25 -93
- package/protocol/subagent-protocol.test.ts +936 -0
- package/protocol/subagent-protocol.ts +19 -1
- package/workflow.ts +176 -67
package/commands/knowledge.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
|
-
import { initSchema, getRelatedDecisions, getRelatedFiles
|
|
2
|
+
import { initSchema, getRelatedDecisions, getRelatedFiles } from "../db/schema";
|
|
3
|
+
import { resolveSpec } from "./spec-resolver";
|
|
4
|
+
import { CodexaError, ValidationError } from "../errors";
|
|
3
5
|
|
|
4
6
|
type KnowledgeCategory = "discovery" | "decision" | "blocker" | "pattern" | "constraint";
|
|
5
7
|
type KnowledgeSeverity = "info" | "warning" | "critical";
|
|
@@ -9,13 +11,7 @@ interface AddKnowledgeOptions {
|
|
|
9
11
|
category: KnowledgeCategory;
|
|
10
12
|
severity?: KnowledgeSeverity;
|
|
11
13
|
broadcastTo?: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
function getActiveSpec(): any {
|
|
15
|
-
const db = getDb();
|
|
16
|
-
return db
|
|
17
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
18
|
-
.get();
|
|
14
|
+
specId?: string;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
function getCurrentTask(specId: string): any {
|
|
@@ -31,24 +27,16 @@ export function addKnowledge(options: AddKnowledgeOptions): void {
|
|
|
31
27
|
const db = getDb();
|
|
32
28
|
const now = new Date().toISOString();
|
|
33
29
|
|
|
34
|
-
const spec =
|
|
35
|
-
if (!spec) {
|
|
36
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
30
|
+
const spec = resolveSpec(options.specId);
|
|
39
31
|
|
|
40
32
|
const currentTask = getCurrentTask(spec.id);
|
|
41
33
|
if (!currentTask) {
|
|
42
|
-
|
|
43
|
-
console.error("Knowledge so pode ser adicionado durante execucao de uma task.\n");
|
|
44
|
-
process.exit(1);
|
|
34
|
+
throw new CodexaError("Nenhuma task em execucao.\nKnowledge so pode ser adicionado durante execucao de uma task.");
|
|
45
35
|
}
|
|
46
36
|
|
|
47
37
|
const validCategories: KnowledgeCategory[] = ["discovery", "decision", "blocker", "pattern", "constraint"];
|
|
48
38
|
if (!validCategories.includes(options.category)) {
|
|
49
|
-
|
|
50
|
-
console.error(`Validas: ${validCategories.join(", ")}\n`);
|
|
51
|
-
process.exit(1);
|
|
39
|
+
throw new ValidationError(`Categoria invalida: ${options.category}\nValidas: ${validCategories.join(", ")}`);
|
|
52
40
|
}
|
|
53
41
|
|
|
54
42
|
const severity = options.severity || "info";
|
|
@@ -79,20 +67,13 @@ export function listKnowledge(options: {
|
|
|
79
67
|
category?: string;
|
|
80
68
|
severity?: string; // v8.0: Filtro por severidade
|
|
81
69
|
json?: boolean;
|
|
70
|
+
specId?: string;
|
|
82
71
|
}): void {
|
|
83
72
|
initSchema();
|
|
84
73
|
|
|
85
74
|
const db = getDb();
|
|
86
75
|
|
|
87
|
-
const spec =
|
|
88
|
-
if (!spec) {
|
|
89
|
-
if (options.json) {
|
|
90
|
-
console.log(JSON.stringify({ knowledge: [], message: "Nenhuma feature ativa" }));
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
76
|
+
const spec = resolveSpec(options.specId);
|
|
96
77
|
|
|
97
78
|
let query = "SELECT * FROM knowledge WHERE spec_id = ?";
|
|
98
79
|
const params: any[] = [spec.id];
|
|
@@ -122,9 +103,10 @@ export function listKnowledge(options: {
|
|
|
122
103
|
const currentTask = getCurrentTask(spec.id);
|
|
123
104
|
if (currentTask) {
|
|
124
105
|
filtered = knowledge.filter((k) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
106
|
+
const isAcked = db.query(
|
|
107
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
|
|
108
|
+
).get(k.id, currentTask.id);
|
|
109
|
+
return !isAcked;
|
|
128
110
|
});
|
|
129
111
|
}
|
|
130
112
|
}
|
|
@@ -158,42 +140,29 @@ export function listKnowledge(options: {
|
|
|
158
140
|
}
|
|
159
141
|
}
|
|
160
142
|
|
|
161
|
-
export function acknowledgeKnowledge(knowledgeId: string): void {
|
|
143
|
+
export function acknowledgeKnowledge(knowledgeId: string, specId?: string): void {
|
|
162
144
|
initSchema();
|
|
163
145
|
|
|
164
146
|
const db = getDb();
|
|
165
147
|
|
|
166
|
-
const spec =
|
|
167
|
-
if (!spec) {
|
|
168
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
169
|
-
process.exit(1);
|
|
170
|
-
}
|
|
148
|
+
const spec = resolveSpec(specId);
|
|
171
149
|
|
|
172
150
|
const currentTask = getCurrentTask(spec.id);
|
|
173
151
|
if (!currentTask) {
|
|
174
|
-
|
|
175
|
-
process.exit(1);
|
|
152
|
+
throw new CodexaError("Nenhuma task em execucao.");
|
|
176
153
|
}
|
|
177
154
|
|
|
178
155
|
const kid = parseInt(knowledgeId);
|
|
179
156
|
const knowledge = db.query("SELECT * FROM knowledge WHERE id = ?").get(kid) as any;
|
|
180
157
|
|
|
181
158
|
if (!knowledge) {
|
|
182
|
-
|
|
183
|
-
process.exit(1);
|
|
159
|
+
throw new CodexaError(`Knowledge #${kid} nao encontrado.`);
|
|
184
160
|
}
|
|
185
161
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!acknowledged.includes(currentTask.id)) {
|
|
191
|
-
acknowledged.push(currentTask.id);
|
|
192
|
-
db.run("UPDATE knowledge SET acknowledged_by = ? WHERE id = ?", [
|
|
193
|
-
JSON.stringify(acknowledged),
|
|
194
|
-
kid,
|
|
195
|
-
]);
|
|
196
|
-
}
|
|
162
|
+
db.run(
|
|
163
|
+
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
164
|
+
[kid, currentTask.id]
|
|
165
|
+
);
|
|
197
166
|
|
|
198
167
|
console.log(`\nKnowledge #${kid} marcado como lido pela Task #${currentTask.number}.\n`);
|
|
199
168
|
}
|
|
@@ -221,11 +190,13 @@ export function getKnowledgeForTask(specId: string, taskId: number): any[] {
|
|
|
221
190
|
|
|
222
191
|
export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[] {
|
|
223
192
|
const all = getKnowledgeForTask(specId, taskId);
|
|
193
|
+
const db = getDb();
|
|
224
194
|
|
|
225
195
|
return all.filter((k) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
196
|
+
const isAcked = db.query(
|
|
197
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
|
|
198
|
+
).get(k.id, taskId);
|
|
199
|
+
return !isAcked;
|
|
229
200
|
});
|
|
230
201
|
}
|
|
231
202
|
|
|
@@ -233,40 +204,13 @@ export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[]
|
|
|
233
204
|
export function queryGraph(options: {
|
|
234
205
|
file?: string;
|
|
235
206
|
decision?: string;
|
|
236
|
-
contradictions?: boolean;
|
|
237
207
|
json?: boolean;
|
|
208
|
+
specId?: string;
|
|
238
209
|
}): void {
|
|
239
210
|
initSchema();
|
|
240
211
|
const db = getDb();
|
|
241
212
|
|
|
242
|
-
const spec =
|
|
243
|
-
if (!spec) {
|
|
244
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Buscar contradicoes
|
|
249
|
-
if (options.contradictions) {
|
|
250
|
-
const contradictions = findContradictions(spec.id);
|
|
251
|
-
|
|
252
|
-
if (options.json) {
|
|
253
|
-
console.log(JSON.stringify({ contradictions }));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (contradictions.length === 0) {
|
|
258
|
-
console.log("\nNenhuma contradicao detectada.\n");
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
console.log(`\nContradicoes detectadas (${contradictions.length}):`);
|
|
263
|
-
console.log(`${"ā".repeat(50)}`);
|
|
264
|
-
for (const c of contradictions) {
|
|
265
|
-
console.log(` [!] "${c.decision1}" <-> "${c.decision2}"`);
|
|
266
|
-
console.log(` Detectada em: ${c.createdAt}\n`);
|
|
267
|
-
}
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
213
|
+
const spec = resolveSpec(options.specId);
|
|
270
214
|
|
|
271
215
|
// Buscar relacoes para um arquivo
|
|
272
216
|
if (options.file) {
|
|
@@ -357,21 +301,175 @@ export function queryGraph(options: {
|
|
|
357
301
|
}
|
|
358
302
|
console.log(`\nComandos:`);
|
|
359
303
|
console.log(` knowledge graph --file <path> Relacoes de um arquivo`);
|
|
360
|
-
console.log(` knowledge graph --decision <id> Arquivos afetados por decisao`);
|
|
361
|
-
|
|
304
|
+
console.log(` knowledge graph --decision <id> Arquivos afetados por decisao\n`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// v9.2: Jaccard similarity for knowledge compaction
|
|
308
|
+
export function jaccardSimilarity(a: string, b: string): number {
|
|
309
|
+
const tokenize = (s: string): Set<string> => {
|
|
310
|
+
const words = s
|
|
311
|
+
.toLowerCase()
|
|
312
|
+
.replace(/[^\w\s-]/g, " ")
|
|
313
|
+
.split(/\s+/)
|
|
314
|
+
.filter((w) => w.length > 2);
|
|
315
|
+
return new Set(words);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const setA = tokenize(a);
|
|
319
|
+
const setB = tokenize(b);
|
|
320
|
+
|
|
321
|
+
if (setA.size === 0 && setB.size === 0) return 1;
|
|
322
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
323
|
+
|
|
324
|
+
let intersection = 0;
|
|
325
|
+
for (const word of setA) {
|
|
326
|
+
if (setB.has(word)) intersection++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const union = setA.size + setB.size - intersection;
|
|
330
|
+
return union === 0 ? 0 : intersection / union;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// v9.2: Knowledge compaction ā merge similar, archive old, archive completed
|
|
334
|
+
export function compactKnowledge(options: {
|
|
335
|
+
specId?: string;
|
|
336
|
+
dryRun?: boolean;
|
|
337
|
+
json?: boolean;
|
|
338
|
+
}): void {
|
|
339
|
+
initSchema();
|
|
340
|
+
const db = getDb();
|
|
341
|
+
|
|
342
|
+
let merged = 0;
|
|
343
|
+
let archivedOld = 0;
|
|
344
|
+
let archivedCompleted = 0;
|
|
345
|
+
|
|
346
|
+
// Phase 1: Merge similar entries (same category + spec_id, Jaccard >= 0.8)
|
|
347
|
+
const specFilter = options.specId
|
|
348
|
+
? "AND spec_id = ?"
|
|
349
|
+
: "";
|
|
350
|
+
const specParams = options.specId ? [options.specId] : [];
|
|
351
|
+
|
|
352
|
+
const entries = db
|
|
353
|
+
.query(
|
|
354
|
+
`SELECT * FROM knowledge WHERE severity != 'archived' ${specFilter} ORDER BY category, spec_id, created_at DESC`
|
|
355
|
+
)
|
|
356
|
+
.all(...specParams) as any[];
|
|
357
|
+
|
|
358
|
+
// Group by category + spec_id
|
|
359
|
+
const groups = new Map<string, any[]>();
|
|
360
|
+
for (const entry of entries) {
|
|
361
|
+
const key = `${entry.category}:${entry.spec_id}`;
|
|
362
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
363
|
+
groups.get(key)!.push(entry);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const toArchive = new Set<number>();
|
|
367
|
+
|
|
368
|
+
for (const [, group] of groups) {
|
|
369
|
+
for (let i = 0; i < group.length; i++) {
|
|
370
|
+
if (toArchive.has(group[i].id)) continue;
|
|
371
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
372
|
+
if (toArchive.has(group[j].id)) continue;
|
|
373
|
+
|
|
374
|
+
const sim = jaccardSimilarity(group[i].content, group[j].content);
|
|
375
|
+
if (sim >= 0.8) {
|
|
376
|
+
// Keep higher severity or newer; archive the other
|
|
377
|
+
const severityOrder: Record<string, number> = { critical: 3, warning: 2, info: 1, archived: 0 };
|
|
378
|
+
const keepI = (severityOrder[group[i].severity] || 0) >= (severityOrder[group[j].severity] || 0);
|
|
379
|
+
const archiveId = keepI ? group[j].id : group[i].id;
|
|
380
|
+
toArchive.add(archiveId);
|
|
381
|
+
merged++;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Phase 2: Archive old info entries (>7 days, no graph references)
|
|
388
|
+
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
389
|
+
const oldInfoEntries = db
|
|
390
|
+
.query(
|
|
391
|
+
`SELECT k.id FROM knowledge k
|
|
392
|
+
WHERE k.severity = 'info' AND k.created_at < ? ${specFilter}
|
|
393
|
+
AND NOT EXISTS (
|
|
394
|
+
SELECT 1 FROM knowledge_graph kg
|
|
395
|
+
WHERE kg.source_id = CAST(k.id AS TEXT) AND kg.source_type = 'knowledge'
|
|
396
|
+
)
|
|
397
|
+
AND NOT EXISTS (
|
|
398
|
+
SELECT 1 FROM knowledge_graph kg
|
|
399
|
+
WHERE kg.target_id = CAST(k.id AS TEXT) AND kg.target_type = 'knowledge'
|
|
400
|
+
)`
|
|
401
|
+
)
|
|
402
|
+
.all(sevenDaysAgo, ...specParams) as any[];
|
|
403
|
+
|
|
404
|
+
for (const entry of oldInfoEntries) {
|
|
405
|
+
if (!toArchive.has(entry.id)) {
|
|
406
|
+
toArchive.add(entry.id);
|
|
407
|
+
archivedOld++;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Phase 3: Archive all entries from completed/cancelled specs
|
|
412
|
+
const completedEntries = db
|
|
413
|
+
.query(
|
|
414
|
+
`SELECT k.id FROM knowledge k
|
|
415
|
+
JOIN specs s ON k.spec_id = s.id
|
|
416
|
+
WHERE s.phase IN ('completed', 'cancelled')
|
|
417
|
+
AND k.severity != 'archived'
|
|
418
|
+
${options.specId ? "AND k.spec_id = ?" : ""}`
|
|
419
|
+
)
|
|
420
|
+
.all(...specParams) as any[];
|
|
421
|
+
|
|
422
|
+
for (const entry of completedEntries) {
|
|
423
|
+
if (!toArchive.has(entry.id)) {
|
|
424
|
+
toArchive.add(entry.id);
|
|
425
|
+
archivedCompleted++;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Apply changes
|
|
430
|
+
if (!options.dryRun && toArchive.size > 0) {
|
|
431
|
+
const ids = Array.from(toArchive);
|
|
432
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
433
|
+
db.run(
|
|
434
|
+
`UPDATE knowledge SET severity = 'archived' WHERE id IN (${placeholders})`,
|
|
435
|
+
ids
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Output
|
|
440
|
+
const result = {
|
|
441
|
+
merged,
|
|
442
|
+
archivedOld,
|
|
443
|
+
archivedCompleted,
|
|
444
|
+
totalArchived: toArchive.size,
|
|
445
|
+
dryRun: !!options.dryRun,
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
if (options.json) {
|
|
449
|
+
console.log(JSON.stringify(result));
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const mode = options.dryRun ? "[DRY RUN] " : "";
|
|
454
|
+
console.log(`\n${mode}Knowledge Compaction:`);
|
|
455
|
+
console.log(`${"ā".repeat(50)}`);
|
|
456
|
+
console.log(` Similares mesclados: ${merged}`);
|
|
457
|
+
console.log(` Info antigos (>7d): ${archivedOld}`);
|
|
458
|
+
console.log(` Specs finalizados: ${archivedCompleted}`);
|
|
459
|
+
console.log(` Total arquivado: ${toArchive.size}`);
|
|
460
|
+
if (options.dryRun) {
|
|
461
|
+
console.log(`\nPara aplicar: knowledge compact${options.specId ? ` --spec ${options.specId}` : ""}`);
|
|
462
|
+
}
|
|
463
|
+
console.log();
|
|
362
464
|
}
|
|
363
465
|
|
|
364
466
|
// v9.0: Resolver/reconhecer knowledge critico (para desbloquear task start)
|
|
365
|
-
export function resolveKnowledge(ids: string, resolution?: string): void {
|
|
467
|
+
export function resolveKnowledge(ids: string, resolution?: string, specId?: string): void {
|
|
366
468
|
initSchema();
|
|
367
469
|
const db = getDb();
|
|
368
470
|
const now = new Date().toISOString();
|
|
369
471
|
|
|
370
|
-
const spec =
|
|
371
|
-
if (!spec) {
|
|
372
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
373
|
-
process.exit(1);
|
|
374
|
-
}
|
|
472
|
+
const spec = resolveSpec(specId);
|
|
375
473
|
|
|
376
474
|
const knowledgeIds = ids.split(",").map(s => parseInt(s.trim()));
|
|
377
475
|
|
|
@@ -383,18 +481,10 @@ export function resolveKnowledge(ids: string, resolution?: string): void {
|
|
|
383
481
|
continue;
|
|
384
482
|
}
|
|
385
483
|
|
|
386
|
-
const acknowledged = knowledge.acknowledged_by
|
|
387
|
-
? (JSON.parse(knowledge.acknowledged_by) as number[])
|
|
388
|
-
: [];
|
|
389
|
-
|
|
390
484
|
// Usar -1 como marker de "resolvido pelo orquestrador"
|
|
391
|
-
if (!acknowledged.includes(-1)) {
|
|
392
|
-
acknowledged.push(-1);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
485
|
db.run(
|
|
396
|
-
"
|
|
397
|
-
[
|
|
486
|
+
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
487
|
+
[kid, -1]
|
|
398
488
|
);
|
|
399
489
|
|
|
400
490
|
console.log(`Knowledge #${kid} resolvido.`);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, afterAll } from "bun:test";
|
|
2
|
+
import { extractUtilitiesFromFile, inferScopeFromPath } from "./patterns";
|
|
3
|
+
import { writeFileSync, mkdirSync, rmSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
const TMP_DIR = join(import.meta.dir, "__test_tmp__");
|
|
7
|
+
|
|
8
|
+
function setupTmpDir() {
|
|
9
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function cleanTmpDir() {
|
|
13
|
+
try { rmSync(TMP_DIR, { recursive: true }); } catch {}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeTmpFile(name: string, content: string): string {
|
|
17
|
+
setupTmpDir();
|
|
18
|
+
const path = join(TMP_DIR, name);
|
|
19
|
+
writeFileSync(path, content);
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("extractUtilitiesFromFile", () => {
|
|
24
|
+
afterAll(() => cleanTmpDir());
|
|
25
|
+
|
|
26
|
+
it("extracts exported function", () => {
|
|
27
|
+
const path = writeTmpFile("func.ts", 'export function createUser(name: string): User {\n return { name };\n}');
|
|
28
|
+
const utils = extractUtilitiesFromFile(path);
|
|
29
|
+
expect(utils).toHaveLength(1);
|
|
30
|
+
expect(utils[0].name).toBe("createUser");
|
|
31
|
+
expect(utils[0].type).toBe("function");
|
|
32
|
+
expect(utils[0].signature).toContain("(name: string)");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("extracts exported async function", () => {
|
|
36
|
+
const path = writeTmpFile("async.ts", 'export async function fetchData(url: string): Promise<Data> {\n return await fetch(url);\n}');
|
|
37
|
+
const utils = extractUtilitiesFromFile(path);
|
|
38
|
+
expect(utils).toHaveLength(1);
|
|
39
|
+
expect(utils[0].name).toBe("fetchData");
|
|
40
|
+
expect(utils[0].type).toBe("function");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("extracts exported const", () => {
|
|
44
|
+
const path = writeTmpFile("const.ts", 'export const MAX_RETRIES: number = 3;');
|
|
45
|
+
const utils = extractUtilitiesFromFile(path);
|
|
46
|
+
expect(utils).toHaveLength(1);
|
|
47
|
+
expect(utils[0].name).toBe("MAX_RETRIES");
|
|
48
|
+
expect(utils[0].type).toBe("const");
|
|
49
|
+
expect(utils[0].signature).toBe("number");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("extracts exported const without type annotation", () => {
|
|
53
|
+
const path = writeTmpFile("const2.ts", 'export const config = { port: 3000 };');
|
|
54
|
+
const utils = extractUtilitiesFromFile(path);
|
|
55
|
+
expect(utils).toHaveLength(1);
|
|
56
|
+
expect(utils[0].name).toBe("config");
|
|
57
|
+
expect(utils[0].type).toBe("const");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("extracts exported class", () => {
|
|
61
|
+
const path = writeTmpFile("class.ts", 'export class UserService {\n constructor() {}\n}');
|
|
62
|
+
const utils = extractUtilitiesFromFile(path);
|
|
63
|
+
expect(utils).toHaveLength(1);
|
|
64
|
+
expect(utils[0].name).toBe("UserService");
|
|
65
|
+
expect(utils[0].type).toBe("class");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("extracts exported interface", () => {
|
|
69
|
+
const path = writeTmpFile("iface.ts", 'export interface UserProps {\n name: string;\n}');
|
|
70
|
+
const utils = extractUtilitiesFromFile(path);
|
|
71
|
+
expect(utils).toHaveLength(1);
|
|
72
|
+
expect(utils[0].name).toBe("UserProps");
|
|
73
|
+
expect(utils[0].type).toBe("interface");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("extracts exported type", () => {
|
|
77
|
+
const path = writeTmpFile("type.ts", 'export type Status = "active" | "inactive";');
|
|
78
|
+
const utils = extractUtilitiesFromFile(path);
|
|
79
|
+
expect(utils).toHaveLength(1);
|
|
80
|
+
expect(utils[0].name).toBe("Status");
|
|
81
|
+
expect(utils[0].type).toBe("type");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("extracts multiple exports from one file", () => {
|
|
85
|
+
const path = writeTmpFile("multi.ts", [
|
|
86
|
+
'export function foo() {}',
|
|
87
|
+
'export const BAR = 1;',
|
|
88
|
+
'export class Baz {}',
|
|
89
|
+
'export interface Qux {}',
|
|
90
|
+
'export type Quux = string;',
|
|
91
|
+
].join("\n"));
|
|
92
|
+
const utils = extractUtilitiesFromFile(path);
|
|
93
|
+
expect(utils).toHaveLength(5);
|
|
94
|
+
expect(utils.map(u => u.name)).toEqual(["foo", "BAR", "Baz", "Qux", "Quux"]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("returns empty array for non-existent file", () => {
|
|
98
|
+
const utils = extractUtilitiesFromFile("/nonexistent/path.ts");
|
|
99
|
+
expect(utils).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("returns empty array for file without exports", () => {
|
|
103
|
+
const path = writeTmpFile("noexport.ts", 'const internal = 1;\nfunction helper() {}');
|
|
104
|
+
const utils = extractUtilitiesFromFile(path);
|
|
105
|
+
expect(utils).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("ignores non-export declarations", () => {
|
|
109
|
+
const path = writeTmpFile("mixed.ts", [
|
|
110
|
+
'const private1 = 1;',
|
|
111
|
+
'function private2() {}',
|
|
112
|
+
'export function public1() {}',
|
|
113
|
+
].join("\n"));
|
|
114
|
+
const utils = extractUtilitiesFromFile(path);
|
|
115
|
+
expect(utils).toHaveLength(1);
|
|
116
|
+
expect(utils[0].name).toBe("public1");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("inferScopeFromPath", () => {
|
|
121
|
+
it("identifies backend paths", () => {
|
|
122
|
+
expect(inferScopeFromPath("src/app/api/users/route.ts")).toBe("backend");
|
|
123
|
+
expect(inferScopeFromPath("src/server/index.ts")).toBe("backend");
|
|
124
|
+
expect(inferScopeFromPath("src/backend/auth.ts")).toBe("backend");
|
|
125
|
+
expect(inferScopeFromPath("src/services/user.ts")).toBe("backend");
|
|
126
|
+
expect(inferScopeFromPath("src/actions/submit.ts")).toBe("backend");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("identifies frontend paths", () => {
|
|
130
|
+
expect(inferScopeFromPath("src/components/Button.tsx")).toBe("frontend");
|
|
131
|
+
expect(inferScopeFromPath("src/hooks/useAuth.ts")).toBe("frontend");
|
|
132
|
+
expect(inferScopeFromPath("src/pages/index.tsx")).toBe("frontend");
|
|
133
|
+
expect(inferScopeFromPath("src/frontend/App.tsx")).toBe("frontend");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("identifies frontend for /app/ paths without /api/", () => {
|
|
137
|
+
expect(inferScopeFromPath("src/app/dashboard/page.tsx")).toBe("frontend");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("identifies database paths", () => {
|
|
141
|
+
expect(inferScopeFromPath("src/db/schema.ts")).toBe("database");
|
|
142
|
+
expect(inferScopeFromPath("src/schema/users.ts")).toBe("database");
|
|
143
|
+
expect(inferScopeFromPath("src/migrations/001.sql")).toBe("database");
|
|
144
|
+
expect(inferScopeFromPath("src/database/connection.ts")).toBe("database");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("identifies testing paths", () => {
|
|
148
|
+
expect(inferScopeFromPath("src/tests/user.test.ts")).toBe("testing");
|
|
149
|
+
// Note: /components/ matches frontend before .test. matches testing
|
|
150
|
+
expect(inferScopeFromPath("src/components/Button.test.tsx")).toBe("frontend");
|
|
151
|
+
expect(inferScopeFromPath("src/lib/auth.spec.ts")).toBe("testing");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns shared for unrecognized paths", () => {
|
|
155
|
+
expect(inferScopeFromPath("src/lib/utils.ts")).toBe("shared");
|
|
156
|
+
expect(inferScopeFromPath("src/config/env.ts")).toBe("shared");
|
|
157
|
+
expect(inferScopeFromPath("package.json")).toBe("shared");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("handles Windows-style paths (backslashes)", () => {
|
|
161
|
+
expect(inferScopeFromPath("src\\components\\Button.tsx")).toBe("frontend");
|
|
162
|
+
expect(inferScopeFromPath("src\\db\\schema.ts")).toBe("database");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("is case-insensitive", () => {
|
|
166
|
+
expect(inferScopeFromPath("src/Components/Button.tsx")).toBe("frontend");
|
|
167
|
+
expect(inferScopeFromPath("src/DB/Schema.ts")).toBe("database");
|
|
168
|
+
});
|
|
169
|
+
});
|
package/commands/patterns.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { execSync, spawnSync } from "child_process";
|
|
9
9
|
import { readFileSync, existsSync } from "fs";
|
|
10
10
|
import { extname, basename, dirname } from "path";
|
|
11
|
+
import { CodexaError, ValidationError } from "../errors";
|
|
11
12
|
import { getDb } from "../db/connection";
|
|
12
13
|
import { initSchema } from "../db/schema";
|
|
13
14
|
|
|
@@ -601,10 +602,7 @@ export function patternsExtract(options: ExtractOptions): void {
|
|
|
601
602
|
|
|
602
603
|
// Verificar grepai
|
|
603
604
|
if (!isGrepaiAvailable()) {
|
|
604
|
-
|
|
605
|
-
console.error("Instale com: go install github.com/your-org/grepai@latest");
|
|
606
|
-
console.error("Ou configure no PATH.\n");
|
|
607
|
-
process.exit(1);
|
|
605
|
+
throw new CodexaError("grepai nao encontrado.\nInstale com: go install github.com/your-org/grepai@latest\nOu configure no PATH.");
|
|
608
606
|
}
|
|
609
607
|
|
|
610
608
|
const db = getDb();
|
|
@@ -617,9 +615,7 @@ export function patternsExtract(options: ExtractOptions): void {
|
|
|
617
615
|
}
|
|
618
616
|
|
|
619
617
|
if (queries.length === 0) {
|
|
620
|
-
|
|
621
|
-
console.error("Escopos validos: frontend, backend, database, testing\n");
|
|
622
|
-
process.exit(1);
|
|
618
|
+
throw new ValidationError("Escopo '" + options.scope + "' nao reconhecido.\nEscopos validos: frontend, backend, database, testing");
|
|
623
619
|
}
|
|
624
620
|
|
|
625
621
|
console.log(`\nš Extraindo patterns via grepai...`);
|
|
@@ -737,15 +733,13 @@ export function patternsExtract(options: ExtractOptions): void {
|
|
|
737
733
|
|
|
738
734
|
export function patternsAnalyze(filePath: string, json: boolean = false): void {
|
|
739
735
|
if (!existsSync(filePath)) {
|
|
740
|
-
|
|
741
|
-
process.exit(1);
|
|
736
|
+
throw new CodexaError("Arquivo nao encontrado: " + filePath);
|
|
742
737
|
}
|
|
743
738
|
|
|
744
739
|
const analysis = analyzeFile(filePath);
|
|
745
740
|
|
|
746
741
|
if (!analysis) {
|
|
747
|
-
|
|
748
|
-
process.exit(1);
|
|
742
|
+
throw new CodexaError("Nao foi possivel analisar o arquivo.");
|
|
749
743
|
}
|
|
750
744
|
|
|
751
745
|
if (json) {
|
|
@@ -855,8 +849,7 @@ export function patternsAnalyzeDeep(files: string[], json: boolean = false): voi
|
|
|
855
849
|
}
|
|
856
850
|
|
|
857
851
|
if (analyses.length === 0) {
|
|
858
|
-
|
|
859
|
-
process.exit(1);
|
|
852
|
+
throw new CodexaError("Nenhum arquivo analisado.");
|
|
860
853
|
}
|
|
861
854
|
|
|
862
855
|
const summary = {
|