Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions __tests__/pipeline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
runMultiPassAnalyzers,
buildSummary,
formatHandoffPrompt,
formatCompactPrompt,
CERTAINTY,
THOROUGHNESS
} = require('../lib/patterns/pipeline');
Expand Down Expand Up @@ -374,6 +375,142 @@ function process(data) {

expect(prompt).not.toContain('[flag]');
});

it('should use compact format when option is set', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove', severity: 'high' },
{ file: 'b.js', line: 2, certainty: 'MEDIUM', patternName: 'old_todos', autoFix: 'flag', severity: 'medium' }
];

const prompt = formatHandoffPrompt(findings, 'report', { compact: true });

// Compact format uses table structure
expect(prompt).toContain('|File|L|Pattern|Cert|Fix|');
expect(prompt).toContain('|---|---|---|---|---|');
// Should use abbreviated certainty
expect(prompt).toContain('|H|');
expect(prompt).toContain('|M|');
});
});

describe('formatCompactPrompt', () => {
it('should format findings in table structure', () => {
const findings = [
{ file: 'app.js', line: 42, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove' }
];

const prompt = formatCompactPrompt(findings, 'report', 50);

expect(prompt).toContain('|File|L|Pattern|Cert|Fix|');
expect(prompt).toContain('|---|---|---|---|---|');
expect(prompt).toContain('|app.js|42|console_debugging|H|remove|');
});

it('should show certainty counts in header', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove' },
{ file: 'b.js', line: 2, certainty: 'HIGH', patternName: 'debug_import', autoFix: 'remove' },
{ file: 'c.js', line: 3, certainty: 'MEDIUM', patternName: 'old_todos', autoFix: 'flag' },
{ file: 'd.js', line: 4, certainty: 'LOW', patternName: 'magic_numbers', autoFix: 'flag' }
];

const prompt = formatCompactPrompt(findings, 'apply', 50);

expect(prompt).toContain('## Slop: apply|H:2|M:1|L:1');
});

it('should abbreviate certainty levels', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'remove' },
{ file: 'b.js', line: 2, certainty: 'MEDIUM', patternName: 'test', autoFix: 'flag' },
{ file: 'c.js', line: 3, certainty: 'LOW', patternName: 'test', autoFix: 'none' }
];

const prompt = formatCompactPrompt(findings, 'report', 50);

// Should use H, M, L abbreviations in the Cert column
expect(prompt).toMatch(/\|a\.js\|1\|test\|H\|/);
expect(prompt).toMatch(/\|b\.js\|2\|test\|M\|/);
expect(prompt).toMatch(/\|c\.js\|3\|test\|L\|/);
});

it('should show dash for non-fixable patterns', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'flag' },
{ file: 'b.js', line: 2, certainty: 'MEDIUM', patternName: 'test', autoFix: 'none' },
{ file: 'c.js', line: 3, certainty: 'LOW', patternName: 'test', autoFix: null }
];

const prompt = formatCompactPrompt(findings, 'report', 50);

// Non-fixable should show '-' in Fix column
expect(prompt).toContain('|a.js|1|test|H|-|');
expect(prompt).toContain('|b.js|2|test|M|-|');
expect(prompt).toContain('|c.js|3|test|L|-|');
});

it('should truncate findings when exceeding maxFindings', () => {
const findings = [];
for (let i = 1; i <= 10; i++) {
findings.push({
file: `file${i}.js`,
line: i,
certainty: 'HIGH',
patternName: 'console_debugging',
autoFix: 'remove'
});
}

const prompt = formatCompactPrompt(findings, 'report', 5);

// Should only have 5 rows plus truncation message
expect(prompt).toContain('file1.js');
expect(prompt).toContain('file5.js');
expect(prompt).not.toContain('file6.js');
expect(prompt).toContain('+5 more findings (truncated)');
});

it('should include auto-fixable summary', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'console_debugging', autoFix: 'remove' },
{ file: 'b.js', line: 2, certainty: 'HIGH', patternName: 'debug_import', autoFix: 'remove' },
{ file: 'c.js', line: 3, certainty: 'MEDIUM', patternName: 'old_todos', autoFix: 'flag' }
];

const prompt = formatCompactPrompt(findings, 'report', 50);

