UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

532 lines 19.6 kB
/** * Laziness Detection Hook * * Pre-write hook that detects and blocks destructive avoidance patterns * including test deletion, feature removal, and coverage regression. * * @implements @.aiwg/requirements/use-cases/UC-AP-001-detect-test-deletion.md * @implements @.aiwg/requirements/use-cases/UC-AP-002-detect-feature-removal.md * @implements @.aiwg/requirements/use-cases/UC-AP-003-detect-coverage-regression.md * @schema @.aiwg/patterns/laziness-patterns.yaml * @agent @agentic/code/frameworks/sdlc-complete/agents/laziness-detector.md */ import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'js-yaml'; /** * Laziness Detection Hook * * Analyzes pending file changes for avoidance patterns. */ export class LazinessDetectionHook { constructor(patternsPath) { const defaultPath = path.join(__dirname, '../../.aiwg/patterns/laziness-patterns.yaml'); const patternFile = patternsPath || defaultPath; try { const content = fs.readFileSync(patternFile, 'utf8'); yaml.load(content); // Validate patterns file loads } catch { // Patterns file is optional — all detection logic is hardcoded. // Missing file is expected in CI and fresh project checkouts // where .aiwg/ doesn't exist. } } /** * Main entry point - analyze file changes before write */ async analyze(changes) { const detectedPatterns = []; // Capture baseline metrics await this.captureBaseline(); // Run pattern detection for (const change of changes) { const patterns = await this.detectPatternsInChange(change); detectedPatterns.push(...patterns); } // Assess overall severity const decision = this.makeBlockDecision(detectedPatterns); return decision; } /** * Detect patterns in a single file change */ async detectPatternsInChange(change) { const detected = []; // Test deletion patterns if (this.isTestFile(change.path)) { detected.push(...this.detectTestDeletion(change)); detected.push(...this.detectTestDisabling(change)); detected.push(...this.detectAssertionWeakening(change)); } // Feature removal patterns (in source files) if (this.isSourceFile(change.path) && !this.isTestFile(change.path)) { detected.push(...this.detectFeatureRemoval(change)); detected.push(...this.detectValidationRemoval(change)); detected.push(...this.detectErrorHandlerDeletion(change)); detected.push(...this.detectHardcodedBypass(change)); detected.push(...this.detectErrorSuppression(change)); detected.push(...this.detectTodoAccumulation(change)); } // Config changes if (this.isConfigFile(change.path)) { detected.push(...this.detectFeatureFlagDisabling(change)); } return detected; } /** * Pattern: LP-001 - Complete Test File Deletion */ detectTestDeletion(change) { if (change.type !== 'deleted') return []; const deletionPercentage = change.linesDeleted / (change.linesAdded + change.linesDeleted); if (deletionPercentage > 0.9) { return [ { id: 'LP-001', name: 'Complete Test File Deletion', category: 'test_deletion', severity: 'CRITICAL', file: change.path, match: `Entire test file deleted (${change.linesDeleted} lines)`, confidence: 1.0, }, ]; } return []; } /** * Pattern: LP-002, LP-003 - Test Suite/Individual Test Disabling */ detectTestDisabling(change) { const detected = []; const lines = change.diff.split('\n'); const skipPatterns = [ /^\+.*describe\.skip\(/, /^\+.*it\.skip\(/, /^\+.*test\.skip\(/, /^\+.*xit\(/, /^\+.*xtest\(/, /^\+\s*@Ignore/, // Fixed: allows leading whitespace /^\+.*@pytest\.mark\.skip/, ]; let skipCount = 0; let isSuiteSkip = false; let isIgnoreAnnotation = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const pattern of skipPatterns) { if (pattern.test(line)) { skipCount++; // Check if it's a suite skip (describe.skip) if (/describe\.skip/.test(line)) { isSuiteSkip = true; } // Check if it's @Ignore annotation if (/@Ignore/.test(line)) { isIgnoreAnnotation = true; } // Only record individual occurrences if threshold not met if (skipCount === 1) { detected.push({ id: isSuiteSkip || isIgnoreAnnotation ? 'LP-002' : 'LP-003', name: isSuiteSkip || isIgnoreAnnotation ? 'Test Suite Disabling' : 'Individual Test Disabling', category: 'test_deletion', severity: isSuiteSkip || isIgnoreAnnotation ? 'HIGH' : 'MEDIUM', file: change.path, line: i + 1, match: line.trim(), confidence: 0.9, }); } } } } // If multiple tests disabled, escalate severity if (skipCount > 3) { detected[0].severity = 'HIGH'; detected[0].id = 'LP-003'; detected[0].name = 'Multiple Individual Test Disabling'; detected[0].match = `${skipCount} tests disabled across file`; } return detected; } /** * Pattern: LP-012 - Trivial Assertion Replacement */ detectAssertionWeakening(change) { const detected = []; const lines = change.diff.split('\n'); const trivialPatterns = [ /expect\(true\)\.toBe\(true\)/, /expect\(1\)\.toBe\(1\)/, /expect\(false\)\.toBe\(false\)/, /assert\(True\)/, /assert True/, ]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('+')) { for (const pattern of trivialPatterns) { if (pattern.test(line)) { detected.push({ id: 'LP-012', name: 'Trivial Assertion Replacement', category: 'assertion_weakening', severity: 'CRITICAL', file: change.path, line: i + 1, match: line.trim(), confidence: 1.0, }); } } } } return detected; } /** * Pattern: LP-005 - Feature Code Commenting */ detectFeatureRemoval(change) { const lines = change.diff.split('\n'); let commentedLineCount = 0; const commentedBlocks = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Detect lines changed to comments (added lines that start with //) if (line.startsWith('+') && /^\+\s*\/\//.test(line)) { // Filter out documentation comments (descriptive text, not code) const commentContent = line.replace(/^\+\s*\/\/\s*/, '').trim(); // Skip if it looks like pure documentation (long explanatory sentences) // But count TODO/FIXME/code-like comments const isDocumentation = /^(This function|This class|This method|Parameters:|Returns:|@param|@returns)/i.test(commentContent); if (!isDocumentation) { commentedLineCount++; commentedBlocks.push(i + 1); } } } if (commentedLineCount > 10) { return [ { id: 'LP-005', name: 'Feature Code Commenting', category: 'feature_removal', severity: 'HIGH', file: change.path, line: commentedBlocks[0], match: `${commentedLineCount} lines commented out`, confidence: 0.85, }, ]; } return []; } /** * Pattern: TODO/FIXME Accumulation (MEDIUM severity) */ detectTodoAccumulation(change) { const lines = change.diff.split('\n'); let todoCount = 0; const todoLines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('+') && /(TODO|FIXME|HACK|XXX|NOTE|WARNING):/i.test(line)) { todoCount++; todoLines.push(i + 1); } } if (todoCount > 3) { return [ { id: 'LP-TODO', name: 'TODO/FIXME Accumulation', category: 'incomplete_work', severity: 'MEDIUM', file: change.path, line: todoLines[0], match: `${todoCount} TODO/FIXME markers added`, confidence: 0.8, }, ]; } return []; } /** * Pattern: LP-006 - Validation Removal */ detectValidationRemoval(change) { const detected = []; const lines = change.diff.split('\n'); const validationPatterns = [ /^-\s*if\s*\(!/, /^-\s*validate\(/, /^-\s*(const|let|var)\s+\w+\s*=\s*sanitize\(/, // Fixed: detect variable assignment /^-\s*sanitize\(/, // Also detect direct call /^-\s*\.includes\('@'\)/, ]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const pattern of validationPatterns) { if (pattern.test(line)) { detected.push({ id: 'LP-006', name: 'Validation Removal', category: 'feature_removal', severity: 'CRITICAL', file: change.path, line: i + 1, match: line.trim(), confidence: 0.9, }); } } } return detected; } /** * Pattern: LP-007 - Error Handler Deletion */ detectErrorHandlerDeletion(change) { const detected = []; const lines = change.diff.split('\n'); const errorPatterns = [ /^-\s*catch\s*\(/, /^-\s*throw new Error/, /^-\s*throw\s+/, ]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const pattern of errorPatterns) { if (pattern.test(line)) { detected.push({ id: 'LP-007', name: 'Error Handler Deletion', category: 'feature_removal', severity: 'HIGH', file: change.path, line: i + 1, match: line.trim(), confidence: 0.85, }); } } } return detected; } /** * Pattern: LP-015 - Hardcoded Test Bypass */ detectHardcodedBypass(change) { const detected = []; const lines = change.diff.split('\n'); const bypassPatterns = [ /test@example\.com/, /NODE_ENV.*test/, /process\.env\.CI/, /if.*CI.*true/, ]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('+')) { for (const pattern of bypassPatterns) { if (pattern.test(line)) { detected.push({ id: 'LP-015', name: 'Hardcoded Test Bypass', category: 'workaround', severity: 'CRITICAL', file: change.path, line: i + 1, match: line.trim(), confidence: 0.95, }); } } } } return detected; } /** * Pattern: LP-016 - Error Suppression */ detectErrorSuppression(change) { const detected = []; const lines = change.diff.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Detect empty catch blocks or catch with ignore comment if (line.startsWith('+') && (/catch\s*\([^)]+\)\s*\{\s*\}/.test(line) || /catch.*\/\/\s*ignore/.test(line) || /catch.*\/\*\s*ignore/.test(line))) { detected.push({ id: 'LP-016', name: 'Error Suppression', category: 'workaround', severity: 'HIGH', file: change.path, line: i + 1, match: line.trim(), confidence: 0.9, }); } } return detected; } /** * Pattern: LP-008 - Feature Flag Disabling */ detectFeatureFlagDisabling(change) { const detected = []; const lines = change.diff.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i]; const nextLine = lines[i + 1] || ''; // Detect true -> false changes in config if (line.startsWith('-') && /:\s*true/.test(line) && nextLine.startsWith('+') && /:\s*false/.test(nextLine)) { detected.push({ id: 'LP-008', name: 'Feature Flag Disabling', category: 'feature_removal', severity: 'HIGH', file: change.path, line: i + 1, match: `${line.trim()} -> ${nextLine.trim()}`, confidence: 0.9, }); } } return detected; } /** * Make blocking decision based on detected patterns */ makeBlockDecision(patterns) { if (patterns.length === 0) { return { block: false, log: true, reason: 'No avoidance patterns detected', patterns: [], }; } // Check for CRITICAL patterns const criticalPatterns = patterns.filter((p) => p.severity === 'CRITICAL'); if (criticalPatterns.length > 0) { return { block: true, warn: true, // Also set warn for completeness log: true, reason: `CRITICAL avoidance patterns detected: ${criticalPatterns.map((p) => p.name).join(', ')}`, recovery: 'FIX_ROOT_CAUSE', patterns, }; } // Check for multiple HIGH patterns (compound avoidance) const highPatterns = patterns.filter((p) => p.severity === 'HIGH'); if (highPatterns.length > 2) { return { block: true, warn: true, log: true, reason: `Multiple HIGH-severity patterns detected (compound avoidance): ${highPatterns.map((p) => p.name).join(', ')}`, recovery: 'FIX_ALL_ISSUES', patterns, }; } // Check for multiple MEDIUM patterns across files (compound avoidance) const mediumPatterns = patterns.filter((p) => p.severity === 'MEDIUM'); const uniqueFiles = new Set(mediumPatterns.map((p) => p.file)); if (mediumPatterns.length > 2 && uniqueFiles.size > 1) { // Multiple MEDIUM patterns across multiple files = compound avoidance return { block: true, warn: true, log: true, reason: `Multiple MEDIUM-severity patterns across files (compound avoidance): ${mediumPatterns.map((p) => p.name).join(', ')}`, recovery: 'FIX_ALL_ISSUES', patterns, }; } // Single HIGH pattern - block with warning if (highPatterns.length > 0) { return { block: true, warn: true, log: true, reason: `HIGH-severity pattern detected: ${highPatterns[0].name}`, recovery: 'PROVIDE_JUSTIFICATION_OR_FIX', patterns, }; } // MEDIUM patterns (not compound) - warn but allow if (mediumPatterns.length > 0) { return { block: false, warn: true, // Fixed: explicitly set warn: true log: true, reason: `MEDIUM-severity patterns detected: ${mediumPatterns.map((p) => p.name).join(', ')}`, patterns, }; } // LOW patterns - log only return { block: false, warn: false, log: true, // Fixed: explicitly set log: true reason: `LOW-severity patterns detected: ${patterns.map((p) => p.name).join(', ')}`, patterns, }; } /** * Helper: Check if file is a test file */ isTestFile(filePath) { return (/test.*\.(ts|js|py|java)$/.test(filePath) || /\.test\.(ts|js|py|java)$/.test(filePath) || /\.spec\.(ts|js|py|java)$/.test(filePath) || /_test\.py$/.test(filePath) || /Test\.java$/.test(filePath) // Added: Java test files ); } /** * Helper: Check if file is source code (not test) */ isSourceFile(filePath) { return (/\.(ts|js|py)$/.test(filePath) && !/node_modules/.test(filePath) && !/\.d\.ts$/.test(filePath)); } /** * Helper: Check if file is config */ isConfigFile(filePath) { return (/config.*\.(json|yaml|yml|ts|js)$/.test(filePath) || /\.env/.test(filePath)); } /** * Capture baseline metrics for comparison */ async captureBaseline() { // Stub - will integrate with actual coverage tooling } } /** * Hook execution function * * Called by AIWG framework before file write operations. */ export async function executeLazinessDetectionHook(changes) { const hook = new LazinessDetectionHook(); return await hook.analyze(changes); } //# sourceMappingURL=laziness-detection.js.map