@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
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { generateSpecId } from "./plan";
|
|
3
|
+
|
|
4
|
+
describe("generateSpecId", () => {
|
|
5
|
+
it("generates ID with date-slug-hash format", () => {
|
|
6
|
+
const id = generateSpecId("Add user authentication");
|
|
7
|
+
// Format: YYYY-MM-DD-slug-hash
|
|
8
|
+
expect(id).toMatch(/^\d{4}-\d{2}-\d{2}-[a-z0-9-]+-[a-z0-9]{4}$/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("normalizes slug to lowercase", () => {
|
|
12
|
+
const id = generateSpecId("Add User Auth");
|
|
13
|
+
const parts = id.split("-");
|
|
14
|
+
// After date (3 parts), all slug parts should be lowercase
|
|
15
|
+
const slugAndHash = parts.slice(3).join("-");
|
|
16
|
+
expect(slugAndHash).toBe(slugAndHash.toLowerCase());
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("removes special characters from slug", () => {
|
|
20
|
+
const id = generateSpecId("Add auth (v2) @special!");
|
|
21
|
+
// Should not contain (, ), @, !
|
|
22
|
+
expect(id).not.toMatch(/[()@!]/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("truncates slug at 30 characters", () => {
|
|
26
|
+
const longName = "This is a very long feature name that exceeds thirty characters by a lot";
|
|
27
|
+
const id = generateSpecId(longName);
|
|
28
|
+
// Date is YYYY-MM-DD (10 chars) + dash + slug (max 30) + dash + hash (4)
|
|
29
|
+
const parts = id.split("-");
|
|
30
|
+
// date = parts[0]-parts[1]-parts[2], hash = last part
|
|
31
|
+
const datePart = parts.slice(0, 3).join("-"); // YYYY-MM-DD
|
|
32
|
+
const hashPart = parts[parts.length - 1]; // 4 char hash
|
|
33
|
+
const slugPart = parts.slice(3, -1).join("-"); // everything between date and hash
|
|
34
|
+
expect(slugPart.length).toBeLessThanOrEqual(30);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("includes a 4-character hash suffix", () => {
|
|
38
|
+
const id = generateSpecId("Test feature");
|
|
39
|
+
const parts = id.split("-");
|
|
40
|
+
const hash = parts[parts.length - 1];
|
|
41
|
+
expect(hash.length).toBe(4);
|
|
42
|
+
// Hash should be base36
|
|
43
|
+
expect(hash).toMatch(/^[a-z0-9]+$/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("starts with today's date", () => {
|
|
47
|
+
const id = generateSpecId("Some feature");
|
|
48
|
+
const today = new Date().toISOString().split("T")[0];
|
|
49
|
+
expect(id.startsWith(today)).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("generates different IDs for different names", () => {
|
|
53
|
+
const id1 = generateSpecId("Feature A");
|
|
54
|
+
const id2 = generateSpecId("Feature B");
|
|
55
|
+
expect(id1).not.toBe(id2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("removes trailing dashes from slug", () => {
|
|
59
|
+
const id = generateSpecId("test-feature-");
|
|
60
|
+
// Should not have double dashes or trailing dash before hash
|
|
61
|
+
expect(id).not.toMatch(/--/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("handles empty-ish name gracefully", () => {
|
|
65
|
+
const id = generateSpecId("a");
|
|
66
|
+
expect(id).toMatch(/^\d{4}-\d{2}-\d{2}-a-[a-z0-9]{4}$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("converts spaces and special chars to dashes", () => {
|
|
70
|
+
const id = generateSpecId("add user auth");
|
|
71
|
+
expect(id).toContain("add-user-auth");
|
|
72
|
+
});
|
|
73
|
+
});
|
package/commands/plan.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
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
|
-
function generateSpecId(name: string): string {
|
|
6
|
+
export function generateSpecId(name: string): string {
|
|
5
7
|
const date = new Date().toISOString().split("T")[0];
|
|
6
8
|
const slug = name
|
|
7
9
|
.toLowerCase()
|
|
8
10
|
.replace(/[^a-z0-9]+/g, "-")
|
|
11
|
+
.replace(/-+$/, "")
|
|
9
12
|
.substring(0, 30);
|
|
10
|
-
|
|
13
|
+
const hash = Bun.hash(name + Date.now()).toString(36).substring(0, 4);
|
|
14
|
+
return `${date}-${slug}-${hash}`;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
// v8.4: Suporte a --from-analysis para import automatico de baby steps
|
|
@@ -15,28 +19,6 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
15
19
|
initSchema();
|
|
16
20
|
const db = getDb();
|
|
17
21
|
|
|
18
|
-
// Verificar se ja existe spec ativo (ignorar completed e cancelled)
|
|
19
|
-
const existing = db
|
|
20
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
21
|
-
.get() as any;
|
|
22
|
-
|
|
23
|
-
if (existing) {
|
|
24
|
-
if (options.json) {
|
|
25
|
-
console.log(JSON.stringify({
|
|
26
|
-
error: "FEATURE_ACTIVE",
|
|
27
|
-
name: existing.name,
|
|
28
|
-
id: existing.id,
|
|
29
|
-
phase: existing.phase,
|
|
30
|
-
}));
|
|
31
|
-
} else {
|
|
32
|
-
console.error(`\nJa existe uma feature ativa: ${existing.name} (${existing.id})`);
|
|
33
|
-
console.error(`Fase atual: ${existing.phase}`);
|
|
34
|
-
console.error(`\nPara continuar, use: status`);
|
|
35
|
-
console.error(`Para cancelar a atual, use: plan cancel\n`);
|
|
36
|
-
}
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
22
|
// v8.4: Buscar analise arquitetural se --from-analysis fornecido
|
|
41
23
|
let analysis: any = null;
|
|
42
24
|
if (options.fromAnalysis) {
|
|
@@ -50,7 +32,7 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
50
32
|
} else {
|
|
51
33
|
console.error(`\n[ERRO] Analise arquitetural '${options.fromAnalysis}' nao encontrada.\n`);
|
|
52
34
|
}
|
|
53
|
-
|
|
35
|
+
throw new CodexaError(`Analise arquitetural '${options.fromAnalysis}' nao encontrada.`);
|
|
54
36
|
}
|
|
55
37
|
|
|
56
38
|
if (analysis.status !== "approved") {
|
|
@@ -60,7 +42,7 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
60
42
|
console.error(`\n[ERRO] Analise '${analysis.id}' nao esta aprovada (status: ${analysis.status}).`);
|
|
61
43
|
console.error("Use 'architect approve' primeiro.\n");
|
|
62
44
|
}
|
|
63
|
-
|
|
45
|
+
throw new CodexaError(`Analise '${analysis.id}' nao esta aprovada (status: ${analysis.status}). Use 'architect approve' primeiro.`);
|
|
64
46
|
}
|
|
65
47
|
} else {
|
|
66
48
|
// v8.4: Auto-deteccao por nome
|
|
@@ -159,19 +141,11 @@ export function planStart(description: string, options: { fromAnalysis?: string;
|
|
|
159
141
|
}
|
|
160
142
|
}
|
|
161
143
|
|
|
162
|
-
export function planShow(json: boolean = false): void {
|
|
144
|
+
export function planShow(json: boolean = false, specId?: string): void {
|
|
163
145
|
initSchema();
|
|
164
146
|
const db = getDb();
|
|
165
147
|
|
|
166
|
-
const spec =
|
|
167
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
168
|
-
.get() as any;
|
|
169
|
-
|
|
170
|
-
if (!spec) {
|
|
171
|
-
console.error("\nNenhuma feature ativa.");
|
|
172
|
-
console.error("Inicie com: /codexa:feature'\n");
|
|
173
|
-
process.exit(1);
|
|
174
|
-
}
|
|
148
|
+
const spec = resolveSpec(specId);
|
|
175
149
|
|
|
176
150
|
const tasks = db
|
|
177
151
|
.query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
|
|
@@ -225,25 +199,12 @@ export function planTaskAdd(options: {
|
|
|
225
199
|
depends?: string;
|
|
226
200
|
files?: string;
|
|
227
201
|
sequential?: boolean;
|
|
202
|
+
specId?: string;
|
|
228
203
|
}): void {
|
|
229
204
|
initSchema();
|
|
230
205
|
const db = getDb();
|
|
231
206
|
|
|
232
|
-
const spec =
|
|
233
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
234
|
-
.get() as any;
|
|
235
|
-
|
|
236
|
-
if (!spec) {
|
|
237
|
-
console.error("\nNenhuma feature ativa.");
|
|
238
|
-
console.error("Inicie com: /codexa:feature\n");
|
|
239
|
-
process.exit(1);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (spec.phase !== "planning") {
|
|
243
|
-
console.error(`\nNao e possivel adicionar tasks na fase '${spec.phase}'.`);
|
|
244
|
-
console.error("Tasks so podem ser adicionadas na fase 'planning'.\n");
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
207
|
+
const spec = resolveSpec(options.specId, ["planning"]);
|
|
247
208
|
|
|
248
209
|
// Pegar proximo numero
|
|
249
210
|
const lastTask = db
|
|
@@ -260,8 +221,7 @@ export function planTaskAdd(options: {
|
|
|
260
221
|
.query("SELECT id FROM tasks WHERE spec_id = ? AND number = ?")
|
|
261
222
|
.get(spec.id, depId);
|
|
262
223
|
if (!exists) {
|
|
263
|
-
|
|
264
|
-
process.exit(2);
|
|
224
|
+
throw new ValidationError(`Dependencia invalida: task #${depId} nao existe.`);
|
|
265
225
|
}
|
|
266
226
|
}
|
|
267
227
|
|
|
@@ -300,10 +260,9 @@ export function planTaskAdd(options: {
|
|
|
300
260
|
}
|
|
301
261
|
|
|
302
262
|
if (hasCycle(nextNumber)) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
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
|
+
);
|
|
307
266
|
}
|
|
308
267
|
}
|
|
309
268
|
|
|
@@ -342,18 +301,11 @@ export function planTaskAdd(options: {
|
|
|
342
301
|
console.log(` Paralelizavel: ${options.sequential ? "Nao" : "Sim"}\n`);
|
|
343
302
|
}
|
|
344
303
|
|
|
345
|
-
export function planCancel(): void {
|
|
304
|
+
export function planCancel(specId?: string): void {
|
|
346
305
|
initSchema();
|
|
347
306
|
const db = getDb();
|
|
348
307
|
|
|
349
|
-
const spec =
|
|
350
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
351
|
-
.get() as any;
|
|
352
|
-
|
|
353
|
-
if (!spec) {
|
|
354
|
-
console.error("\nNenhuma feature ativa para cancelar.\n");
|
|
355
|
-
process.exit(1);
|
|
356
|
-
}
|
|
308
|
+
const spec = resolveSpec(specId);
|
|
357
309
|
|
|
358
310
|
const now = new Date().toISOString();
|
|
359
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";
|
package/commands/review.ts
CHANGED
|
@@ -1,17 +1,135 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema, getArchitecturalAnalysisForSpec } from "../db/schema";
|
|
3
3
|
import { enforceGate } from "../gates/validator";
|
|
4
|
+
import { resolveSpec } from "./spec-resolver";
|
|
5
|
+
import { CodexaError, GateError } from "../errors";
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════
|
|
8
|
+
// P1-2: Review Score — Threshold Automatico
|
|
9
|
+
// ═══════════════════════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
export interface ReviewScore {
|
|
12
|
+
total: number; // 0-100
|
|
13
|
+
breakdown: {
|
|
14
|
+
tasksCompleted: number; // % tasks done vs planned (25 pts)
|
|
15
|
+
gatesPassedClean: number; // % gates sem bypass (25 pts)
|
|
16
|
+
filesDelivered: number; // % arquivos planejados vs criados (25 pts)
|
|
17
|
+
standardsFollowed: number; // % standards sem violacao (25 pts)
|
|
18
|
+
};
|
|
19
|
+
autoApproveEligible: boolean; // score >= 80 AND zero critical bypasses
|
|
20
|
+
mustReviewItems: string[]; // Items que exigem atencao humana
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function calculateReviewScore(specId: string): ReviewScore {
|
|
24
|
+
const db = getDb();
|
|
25
|
+
|
|
26
|
+
const mustReviewItems: string[] = [];
|
|
27
|
+
|
|
28
|
+
// 1. Tasks: (completed / total) * 25
|
|
29
|
+
const totalTasks = (db.query(
|
|
30
|
+
"SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?"
|
|
31
|
+
).get(specId) as any).c;
|
|
32
|
+
const completedTasks = (db.query(
|
|
33
|
+
"SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND status = 'done'"
|
|
34
|
+
).get(specId) as any).c;
|
|
35
|
+
const tasksCompleted = totalTasks > 0
|
|
36
|
+
? Math.round((completedTasks / totalTasks) * 25)
|
|
37
|
+
: 25;
|
|
38
|
+
|
|
39
|
+
if (completedTasks < totalTasks) {
|
|
40
|
+
mustReviewItems.push(`${totalTasks - completedTasks} task(s) nao concluida(s)`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. Gates: (clean passes / total gate events) * 25
|
|
44
|
+
// Total gate events = tasks * 7 gates per task
|
|
45
|
+
const totalGateEvents = totalTasks * 7;
|
|
46
|
+
const bypassCount = (db.query(
|
|
47
|
+
"SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ?"
|
|
48
|
+
).get(specId) as any).c;
|
|
49
|
+
const cleanGateEvents = Math.max(0, totalGateEvents - bypassCount);
|
|
50
|
+
const gatesPassedClean = totalGateEvents > 0
|
|
51
|
+
? Math.round((cleanGateEvents / totalGateEvents) * 25)
|
|
52
|
+
: 25;
|
|
53
|
+
|
|
54
|
+
// Check for critical bypasses
|
|
55
|
+
const criticalBypasses = db.query(
|
|
56
|
+
"SELECT * FROM gate_bypasses WHERE spec_id = ? AND gate_name IN ('standards-follow', 'dry-check', 'typecheck-pass')"
|
|
57
|
+
).all(specId) as any[];
|
|
58
|
+
if (criticalBypasses.length > 0) {
|
|
59
|
+
mustReviewItems.push(`${criticalBypasses.length} bypass(es) de gates criticos`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 3. Files: (delivered / planned) * 25
|
|
63
|
+
const plannedFiles = db.query(
|
|
64
|
+
"SELECT files FROM tasks WHERE spec_id = ? AND files IS NOT NULL"
|
|
65
|
+
).all(specId) as any[];
|
|
66
|
+
const allPlannedFiles = new Set<string>();
|
|
67
|
+
for (const t of plannedFiles) {
|
|
68
|
+
try {
|
|
69
|
+
const files = JSON.parse(t.files) as string[];
|
|
70
|
+
for (const f of files) allPlannedFiles.add(f);
|
|
71
|
+
} catch { /* ignore */ }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const deliveredFiles = new Set(
|
|
75
|
+
(db.query(
|
|
76
|
+
"SELECT DISTINCT path FROM artifacts WHERE spec_id = ?"
|
|
77
|
+
).all(specId) as any[]).map(a => a.path)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
let filesDelivered: number;
|
|
81
|
+
if (allPlannedFiles.size === 0) {
|
|
82
|
+
filesDelivered = deliveredFiles.size > 0 ? 25 : 0;
|
|
83
|
+
} else {
|
|
84
|
+
let matchCount = 0;
|
|
85
|
+
for (const f of allPlannedFiles) {
|
|
86
|
+
if (deliveredFiles.has(f)) matchCount++;
|
|
87
|
+
}
|
|
88
|
+
filesDelivered = Math.round((matchCount / allPlannedFiles.size) * 25);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const missingFiles = [...allPlannedFiles].filter(f => !deliveredFiles.has(f));
|
|
92
|
+
if (missingFiles.length > 0) {
|
|
93
|
+
mustReviewItems.push(`${missingFiles.length} arquivo(s) planejado(s) nao entregue(s)`);
|
|
94
|
+
}
|
|
4
95
|
|
|
5
|
-
|
|
96
|
+
// 4. Standards: (followed / total) * 25
|
|
97
|
+
// Inverse of standards-follow bypasses relative to total tasks
|
|
98
|
+
const standardsBypasses = (db.query(
|
|
99
|
+
"SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ? AND gate_name = 'standards-follow'"
|
|
100
|
+
).get(specId) as any).c;
|
|
101
|
+
const standardsFollowed = totalTasks > 0
|
|
102
|
+
? Math.round(((totalTasks - standardsBypasses) / totalTasks) * 25)
|
|
103
|
+
: 25;
|
|
104
|
+
|
|
105
|
+
if (standardsBypasses > 0) {
|
|
106
|
+
mustReviewItems.push(`${standardsBypasses} bypass(es) de standards`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const total = tasksCompleted + gatesPassedClean + filesDelivered + standardsFollowed;
|
|
110
|
+
const autoApproveEligible = total >= 80 && criticalBypasses.length === 0;
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
total,
|
|
114
|
+
breakdown: {
|
|
115
|
+
tasksCompleted,
|
|
116
|
+
gatesPassedClean,
|
|
117
|
+
filesDelivered,
|
|
118
|
+
standardsFollowed,
|
|
119
|
+
},
|
|
120
|
+
autoApproveEligible,
|
|
121
|
+
mustReviewItems,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function reviewStart(json: boolean = false, specId?: string): void {
|
|
6
126
|
initSchema();
|
|
7
127
|
enforceGate("review-start");
|
|
8
128
|
|
|
9
129
|
const db = getDb();
|
|
10
130
|
const now = new Date().toISOString();
|
|
11
131
|
|
|
12
|
-
const spec =
|
|
13
|
-
.query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
|
|
14
|
-
.get() as any;
|
|
132
|
+
const spec = resolveSpec(specId, ["implementing"]);
|
|
15
133
|
|
|
16
134
|
const tasks = db
|
|
17
135
|
.query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
|
|
@@ -87,8 +205,11 @@ export function reviewStart(json: boolean = false): void {
|
|
|
87
205
|
// Atualizar fase
|
|
88
206
|
db.run("UPDATE specs SET phase = 'reviewing', updated_at = ? WHERE id = ?", [now, spec.id]);
|
|
89
207
|
|
|
208
|
+
// P1-2: Calcular score automatico
|
|
209
|
+
const score = calculateReviewScore(spec.id);
|
|
210
|
+
|
|
90
211
|
if (json) {
|
|
91
|
-
console.log(JSON.stringify({ spec, reviewData, deviations }));
|
|
212
|
+
console.log(JSON.stringify({ spec, reviewData, deviations, score }));
|
|
92
213
|
return;
|
|
93
214
|
}
|
|
94
215
|
|
|
@@ -96,6 +217,25 @@ export function reviewStart(json: boolean = false): void {
|
|
|
96
217
|
console.log(`REVIEW: ${spec.name}`);
|
|
97
218
|
console.log(`${"=".repeat(60)}`);
|
|
98
219
|
|
|
220
|
+
// P1-2: Mostrar score
|
|
221
|
+
const scoreIcon = score.total >= 80 ? "[OK]" : score.total >= 50 ? "[!]" : "[X]";
|
|
222
|
+
console.log(`\n${scoreIcon} REVIEW SCORE: ${score.total}/100`);
|
|
223
|
+
console.log(` Tasks concluidas: ${score.breakdown.tasksCompleted}/25`);
|
|
224
|
+
console.log(` Gates limpos: ${score.breakdown.gatesPassedClean}/25`);
|
|
225
|
+
console.log(` Arquivos entregues: ${score.breakdown.filesDelivered}/25`);
|
|
226
|
+
console.log(` Standards seguidos: ${score.breakdown.standardsFollowed}/25`);
|
|
227
|
+
|
|
228
|
+
if (score.autoApproveEligible) {
|
|
229
|
+
console.log(`\n Elegivel para aprovacao automatica.`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (score.mustReviewItems.length > 0) {
|
|
233
|
+
console.log(`\n Items que exigem atencao:`);
|
|
234
|
+
for (const item of score.mustReviewItems) {
|
|
235
|
+
console.log(` - ${item}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
99
239
|
console.log(`\nResumo da implementacao:`);
|
|
100
240
|
console.log(` Tasks concluidas: ${tasks.length}`);
|
|
101
241
|
console.log(` Artefatos criados: ${artifacts.length}`);
|
|
@@ -123,24 +263,48 @@ export function reviewStart(json: boolean = false): void {
|
|
|
123
263
|
console.log(`\nNenhum desvio encontrado.`);
|
|
124
264
|
}
|
|
125
265
|
|
|
126
|
-
|
|
266
|
+
if (score.total < 50) {
|
|
267
|
+
console.log(`\n[!] Score abaixo de 50. Aprovacao requer: review approve --force --force-reason "motivo"`);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(`\nPara aprovar: review approve`);
|
|
270
|
+
}
|
|
127
271
|
console.log(`Para ver status: status\n`);
|
|
128
272
|
}
|
|
129
273
|
|
|
130
|
-
export function reviewApprove(): void {
|
|
274
|
+
export function reviewApprove(options?: { specId?: string; force?: boolean; forceReason?: string } | string): void {
|
|
131
275
|
initSchema();
|
|
132
276
|
enforceGate("review-approve");
|
|
133
277
|
|
|
134
278
|
const db = getDb();
|
|
135
279
|
const now = new Date().toISOString();
|
|
136
280
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
281
|
+
// Backward compat: accept string (old API) or options object
|
|
282
|
+
const opts = typeof options === "string"
|
|
283
|
+
? { specId: options }
|
|
284
|
+
: (options || {});
|
|
285
|
+
|
|
286
|
+
const spec = resolveSpec(opts.specId, ["reviewing"]);
|
|
287
|
+
|
|
288
|
+
// P1-2: Enforce minimum score
|
|
289
|
+
const score = calculateReviewScore(spec.id);
|
|
290
|
+
if (score.total < 50 && !opts.force) {
|
|
291
|
+
throw new GateError(
|
|
292
|
+
`Review score muito baixo: ${score.total}/100.\n` +
|
|
293
|
+
` Items criticos:\n` +
|
|
294
|
+
score.mustReviewItems.map(i => ` - ${i}`).join("\n"),
|
|
295
|
+
`Use: review approve --force --force-reason "motivo" para aprovar mesmo assim`,
|
|
296
|
+
"review-approve"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
140
299
|
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
300
|
+
if (score.total < 50 && opts.force) {
|
|
301
|
+
// Log the forced approval
|
|
302
|
+
db.run(
|
|
303
|
+
`INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason, created_at)
|
|
304
|
+
VALUES (?, 0, 'review-low-score', ?, ?)`,
|
|
305
|
+
[spec.id, opts.forceReason || `Score ${score.total}/100 - forced approval`, now]
|
|
306
|
+
);
|
|
307
|
+
console.log(`\n[!] Review aprovado com score baixo (${score.total}/100). Bypass registrado.`);
|
|
144
308
|
}
|
|
145
309
|
|
|
146
310
|
// Atualizar review
|
|
@@ -175,27 +339,19 @@ export function reviewApprove(): void {
|
|
|
175
339
|
]);
|
|
176
340
|
|
|
177
341
|
// Mostrar relatorio final completo
|
|
178
|
-
showReviewReport(spec, tasks, artifacts, decisions, knowledge, review);
|
|
342
|
+
showReviewReport(spec, tasks, artifacts, decisions, knowledge, review, score);
|
|
179
343
|
}
|
|
180
344
|
|
|
181
345
|
/**
|
|
182
346
|
* Pula o review e finaliza a feature diretamente
|
|
183
347
|
*/
|
|
184
|
-
export function reviewSkip(): void {
|
|
348
|
+
export function reviewSkip(specId?: string): void {
|
|
185
349
|
initSchema();
|
|
186
350
|
|
|
187
351
|
const db = getDb();
|
|
188
352
|
const now = new Date().toISOString();
|
|
189
353
|
|
|
190
|
-
|
|
191
|
-
const spec = db
|
|
192
|
-
.query("SELECT * FROM specs WHERE phase = 'implementing' ORDER BY created_at DESC LIMIT 1")
|
|
193
|
-
.get() as any;
|
|
194
|
-
|
|
195
|
-
if (!spec) {
|
|
196
|
-
console.error("\nNenhuma feature em fase de implementacao.\n");
|
|
197
|
-
process.exit(1);
|
|
198
|
-
}
|
|
354
|
+
const spec = resolveSpec(specId, ["implementing"]);
|
|
199
355
|
|
|
200
356
|
// Verificar se todas tasks estao done
|
|
201
357
|
const pending = db
|
|
@@ -203,9 +359,7 @@ export function reviewSkip(): void {
|
|
|
203
359
|
.get(spec.id) as any;
|
|
204
360
|
|
|
205
361
|
if (pending.c > 0) {
|
|
206
|
-
|
|
207
|
-
console.error("Complete todas as tasks antes de pular o review.\n");
|
|
208
|
-
process.exit(1);
|
|
362
|
+
throw new CodexaError(`Ainda existem ${pending.c} tasks pendentes.\nComplete todas as tasks antes de pular o review.`);
|
|
209
363
|
}
|
|
210
364
|
|
|
211
365
|
// Buscar dados
|
|
@@ -265,7 +419,8 @@ function showReviewReport(
|
|
|
265
419
|
artifacts: any[],
|
|
266
420
|
decisions: any[],
|
|
267
421
|
knowledge: any[],
|
|
268
|
-
review: any
|
|
422
|
+
review: any,
|
|
423
|
+
score?: ReviewScore
|
|
269
424
|
): void {
|
|
270
425
|
const reviewData = review.planned_vs_done ? JSON.parse(review.planned_vs_done) : {};
|
|
271
426
|
const deviations = review.deviations ? JSON.parse(review.deviations) : [];
|
|
@@ -278,6 +433,13 @@ function showReviewReport(
|
|
|
278
433
|
console.log(` Status: ✅ COMPLETA E APROVADA`);
|
|
279
434
|
console.log(` ID: ${spec.id}`);
|
|
280
435
|
|
|
436
|
+
// P1-2: Score no relatorio final
|
|
437
|
+
if (score) {
|
|
438
|
+
const scoreIcon = score.total >= 80 ? "✅" : score.total >= 50 ? "⚠️" : "❌";
|
|
439
|
+
console.log(` Score: ${scoreIcon} ${score.total}/100`);
|
|
440
|
+
console.log(` Tasks: ${score.breakdown.tasksCompleted}/25 | Gates: ${score.breakdown.gatesPassedClean}/25 | Files: ${score.breakdown.filesDelivered}/25 | Standards: ${score.breakdown.standardsFollowed}/25`);
|
|
441
|
+
}
|
|
442
|
+
|
|
281
443
|
// Resumo de execucao
|
|
282
444
|
console.log(`\n${"─".repeat(60)}`);
|
|
283
445
|
console.log(`📊 RESUMO DA EXECUCAO`);
|