@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.
@@ -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
+ }