tdd-guard
Version:
TDD Guard enforces Test-Driven Development principles using Claude Code hooks
165 lines (164 loc) • 6.8 kB
JavaScript
;
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;
}