/*! * SPDX-License-Identifier: AGPL-3.0-or-later * Copyright © 2026 Diego Lima Nogueira de Paula * * /graph-quality — Code quality audit: lint, typecheck, complexity, naming. * No MCP dependency. Operates directly against SqliteStore + FS. */ import type { SkillHandlerPort, SkillExecutionContext } from "../../tui/skill-handler-port.js"; import { fmtElapsed } from "../shared/handler-utils.js"; import { createLogger } from "../../core/utils/logger.js"; const _log = createLogger({ layer: "core", source: "graph-quality.ts" }); export class GraphQualityHandler implements SkillHandlerPort { async execute(args: string, ctx: SkillExecutionContext): Promise { const { store, dir, onProgress } = ctx; const startMs = Date.now(); const lines: string[] = ["═ /graph-quality ═"]; // Step 1: ESLint onProgress({ step: 1, total: 4, label: "Rodando ESLint...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); try { const { execSync } = await import("node:child_process"); const lintOut = execSync("npx eslint src/ --quiet 2>&1 || true", { timeout: 60000, cwd: dir }); const text = lintOut.toString(); const errorMatch = text.match(/(\d+) errors?/); const warnMatch = text.match(/(\d+) warnings?/); const errors = errorMatch ? parseInt(errorMatch[1], 10) : 0; const warnings = warnMatch ? parseInt(warnMatch[1], 10) : 0; lines.push(`ESLint: ${errors} errors · ${warnings} warnings`); if (errors === 0 && warnings === 0) lines.push(" ✓ Código limpo"); } catch { lines.push(" ESLint: indisponível"); } // Step 2: TypeScript check onProgress({ step: 2, total: 4, label: "TypeScript typecheck...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); try { const { execSync } = await import("node:child_process"); const tsOut = execSync("npx tsc --noEmit 2>&1 || true", { timeout: 60000, cwd: dir }); const lines2 = tsOut.toString().trim().split("\n").filter(Boolean); const errorCount = lines2.filter((l) => l.includes("error TS")).length; if (errorCount > 0) { lines.push(`TypeScript: ${errorCount} erro(s)`); for (const l of lines2.slice(0, 5)) lines.push(` • ${l.split("(")[0]}`); } else { lines.push(" ✓ Sem erros de tipo"); } } catch { lines.push(" TypeScript: indisponível"); } // Step 3: Graph quality metrics onProgress({ step: 3, total: 4, label: "Métricas de qualidade do grafo...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const doc = store.toGraphDocument(); const tasksWithoutAC = doc.nodes.filter((n) => (n.type === "task" || n.type === "subtask") && (!n.acceptanceCriteria || n.acceptanceCriteria.length === 0)); const tasksWithoutDesc = doc.nodes.filter((n) => (n.type === "task" || n.type === "subtask") && (!n.description || n.description.length < 10)); lines.push(`Tasks sem AC: ${tasksWithoutAC.length} · Sem descrição: ${tasksWithoutDesc.length}`); // Step 4: Summary onProgress({ step: 4, total: 4, label: "Resumo...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const stats = store.getStats(); lines.push(`Total: ${stats.totalNodes} nós · ${stats.totalEdges} arestas`); if (tasksWithoutAC.length > 0) { lines.push("Recomendação: adicione acceptance criteria às tasks pendentes."); } if (tasksWithoutDesc.length > 0) { lines.push("Recomendação: adicione descrições às tasks."); } lines.push(`═ ${fmtElapsed(Date.now() - startMs)} ═`); return lines.join("\n"); } }