expect(prompt).toContain('**Auto-fixable: 2**');
expect(prompt).toContain('Manual: 1');
});

it('should handle empty findings', () => {
const prompt = formatCompactPrompt([], 'report', 50);

expect(prompt).toContain('## Slop: report|H:0|M:0|L:0');
expect(prompt).toContain('**Auto-fixable: 0**');
});

it('should not show truncation message when under limit', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'remove' }
];

const prompt = formatCompactPrompt(findings, 'report', 50);

expect(prompt).not.toContain('truncated');
});

it('should include mode in header', () => {
const findings = [
{ file: 'a.js', line: 1, certainty: 'HIGH', patternName: 'test', autoFix: 'remove' }
];

const reportPrompt = formatCompactPrompt(findings, 'report', 50);
const applyPrompt = formatCompactPrompt(findings, 'apply', 50);

expect(reportPrompt).toContain('## Slop: report|');
expect(applyPrompt).toContain('## Slop: apply|');
});
});

describe('runPipeline', () => {
Expand Down
65 changes: 64 additions & 1 deletion lib/patterns/pipeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,13 +468,23 @@ function buildSummary(findings) {
*
* @param {Array} findings - All findings
* @param {string} mode - report | apply
* @param {Object} options - Formatting options
* @param {boolean} options.compact - Use compact table format (60-70% fewer tokens)
* @param {number} options.maxFindings - Maximum findings to include (default: 50)
* @returns {string} Formatted prompt
*/
function formatHandoffPrompt(findings, mode) {
function formatHandoffPrompt(findings, mode, options = {}) {
const { compact = false, maxFindings = 50 } = options;

if (findings.length === 0) {
return '## Slop Detection Results\n\nNo issues detected.';
}

// Use compact format if requested
if (compact) {
return formatCompactPrompt(findings, mode, maxFindings);
}

// Group findings by certainty
const byGroup = {
HIGH: findings.filter(f => f.certainty === CERTAINTY.HIGH),
Expand Down Expand Up @@ -522,6 +532,58 @@ function formatHandoffPrompt(findings, mode) {
return prompt;
}

/**
* Format findings in compact table format for token efficiency
*
* Reduces token usage by ~60-70% compared to verbose format.
* Best for large finding sets where full descriptions aren't needed.
*
* @param {Array} findings - All findings
* @param {string} mode - report | apply
* @param {number} maxFindings - Maximum findings to include
* @returns {string} Compact formatted prompt
*/
function formatCompactPrompt(findings, mode, maxFindings) {
// Single pass to count certainty levels and auto-fixable findings
const { highCount, mediumCount, lowCount, autoFixableCount } = findings.reduce((acc, f) => {
switch (f.certainty) {
case CERTAINTY.HIGH: acc.highCount++; break;
case CERTAINTY.MEDIUM: acc.mediumCount++; break;
case CERTAINTY.LOW: acc.lowCount++; break;
}
if (f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none') {
acc.autoFixableCount++;
}
return acc;
}, { highCount: 0, mediumCount: 0, lowCount: 0, autoFixableCount: 0 });

// Truncate if needed
const limited = findings.slice(0, maxFindings);
const truncated = findings.length > maxFindings;

// Summary header
let output = `## Slop: ${mode}|H:${highCount}|M:${mediumCount}|L:${lowCount}\n\n`;

// Table format
output += '|File|L|Pattern|Cert|Fix|\n';
output += '|---|---|---|---|---|\n';

for (const f of limited) {
const fix = f.autoFix && f.autoFix !== 'flag' && f.autoFix !== 'none' ? f.autoFix : '-';
const cert = f.certainty.charAt(0); // H, M, or L
output += `|${f.file}|${f.line}|${f.patternName}|${cert}|${fix}|\n`;
}

if (truncated) {
output += `\n_+${findings.length - maxFindings} more findings (truncated)_\n`;
}

// Auto-fix summary
output += `\n**Auto-fixable: ${autoFixableCount}** | Manual: ${findings.length - autoFixableCount}`;

return output;
}

/**
* Format a list of findings for the prompt
*
Expand Down Expand Up @@ -559,6 +621,7 @@ module.exports = {
runPhase2,
buildSummary,
formatHandoffPrompt,
formatCompactPrompt,
// Constants
CERTAINTY,
THOROUGHNESS
Expand Down
72 changes: 36 additions & 36 deletions plugins/deslop-around/commands/deslop-around.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@ argument-hint: "[report|apply] [scope-path] [max-changes]"

You are a senior maintainer doing periodic repo hygiene. Your mission: remove "AI slop" while preserving behavior and minimizing diffs.

## Modes (User Choice)

This command supports **two scope modes** - you choose:

| Mode | Scope | Command |
|------|-------|---------|
| **Path-based** | Specific directory/files | `/deslop-around [apply] src/` |
| **Codebase** | Entire repository | `/deslop-around [apply]` |

For **diff-based cleanup** of new work only, use the `deslop-work` agent via `/next-task`.

## Arguments

- **Mode**: `report` (default) or `apply`
- **Scope**: Path or glob pattern (default: `.`)
- **Scope**: Path or glob pattern (default: `.` = codebase)
- **Max changes**: Number of changesets (default: 5)

Parse from $ARGUMENTS or use defaults.
Expand Down Expand Up @@ -71,41 +82,30 @@ git ls-files | wc -l # File count

## AI Slop Definitions

Detect and remove:

- **Console Debugging**: `console.log()`, `print()`, `println!()`, `dbg!()`
- **Old TODOs**: Comments >90 days old (check line age)
- **Commented Code**: >5 consecutive commented lines
- **Placeholder Text**: "lorem ipsum", "test test", "TODO: implement"
- **Empty Catch**: Empty catch/except blocks without logging
- **Magic Numbers**: Large hardcoded numbers (>1000)
- **Disabled Linters**: eslint-disable, pylint: disable, #noqa
- **Trailing Whitespace**: Whitespace at end of lines
- **Mixed Indentation**: Tabs and spaces mixed
- **Unused Imports**: Imports marked as unused
- **Hardcoded URLs**: URLs that should be config
- **Debug Imports**: `import pdb`, `import ipdb`
- **Placeholder Functions**: `return 0`, `todo!()`, `raise NotImplementedError`, `throw Error("TODO")`
- **Excessive Documentation**: JSDoc >3x function body length
- **Phantom References**: Issue/PR mentions, file path references in comments
- **Generic Naming**: Variables named `data`, `result`, `item`, `temp`, `value` (suggests more specific names)

### Code Smell Detection

High-impact code smells that indicate maintainability issues:

- **Boolean Blindness**: Function calls with 3+ consecutive boolean params (e.g., `process(true, false, true)`)
- **Message Chains**: Long method chains (4+ calls) or deep property access (5+ levels)
- **Mutable Globals**: Module-level mutable state with UPPERCASE names (`let CONFIG = {}`)
- **Dead Code**: Unreachable code after `return`, `throw`, `break`, `continue`
- **Shotgun Surgery**: Files that frequently change together (git history analysis)

Heuristic patterns (may have false positives, use judgment):

- **Feature Envy**: Method accessing another object 3+ times (may belong in that class)
- **Speculative Generality**: Underscore-prefixed unused params, empty interfaces

Reference patterns from `${CLAUDE_PLUGIN_ROOT}/lib/patterns/slop-patterns.js`
Detect and remove patterns from `${CLAUDE_PLUGIN_ROOT}/lib/patterns/slop-patterns.js`.

**Categories detected:**

| Category | Examples |
|----------|----------|
| Console debugging | `console.log()`, `print()`, `dbg!()`, `println!()` |
| Old TODOs | Comments with TODO/FIXME >90 days old |
| Placeholder code | `return 0`, `todo!()`, `raise NotImplementedError` |
| Empty catch/except | Empty error handlers without logging |
| Hardcoded secrets | API keys, tokens, credentials |
| Excessive docs | JSDoc >3x function body length |
| Phantom references | Issue/PR mentions in comments |
| Code smells | Boolean blindness, message chains, mutable globals |

**Certainty levels:**

| Level | Action | Description |
|-------|--------|-------------|
| **HIGH** | Auto-fix | Direct regex match - definitive slop |
| **MEDIUM** | Verify context | Multi-pass analysis - review before fixing |
| **LOW** | Flag only | Heuristic - may be false positive |

See pattern library for full regex patterns and language-specific variants.

## Phase A: Map + Diagnose (Always)

Expand Down
Loading
Loading