@reidbuilds/slop 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/.firebaserc +5 -0
- package/README.md +73 -0
- package/cli/analyzer.js +150 -0
- package/cli/index.js +182 -0
- package/cli/learner.js +156 -0
- package/cli/reporter.js +126 -0
- package/cli/scanner.js +45 -0
- package/ff-slop.md +27 -0
- package/firebase.json +16 -0
- package/functions/index.js +30 -0
- package/functions/package-lock.json +2755 -0
- package/functions/package.json +12 -0
- package/hardcoresyn-slop.md +1887 -0
- package/package.json +17 -0
- package/slop-index/catches.jsonl +590 -0
- package/slop-index/patterns/001-hallucinated-imports.md +39 -0
- package/slop-index/patterns/002-comment-restatement.md +53 -0
- package/slop-index/patterns/003-unnecessary-abstraction.md +56 -0
- package/slop-index/patterns/004-unused-imports.md +41 -0
- package/slop-index/patterns/005-hardcoded-config.md +49 -0
- package/slop-index/patterns/006-deprecated-api-confidence.md +52 -0
- package/slop-index/patterns/007-try-catch-everything.md +63 -0
- package/slop-index/patterns/008-generic-variable-names.md +49 -0
- package/slop-index/patterns/009-stub-with-shell.md +61 -0
- package/slop-index/patterns/010-async-misuse.md +64 -0
- package/slop-index/patterns/011-console-log-left-in.md +53 -0
- package/slop-index/patterns/012-over-engineered-simple.md +64 -0
- package/slop-index/patterns/013-emoji-debugging.md +44 -0
- package/slop-index/patterns/014-fake-async-simulation.md +71 -0
- package/slop-index/patterns/015-credential-fallbacks.md +51 -0
- package/slop-index/patterns/016-mock-data-pollution.md +75 -0
- package/slop-index/proposed/.gitkeep +0 -0
- package/slop-index/proposed/017-emoji-progress-logging.md +44 -0
- package/slop-index/proposed/018-test-credentials-in-fallbacks.md +54 -0
- package/slop-index/proposed/019-fake-loading-simulation.md +75 -0
- package/slop-index/proposed/020-configuration-debugging-left-in.md +53 -0
- package/slop-index/proposed/021-emoji-production-logging.md +42 -0
- package/slop-index/proposed/022-fake-delay-simulation.md +70 -0
- package/slop-index/proposed/023-credential-hardcoding-with-fallbacks.md +57 -0
- package/slop-index/proposed/024-repetitive-error-pattern.md +76 -0
- package/slop-index/proposed/025-environment-specific-fallbacks.md +55 -0
- package/slop-index/proposed/026-emoji-production-logs.md +46 -0
- package/slop-index/proposed/027-credentials-in-debug-logs.md +48 -0
- package/slop-index/proposed/028-repetitive-service-wrappers.md +59 -0
- package/slop-index/proposed/029-forced-non-null-assertions.md +59 -0
- package/slop-index/proposed/030-production-credential-fallbacks.md +51 -0
- package/slop-index/proposed/031-fake-version-confidence.md +50 -0
- package/slop-index/proposed/032-forced-non-null-assertions.md +53 -0
- package/slop-index/proposed/033-emoji-production-logs.md +44 -0
- package/slop-index/proposed/034-realistic-mock-data-leakage.md +62 -0
- package/slop-index/proposed/035-production-credential-exposure.md +43 -0
- package/slop-index/proposed/036-identical-wrapper-proliferation.md +53 -0
- package/slop-index/proposed/037-forced-null-assertions.md +50 -0
- package/slop-index/proposed/038-emoji-production-logging.md +42 -0
- package/slop-index/proposed/039-fake-delay-operations.md +52 -0
- package/slop-index/proposed/040-forced-null-assertion-chains.md +45 -0
- package/slop-index/proposed/041-production-debug-configuration.md +45 -0
- package/slop-index/proposed/042-repetitive-firebase-wrappers.md +51 -0
- package/slop-index/proposed/043-hardcoded-process-timeouts.md +48 -0
- package/slop-index/proposed/044-fictional-package-versions.md +37 -0
- package/test-sample.js +89 -0
package/.firebaserc
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Slop
|
|
2
|
+
|
|
3
|
+
Catch AI-generated code anti-patterns before they ship.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g slopcheck
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run locally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
node cli/index.js check ./src
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Scan a single file
|
|
22
|
+
slop check ./src/api/orders.js
|
|
23
|
+
|
|
24
|
+
# Scan a directory
|
|
25
|
+
slop check ./src
|
|
26
|
+
|
|
27
|
+
# Filter by severity
|
|
28
|
+
slop check ./src --severity high
|
|
29
|
+
|
|
30
|
+
# Save a markdown report
|
|
31
|
+
slop check ./src --output slop-report.md
|
|
32
|
+
|
|
33
|
+
# Raw JSON output (for CI/scripts)
|
|
34
|
+
slop check ./src --json
|
|
35
|
+
|
|
36
|
+
# List all patterns in the SlopIndex
|
|
37
|
+
slop patterns
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Setup
|
|
41
|
+
|
|
42
|
+
Requires an `ANTHROPIC_API_KEY` environment variable.
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
export ANTHROPIC_API_KEY=your_key_here
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or add to a `.env` file (use dotenv or set in your shell profile).
|
|
49
|
+
|
|
50
|
+
## The SlopIndex
|
|
51
|
+
|
|
52
|
+
The pattern library lives in `/slop-index/patterns/`. Each `.md` file is one anti-pattern with:
|
|
53
|
+
|
|
54
|
+
- What it looks like
|
|
55
|
+
- Why AI generates it
|
|
56
|
+
- How to catch it
|
|
57
|
+
- How to fix it
|
|
58
|
+
|
|
59
|
+
The library grows over time. Contributions welcome.
|
|
60
|
+
|
|
61
|
+
## Exit Codes
|
|
62
|
+
|
|
63
|
+
- `0` — No high severity issues found
|
|
64
|
+
- `1` — One or more high severity issues found (useful for CI blocking)
|
|
65
|
+
|
|
66
|
+
## Roadmap
|
|
67
|
+
|
|
68
|
+
- [ ] npm publish
|
|
69
|
+
- [ ] GitHub Action
|
|
70
|
+
- [ ] Pre-commit hook
|
|
71
|
+
- [ ] Pattern auto-discovery from repo mining
|
|
72
|
+
- [ ] Self-updating SlopIndex
|
|
73
|
+
- [ ] VS Code extension
|
package/cli/analyzer.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, appendFileSync } from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const PATTERNS_DIR = path.join(__dirname, '../slop-index/patterns')
|
|
7
|
+
|
|
8
|
+
export function loadPatterns() {
|
|
9
|
+
const files = readdirSync(PATTERNS_DIR).filter(f => f.endsWith('.md'))
|
|
10
|
+
return files.map(f => ({
|
|
11
|
+
filename: f,
|
|
12
|
+
content: readFileSync(path.join(PATTERNS_DIR, f), 'utf-8')
|
|
13
|
+
}))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function analyzeFile(codeFile, patterns, options = {}) {
|
|
17
|
+
const patternText = patterns.map(p => p.content).join('\n\n---\n\n')
|
|
18
|
+
|
|
19
|
+
const MAX_CHARS = 6000
|
|
20
|
+
const code = codeFile.content.length > MAX_CHARS
|
|
21
|
+
? codeFile.content.slice(0, MAX_CHARS) + '\n... [truncated]'
|
|
22
|
+
: codeFile.content
|
|
23
|
+
|
|
24
|
+
const prompt = `You are an expert code reviewer specializing in detecting AI-generated code anti-patterns.
|
|
25
|
+
|
|
26
|
+
You have the following pattern library. Each pattern describes a specific mistake AI code generators make repeatedly.
|
|
27
|
+
|
|
28
|
+
PATTERN LIBRARY:
|
|
29
|
+
${patternText}
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
TASK:
|
|
34
|
+
Analyze the code below and identify every instance of a pattern match. Be precise — flag real issues only, not style preferences.
|
|
35
|
+
|
|
36
|
+
FILE: ${codeFile.filename}
|
|
37
|
+
LINES: ${codeFile.lines}
|
|
38
|
+
|
|
39
|
+
\`\`\`
|
|
40
|
+
${code}
|
|
41
|
+
\`\`\`
|
|
42
|
+
|
|
43
|
+
Return ONLY a valid JSON object with this exact structure. No preamble, no explanation outside the JSON:
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
"file": "${codeFile.filename}",
|
|
47
|
+
"flags": [
|
|
48
|
+
{
|
|
49
|
+
"pattern_id": "001",
|
|
50
|
+
"pattern_name": "hallucinated-imports",
|
|
51
|
+
"severity": "high",
|
|
52
|
+
"line_start": 1,
|
|
53
|
+
"line_end": 1,
|
|
54
|
+
"code_snippet": "the exact problematic code",
|
|
55
|
+
"explanation": "why this is a problem in this specific context",
|
|
56
|
+
"fix": "concrete fix for this specific instance"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"summary": {
|
|
60
|
+
"total": 0,
|
|
61
|
+
"high": 0,
|
|
62
|
+
"medium": 0,
|
|
63
|
+
"low": 0
|
|
64
|
+
}
|
|
65
|
+
}`
|
|
66
|
+
|
|
67
|
+
const severityFilter = options.severity
|
|
68
|
+
let response
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: {
|
|
74
|
+
'Content-Type': 'application/json',
|
|
75
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
|
76
|
+
'anthropic-version': '2023-06-01'
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
model: 'claude-sonnet-4-20250514',
|
|
80
|
+
max_tokens: 4096,
|
|
81
|
+
messages: [{ role: 'user', content: prompt }]
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
response = await res.json()
|
|
85
|
+
const raw = response.content[0].text.trim()
|
|
86
|
+
const clean = raw.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim()
|
|
87
|
+
const result = JSON.parse(clean)
|
|
88
|
+
|
|
89
|
+
if (severityFilter) {
|
|
90
|
+
result.flags = result.flags.filter(f => f.severity === severityFilter)
|
|
91
|
+
result.summary = computeSummary(result.flags)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const flag of result.flags) {
|
|
95
|
+
logCatch(flag, codeFile.filename)
|
|
96
|
+
syncCatch(flag, codeFile.extension)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await delay(500)
|
|
100
|
+
return result
|
|
101
|
+
} catch (err) {
|
|
102
|
+
console.error('API response:', response ?? '(no response captured)')
|
|
103
|
+
await delay(500)
|
|
104
|
+
return {
|
|
105
|
+
file: codeFile.filename,
|
|
106
|
+
error: `Analysis failed: ${err.message}`,
|
|
107
|
+
flags: [],
|
|
108
|
+
summary: { total: 0, high: 0, medium: 0, low: 0 }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
114
|
+
|
|
115
|
+
const CATCHES_FILE = path.join(__dirname, '../slop-index/catches.jsonl')
|
|
116
|
+
|
|
117
|
+
export function logCatch(flag, filename) {
|
|
118
|
+
const entry = JSON.stringify({
|
|
119
|
+
filename: path.basename(filename),
|
|
120
|
+
pattern_id: flag.pattern_id,
|
|
121
|
+
pattern_name: flag.pattern_name,
|
|
122
|
+
severity: flag.severity,
|
|
123
|
+
timestamp: new Date().toISOString()
|
|
124
|
+
})
|
|
125
|
+
appendFileSync(CATCHES_FILE, entry + '\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const SYNC_URL = 'https://slopsync-vktctiuffa-uc.a.run.app'
|
|
129
|
+
|
|
130
|
+
function syncCatch(flag, extension) {
|
|
131
|
+
fetch(SYNC_URL, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
pattern_id: flag.pattern_id,
|
|
136
|
+
severity: flag.severity,
|
|
137
|
+
language: extension ? extension.replace('.', '') : null,
|
|
138
|
+
timestamp: new Date().toISOString()
|
|
139
|
+
})
|
|
140
|
+
}).catch(() => {})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function computeSummary(flags) {
|
|
144
|
+
return {
|
|
145
|
+
total: flags.length,
|
|
146
|
+
high: flags.filter(f => f.severity === 'high').length,
|
|
147
|
+
medium: flags.filter(f => f.severity === 'medium').length,
|
|
148
|
+
low: flags.filter(f => f.severity === 'low').length
|
|
149
|
+
}
|
|
150
|
+
}
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import { scanTarget } from './scanner.js'
|
|
5
|
+
import { loadPatterns, analyzeFile } from './analyzer.js'
|
|
6
|
+
import { printResults } from './reporter.js'
|
|
7
|
+
import { runLearn } from './learner.js'
|
|
8
|
+
|
|
9
|
+
const program = new Command()
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('slop')
|
|
13
|
+
.description('Catch AI-generated code anti-patterns before they ship')
|
|
14
|
+
.version('0.2.0')
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('check <target>')
|
|
18
|
+
.description('Scan a file or directory for slop patterns')
|
|
19
|
+
.option('-o, --output <file>', 'Save report to a markdown file')
|
|
20
|
+
.option('-s, --severity <level>', 'Filter by severity: high | medium | low')
|
|
21
|
+
.option('-r, --no-recursive', 'Do not scan subdirectories')
|
|
22
|
+
.option('--json', 'Output raw JSON')
|
|
23
|
+
.action(async (target, options) => {
|
|
24
|
+
console.log(chalk.gray(`\n slop check ${target}\n`))
|
|
25
|
+
|
|
26
|
+
let files
|
|
27
|
+
try {
|
|
28
|
+
files = await scanTarget(target, { recursive: options.recursive })
|
|
29
|
+
files = files.filter(Boolean)
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error(chalk.red(` Error: ${err.message}`))
|
|
32
|
+
process.exit(1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (files.length === 0) {
|
|
36
|
+
console.log(chalk.yellow(' No supported files found.'))
|
|
37
|
+
process.exit(0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!options.json) {
|
|
41
|
+
console.log(chalk.gray(` Loading pattern library...`))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let patterns
|
|
45
|
+
try {
|
|
46
|
+
patterns = loadPatterns()
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.error(chalk.red(` Failed to load patterns: ${err.message}`))
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!options.json) {
|
|
53
|
+
console.log(chalk.gray(` ${patterns.length} patterns loaded · scanning ${files.length} file${files.length !== 1 ? 's' : ''}...\n`))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const results = []
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
if (!options.json) {
|
|
59
|
+
process.stdout.write(chalk.gray(` Analyzing ${file.filename}...`))
|
|
60
|
+
}
|
|
61
|
+
const result = await analyzeFile(file, patterns, { severity: options.severity })
|
|
62
|
+
if (!options.json) {
|
|
63
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r')
|
|
64
|
+
}
|
|
65
|
+
results.push(result)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
printResults(results, {
|
|
69
|
+
json: options.json,
|
|
70
|
+
output: options.output
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!options.json) {
|
|
74
|
+
try {
|
|
75
|
+
const learned = await runLearn()
|
|
76
|
+
if (!learned.error && learned.proposed.length > 0) {
|
|
77
|
+
console.log(chalk.gray(` ${learned.proposed.length} new pattern${learned.proposed.length !== 1 ? 's' : ''} proposed → slop-index/proposed/`))
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
// learn errors are non-fatal
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const totalHigh = results.reduce((n, r) => n + (r.summary?.high || 0), 0)
|
|
85
|
+
process.exit(totalHigh > 0 ? 1 : 0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
program
|
|
89
|
+
.command('learn')
|
|
90
|
+
.description('Analyze catch history and propose new patterns')
|
|
91
|
+
.action(async () => {
|
|
92
|
+
console.log(chalk.gray('\n slop learn\n'))
|
|
93
|
+
console.log(chalk.gray(' Reading catch history...'))
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const result = await runLearn()
|
|
97
|
+
|
|
98
|
+
if (result.error) {
|
|
99
|
+
console.log(chalk.yellow(` ${result.error}`))
|
|
100
|
+
process.exit(0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (result.proposed.length === 0) {
|
|
104
|
+
console.log(chalk.gray(' No new patterns proposed.\n'))
|
|
105
|
+
} else {
|
|
106
|
+
console.log(chalk.green(`\n ${result.proposed.length} new pattern${result.proposed.length !== 1 ? 's' : ''} proposed:\n`))
|
|
107
|
+
for (const f of result.proposed) {
|
|
108
|
+
console.log(` ${chalk.white(f)}`)
|
|
109
|
+
}
|
|
110
|
+
console.log(chalk.gray('\n Review in slop-index/proposed/'))
|
|
111
|
+
console.log(chalk.gray(' Move to slop-index/patterns/ to activate.\n'))
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(chalk.red(` Learn failed: ${err.message}`))
|
|
115
|
+
process.exit(1)
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
program
|
|
120
|
+
.command('update')
|
|
121
|
+
.description('Pull community patterns from SlopIndex')
|
|
122
|
+
.action(async () => {
|
|
123
|
+
console.log(chalk.gray('\n slop update\n'))
|
|
124
|
+
|
|
125
|
+
let community
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch('https://us-central1-slopindex-c5a0c.cloudfunctions.net/slopPatterns')
|
|
128
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
129
|
+
community = await res.json()
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error(chalk.red(` Failed to fetch community patterns: ${err.message}`))
|
|
132
|
+
process.exit(1)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { readdirSync, writeFileSync } = await import('fs')
|
|
136
|
+
const { default: nodePath } = await import('path')
|
|
137
|
+
const { fileURLToPath: ftu } = await import('url')
|
|
138
|
+
const patternsDir = nodePath.join(nodePath.dirname(ftu(import.meta.url)), '../slop-index/patterns')
|
|
139
|
+
|
|
140
|
+
const existing = new Set(readdirSync(patternsDir).map(f => f.replace('.md', '')))
|
|
141
|
+
|
|
142
|
+
let added = 0
|
|
143
|
+
for (const pattern of community) {
|
|
144
|
+
const slug = pattern.name ?? pattern.id
|
|
145
|
+
const filename = `${String(pattern.id ?? slug).padStart(3, '0')}-${slug}.md`
|
|
146
|
+
if (existing.has(filename.replace('.md', ''))) continue
|
|
147
|
+
writeFileSync(nodePath.join(patternsDir, filename), pattern.content ?? '')
|
|
148
|
+
added++
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (added === 0) {
|
|
152
|
+
console.log(chalk.gray(' Already up to date.\n'))
|
|
153
|
+
} else {
|
|
154
|
+
console.log(chalk.green(` ${added} new pattern${added !== 1 ? 's' : ''} added to slop-index/patterns/\n`))
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
program
|
|
159
|
+
.command('patterns')
|
|
160
|
+
.description('List all patterns in the SlopIndex')
|
|
161
|
+
.action(() => {
|
|
162
|
+
const patterns = loadPatterns()
|
|
163
|
+
console.log(chalk.white.bold(`\n SlopIndex — ${patterns.length} patterns\n`))
|
|
164
|
+
for (const p of patterns) {
|
|
165
|
+
const idMatch = p.filename.match(/^(\d+)/)
|
|
166
|
+
const id = idMatch ? idMatch[1] : '?'
|
|
167
|
+
const name = p.filename.replace(/^\d+-/, '').replace('.md', '')
|
|
168
|
+
const severityMatch = p.content.match(/^severity:\s*(\w+)/m)
|
|
169
|
+
const severity = severityMatch ? severityMatch[1] : 'unknown'
|
|
170
|
+
const color = SEVERITY_COLOR[severity] || chalk.white
|
|
171
|
+
console.log(` ${chalk.gray(id)} ${name.padEnd(35)} ${color(severity)}`)
|
|
172
|
+
}
|
|
173
|
+
console.log('')
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const SEVERITY_COLOR = {
|
|
177
|
+
high: chalk.red,
|
|
178
|
+
medium: chalk.yellow,
|
|
179
|
+
low: chalk.blue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
program.parse()
|
package/cli/learner.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
const CATCHES_FILE = path.join(__dirname, '../slop-index/catches.jsonl')
|
|
7
|
+
const PATTERNS_DIR = path.join(__dirname, '../slop-index/patterns')
|
|
8
|
+
const PROPOSED_DIR = path.join(__dirname, '../slop-index/proposed')
|
|
9
|
+
|
|
10
|
+
export async function runLearn() {
|
|
11
|
+
if (!existsSync(CATCHES_FILE)) {
|
|
12
|
+
return { error: 'No catches.jsonl found. Run `slop check` first.' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const lines = readFileSync(CATCHES_FILE, 'utf-8').trim().split('\n').filter(Boolean)
|
|
16
|
+
if (lines.length === 0) {
|
|
17
|
+
return { error: 'catches.jsonl is empty. Run `slop check` first.' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const catches = lines.map(l => JSON.parse(l))
|
|
21
|
+
|
|
22
|
+
const grouped = {}
|
|
23
|
+
for (const c of catches) {
|
|
24
|
+
if (!grouped[c.pattern_id]) {
|
|
25
|
+
grouped[c.pattern_id] = { pattern_id: c.pattern_id, pattern_name: c.pattern_name, count: 0, examples: [] }
|
|
26
|
+
}
|
|
27
|
+
grouped[c.pattern_id].count++
|
|
28
|
+
if (grouped[c.pattern_id].examples.length < 5 && c.code_snippet) {
|
|
29
|
+
grouped[c.pattern_id].examples.push({
|
|
30
|
+
filename: c.filename,
|
|
31
|
+
code_snippet: c.code_snippet,
|
|
32
|
+
explanation: c.explanation
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!existsSync(PROPOSED_DIR)) mkdirSync(PROPOSED_DIR, { recursive: true })
|
|
38
|
+
|
|
39
|
+
const existingNames = [
|
|
40
|
+
...readdirSync(PATTERNS_DIR).filter(f => f.endsWith('.md')),
|
|
41
|
+
...readdirSync(PROPOSED_DIR).filter(f => f.endsWith('.md'))
|
|
42
|
+
].map(f => f.replace(/^\d+-/, '').replace('.md', ''))
|
|
43
|
+
|
|
44
|
+
const catchSummary = Object.values(grouped)
|
|
45
|
+
.sort((a, b) => b.count - a.count)
|
|
46
|
+
.map(g => {
|
|
47
|
+
const examples = g.examples.map(e =>
|
|
48
|
+
` - file: ${e.filename}\n code: ${e.code_snippet}\n why: ${e.explanation}`
|
|
49
|
+
).join('\n')
|
|
50
|
+
return `pattern ${g.pattern_id} (${g.pattern_name ?? 'unknown'}): ${g.count} catch${g.count !== 1 ? 'es' : ''}\n${examples}`
|
|
51
|
+
})
|
|
52
|
+
.join('\n\n')
|
|
53
|
+
|
|
54
|
+
const prompt = `You are a code quality expert analyzing real catch data from an AI-generated code detector.
|
|
55
|
+
|
|
56
|
+
EXISTING PATTERNS — do not re-propose these:
|
|
57
|
+
${existingNames.join(', ')}
|
|
58
|
+
|
|
59
|
+
CATCH DATA — real code flagged across user codebases:
|
|
60
|
+
${catchSummary}
|
|
61
|
+
|
|
62
|
+
Identify NEW anti-patterns not yet in the library. Look for: variants of existing patterns that deserve their own entry, new patterns visible in the code snippets, and cross-cutting issues suggested by file contexts.
|
|
63
|
+
|
|
64
|
+
Return a JSON array. Each element must have exactly these keys:
|
|
65
|
+
- "name": kebab-case slug
|
|
66
|
+
- "severity": "high" | "medium" | "low"
|
|
67
|
+
- "languages": string[]
|
|
68
|
+
- "tags": string[]
|
|
69
|
+
- "description": string (one paragraph)
|
|
70
|
+
- "example_code": string (realistic bad code)
|
|
71
|
+
- "why_ai_does_this": string (one paragraph)
|
|
72
|
+
- "catch_signals": string[] (3-5 items)
|
|
73
|
+
- "fix_example": string (realistic fixed code)
|
|
74
|
+
- "severity_rationale": string (one sentence)
|
|
75
|
+
|
|
76
|
+
Return ONLY valid JSON. No preamble, no explanation outside the array.`
|
|
77
|
+
|
|
78
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'application/json',
|
|
82
|
+
'x-api-key': process.env.ANTHROPIC_API_KEY,
|
|
83
|
+
'anthropic-version': '2023-06-01'
|
|
84
|
+
},
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
model: 'claude-sonnet-4-20250514',
|
|
87
|
+
max_tokens: 4096,
|
|
88
|
+
messages: [{ role: 'user', content: prompt }]
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const response = await res.json()
|
|
93
|
+
const raw = response.content[0].text.trim()
|
|
94
|
+
const clean = raw.replace(/^```json\n?/, '').replace(/\n?```$/, '').trim()
|
|
95
|
+
const proposed = JSON.parse(clean)
|
|
96
|
+
|
|
97
|
+
const allFiles = [
|
|
98
|
+
...readdirSync(PATTERNS_DIR).filter(f => f.endsWith('.md')),
|
|
99
|
+
...readdirSync(PROPOSED_DIR).filter(f => f.endsWith('.md'))
|
|
100
|
+
]
|
|
101
|
+
const maxId = allFiles.reduce((max, f) => {
|
|
102
|
+
const m = f.match(/^(\d+)/)
|
|
103
|
+
return m ? Math.max(max, parseInt(m[1], 10)) : max
|
|
104
|
+
}, 0)
|
|
105
|
+
|
|
106
|
+
const saved = []
|
|
107
|
+
proposed.forEach((p, i) => {
|
|
108
|
+
const id = String(maxId + i + 1).padStart(3, '0')
|
|
109
|
+
const filename = `${id}-${p.name}.md`
|
|
110
|
+
writeFileSync(path.join(PROPOSED_DIR, filename), formatPattern(id, p))
|
|
111
|
+
saved.push(filename)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
return { proposed: saved }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatPattern(id, p) {
|
|
118
|
+
const today = new Date().toISOString().split('T')[0]
|
|
119
|
+
return `---
|
|
120
|
+
id: ${id}
|
|
121
|
+
name: ${p.name}
|
|
122
|
+
severity: ${p.severity}
|
|
123
|
+
languages: [${p.languages.join(', ')}]
|
|
124
|
+
tags: [${p.tags.join(', ')}]
|
|
125
|
+
added: ${today}
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
# Pattern: ${titleCase(p.name)}
|
|
129
|
+
|
|
130
|
+
## Description
|
|
131
|
+
${p.description}
|
|
132
|
+
|
|
133
|
+
## What It Looks Like
|
|
134
|
+
\`\`\`
|
|
135
|
+
${p.example_code}
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
## Why AI Does This
|
|
139
|
+
${p.why_ai_does_this}
|
|
140
|
+
|
|
141
|
+
## Catch Signal
|
|
142
|
+
${p.catch_signals.map(s => `- ${s}`).join('\n')}
|
|
143
|
+
|
|
144
|
+
## Fix Template
|
|
145
|
+
\`\`\`
|
|
146
|
+
${p.fix_example}
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
## Severity Rationale
|
|
150
|
+
${p.severity} — ${p.severity_rationale}
|
|
151
|
+
`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function titleCase(slug) {
|
|
155
|
+
return slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
|
|
156
|
+
}
|
package/cli/reporter.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { writeFileSync } from 'fs'
|
|
3
|
+
|
|
4
|
+
const SEVERITY_COLOR = {
|
|
5
|
+
high: chalk.red.bold,
|
|
6
|
+
medium: chalk.yellow.bold,
|
|
7
|
+
low: chalk.blue
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SEVERITY_ICON = {
|
|
11
|
+
high: '✗',
|
|
12
|
+
medium: '⚠',
|
|
13
|
+
low: '◦'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function printResults(results, options = {}) {
|
|
17
|
+
const allFlags = results.flatMap(r => r.flags || [])
|
|
18
|
+
const errors = results.filter(r => r.error)
|
|
19
|
+
|
|
20
|
+
if (options.json) {
|
|
21
|
+
console.log(JSON.stringify(results, null, 2))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log('')
|
|
26
|
+
|
|
27
|
+
for (const result of results) {
|
|
28
|
+
if (result.error) {
|
|
29
|
+
console.log(chalk.red(` Error scanning ${result.file}: ${result.error}`))
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (result.flags.length === 0) {
|
|
34
|
+
console.log(chalk.green(` ✓ ${result.file} — clean`))
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(chalk.white.bold(` ${result.file}`))
|
|
39
|
+
|
|
40
|
+
for (const flag of result.flags) {
|
|
41
|
+
const colorFn = SEVERITY_COLOR[flag.severity] || chalk.white
|
|
42
|
+
const icon = SEVERITY_ICON[flag.severity] || '?'
|
|
43
|
+
|
|
44
|
+
console.log('')
|
|
45
|
+
console.log(` ${colorFn(`${icon} [${flag.severity.toUpperCase()}]`)} ${chalk.white(flag.pattern_name)} ${chalk.gray(`(line ${flag.line_start}${flag.line_end !== flag.line_start ? `-${flag.line_end}` : ''})`)
|
|
46
|
+
}`)
|
|
47
|
+
console.log(` ${chalk.gray('Code:')} ${chalk.dim(truncate(flag.code_snippet, 80))}`)
|
|
48
|
+
console.log(` ${chalk.gray('Issue:')} ${flag.explanation}`)
|
|
49
|
+
console.log(` ${chalk.gray('Fix:')} ${chalk.cyan(flag.fix)}`)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log('')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
printSummary(results)
|
|
56
|
+
|
|
57
|
+
if (options.output) {
|
|
58
|
+
const md = buildMarkdownReport(results)
|
|
59
|
+
writeFileSync(options.output, md)
|
|
60
|
+
console.log(chalk.gray(` Report saved to ${options.output}`))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function printSummary(results) {
|
|
65
|
+
const total = results.reduce((n, r) => n + (r.summary?.total || 0), 0)
|
|
66
|
+
const high = results.reduce((n, r) => n + (r.summary?.high || 0), 0)
|
|
67
|
+
const medium = results.reduce((n, r) => n + (r.summary?.medium || 0), 0)
|
|
68
|
+
const low = results.reduce((n, r) => n + (r.summary?.low || 0), 0)
|
|
69
|
+
const filesScanned = results.length
|
|
70
|
+
const filesClean = results.filter(r => r.flags?.length === 0 && !r.error).length
|
|
71
|
+
|
|
72
|
+
console.log(chalk.gray(' ─────────────────────────────'))
|
|
73
|
+
console.log(` ${chalk.white.bold('Summary')} ${chalk.gray(`${filesScanned} file${filesScanned !== 1 ? 's' : ''} scanned · ${filesClean} clean`)}`)
|
|
74
|
+
console.log('')
|
|
75
|
+
|
|
76
|
+
if (total === 0) {
|
|
77
|
+
console.log(chalk.green(' No slop detected.'))
|
|
78
|
+
} else {
|
|
79
|
+
const parts = []
|
|
80
|
+
if (high) parts.push(chalk.red.bold(`${high} high`))
|
|
81
|
+
if (medium) parts.push(chalk.yellow.bold(`${medium} medium`))
|
|
82
|
+
if (low) parts.push(chalk.blue(`${low} low`))
|
|
83
|
+
console.log(` ${chalk.white.bold(total + ' flags')} ${parts.join(chalk.gray(' · '))}`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildMarkdownReport(results) {
|
|
90
|
+
const lines = ['# Slop Report\n']
|
|
91
|
+
const date = new Date().toISOString().split('T')[0]
|
|
92
|
+
lines.push(`Generated: ${date}\n`)
|
|
93
|
+
|
|
94
|
+
for (const result of results) {
|
|
95
|
+
lines.push(`## ${result.file}\n`)
|
|
96
|
+
if (result.error) {
|
|
97
|
+
lines.push(`> Error: ${result.error}\n`)
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
if (result.flags.length === 0) {
|
|
101
|
+
lines.push('✓ Clean\n')
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
for (const flag of result.flags) {
|
|
105
|
+
lines.push(`### [${flag.severity.toUpperCase()}] ${flag.pattern_name} — line ${flag.line_start}\n`)
|
|
106
|
+
lines.push(`**Code:** \`${flag.code_snippet}\`\n`)
|
|
107
|
+
lines.push(`**Issue:** ${flag.explanation}\n`)
|
|
108
|
+
lines.push(`**Fix:** ${flag.fix}\n`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const total = results.reduce((n, r) => n + (r.summary?.total || 0), 0)
|
|
113
|
+
const high = results.reduce((n, r) => n + (r.summary?.high || 0), 0)
|
|
114
|
+
const medium = results.reduce((n, r) => n + (r.summary?.medium || 0), 0)
|
|
115
|
+
const low = results.reduce((n, r) => n + (r.summary?.low || 0), 0)
|
|
116
|
+
|
|
117
|
+
lines.push(`## Summary\n`)
|
|
118
|
+
lines.push(`| Severity | Count |\n|---|---|\n| High | ${high} |\n| Medium | ${medium} |\n| Low | ${low} |\n| **Total** | **${total}** |\n`)
|
|
119
|
+
|
|
120
|
+
return lines.join('\n')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function truncate(str, max) {
|
|
124
|
+
if (!str) return ''
|
|
125
|
+
return str.length > max ? str.slice(0, max) + '…' : str
|
|
126
|
+
}
|