@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.
Files changed (62) hide show
  1. package/.claude/settings.local.json +11 -0
  2. package/.firebaserc +5 -0
  3. package/README.md +73 -0
  4. package/cli/analyzer.js +150 -0
  5. package/cli/index.js +182 -0
  6. package/cli/learner.js +156 -0
  7. package/cli/reporter.js +126 -0
  8. package/cli/scanner.js +45 -0
  9. package/ff-slop.md +27 -0
  10. package/firebase.json +16 -0
  11. package/functions/index.js +30 -0
  12. package/functions/package-lock.json +2755 -0
  13. package/functions/package.json +12 -0
  14. package/hardcoresyn-slop.md +1887 -0
  15. package/package.json +17 -0
  16. package/slop-index/catches.jsonl +590 -0
  17. package/slop-index/patterns/001-hallucinated-imports.md +39 -0
  18. package/slop-index/patterns/002-comment-restatement.md +53 -0
  19. package/slop-index/patterns/003-unnecessary-abstraction.md +56 -0
  20. package/slop-index/patterns/004-unused-imports.md +41 -0
  21. package/slop-index/patterns/005-hardcoded-config.md +49 -0
  22. package/slop-index/patterns/006-deprecated-api-confidence.md +52 -0
  23. package/slop-index/patterns/007-try-catch-everything.md +63 -0
  24. package/slop-index/patterns/008-generic-variable-names.md +49 -0
  25. package/slop-index/patterns/009-stub-with-shell.md +61 -0
  26. package/slop-index/patterns/010-async-misuse.md +64 -0
  27. package/slop-index/patterns/011-console-log-left-in.md +53 -0
  28. package/slop-index/patterns/012-over-engineered-simple.md +64 -0
  29. package/slop-index/patterns/013-emoji-debugging.md +44 -0
  30. package/slop-index/patterns/014-fake-async-simulation.md +71 -0
  31. package/slop-index/patterns/015-credential-fallbacks.md +51 -0
  32. package/slop-index/patterns/016-mock-data-pollution.md +75 -0
  33. package/slop-index/proposed/.gitkeep +0 -0
  34. package/slop-index/proposed/017-emoji-progress-logging.md +44 -0
  35. package/slop-index/proposed/018-test-credentials-in-fallbacks.md +54 -0
  36. package/slop-index/proposed/019-fake-loading-simulation.md +75 -0
  37. package/slop-index/proposed/020-configuration-debugging-left-in.md +53 -0
  38. package/slop-index/proposed/021-emoji-production-logging.md +42 -0
  39. package/slop-index/proposed/022-fake-delay-simulation.md +70 -0
  40. package/slop-index/proposed/023-credential-hardcoding-with-fallbacks.md +57 -0
  41. package/slop-index/proposed/024-repetitive-error-pattern.md +76 -0
  42. package/slop-index/proposed/025-environment-specific-fallbacks.md +55 -0
  43. package/slop-index/proposed/026-emoji-production-logs.md +46 -0
  44. package/slop-index/proposed/027-credentials-in-debug-logs.md +48 -0
  45. package/slop-index/proposed/028-repetitive-service-wrappers.md +59 -0
  46. package/slop-index/proposed/029-forced-non-null-assertions.md +59 -0
  47. package/slop-index/proposed/030-production-credential-fallbacks.md +51 -0
  48. package/slop-index/proposed/031-fake-version-confidence.md +50 -0
  49. package/slop-index/proposed/032-forced-non-null-assertions.md +53 -0
  50. package/slop-index/proposed/033-emoji-production-logs.md +44 -0
  51. package/slop-index/proposed/034-realistic-mock-data-leakage.md +62 -0
  52. package/slop-index/proposed/035-production-credential-exposure.md +43 -0
  53. package/slop-index/proposed/036-identical-wrapper-proliferation.md +53 -0
  54. package/slop-index/proposed/037-forced-null-assertions.md +50 -0
  55. package/slop-index/proposed/038-emoji-production-logging.md +42 -0
  56. package/slop-index/proposed/039-fake-delay-operations.md +52 -0
  57. package/slop-index/proposed/040-forced-null-assertion-chains.md +45 -0
  58. package/slop-index/proposed/041-production-debug-configuration.md +45 -0
  59. package/slop-index/proposed/042-repetitive-firebase-wrappers.md +51 -0
  60. package/slop-index/proposed/043-hardcoded-process-timeouts.md +48 -0
  61. package/slop-index/proposed/044-fictional-package-versions.md +37 -0
  62. package/test-sample.js +89 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(git init *)",
5
+ "Bash(git add *)",
6
+ "Bash(git commit *)",
7
+ "Bash(gh auth *)",
8
+ "Bash(gh repo *)"
9
+ ]
10
+ }
11
+ }
package/.firebaserc ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "projects": {
3
+ "default": "slopindex-c5a0c"
4
+ }
5
+ }
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
@@ -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
+ }
@@ -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
+ }