@codexa/cli 9.0.3 → 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.ts +7 -6
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -0
- package/commands/decide.ts +9 -21
- package/commands/discover.ts +11 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +190 -75
- package/commands/patterns.ts +6 -13
- package/commands/plan.ts +14 -64
- 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 +2 -3
- package/commands/task.ts +30 -9
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -708
- package/db/schema.test.ts +475 -48
- package/db/schema.ts +136 -12
- package/gates/validator.test.ts +58 -0
- package/gates/validator.ts +83 -30
- package/package.json +1 -1
- package/workflow.ts +102 -51
package/commands/knowledge.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
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
|
|
|
@@ -234,15 +205,12 @@ export function queryGraph(options: {
|
|
|
234
205
|
file?: string;
|
|
235
206
|
decision?: string;
|
|
236
207
|
json?: boolean;
|
|
208
|
+
specId?: string;
|
|
237
209
|
}): void {
|
|
238
210
|
initSchema();
|
|
239
211
|
const db = getDb();
|
|
240
212
|
|
|
241
|
-
const spec =
|
|
242
|
-
if (!spec) {
|
|
243
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
244
|
-
process.exit(1);
|
|
245
|
-
}
|
|
213
|
+
const spec = resolveSpec(options.specId);
|
|
246
214
|
|
|
247
215
|
// Buscar relacoes para um arquivo
|
|
248
216
|
if (options.file) {
|
|
@@ -336,17 +304,172 @@ export function queryGraph(options: {
|
|
|
336
304
|
console.log(` knowledge graph --decision <id> Arquivos afetados por decisao\n`);
|
|
337
305
|
}
|
|
338
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();
|
|
464
|
+
}
|
|
465
|
+
|
|
339
466
|
// v9.0: Resolver/reconhecer knowledge critico (para desbloquear task start)
|
|
340
|
-
export function resolveKnowledge(ids: string, resolution?: string): void {
|
|
467
|
+
export function resolveKnowledge(ids: string, resolution?: string, specId?: string): void {
|
|
341
468
|
initSchema();
|
|
342
469
|
const db = getDb();
|
|
343
470
|
const now = new Date().toISOString();
|
|
344
471
|
|
|
345
|
-
const spec =
|
|
346
|
-
if (!spec) {
|
|
347
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
348
|
-
process.exit(1);
|
|
349
|
-
}
|
|
472
|
+
const spec = resolveSpec(specId);
|
|
350
473
|
|
|
351
474
|
const knowledgeIds = ids.split(",").map(s => parseInt(s.trim()));
|
|
352
475
|
|
|
@@ -358,18 +481,10 @@ export function resolveKnowledge(ids: string, resolution?: string): void {
|
|
|
358
481
|
continue;
|
|
359
482
|
}
|
|
360
483
|
|
|
361
|
-
const acknowledged = knowledge.acknowledged_by
|
|
362
|
-
? (JSON.parse(knowledge.acknowledged_by) as number[])
|
|
363
|
-
: [];
|
|
364
|
-
|
|
365
484
|
// Usar -1 como marker de "resolvido pelo orquestrador"
|
|
366
|
-
if (!acknowledged.includes(-1)) {
|
|
367
|
-
acknowledged.push(-1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
485
|
db.run(
|
|
371
|
-
"
|
|
372
|
-
[
|
|
486
|
+
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
487
|
+
[kid, -1]
|
|
373
488
|
);
|
|
374
489
|
|
|
375
490
|
console.log(`Knowledge #${kid} resolvido.`);
|
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 = {
|
package/commands/plan.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
|
|
3
|
+
import { resolveSpec } from "./spec-resolver";
|
|
4
|
+
import { CodexaError, ValidationError } from "../errors";
|
|
3
5
|
|
|
4
6
|
export function generateSpecId(name: string): string {
|
|
5
7
|
const date = new Date().toISOString().split("T")[0];
|
|
@@ -17,28 +19,6 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
17
19
|
initSchema();
|
|
18
20
|
const db = getDb();
|
|
19
21
|
|
|
20
|
-
// Verificar se ja existe spec ativo (ignorar completed e cancelled)
|
|
21
|
-
const existing = db
|
|
22
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
23
|
-
.get() as any;
|
|
24
|
-
|
|
25
|
-
if (existing) {
|
|
26
|
-
if (options.json) {
|
|
27
|
-
console.log(JSON.stringify({
|
|
28
|
-
error: "FEATURE_ACTIVE",
|
|
29
|
-
name: existing.name,
|
|
30
|
-
id: existing.id,
|
|
31
|
-
phase: existing.phase,
|
|
32
|
-
}));
|
|
33
|
-
} else {
|
|
34
|
-
console.error(`\nJa existe uma feature ativa: ${existing.name} (${existing.id})`);
|
|
35
|
-
console.error(`Fase atual: ${existing.phase}`);
|
|
36
|
-
console.error(`\nPara continuar, use: status`);
|
|
37
|
-
console.error(`Para cancelar a atual, use: plan cancel\n`);
|
|
38
|
-
}
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
22
|
// v8.4: Buscar analise arquitetural se --from-analysis fornecido
|
|
43
23
|
let analysis: any = null;
|
|
44
24
|
if (options.fromAnalysis) {
|
|
@@ -52,7 +32,7 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
52
32
|
} else {
|
|
53
33
|
console.error(`\n[ERRO] Analise arquitetural '${options.fromAnalysis}' nao encontrada.\n`);
|
|
54
34
|
}
|
|
55
|
-
|
|
35
|
+
throw new CodexaError(`Analise arquitetural '${options.fromAnalysis}' nao encontrada.`);
|
|
56
36
|
}
|
|
57
37
|
|
|
58
38
|
if (analysis.status !== "approved") {
|
|
@@ -62,7 +42,7 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
62
42
|
console.error(`\n[ERRO] Analise '${analysis.id}' nao esta aprovada (status: ${analysis.status}).`);
|
|
63
43
|
console.error("Use 'architect approve' primeiro.\n");
|
|
64
44
|
}
|
|
65
|
-
|
|
45
|
+
throw new CodexaError(`Analise '${analysis.id}' nao esta aprovada (status: ${analysis.status}). Use 'architect approve' primeiro.`);
|
|
66
46
|
}
|
|
67
47
|
} else {
|
|
68
48
|
// v8.4: Auto-deteccao por nome
|
|
@@ -161,19 +141,11 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
161
141
|
}
|
|
162
142
|
}
|
|
163
143
|
|
|
164
|
-
export function planShow(json: boolean = false): void {
|
|
144
|
+
export function planShow(json: boolean = false, specId?: string): void {
|
|
165
145
|
initSchema();
|
|
166
146
|
const db = getDb();
|
|
167
147
|
|
|
168
|
-
const spec =
|
|
169
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
170
|
-
.get() as any;
|
|
171
|
-
|
|
172
|
-
if (!spec) {
|
|
173
|
-
console.error("\nNenhuma feature ativa.");
|
|
174
|
-
console.error("Inicie com: /codexa:feature'\n");
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
148
|
+
const spec = resolveSpec(specId);
|
|
177
149
|
|
|
178
150
|
const tasks = db
|
|
179
151
|
.query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
|
|
@@ -227,25 +199,12 @@ export function planTaskAdd(options: {
|
|
|
227
199
|
depends?: string;
|
|
228
200
|
files?: string;
|
|
229
201
|
sequential?: boolean;
|
|
202
|
+
specId?: string;
|
|
230
203
|
}): void {
|
|
231
204
|
initSchema();
|
|
232
205
|
const db = getDb();
|
|
233
206
|
|
|
234
|
-
const spec =
|
|
235
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
236
|
-
.get() as any;
|
|
237
|
-
|
|
238
|
-
if (!spec) {
|
|
239
|
-
console.error("\nNenhuma feature ativa.");
|
|
240
|
-
console.error("Inicie com: /codexa:feature\n");
|
|
241
|
-
process.exit(1);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
if (spec.phase !== "planning") {
|
|
245
|
-
console.error(`\nNao e possivel adicionar tasks na fase '${spec.phase}'.`);
|
|
246
|
-
console.error("Tasks so podem ser adicionadas na fase 'planning'.\n");
|
|
247
|
-
process.exit(1);
|
|
248
|
-
}
|
|
207
|
+
const spec = resolveSpec(options.specId, ["planning"]);
|
|
249
208
|
|
|
250
209
|
// Pegar proximo numero
|
|
251
210
|
const lastTask = db
|
|
@@ -262,8 +221,7 @@ export function planTaskAdd(options: {
|
|
|
262
221
|
.query("SELECT id FROM tasks WHERE spec_id = ? AND number = ?")
|
|
263
222
|
.get(spec.id, depId);
|
|
264
223
|
if (!exists) {
|
|
265
|
-
|
|
266
|
-
process.exit(2);
|
|
224
|
+
throw new ValidationError(`Dependencia invalida: task #${depId} nao existe.`);
|
|
267
225
|
}
|
|
268
226
|
}
|
|
269
227
|
|
|
@@ -302,10 +260,9 @@ export function planTaskAdd(options: {
|
|
|
302
260
|
}
|
|
303
261
|
|
|
304
262
|
if (hasCycle(nextNumber)) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
process.exit(2);
|
|
263
|
+
throw new ValidationError(
|
|
264
|
+
`Dependencia circular detectada! Task #${nextNumber} -> [${dependsOn.join(", ")}] cria um ciclo.\nCorrija as dependencias para evitar deadlocks.`
|
|
265
|
+
);
|
|
309
266
|
}
|
|
310
267
|
}
|
|
311
268
|
|
|
@@ -344,18 +301,11 @@ export function planTaskAdd(options: {
|
|
|
344
301
|
console.log(` Paralelizavel: ${options.sequential ? "Nao" : "Sim"}\n`);
|
|
345
302
|
}
|
|
346
303
|
|
|
347
|
-
export function planCancel(): void {
|
|
304
|
+
export function planCancel(specId?: string): void {
|
|
348
305
|
initSchema();
|
|
349
306
|
const db = getDb();
|
|
350
307
|
|
|
351
|
-
const spec =
|
|
352
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
353
|
-
.get() as any;
|
|
354
|
-
|
|
355
|
-
if (!spec) {
|
|
356
|
-
console.error("\nNenhuma feature ativa para cancelar.\n");
|
|
357
|
-
process.exit(1);
|
|
358
|
-
}
|
|
308
|
+
const spec = resolveSpec(specId);
|
|
359
309
|
|
|
360
310
|
const now = new Date().toISOString();
|
|
361
311
|
|
package/commands/product.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
|
|
|
2
2
|
import { initSchema } from "../db/schema";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
import { CodexaError } from "../errors";
|
|
5
6
|
|
|
6
7
|
interface ProductContext {
|
|
7
8
|
name: string;
|
|
@@ -93,15 +94,13 @@ export function productImport(options: { file?: string; content?: string }): voi
|
|
|
93
94
|
|
|
94
95
|
if (options.file) {
|
|
95
96
|
if (!existsSync(options.file)) {
|
|
96
|
-
|
|
97
|
-
process.exit(1);
|
|
97
|
+
throw new CodexaError("Arquivo nao encontrado: " + options.file);
|
|
98
98
|
}
|
|
99
99
|
prdContent = readFileSync(options.file, "utf-8");
|
|
100
100
|
} else if (options.content) {
|
|
101
101
|
prdContent = options.content;
|
|
102
102
|
} else {
|
|
103
|
-
|
|
104
|
-
process.exit(1);
|
|
103
|
+
throw new CodexaError("Forneca --file ou --content");
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
// Salvar como pendente para o agente processar
|
|
@@ -286,21 +285,15 @@ export function productConfirm(): void {
|
|
|
286
285
|
|
|
287
286
|
const pending = db.query("SELECT * FROM product_context WHERE id = 'pending'").get() as any;
|
|
288
287
|
if (!pending) {
|
|
289
|
-
|
|
290
|
-
console.error("Execute: product guide ou product import primeiro\n");
|
|
291
|
-
process.exit(1);
|
|
288
|
+
throw new CodexaError("Nenhum contexto de produto pendente.\nExecute: product guide ou product import primeiro");
|
|
292
289
|
}
|
|
293
290
|
|
|
294
291
|
// Validar campos obrigatorios
|
|
295
292
|
if (!pending.name || pending.name === "Pendente") {
|
|
296
|
-
|
|
297
|
-
console.error("Use: product set --name \"Nome do Produto\"\n");
|
|
298
|
-
process.exit(1);
|
|
293
|
+
throw new CodexaError("Campo obrigatorio ausente: name\nUse: product set --name \"Nome do Produto\"");
|
|
299
294
|
}
|
|
300
295
|
if (!pending.problem || pending.problem === "") {
|
|
301
|
-
|
|
302
|
-
console.error("Use: product set --problem \"Problema que resolve\"\n");
|
|
303
|
-
process.exit(1);
|
|
296
|
+
throw new CodexaError("Campo obrigatorio ausente: problem\nUse: product set --problem \"Problema que resolve\"");
|
|
304
297
|
}
|
|
305
298
|
|
|
306
299
|
const now = new Date().toISOString();
|
|
@@ -358,12 +351,10 @@ export function productShow(options: { json?: boolean; pending?: boolean } = {})
|
|
|
358
351
|
|
|
359
352
|
if (!product) {
|
|
360
353
|
if (options.pending) {
|
|
361
|
-
|
|
354
|
+
throw new CodexaError("Nenhum contexto pendente.");
|
|
362
355
|
} else {
|
|
363
|
-
|
|
364
|
-
console.error("Execute: product guide ou product import\n");
|
|
356
|
+
throw new CodexaError("Contexto de produto nao definido.\nExecute: product guide ou product import");
|
|
365
357
|
}
|
|
366
|
-
process.exit(1);
|
|
367
358
|
}
|
|
368
359
|
|
|
369
360
|
const goals = db.query(`SELECT * FROM product_goals WHERE product_id = ? ORDER BY priority, category`).all(id) as any[];
|
package/commands/research.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { getDb } from "../db/connection";
|
|
|
2
2
|
import { initSchema } from "../db/schema";
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
4
4
|
import { join } from "path";
|
|
5
|
+
import { CodexaError } from "../errors";
|
|
5
6
|
import { getDetectors } from "../detectors/loader";
|
|
6
7
|
|
|
7
8
|
// ============================================================
|
|
@@ -359,7 +360,7 @@ export async function researchStart(options: { json?: boolean } = {}): Promise<v
|
|
|
359
360
|
console.error("Certifique-se de estar na raiz do projeto.");
|
|
360
361
|
console.error("Ecossistemas suportados: Node.js, Python, Go, .NET, Rust, Java/Kotlin, Flutter\n");
|
|
361
362
|
}
|
|
362
|
-
|
|
363
|
+
throw new CodexaError("Nenhum ecossistema ou biblioteca detectada.\nCertifique-se de estar na raiz do projeto.");
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
const libContextDir = ensureLibContextDir();
|
|
@@ -517,7 +518,7 @@ export function researchShow(options: { json?: boolean; lib?: string } = {}): vo
|
|
|
517
518
|
console.error("\nNenhuma biblioteca registrada.");
|
|
518
519
|
console.error("Execute: research start\n");
|
|
519
520
|
}
|
|
520
|
-
|
|
521
|
+
throw new CodexaError("Nenhuma biblioteca registrada.\nExecute: research start");
|
|
521
522
|
}
|
|
522
523
|
|
|
523
524
|
if (options.json) {
|
|
@@ -590,7 +591,7 @@ export function researchFill(libName: string, options: { json?: boolean } = {}):
|
|
|
590
591
|
console.error(`\nBiblioteca '${libName}' nao encontrada.`);
|
|
591
592
|
console.error("Execute: research show para ver bibliotecas disponiveis.\n");
|
|
592
593
|
}
|
|
593
|
-
|
|
594
|
+
throw new CodexaError("Biblioteca '" + libName + "' nao encontrada.\nExecute: research show para ver bibliotecas disponiveis.");
|
|
594
595
|
}
|
|
595
596
|
|
|
596
597
|
const versionStr = lib.version && lib.version !== "latest" ? lib.version : "latest";
|