UNPKG

tdd-guard

Version:

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

165 lines (164 loc) 6.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.defaultResult = void 0; exports.processHookData = processHookData; const buildContext_1 = require("../cli/buildContext"); const HookEvents_1 = require("./HookEvents"); const postToolLint_1 = require("./postToolLint"); const fileTypeDetection_1 = require("./fileTypeDetection"); const LinterProvider_1 = require("../providers/LinterProvider"); const userPromptHandler_1 = require("./userPromptHandler"); const sessionHandler_1 = require("./sessionHandler"); const GuardManager_1 = require("../guard/GuardManager"); const FileStorage_1 = require("../storage/FileStorage"); const toolSchemas_1 = require("../contracts/schemas/toolSchemas"); const pytestSchemas_1 = require("../contracts/schemas/pytestSchemas"); const reporterSchemas_1 = require("../contracts/schemas/reporterSchemas"); const lintSchemas_1 = require("../contracts/schemas/lintSchemas"); exports.defaultResult = { decision: undefined, reason: '', }; function extractFilePath(parsedData) { if (!parsedData || typeof parsedData !== 'object') { return null; } const data = parsedData; const toolInput = data.tool_input; if (!toolInput || typeof toolInput !== 'object' || !('file_path' in toolInput)) { return null; } const filePath = toolInput.file_path; if (typeof filePath !== 'string') { return null; } return filePath; } async function processHookData(inputData, deps = {}) { const parsedData = JSON.parse(inputData); // Initialize dependencies const storage = deps.storage ?? new FileStorage_1.FileStorage(); const guardManager = new GuardManager_1.GuardManager(storage); const userPromptHandler = deps.userPromptHandler ?? new userPromptHandler_1.UserPromptHandler(guardManager); // Skip validation for ignored files based on patterns const filePath = extractFilePath(parsedData); if (filePath && await guardManager.shouldIgnoreFile(filePath)) { return exports.defaultResult; } const sessionHandler = new sessionHandler_1.SessionHandler(storage); // Process SessionStart events if (parsedData.hook_event_name === 'SessionStart') { await sessionHandler.processSessionStart(inputData); return exports.defaultResult; } // Process user commands const stateResult = await userPromptHandler.processUserCommand(inputData); if (stateResult) { return stateResult; } // Check if guard is disabled and return early if so const disabledResult = await userPromptHandler.getDisabledResult(); if (disabledResult) { return disabledResult; } // Create lintHandler with linter from provider const linterProvider = new LinterProvider_1.LinterProvider(); const linter = linterProvider.getLinter(); const lintHandler = new postToolLint_1.PostToolLintHandler(storage, linter); const hookResult = toolSchemas_1.HookDataSchema.safeParse(parsedData); if (!hookResult.success) { return exports.defaultResult; } await processHookEvent(parsedData, storage); // Check if this is a PostToolUse event if (hookResult.data.hook_event_name === 'PostToolUse') { return await lintHandler.handle(inputData); } if (shouldSkipValidation(hookResult.data)) { return exports.defaultResult; } // For PreToolUse, check if we should notify about lint issues if (hookResult.data.hook_event_name === 'PreToolUse') { const lintNotification = await checkLintNotification(storage, hookResult.data); if (lintNotification.decision === 'block') { return lintNotification; } } return await performValidation(deps, hookResult.data); } async function processHookEvent(parsedData, storage) { if (storage) { const hookEvents = new HookEvents_1.HookEvents(storage); await hookEvents.processEvent(parsedData); } } function shouldSkipValidation(hookData) { const operationResult = toolSchemas_1.ToolOperationSchema.safeParse({ ...hookData, tool_input: hookData.tool_input, }); return !operationResult.success || (0, toolSchemas_1.isTodoWriteOperation)(operationResult.data); } async function performValidation(deps, hookData) { if (deps.validator && deps.storage) { const context = await (0, buildContext_1.buildContext)(deps.storage, hookData); return await deps.validator(context); } return exports.defaultResult; } async function checkLintNotification(storage, hookData) { // Get test results to check if tests are passing let testsPassing = false; try { const testStr = await storage.getTest(); if (testStr) { const fileType = (0, fileTypeDetection_1.detectFileType)(hookData); const testResult = fileType === 'python' ? pytestSchemas_1.PytestResultSchema.safeParse(JSON.parse(testStr)) : reporterSchemas_1.TestResultSchema.safeParse(JSON.parse(testStr)); if (testResult.success) { testsPassing = (0, reporterSchemas_1.isTestPassing)(testResult.data); } } } catch { testsPassing = false; } // Only proceed if tests are passing if (!testsPassing) { return exports.defaultResult; } // Get lint data let lintData; try { const lintStr = await storage.getLint(); if (lintStr) { lintData = lintSchemas_1.LintDataSchema.parse(JSON.parse(lintStr)); } } catch { return exports.defaultResult; } // Only proceed if lint data exists if (!lintData) { return exports.defaultResult; } const hasIssues = lintData.errorCount > 0 || lintData.warningCount > 0; // Block if: // 1. Tests are passing (already checked) // 2. There are lint issues // 3. hasNotifiedAboutLintIssues is false (not yet notified) if (hasIssues && !lintData.hasNotifiedAboutLintIssues) { // Update the notification flag and save const updatedLintData = { ...lintData, hasNotifiedAboutLintIssues: true }; await storage.saveLint(JSON.stringify(updatedLintData)); return { decision: 'block', reason: 'Code quality issues detected. You need to fix those first before making any other changes. Remember to exercise system thinking and design awareness to ensure continuous architectural improvements. Consider: design patterns, SOLID principles, DRY, types and interfaces, and architectural improvements. Apply equally to implementation and test code. Use test data factories, helpers, and beforeEach to better organize tests.' }; } return exports.defaultResult; }