import { BaseBlackboardAgent, type AgentConfig } from "../BaseBlackboardAgent"; import { blackboardService } from "../service"; import { type BlackboardTask } from "@shared/schema"; import { toolManager } from "../../autonomous/tools"; const SYSTEM_PROMPT = `Você é o Agente Validador do Arcadia Suite. ## Seu Papel Você valida código gerado executando verificações reais de TypeScript e análise de qualidade. ## Verificações Realizadas (Quality Gates) 1. Gate 1: Verificação de tipos TypeScript (tsc --noEmit) 2. Gate 2: Análise de sintaxe e estrutura (lint) 3. Gate 3: Verificação de imports e dependências 4. Gate 4: Identificação de problemas de segurança ## Critérios de Aprovação - Score >= 60: Código aprovado para deploy - Score < 60: Código rejeitado, volta para correção ## Formato de Saída { "valid": true/false, "score": 0-100, "gates": { "typescript": true/false, "lint": true/false, "security": true/false }, "typeErrors": [], "lintErrors": [], "warnings": [], "summary": "resumo" }`; interface QualityGateResult { name: string; passed: boolean; errors: Array<{ file: string; line: number; message: string }>; warnings: Array<{ file: string; line: number; message: string }>; score: number; } export class ValidatorAgent extends BaseBlackboardAgent { constructor() { const config: AgentConfig = { name: "validator", displayName: "Agente Validador", description: "Valida código com TypeScript check real e quality gates", systemPrompt: SYSTEM_PROMPT, capabilities: [ "Verificação TypeScript", "Quality Gates", "Análise de tipos", "Lint check", "Detecção de erros", "Score de qualidade" ], pollInterval: 2000 }; super(config); } private checkTypescriptSyntax(content: string, filePath: string): Array<{ file: string; line: number; message: string }> { const errors: Array<{ file: string; line: number; message: string }> = []; const lines = content.split("\n"); let braceCount = 0; let parenCount = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNum = i + 1; const cleanLine = line.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, ""); braceCount += (cleanLine.match(/{/g) || []).length - (cleanLine.match(/}/g) || []).length; parenCount += (cleanLine.match(/\(/g) || []).length - (cleanLine.match(/\)/g) || []).length; if (line.includes("import") && !line.includes("from") && !line.includes("type")) { if (line.trim().startsWith("import") && !line.includes("{")) { errors.push({ file: filePath, line: lineNum, message: "Import incompleto" }); } } if (line.trim() === "export" || line.trim() === "export default") { errors.push({ file: filePath, line: lineNum, message: "Export sem conteúdo" }); } } if (braceCount !== 0) { errors.push({ file: filePath, line: lines.length, message: `Chaves desbalanceadas (${braceCount > 0 ? "faltando }" : "extra }"})` }); } if (parenCount !== 0) { errors.push({ file: filePath, line: lines.length, message: `Parênteses desbalanceados` }); } return errors; } private runLintGate(content: string, filePath: string): QualityGateResult { const errors: Array<{ file: string; line: number; message: string }> = []; const warnings: Array<{ file: string; line: number; message: string }> = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNum = i + 1; if (line.includes("console.log") && !filePath.includes("test")) { warnings.push({ file: filePath, line: lineNum, message: "console.log em código de produção" }); } if (line.includes("// TODO") || line.includes("// FIXME") || line.includes("// HACK")) { warnings.push({ file: filePath, line: lineNum, message: "Comentário TODO/FIXME/HACK encontrado" }); } if (/\bany\b/.test(line) && !line.includes("//") && !line.includes("*")) { if (line.includes(": any") || line.includes("as any")) { warnings.push({ file: filePath, line: lineNum, message: "Uso de 'any' detectado - considere tipagem específica" }); } } if (line.includes("eval(")) { errors.push({ file: filePath, line: lineNum, message: "Uso de eval() é proibido por segurança" }); } if (line.includes("innerHTML") && !filePath.includes(".test.")) { warnings.push({ file: filePath, line: lineNum, message: "innerHTML pode causar XSS - use textContent ou sanitize" }); } if (line.length > 200) { warnings.push({ file: filePath, line: lineNum, message: "Linha muito longa (>200 caracteres)" }); } if (/catch\s*\(\s*\w*\s*\)\s*\{\s*\}/.test(line)) { warnings.push({ file: filePath, line: lineNum, message: "Catch vazio - trate ou registre o erro" }); } } if (content.includes("password") || content.includes("secret") || content.includes("api_key")) { if (content.includes("= \"") || content.includes("= '")) { const match = content.match(/(password|secret|api_key)\s*=\s*["'][^"']+["']/i); if (match) { errors.push({ file: filePath, line: 0, message: "Possível credencial hardcoded detectada" }); } } } const errorPenalty = errors.length * 20; const warningPenalty = warnings.length * 3; const score = Math.max(0, 100 - errorPenalty - warningPenalty); return { name: "lint", passed: errors.length === 0, errors, warnings, score, }; } private runSecurityGate(content: string, filePath: string): QualityGateResult { const errors: Array<{ file: string; line: number; message: string }> = []; const warnings: Array<{ file: string; line: number; message: string }> = []; const lines = content.split("\n"); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineNum = i + 1; if (/require\s*\(\s*[^'"]*\+/.test(line) || /require\s*\(\s*`/.test(line)) { errors.push({ file: filePath, line: lineNum, message: "Dynamic require detectado - risco de injeção de código" }); } if (line.includes("exec(") || line.includes("execSync(")) { if (!filePath.includes("RunCommandTool") && !filePath.includes("TypeCheckTool")) { warnings.push({ file: filePath, line: lineNum, message: "Uso de exec/execSync - verificar sanitização de input" }); } } if (line.includes("dangerouslySetInnerHTML")) { warnings.push({ file: filePath, line: lineNum, message: "dangerouslySetInnerHTML detectado - garantir sanitização" }); } if (/process\.env\.\w+/.test(line) && line.includes("console")) { errors.push({ file: filePath, line: lineNum, message: "Possível exposição de variável de ambiente em log" }); } } const score = Math.max(0, 100 - (errors.length * 25) - (warnings.length * 5)); return { name: "security", passed: errors.length === 0, errors, warnings, score, }; } canHandle(task: BlackboardTask): boolean { const context = task.context as any; return context?.phase === "validation" || task.assignedAgent === "validator"; } async process(task: BlackboardTask): Promise { await this.log(task.id, "thinking", "Iniciando validação com quality gates..."); const codeArtifacts = await blackboardService.getArtifactsForTask(task.id, "code"); if (codeArtifacts.length === 0) { await blackboardService.failTask(task.id, "validator", "Nenhum código para validar"); return; } await this.log(task.id, "validating", `Executando quality gates em ${codeArtifacts.length} arquivos...`); const allGates: QualityGateResult[] = []; const allTypeErrors: Array<{ file: string; line: number; message: string }> = []; for (const artifact of codeArtifacts) { const content = artifact.content || ""; const filePath = artifact.name; const pathValidation = blackboardService.validateFilePath(filePath); if (!pathValidation.valid) { allTypeErrors.push({ file: filePath, line: 0, message: `Path inválido: ${pathValidation.error}` }); continue; } const contentValidation = blackboardService.validateContent(content); if (!contentValidation.valid) { allTypeErrors.push({ file: filePath, line: 0, message: `Conteúdo inválido: ${contentValidation.error}` }); continue; } const syntaxErrors = this.checkTypescriptSyntax(content, filePath); allTypeErrors.push(...syntaxErrors); const lintResult = this.runLintGate(content, filePath); allGates.push(lintResult); const securityResult = this.runSecurityGate(content, filePath); allGates.push(securityResult); } const typeCheckResult = await toolManager.execute("typecheck", {}); const repoErrors = typeCheckResult.data?.errors || []; const typeGate: QualityGateResult = { name: "typescript", passed: allTypeErrors.length === 0 && repoErrors.length <= 5, errors: [...allTypeErrors, ...repoErrors.slice(0, 10)], warnings: [], score: Math.max(0, 100 - (allTypeErrors.length * 15) - (repoErrors.length * 5)), }; allGates.unshift(typeGate); const codeContent = codeArtifacts.map(a => `\n--- ${a.name} ---\n${a.content?.slice(0, 1000)}`).join("\n\n"); const allErrors = [...allTypeErrors, ...repoErrors.slice(0, 10)]; const analysisPrompt = `Analise este código TypeScript/React e identifique problemas: ${codeContent.slice(0, 3000)} Erros encontrados na validação: ${allErrors.slice(0, 10).map((e: any) => `- ${e.file}:${e.line} - ${e.message}`).join("\n") || "Nenhum erro encontrado"} Retorne JSON: { "issues": [{"severity": "error|warning", "message": "desc"}], "suggestions": [], "codeQuality": 0-100 }`; let aiAnalysis: any = { issues: [], suggestions: [], codeQuality: 80 }; try { const response = await this.generateWithAI(analysisPrompt); const parsed = JSON.parse(response); aiAnalysis = { issues: Array.isArray(parsed.issues) ? parsed.issues : [], suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [], codeQuality: typeof parsed.codeQuality === 'number' ? parsed.codeQuality : 80 }; } catch {} const gateScores = allGates.map(g => g.score); const avgGateScore = gateScores.length > 0 ? gateScores.reduce((a, b) => a + b, 0) / gateScores.length : 80; const totalErrors = allGates.reduce((sum, g) => sum + g.errors.length, 0); const totalWarnings = allGates.reduce((sum, g) => sum + g.warnings.length, 0); let score = Math.round((avgGateScore + (aiAnalysis.codeQuality || 80)) / 2); score = Math.max(0, Math.min(100, score)); const gatesSummary: Record = {}; const uniqueGates = new Map(); for (const gate of allGates) { const current = uniqueGates.get(gate.name); uniqueGates.set(gate.name, current === undefined ? gate.passed : current && gate.passed); } uniqueGates.forEach((passed, name) => { gatesSummary[name] = passed; }); const validation = { valid: score >= 60, score, gates: gatesSummary, typeErrors: allErrors.slice(0, 20), lintErrors: allGates.filter(g => g.name === "lint").flatMap(g => g.errors).slice(0, 20), securityIssues: allGates.filter(g => g.name === "security").flatMap(g => g.errors).slice(0, 10), issues: aiAnalysis.issues || [], suggestions: aiAnalysis.suggestions || [], warnings: allGates.flatMap(g => g.warnings).slice(0, 20), gateDetails: allGates.map(g => ({ name: g.name, passed: g.passed, score: g.score, errorCount: g.errors.length, warningCount: g.warnings.length })), summary: score >= 60 ? `Código aprovado (score ${score}). Gates: ${Object.entries(gatesSummary).map(([k, v]) => `${k}:${v ? "OK" : "FALHA"}`).join(", ")}. ${totalErrors} erros, ${totalWarnings} avisos.` : `Código reprovado (score ${score}). Gates falhados: ${Object.entries(gatesSummary).filter(([_, v]) => !v).map(([k]) => k).join(", ")}. ${totalErrors} erros.`, }; await blackboardService.addArtifact( task.id, "doc", "validation-report.json", JSON.stringify(validation, null, 2), "validator" ); await this.log(task.id, "completed", `Validação: score ${score}, ${validation.valid ? "APROVADO" : "REPROVADO"}. Gates: ${JSON.stringify(gatesSummary)}`); const mainTask = await blackboardService.getMainTask(task.id); if (mainTask) { if (validation.valid) { await blackboardService.createSubtask( mainTask.id, "Executar deploy", "Aplicar código validado ao projeto", "executor", [task.id], { phase: "deploy" } ); } else { await this.log(task.id, "blocked", "Código reprovado - deploy bloqueado"); } } await blackboardService.completeTask(task.id, "validator", validation); } } export const validatorAgent = new ValidatorAgent();