arcadia-suite-sv/server/blackboard/agents/ValidatorAgent.ts

344 lines
13 KiB
TypeScript

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<void> {
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<string, boolean> = {};
const uniqueGates = new Map<string, boolean>();
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();