/*! * SPDX-License-Identifier: AGPL-3.0-or-later * Copyright © 2026 Diego Lima Nogueira de Paula * * /graph-review — REVIEW phase: code review, blast radius, diff analysis. * No MCP dependency. Operates directly against SqliteStore. */ import type { SkillHandlerPort, SkillExecutionContext } from "../../tui/skill-handler-port.js"; import { fmtElapsed, fmtNode } from "../shared/handler-utils.js"; import { checkDefinitionOfDone } from "../../core/implementer/definition-of-done.js"; import { createLogger } from "../../core/utils/logger.js"; const _log = createLogger({ layer: "core", source: "graph-review.ts" }); export class GraphReviewHandler implements SkillHandlerPort { async execute(args: string, ctx: SkillExecutionContext): Promise { const { store, onProgress } = ctx; const startMs = Date.now(); const lines: string[] = ["═ /graph-review ═"]; const doc = store.toGraphDocument(); // Step 1: Find tasks ready for review (done but unreviewed) onProgress({ step: 1, total: 5, label: "Tasks para revisão...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const doneRecently = doc.nodes .filter((n) => n.status === "done" && (n.type === "task" || n.type === "subtask")) .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); lines.push(`Tasks done: ${doneRecently.length}`); const reviewCandidates = doneRecently.slice(0, 10); for (const task of reviewCandidates) { const dod = checkDefinitionOfDone(doc, task.id); lines.push(` ${fmtNode(task)} — DoD: ${dod.grade} (${dod.score}%)`); } // Step 2: Check test files onProgress({ step: 2, total: 5, label: "Verificando testFiles...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const withTests = reviewCandidates.filter((t) => t.testFiles && t.testFiles.length > 0); const withoutTests = reviewCandidates.filter((t) => !t.testFiles || t.testFiles.length === 0); lines.push(`Com testFiles: ${withTests.length} · Sem testFiles: ${withoutTests.length}`); // Step 3: Check descriptions onProgress({ step: 3, total: 5, label: "Qualidade das descrições...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const withDesc = doneRecently.filter((t) => t.description && t.description.length > 20); lines.push(`Com descrição detalhada: ${withDesc.length}/${doneRecently.length}`); // Step 4: Dependency integrity onProgress({ step: 4, total: 5, label: "Integridade de dependências...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const deps = doc.edges.filter((e) => e.relationType === "depends_on"); const doneIds = new Set(doc.nodes.filter((n) => n.status === "done").map((n) => n.id)); const unresolved = deps.filter((d) => doneIds.has(d.from) && !doneIds.has(d.to)); if (unresolved.length > 0) { lines.push(`⚠ ${unresolved.length} dependência(s) de tasks done para não-done:`); for (const d of unresolved.slice(0, 5)) { const from = doc.nodes.find((n) => n.id === d.from); const to = doc.nodes.find((n) => n.id === d.to); lines.push(` ${from?.title ?? d.from} → ${to?.title ?? d.to} (${to?.status ?? "?"})`); } } else { lines.push("✓ Todas as dependências estão consistentes"); } // Step 5: Project health onProgress({ step: 5, total: 5, label: "Saúde do projeto...", elapsedMs: Date.now() - startMs, tokensUsed: 0 }); const stats = store.getStats(); const completion = stats.totalNodes > 0 ? Math.round(((stats.byStatus.done ?? 0) / stats.totalNodes) * 100) : 0; lines.push(`Progresso: ${completion}% done (${stats.byStatus.done ?? 0}/${stats.totalNodes})`); lines.push(`═ ${fmtElapsed(Date.now() - startMs)} ═`); return lines.join("\n"); } }