UNPKG

ccguard

Version:

Automated enforcement of net-negative LOC, complexity constraints, and quality standards for Claude code

147 lines 6.91 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Validator = void 0; exports.createValidator = createValidator; const locCounter_1 = require("./locCounter"); const GuardManager_1 = require("../ccguard/GuardManager"); const ConfigLoader_1 = require("../config/ConfigLoader"); class Validator { locCounter; guardManager; configLoader; constructor(storage, configLoader) { this.configLoader = configLoader ?? new ConfigLoader_1.ConfigLoader(); const config = this.configLoader.getConfig(); this.locCounter = new locCounter_1.LocCounter({ ignoreEmptyLines: config.enforcement.ignoreEmptyLines }); this.guardManager = new GuardManager_1.GuardManager(storage, this.configLoader); } async validate(context) { // Check if guard is enabled if (!context.guardEnabled) { return { decision: 'approve', reason: 'CCGuard is disabled', }; } // Check if file is locked const filePath = this.getFilePath(context.operation); if (filePath) { const isLocked = await this.guardManager.isFileLocked(filePath); if (isLocked) { return { decision: 'block', reason: `File is locked and cannot be modified: ${filePath}\n\nTo unlock this file, use: ccguard unlock @${filePath}`, }; } } // Check if file is whitelisted if (filePath && this.configLoader.isFileWhitelisted(filePath)) { return { decision: 'approve', reason: 'File is whitelisted - LOC enforcement skipped', }; } // Calculate LOC change for this operation const change = this.locCounter.calculateChange(context.operation.tool_name, context.operation.tool_input); const config = await this.guardManager.getConfig(); // Use hot config // Per-operation mode: check this operation only if (config.enforcement.mode === 'per-operation') { const threshold = config.thresholds?.allowedPositiveLines ?? 0; const decision = change.netChange > threshold ? 'block' : 'approve'; const result = decision === 'block' ? this.createBlockResponse(change, null, true) : this.createApproveResponse(change, null, true); // Track operation in history if (filePath) { await this.guardManager.addOperationToHistory({ toolName: context.operation.tool_name, filePath, linesAdded: change.linesAdded, linesRemoved: change.linesRemoved, netChange: change.netChange, decision, reason: result.reason.split('\n')[0] // First line only }); } return result; } // Session-wide mode: calculate what stats would be const currentStats = await this.guardManager.getSessionStats(); const projectedStats = { totalLinesAdded: (currentStats?.totalLinesAdded ?? 0) + change.linesAdded, totalLinesRemoved: (currentStats?.totalLinesRemoved ?? 0) + change.linesRemoved, netChange: 0, operationCount: (currentStats?.operationCount ?? 0) + 1, lastUpdated: new Date().toISOString(), }; projectedStats.netChange = projectedStats.totalLinesAdded - projectedStats.totalLinesRemoved; const threshold = config.thresholds?.allowedPositiveLines ?? 0; const decision = projectedStats.netChange > threshold ? 'block' : 'approve'; let result; if (decision === 'block') { result = this.createBlockResponse(change, projectedStats); } else { // Only update stats if approved const updatedStats = await this.guardManager.updateSessionStats(change.linesAdded, change.linesRemoved); result = this.createApproveResponse(change, updatedStats); } // Track operation in history if (filePath) { await this.guardManager.addOperationToHistory({ toolName: context.operation.tool_name, filePath, linesAdded: change.linesAdded, linesRemoved: change.linesRemoved, netChange: change.netChange, decision, reason: result.reason.split('\n')[0] // First line only }); } return result; } getFilePath(operation) { const input = operation.tool_input; return input?.file_path ?? null; } createBlockResponse(change, stats, perOperation = false) { const changeStr = change.netChange > 0 ? `+${change.netChange}` : `${change.netChange}`; const totalStr = stats ? (stats.netChange > 0 ? `+${stats.netChange}` : `${stats.netChange}`) : 'N/A'; let reason = `Operation blocked: Net positive LOC change detected!\n\n`; reason += `This operation would:\n`; reason += ` • Add ${change.linesAdded} lines\n`; reason += ` • Remove ${change.linesRemoved} lines\n`; reason += ` • Net change: ${changeStr} lines\n\n`; reason += perOperation ? `` : `Session total would become: ${totalStr} lines\n\n`; reason += `Suggestions:\n`; reason += ` • Use MultiEdit to batch this change with code removal in other files\n`; reason += ` (e.g., add feature in one file while removing old code in another)\n`; reason += ` • Refactor existing code to be more concise before adding new code\n`; reason += ` • Extract common patterns to reduce duplication\n`; reason += ` • Remove unnecessary code, comments, or deprecated features\n`; reason += ` • Consider if this feature is truly needed`; return { decision: 'block', reason, }; } createApproveResponse(change, stats, perOperation = false) { const changeStr = change.netChange > 0 ? `+${change.netChange}` : `${change.netChange}`; const totalStr = stats ? (stats.netChange > 0 ? `+${stats.netChange}` : `${stats.netChange}`) : 'N/A'; let reason = `Operation approved\n\n`; reason += perOperation ? `LOC change: ${changeStr}` : `LOC change: ${changeStr} (Session total: ${totalStr})`; return { decision: 'approve', reason, }; } } exports.Validator = Validator; // Export a factory function for easier use async function createValidator(storage, configLoader) { const validator = new Validator(storage, configLoader); return (context) => validator.validate(context); } //# sourceMappingURL=validator.js.map