UNPKG

tdd-guard

Version:

TDD Guard enforces Test-Driven Development principles using Claude Code hooks

132 lines (131 loc) 4.77 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PostToolLintHandler = exports.DEFAULT_RESULT = void 0; exports.handlePostToolLint = handlePostToolLint; const toolSchemas_1 = require("../contracts/schemas/toolSchemas"); const lintSchemas_1 = require("../contracts/schemas/lintSchemas"); exports.DEFAULT_RESULT = { decision: undefined, reason: '' }; class PostToolLintHandler { linter; storage; constructor(storage, linter) { this.storage = storage; this.linter = linter ?? null; } async handle(hookData) { // If no linter is configured, skip linting if (!this.linter) { return exports.DEFAULT_RESULT; } return handlePostToolLint(hookData, this.storage, this.linter); } } exports.PostToolLintHandler = PostToolLintHandler; function parseAndValidateHookData(hookData) { try { const parsedData = JSON.parse(hookData); const hookResult = toolSchemas_1.HookDataSchema.safeParse(parsedData); if (!hookResult.success) { return null; } // Only process PostToolUse hooks if (hookResult.data.hook_event_name !== 'PostToolUse') { return null; } return hookResult.data; } catch { return null; } } async function getStoredLintData(storage) { try { const lintDataStr = await storage.getLint(); if (lintDataStr) { return lintSchemas_1.LintDataSchema.parse(JSON.parse(lintDataStr)); } } catch { // Treat any error as no stored data } return null; } function createLintData(lintResults, storedLintData) { const hasIssues = lintResults.errorCount > 0 || lintResults.warningCount > 0; return { ...lintResults, hasNotifiedAboutLintIssues: hasIssues ? (storedLintData?.hasNotifiedAboutLintIssues ?? false) // Preserve flag when issues exist : false // Reset flag when no issues }; } function createBlockResult(lintData) { const formattedIssues = formatLintIssues(lintData.issues); const summary = `\n✖ ${lintData.errorCount + lintData.warningCount} problems (${lintData.errorCount} errors, ${lintData.warningCount} warnings)`; return { decision: 'block', reason: `Lint issues detected:${formattedIssues}\n${summary}\n\nPlease fix these issues before proceeding.` }; } function formatLintIssues(issues) { const issuesByFile = new Map(); for (const issue of issues) { if (!issuesByFile.has(issue.file)) { issuesByFile.set(issue.file, []); } const ruleInfo = issue.rule ? ` ${issue.rule}` : ''; issuesByFile.get(issue.file).push(` ${issue.line}:${issue.column} ${issue.severity} ${issue.message}${ruleInfo}`); } let formattedIssues = ''; for (const [file, fileIssues] of issuesByFile) { formattedIssues += `\n${file}\n${fileIssues.join('\n')}`; } return formattedIssues; } async function handlePostToolLint(hookData, storage, linter) { const validatedHookData = parseAndValidateHookData(hookData); if (!validatedHookData) { return exports.DEFAULT_RESULT; } // Extract file paths from tool operation const filePaths = extractFilePaths(validatedHookData); if (filePaths.length === 0) { return exports.DEFAULT_RESULT; } // Get current lint data to check hasNotifiedAboutLintIssues state const storedLintData = await getStoredLintData(storage); // Run linting on the files const lintResults = await linter.lint(filePaths); // Create and save lint data const lintData = createLintData(lintResults, storedLintData); await storage.saveLint(JSON.stringify(lintData)); const hasIssues = lintResults.errorCount > 0 || lintResults.warningCount > 0; // Block if: // 1. PreToolUse has notified (flag is true) // 2. There are still issues if (storedLintData?.hasNotifiedAboutLintIssues && hasIssues) { return createBlockResult(lintData); } return exports.DEFAULT_RESULT; } function extractFilePaths(hookData) { const toolInput = hookData.tool_input; if (!toolInput || typeof toolInput !== 'object') return []; const paths = []; if ('file_path' in toolInput && typeof toolInput.file_path === 'string') { paths.push(toolInput.file_path); } // Handle multi-edit operations if ('edits' in toolInput && Array.isArray(toolInput.edits)) { for (const edit of toolInput.edits) { if ('file_path' in edit && typeof edit.file_path === 'string') { paths.push(edit.file_path); } } } return [...new Set(paths)]; // Remove duplicates }