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
JavaScript
/**
* 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