@codexa/cli 8.6.0 → 8.6.9
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 -760
- package/commands/check.ts +131 -131
- package/commands/clear.ts +170 -170
- package/commands/decide.ts +249 -249
- package/commands/discover.ts +1071 -1071
- package/commands/knowledge.ts +361 -361
- package/commands/patterns.ts +621 -621
- package/commands/plan.ts +376 -376
- package/commands/product.ts +626 -626
- package/commands/research.ts +754 -754
- package/commands/review.ts +463 -463
- package/commands/standards.ts +200 -200
- package/commands/task.ts +623 -623
- package/commands/utils.ts +1021 -1021
- package/db/connection.ts +32 -32
- package/db/schema.ts +719 -719
- package/detectors/README.md +109 -109
- package/detectors/dotnet.ts +357 -357
- package/detectors/flutter.ts +350 -350
- package/detectors/go.ts +324 -324
- package/detectors/index.ts +387 -387
- package/detectors/jvm.ts +433 -433
- package/detectors/loader.ts +128 -128
- package/detectors/node.ts +493 -493
- package/detectors/python.ts +423 -423
- package/detectors/rust.ts +348 -348
- package/gates/standards-validator.ts +204 -204
- package/gates/validator.ts +441 -441
- package/package.json +44 -43
- package/protocol/process-return.ts +450 -450
- package/protocol/subagent-protocol.ts +401 -401
- package/workflow.ts +783 -782
package/gates/validator.ts
CHANGED
|
@@ -1,441 +1,441 @@
|
|
|
1
|
-
import { getDb } from "../db/connection";
|
|
2
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
|
-
import { extname } from "path";
|
|
4
|
-
import { validateAgainstStandards, printValidationResult } from "./standards-validator";
|
|
5
|
-
import { extractUtilitiesFromFile } from "../commands/patterns";
|
|
6
|
-
import { findDuplicateUtilities } from "../db/schema";
|
|
7
|
-
|
|
8
|
-
export interface GateResult {
|
|
9
|
-
passed: boolean;
|
|
10
|
-
reason?: string;
|
|
11
|
-
resolution?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface GateCheck {
|
|
15
|
-
check: string;
|
|
16
|
-
message: string;
|
|
17
|
-
resolution: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const GATES: Record<string, GateCheck[]> = {
|
|
21
|
-
"check-request": [
|
|
22
|
-
{
|
|
23
|
-
check: "plan-exists",
|
|
24
|
-
message: "Plano nao existe no SQLite",
|
|
25
|
-
resolution: "Execute: plan start 'descricao'",
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
check: "has-tasks",
|
|
29
|
-
message: "Nenhuma task definida",
|
|
30
|
-
resolution: "Execute: plan task-add para adicionar tasks",
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
"check-approve": [
|
|
34
|
-
{
|
|
35
|
-
check: "phase-is-checking",
|
|
36
|
-
message: "Fase atual nao e 'checking'",
|
|
37
|
-
resolution: "Execute: check request primeiro",
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
"task-start": [
|
|
41
|
-
{
|
|
42
|
-
check: "spec-approved",
|
|
43
|
-
message: "Plano nao aprovado",
|
|
44
|
-
resolution: "Execute: check approve",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
check: "dependencies-done",
|
|
48
|
-
message: "Dependencias pendentes",
|
|
49
|
-
resolution: "Complete as tasks dependentes primeiro",
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
"task-done": [
|
|
53
|
-
{
|
|
54
|
-
check: "task-is-running",
|
|
55
|
-
message: "Task nao esta em execucao",
|
|
56
|
-
resolution: "Execute: task start <id> primeiro",
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
check: "checkpoint-filled",
|
|
60
|
-
message: "Checkpoint obrigatorio",
|
|
61
|
-
resolution: "Forneca --checkpoint 'resumo do que foi feito'",
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
check: "files-exist",
|
|
65
|
-
message: "Arquivos esperados nao encontrados",
|
|
66
|
-
resolution: "Crie os arquivos declarados em --files antes de completar a task",
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
check: "standards-follow",
|
|
70
|
-
message: "Codigo viola standards obrigatorios",
|
|
71
|
-
resolution: "Corrija as violacoes listadas ou use --force para bypass (sera registrado)",
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
check: "dry-check",
|
|
75
|
-
message: "Duplicacao de utilities detectada (DRY)",
|
|
76
|
-
resolution: "Importe do arquivo existente ou use --force --force-reason para bypass",
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
"review-start": [
|
|
80
|
-
{
|
|
81
|
-
check: "all-tasks-done",
|
|
82
|
-
message: "Tasks pendentes",
|
|
83
|
-
resolution: "Complete todas as tasks primeiro",
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
"review-approve": [
|
|
87
|
-
{
|
|
88
|
-
check: "review-exists",
|
|
89
|
-
message: "Review nao iniciado",
|
|
90
|
-
resolution: "Execute: review start primeiro",
|
|
91
|
-
},
|
|
92
|
-
],
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
function getActiveSpec(): any {
|
|
96
|
-
const db = getDb();
|
|
97
|
-
return db.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1").get();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function executeCheck(check: string, context: any): { passed: boolean; details?: string } {
|
|
101
|
-
const db = getDb();
|
|
102
|
-
|
|
103
|
-
switch (check) {
|
|
104
|
-
case "plan-exists": {
|
|
105
|
-
const spec = getActiveSpec();
|
|
106
|
-
return { passed: spec !== null };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
case "has-tasks": {
|
|
110
|
-
const spec = getActiveSpec();
|
|
111
|
-
if (!spec) return { passed: false };
|
|
112
|
-
const count = db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?").get(spec.id) as any;
|
|
113
|
-
return { passed: count.c > 0 };
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
case "phase-is-checking": {
|
|
117
|
-
const spec = getActiveSpec();
|
|
118
|
-
return { passed: spec?.phase === "checking" };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
case "spec-approved": {
|
|
122
|
-
const spec = getActiveSpec();
|
|
123
|
-
return { passed: spec?.approved_at !== null };
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
case "dependencies-done": {
|
|
127
|
-
if (!context.taskId) return { passed: true };
|
|
128
|
-
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
129
|
-
if (!task || !task.depends_on) return { passed: true };
|
|
130
|
-
|
|
131
|
-
const deps = JSON.parse(task.depends_on) as number[];
|
|
132
|
-
const pending = deps.filter((depNum) => {
|
|
133
|
-
// Buscar por number (não id) - depends_on armazena números de tasks
|
|
134
|
-
const depTask = db.query("SELECT status FROM tasks WHERE spec_id = ? AND number = ?").get(task.spec_id, depNum) as any;
|
|
135
|
-
return depTask?.status !== "done";
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
passed: pending.length === 0,
|
|
140
|
-
details: pending.join(", "),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
case "task-is-running": {
|
|
145
|
-
if (!context.taskId) return { passed: false };
|
|
146
|
-
const task = db.query("SELECT status FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
147
|
-
return { passed: task?.status === "running" };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
case "checkpoint-filled": {
|
|
151
|
-
return {
|
|
152
|
-
passed: !!context.checkpoint && context.checkpoint.length >= 10,
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
case "all-tasks-done": {
|
|
157
|
-
const spec = getActiveSpec();
|
|
158
|
-
if (!spec) return { passed: false };
|
|
159
|
-
const pending = db.query(
|
|
160
|
-
"SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'"
|
|
161
|
-
).all(spec.id) as any[];
|
|
162
|
-
return {
|
|
163
|
-
passed: pending.length === 0,
|
|
164
|
-
details: pending.map((t) => `#${t.number}`).join(", "),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
case "review-exists": {
|
|
169
|
-
const spec = getActiveSpec();
|
|
170
|
-
if (!spec) return { passed: false };
|
|
171
|
-
const review = db.query("SELECT * FROM review WHERE spec_id = ?").get(spec.id);
|
|
172
|
-
return { passed: review !== null };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
case "files-exist": {
|
|
176
|
-
if (!context.files || context.files.length === 0) return { passed: true };
|
|
177
|
-
|
|
178
|
-
// v8.0: Validar não apenas existência, mas conteúdo mínimo
|
|
179
|
-
const issues: string[] = [];
|
|
180
|
-
|
|
181
|
-
for (const file of context.files) {
|
|
182
|
-
const validation = validateFileContent(file);
|
|
183
|
-
if (!validation.valid) {
|
|
184
|
-
issues.push(`${file}: ${validation.reason}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
passed: issues.length === 0,
|
|
190
|
-
details: issues.join("\n"),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
case "standards-follow": {
|
|
195
|
-
// Se --force foi passado, registrar bypass e passar
|
|
196
|
-
if (context.force) {
|
|
197
|
-
logGateBypass(context.taskId, "standards-follow", context.forceReason);
|
|
198
|
-
return { passed: true };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Se nenhum arquivo, passar
|
|
202
|
-
if (!context.files || context.files.length === 0) return { passed: true };
|
|
203
|
-
|
|
204
|
-
// Obter agente da task
|
|
205
|
-
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
206
|
-
const agentDomain = task?.agent?.split("-")[0] || "all";
|
|
207
|
-
|
|
208
|
-
// Validar arquivos contra standards
|
|
209
|
-
const result = validateAgainstStandards(context.files, agentDomain);
|
|
210
|
-
|
|
211
|
-
// Mostrar resultado se houver violacoes
|
|
212
|
-
if (!result.passed || result.warnings.length > 0) {
|
|
213
|
-
printValidationResult(result);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
passed: result.passed,
|
|
218
|
-
details: result.violations.map(
|
|
219
|
-
(v) => `[${v.enforcement}] ${v.file}: ${v.rule}${v.detail ? ` (${v.detail})` : ""}`
|
|
220
|
-
).join("\n"),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
case "dry-check": {
|
|
225
|
-
// v8.5: Verificar se arquivos criados exportam utilities que ja existem
|
|
226
|
-
if (context.force) {
|
|
227
|
-
logGateBypass(context.taskId, "dry-check", context.forceReason);
|
|
228
|
-
return { passed: true };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!context.files || context.files.length === 0) return { passed: true };
|
|
232
|
-
|
|
233
|
-
const duplicates: string[] = [];
|
|
234
|
-
|
|
235
|
-
for (const file of context.files) {
|
|
236
|
-
if (!existsSync(file)) continue;
|
|
237
|
-
const utilities = extractUtilitiesFromFile(file);
|
|
238
|
-
|
|
239
|
-
for (const util of utilities) {
|
|
240
|
-
const existing = findDuplicateUtilities(util.name, file);
|
|
241
|
-
if (existing.length > 0) {
|
|
242
|
-
duplicates.push(
|
|
243
|
-
`"${util.name}" (${util.type}) em ${file} -- ja existe em: ${existing.map((e: any) => e.file_path).join(", ")}`
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (duplicates.length > 0) {
|
|
250
|
-
// Apenas function/const/class bloqueiam; interface/type so avisam
|
|
251
|
-
const blockingDupes = duplicates.filter(d =>
|
|
252
|
-
d.includes("(function)") || d.includes("(const)") || d.includes("(class)")
|
|
253
|
-
);
|
|
254
|
-
|
|
255
|
-
if (blockingDupes.length > 0) {
|
|
256
|
-
console.warn("\n[DRY] Possiveis duplicacoes detectadas:\n");
|
|
257
|
-
for (const d of duplicates) {
|
|
258
|
-
console.warn(` - ${d}`);
|
|
259
|
-
}
|
|
260
|
-
console.warn("\nImporte do arquivo existente ou use --force --force-reason 'motivo' para bypass.\n");
|
|
261
|
-
return {
|
|
262
|
-
passed: false,
|
|
263
|
-
details: blockingDupes.join("\n"),
|
|
264
|
-
};
|
|
265
|
-
} else {
|
|
266
|
-
// Types/interfaces: aviso nao-bloqueante
|
|
267
|
-
console.warn("\n[DRY] Aviso: nomes de tipo duplicados (nao bloqueante):");
|
|
268
|
-
for (const d of duplicates) {
|
|
269
|
-
console.warn(` - ${d}`);
|
|
270
|
-
}
|
|
271
|
-
return { passed: true };
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return { passed: true };
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
default:
|
|
279
|
-
return { passed: true };
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function logGateBypass(taskId: number, gateName: string, reason?: string): void {
|
|
284
|
-
const db = getDb();
|
|
285
|
-
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
|
|
286
|
-
if (!task) return;
|
|
287
|
-
|
|
288
|
-
const now = new Date().toISOString();
|
|
289
|
-
db.run(
|
|
290
|
-
`INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason, created_at)
|
|
291
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
292
|
-
[task.spec_id, taskId, gateName, reason || "Nao informado", now]
|
|
293
|
-
);
|
|
294
|
-
|
|
295
|
-
console.warn(`\n[!] BYPASS REGISTRADO: Gate '${gateName}' foi ignorado para Task #${task.number}`);
|
|
296
|
-
if (reason) console.warn(` Motivo: ${reason}`);
|
|
297
|
-
console.warn(` Isso sera auditado no review.\n`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
export function validateGate(command: string, context: any = {}): GateResult {
|
|
301
|
-
const gates = GATES[command] || [];
|
|
302
|
-
|
|
303
|
-
for (const gate of gates) {
|
|
304
|
-
const result = executeCheck(gate.check, context);
|
|
305
|
-
|
|
306
|
-
if (!result.passed) {
|
|
307
|
-
return {
|
|
308
|
-
passed: false,
|
|
309
|
-
reason: result.details
|
|
310
|
-
? `${gate.message}: ${result.details}`
|
|
311
|
-
: gate.message,
|
|
312
|
-
resolution: gate.resolution,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return { passed: true };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export function enforceGate(command: string, context: any = {}): void {
|
|
321
|
-
const result = validateGate(command, context);
|
|
322
|
-
|
|
323
|
-
if (!result.passed) {
|
|
324
|
-
console.error(`\nBLOQUEADO: ${result.reason}`);
|
|
325
|
-
console.error(`Resolva: ${result.resolution}\n`);
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// v8.0: Validar conteúdo de arquivos criados (não apenas existência)
|
|
331
|
-
interface FileValidationResult {
|
|
332
|
-
valid: boolean;
|
|
333
|
-
reason?: string;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function validateFileContent(filePath: string): FileValidationResult {
|
|
337
|
-
// 1. Verificar existência
|
|
338
|
-
if (!existsSync(filePath)) {
|
|
339
|
-
return { valid: false, reason: "arquivo nao encontrado" };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
// 2. Verificar tamanho (arquivo vazio ou muito pequeno)
|
|
344
|
-
const stats = statSync(filePath);
|
|
345
|
-
if (stats.size === 0) {
|
|
346
|
-
return { valid: false, reason: "arquivo vazio (0 bytes)" };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// 3. Ler conteúdo e verificar estrutura mínima
|
|
350
|
-
const content = readFileSync(filePath, "utf-8");
|
|
351
|
-
const trimmed = content.trim();
|
|
352
|
-
|
|
353
|
-
// Arquivo trivialmente pequeno (< 20 caracteres não é código real)
|
|
354
|
-
if (trimmed.length < 20) {
|
|
355
|
-
return { valid: false, reason: `conteudo trivial (${trimmed.length} chars)` };
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// 4. Validações específicas por extensão
|
|
359
|
-
const ext = extname(filePath).toLowerCase();
|
|
360
|
-
const validation = validateByExtension(ext, content);
|
|
361
|
-
if (!validation.valid) {
|
|
362
|
-
return validation;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return { valid: true };
|
|
366
|
-
} catch (error: any) {
|
|
367
|
-
return { valid: false, reason: `erro ao ler: ${error.message}` };
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function validateByExtension(ext: string, content: string): FileValidationResult {
|
|
372
|
-
const trimmed = content.trim();
|
|
373
|
-
|
|
374
|
-
switch (ext) {
|
|
375
|
-
case ".ts":
|
|
376
|
-
case ".tsx":
|
|
377
|
-
case ".js":
|
|
378
|
-
case ".jsx":
|
|
379
|
-
// Arquivos JS/TS devem ter pelo menos uma declaração
|
|
380
|
-
if (!hasCodeStructure(trimmed, ["export", "import", "function", "const", "let", "var", "class", "interface", "type"])) {
|
|
381
|
-
return { valid: false, reason: "sem declaracoes validas (export, function, class, etc)" };
|
|
382
|
-
}
|
|
383
|
-
// TSX/JSX devem ter JSX ou export de componente
|
|
384
|
-
if ((ext === ".tsx" || ext === ".jsx") && !trimmed.includes("<") && !trimmed.includes("export")) {
|
|
385
|
-
return { valid: false, reason: "componente sem JSX ou export" };
|
|
386
|
-
}
|
|
387
|
-
break;
|
|
388
|
-
|
|
389
|
-
case ".css":
|
|
390
|
-
case ".scss":
|
|
391
|
-
case ".sass":
|
|
392
|
-
// CSS deve ter pelo menos um seletor e propriedade
|
|
393
|
-
if (!trimmed.includes("{") || !trimmed.includes(":")) {
|
|
394
|
-
return { valid: false, reason: "CSS sem regras validas" };
|
|
395
|
-
}
|
|
396
|
-
break;
|
|
397
|
-
|
|
398
|
-
case ".json":
|
|
399
|
-
// JSON deve ser válido
|
|
400
|
-
try {
|
|
401
|
-
JSON.parse(content);
|
|
402
|
-
} catch {
|
|
403
|
-
return { valid: false, reason: "JSON invalido" };
|
|
404
|
-
}
|
|
405
|
-
break;
|
|
406
|
-
|
|
407
|
-
case ".sql":
|
|
408
|
-
// SQL deve ter statements
|
|
409
|
-
if (!hasCodeStructure(trimmed.toUpperCase(), ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP"])) {
|
|
410
|
-
return { valid: false, reason: "SQL sem statements validos" };
|
|
411
|
-
}
|
|
412
|
-
break;
|
|
413
|
-
|
|
414
|
-
case ".py":
|
|
415
|
-
// Python deve ter definições
|
|
416
|
-
if (!hasCodeStructure(trimmed, ["def ", "class ", "import ", "from "])) {
|
|
417
|
-
return { valid: false, reason: "Python sem definicoes (def, class, import)" };
|
|
418
|
-
}
|
|
419
|
-
break;
|
|
420
|
-
|
|
421
|
-
case ".go":
|
|
422
|
-
// Go deve ter package
|
|
423
|
-
if (!trimmed.includes("package ")) {
|
|
424
|
-
return { valid: false, reason: "Go sem declaracao package" };
|
|
425
|
-
}
|
|
426
|
-
break;
|
|
427
|
-
|
|
428
|
-
case ".md":
|
|
429
|
-
// Markdown deve ter conteúdo mínimo
|
|
430
|
-
if (trimmed.length < 50) {
|
|
431
|
-
return { valid: false, reason: "Markdown muito curto" };
|
|
432
|
-
}
|
|
433
|
-
break;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return { valid: true };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function hasCodeStructure(content: string, keywords: string[]): boolean {
|
|
440
|
-
return keywords.some((keyword) => content.includes(keyword));
|
|
441
|
-
}
|
|
1
|
+
import { getDb } from "../db/connection";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "fs";
|
|
3
|
+
import { extname } from "path";
|
|
4
|
+
import { validateAgainstStandards, printValidationResult } from "./standards-validator";
|
|
5
|
+
import { extractUtilitiesFromFile } from "../commands/patterns";
|
|
6
|
+
import { findDuplicateUtilities } from "../db/schema";
|
|
7
|
+
|
|
8
|
+
export interface GateResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
reason?: string;
|
|
11
|
+
resolution?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface GateCheck {
|
|
15
|
+
check: string;
|
|
16
|
+
message: string;
|
|
17
|
+
resolution: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const GATES: Record<string, GateCheck[]> = {
|
|
21
|
+
"check-request": [
|
|
22
|
+
{
|
|
23
|
+
check: "plan-exists",
|
|
24
|
+
message: "Plano nao existe no SQLite",
|
|
25
|
+
resolution: "Execute: plan start 'descricao'",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
check: "has-tasks",
|
|
29
|
+
message: "Nenhuma task definida",
|
|
30
|
+
resolution: "Execute: plan task-add para adicionar tasks",
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
"check-approve": [
|
|
34
|
+
{
|
|
35
|
+
check: "phase-is-checking",
|
|
36
|
+
message: "Fase atual nao e 'checking'",
|
|
37
|
+
resolution: "Execute: check request primeiro",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
"task-start": [
|
|
41
|
+
{
|
|
42
|
+
check: "spec-approved",
|
|
43
|
+
message: "Plano nao aprovado",
|
|
44
|
+
resolution: "Execute: check approve",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
check: "dependencies-done",
|
|
48
|
+
message: "Dependencias pendentes",
|
|
49
|
+
resolution: "Complete as tasks dependentes primeiro",
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
"task-done": [
|
|
53
|
+
{
|
|
54
|
+
check: "task-is-running",
|
|
55
|
+
message: "Task nao esta em execucao",
|
|
56
|
+
resolution: "Execute: task start <id> primeiro",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
check: "checkpoint-filled",
|
|
60
|
+
message: "Checkpoint obrigatorio",
|
|
61
|
+
resolution: "Forneca --checkpoint 'resumo do que foi feito'",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
check: "files-exist",
|
|
65
|
+
message: "Arquivos esperados nao encontrados",
|
|
66
|
+
resolution: "Crie os arquivos declarados em --files antes de completar a task",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
check: "standards-follow",
|
|
70
|
+
message: "Codigo viola standards obrigatorios",
|
|
71
|
+
resolution: "Corrija as violacoes listadas ou use --force para bypass (sera registrado)",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
check: "dry-check",
|
|
75
|
+
message: "Duplicacao de utilities detectada (DRY)",
|
|
76
|
+
resolution: "Importe do arquivo existente ou use --force --force-reason para bypass",
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
"review-start": [
|
|
80
|
+
{
|
|
81
|
+
check: "all-tasks-done",
|
|
82
|
+
message: "Tasks pendentes",
|
|
83
|
+
resolution: "Complete todas as tasks primeiro",
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
"review-approve": [
|
|
87
|
+
{
|
|
88
|
+
check: "review-exists",
|
|
89
|
+
message: "Review nao iniciado",
|
|
90
|
+
resolution: "Execute: review start primeiro",
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function getActiveSpec(): any {
|
|
96
|
+
const db = getDb();
|
|
97
|
+
return db.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1").get();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function executeCheck(check: string, context: any): { passed: boolean; details?: string } {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
|
|
103
|
+
switch (check) {
|
|
104
|
+
case "plan-exists": {
|
|
105
|
+
const spec = getActiveSpec();
|
|
106
|
+
return { passed: spec !== null };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case "has-tasks": {
|
|
110
|
+
const spec = getActiveSpec();
|
|
111
|
+
if (!spec) return { passed: false };
|
|
112
|
+
const count = db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?").get(spec.id) as any;
|
|
113
|
+
return { passed: count.c > 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "phase-is-checking": {
|
|
117
|
+
const spec = getActiveSpec();
|
|
118
|
+
return { passed: spec?.phase === "checking" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "spec-approved": {
|
|
122
|
+
const spec = getActiveSpec();
|
|
123
|
+
return { passed: spec?.approved_at !== null };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case "dependencies-done": {
|
|
127
|
+
if (!context.taskId) return { passed: true };
|
|
128
|
+
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
129
|
+
if (!task || !task.depends_on) return { passed: true };
|
|
130
|
+
|
|
131
|
+
const deps = JSON.parse(task.depends_on) as number[];
|
|
132
|
+
const pending = deps.filter((depNum) => {
|
|
133
|
+
// Buscar por number (não id) - depends_on armazena números de tasks
|
|
134
|
+
const depTask = db.query("SELECT status FROM tasks WHERE spec_id = ? AND number = ?").get(task.spec_id, depNum) as any;
|
|
135
|
+
return depTask?.status !== "done";
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
passed: pending.length === 0,
|
|
140
|
+
details: pending.join(", "),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
case "task-is-running": {
|
|
145
|
+
if (!context.taskId) return { passed: false };
|
|
146
|
+
const task = db.query("SELECT status FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
147
|
+
return { passed: task?.status === "running" };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
case "checkpoint-filled": {
|
|
151
|
+
return {
|
|
152
|
+
passed: !!context.checkpoint && context.checkpoint.length >= 10,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case "all-tasks-done": {
|
|
157
|
+
const spec = getActiveSpec();
|
|
158
|
+
if (!spec) return { passed: false };
|
|
159
|
+
const pending = db.query(
|
|
160
|
+
"SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'"
|
|
161
|
+
).all(spec.id) as any[];
|
|
162
|
+
return {
|
|
163
|
+
passed: pending.length === 0,
|
|
164
|
+
details: pending.map((t) => `#${t.number}`).join(", "),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
case "review-exists": {
|
|
169
|
+
const spec = getActiveSpec();
|
|
170
|
+
if (!spec) return { passed: false };
|
|
171
|
+
const review = db.query("SELECT * FROM review WHERE spec_id = ?").get(spec.id);
|
|
172
|
+
return { passed: review !== null };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
case "files-exist": {
|
|
176
|
+
if (!context.files || context.files.length === 0) return { passed: true };
|
|
177
|
+
|
|
178
|
+
// v8.0: Validar não apenas existência, mas conteúdo mínimo
|
|
179
|
+
const issues: string[] = [];
|
|
180
|
+
|
|
181
|
+
for (const file of context.files) {
|
|
182
|
+
const validation = validateFileContent(file);
|
|
183
|
+
if (!validation.valid) {
|
|
184
|
+
issues.push(`${file}: ${validation.reason}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
passed: issues.length === 0,
|
|
190
|
+
details: issues.join("\n"),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case "standards-follow": {
|
|
195
|
+
// Se --force foi passado, registrar bypass e passar
|
|
196
|
+
if (context.force) {
|
|
197
|
+
logGateBypass(context.taskId, "standards-follow", context.forceReason);
|
|
198
|
+
return { passed: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Se nenhum arquivo, passar
|
|
202
|
+
if (!context.files || context.files.length === 0) return { passed: true };
|
|
203
|
+
|
|
204
|
+
// Obter agente da task
|
|
205
|
+
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
206
|
+
const agentDomain = task?.agent?.split("-")[0] || "all";
|
|
207
|
+
|
|
208
|
+
// Validar arquivos contra standards
|
|
209
|
+
const result = validateAgainstStandards(context.files, agentDomain);
|
|
210
|
+
|
|
211
|
+
// Mostrar resultado se houver violacoes
|
|
212
|
+
if (!result.passed || result.warnings.length > 0) {
|
|
213
|
+
printValidationResult(result);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
passed: result.passed,
|
|
218
|
+
details: result.violations.map(
|
|
219
|
+
(v) => `[${v.enforcement}] ${v.file}: ${v.rule}${v.detail ? ` (${v.detail})` : ""}`
|
|
220
|
+
).join("\n"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
case "dry-check": {
|
|
225
|
+
// v8.5: Verificar se arquivos criados exportam utilities que ja existem
|
|
226
|
+
if (context.force) {
|
|
227
|
+
logGateBypass(context.taskId, "dry-check", context.forceReason);
|
|
228
|
+
return { passed: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (!context.files || context.files.length === 0) return { passed: true };
|
|
232
|
+
|
|
233
|
+
const duplicates: string[] = [];
|
|
234
|
+
|
|
235
|
+
for (const file of context.files) {
|
|
236
|
+
if (!existsSync(file)) continue;
|
|
237
|
+
const utilities = extractUtilitiesFromFile(file);
|
|
238
|
+
|
|
239
|
+
for (const util of utilities) {
|
|
240
|
+
const existing = findDuplicateUtilities(util.name, file);
|
|
241
|
+
if (existing.length > 0) {
|
|
242
|
+
duplicates.push(
|
|
243
|
+
`"${util.name}" (${util.type}) em ${file} -- ja existe em: ${existing.map((e: any) => e.file_path).join(", ")}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (duplicates.length > 0) {
|
|
250
|
+
// Apenas function/const/class bloqueiam; interface/type so avisam
|
|
251
|
+
const blockingDupes = duplicates.filter(d =>
|
|
252
|
+
d.includes("(function)") || d.includes("(const)") || d.includes("(class)")
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (blockingDupes.length > 0) {
|
|
256
|
+
console.warn("\n[DRY] Possiveis duplicacoes detectadas:\n");
|
|
257
|
+
for (const d of duplicates) {
|
|
258
|
+
console.warn(` - ${d}`);
|
|
259
|
+
}
|
|
260
|
+
console.warn("\nImporte do arquivo existente ou use --force --force-reason 'motivo' para bypass.\n");
|
|
261
|
+
return {
|
|
262
|
+
passed: false,
|
|
263
|
+
details: blockingDupes.join("\n"),
|
|
264
|
+
};
|
|
265
|
+
} else {
|
|
266
|
+
// Types/interfaces: aviso nao-bloqueante
|
|
267
|
+
console.warn("\n[DRY] Aviso: nomes de tipo duplicados (nao bloqueante):");
|
|
268
|
+
for (const d of duplicates) {
|
|
269
|
+
console.warn(` - ${d}`);
|
|
270
|
+
}
|
|
271
|
+
return { passed: true };
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { passed: true };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
default:
|
|
279
|
+
return { passed: true };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function logGateBypass(taskId: number, gateName: string, reason?: string): void {
|
|
284
|
+
const db = getDb();
|
|
285
|
+
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(taskId) as any;
|
|
286
|
+
if (!task) return;
|
|
287
|
+
|
|
288
|
+
const now = new Date().toISOString();
|
|
289
|
+
db.run(
|
|
290
|
+
`INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason, created_at)
|
|
291
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
292
|
+
[task.spec_id, taskId, gateName, reason || "Nao informado", now]
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
console.warn(`\n[!] BYPASS REGISTRADO: Gate '${gateName}' foi ignorado para Task #${task.number}`);
|
|
296
|
+
if (reason) console.warn(` Motivo: ${reason}`);
|
|
297
|
+
console.warn(` Isso sera auditado no review.\n`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function validateGate(command: string, context: any = {}): GateResult {
|
|
301
|
+
const gates = GATES[command] || [];
|
|
302
|
+
|
|
303
|
+
for (const gate of gates) {
|
|
304
|
+
const result = executeCheck(gate.check, context);
|
|
305
|
+
|
|
306
|
+
if (!result.passed) {
|
|
307
|
+
return {
|
|
308
|
+
passed: false,
|
|
309
|
+
reason: result.details
|
|
310
|
+
? `${gate.message}: ${result.details}`
|
|
311
|
+
: gate.message,
|
|
312
|
+
resolution: gate.resolution,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { passed: true };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function enforceGate(command: string, context: any = {}): void {
|
|
321
|
+
const result = validateGate(command, context);
|
|
322
|
+
|
|
323
|
+
if (!result.passed) {
|
|
324
|
+
console.error(`\nBLOQUEADO: ${result.reason}`);
|
|
325
|
+
console.error(`Resolva: ${result.resolution}\n`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// v8.0: Validar conteúdo de arquivos criados (não apenas existência)
|
|
331
|
+
interface FileValidationResult {
|
|
332
|
+
valid: boolean;
|
|
333
|
+
reason?: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function validateFileContent(filePath: string): FileValidationResult {
|
|
337
|
+
// 1. Verificar existência
|
|
338
|
+
if (!existsSync(filePath)) {
|
|
339
|
+
return { valid: false, reason: "arquivo nao encontrado" };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
// 2. Verificar tamanho (arquivo vazio ou muito pequeno)
|
|
344
|
+
const stats = statSync(filePath);
|
|
345
|
+
if (stats.size === 0) {
|
|
346
|
+
return { valid: false, reason: "arquivo vazio (0 bytes)" };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 3. Ler conteúdo e verificar estrutura mínima
|
|
350
|
+
const content = readFileSync(filePath, "utf-8");
|
|
351
|
+
const trimmed = content.trim();
|
|
352
|
+
|
|
353
|
+
// Arquivo trivialmente pequeno (< 20 caracteres não é código real)
|
|
354
|
+
if (trimmed.length < 20) {
|
|
355
|
+
return { valid: false, reason: `conteudo trivial (${trimmed.length} chars)` };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 4. Validações específicas por extensão
|
|
359
|
+
const ext = extname(filePath).toLowerCase();
|
|
360
|
+
const validation = validateByExtension(ext, content);
|
|
361
|
+
if (!validation.valid) {
|
|
362
|
+
return validation;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return { valid: true };
|
|
366
|
+
} catch (error: any) {
|
|
367
|
+
return { valid: false, reason: `erro ao ler: ${error.message}` };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function validateByExtension(ext: string, content: string): FileValidationResult {
|
|
372
|
+
const trimmed = content.trim();
|
|
373
|
+
|
|
374
|
+
switch (ext) {
|
|
375
|
+
case ".ts":
|
|
376
|
+
case ".tsx":
|
|
377
|
+
case ".js":
|
|
378
|
+
case ".jsx":
|
|
379
|
+
// Arquivos JS/TS devem ter pelo menos uma declaração
|
|
380
|
+
if (!hasCodeStructure(trimmed, ["export", "import", "function", "const", "let", "var", "class", "interface", "type"])) {
|
|
381
|
+
return { valid: false, reason: "sem declaracoes validas (export, function, class, etc)" };
|
|
382
|
+
}
|
|
383
|
+
// TSX/JSX devem ter JSX ou export de componente
|
|
384
|
+
if ((ext === ".tsx" || ext === ".jsx") && !trimmed.includes("<") && !trimmed.includes("export")) {
|
|
385
|
+
return { valid: false, reason: "componente sem JSX ou export" };
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
|
|
389
|
+
case ".css":
|
|
390
|
+
case ".scss":
|
|
391
|
+
case ".sass":
|
|
392
|
+
// CSS deve ter pelo menos um seletor e propriedade
|
|
393
|
+
if (!trimmed.includes("{") || !trimmed.includes(":")) {
|
|
394
|
+
return { valid: false, reason: "CSS sem regras validas" };
|
|
395
|
+
}
|
|
396
|
+
break;
|
|
397
|
+
|
|
398
|
+
case ".json":
|
|
399
|
+
// JSON deve ser válido
|
|
400
|
+
try {
|
|
401
|
+
JSON.parse(content);
|
|
402
|
+
} catch {
|
|
403
|
+
return { valid: false, reason: "JSON invalido" };
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
|
|
407
|
+
case ".sql":
|
|
408
|
+
// SQL deve ter statements
|
|
409
|
+
if (!hasCodeStructure(trimmed.toUpperCase(), ["SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP"])) {
|
|
410
|
+
return { valid: false, reason: "SQL sem statements validos" };
|
|
411
|
+
}
|
|
412
|
+
break;
|
|
413
|
+
|
|
414
|
+
case ".py":
|
|
415
|
+
// Python deve ter definições
|
|
416
|
+
if (!hasCodeStructure(trimmed, ["def ", "class ", "import ", "from "])) {
|
|
417
|
+
return { valid: false, reason: "Python sem definicoes (def, class, import)" };
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
|
|
421
|
+
case ".go":
|
|
422
|
+
// Go deve ter package
|
|
423
|
+
if (!trimmed.includes("package ")) {
|
|
424
|
+
return { valid: false, reason: "Go sem declaracao package" };
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case ".md":
|
|
429
|
+
// Markdown deve ter conteúdo mínimo
|
|
430
|
+
if (trimmed.length < 50) {
|
|
431
|
+
return { valid: false, reason: "Markdown muito curto" };
|
|
432
|
+
}
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { valid: true };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function hasCodeStructure(content: string, keywords: string[]): boolean {
|
|
440
|
+
return keywords.some((keyword) => content.includes(keyword));
|
|
441
|
+
}
|