aislop 0.8.0 → 0.8.2
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/README.md +105 -225
- package/dist/cli.js +90 -11
- package/dist/index.js +91 -12
- package/dist/{json-BJGLCIK-.js → json-DxLkV8n2.js} +1 -1
- package/dist/mcp.js +26 -11
- package/dist/{version-B9ZchFMv.js → version-G3ekYjY1.js} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,107 +9,36 @@
|
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
### The killer feature: the per-edit hook
|
|
15
|
-
|
|
16
|
-
Install once into your coding agent:
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
npx aislop hook install --claude # also: --cursor, --codex, --gemini, --windsurf, --cline, --kilo, --antigravity, --copilot
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
After every `Edit` / `Write` your agent makes, `aislop` runs and feeds the diagnostics back into the agent's next turn as structured `additionalContext` (envelope: `aislop.hook.v1` — score, counts, findings, regression flag, suggested next steps). **The agent sees the score regression on the same turn it wrote the code, before you prompt again.** No more PR-time surprises; the slop never leaves the keystroke that produced it.
|
|
23
|
-
|
|
24
|
-
CI is the second gate: `aislop ci` exits non-zero when score drops below your threshold, so the same standard is enforced on every PR.
|
|
25
|
-
|
|
26
|
-
Every check is deterministic. Regex patterns, AST analysis, and standard tooling (Biome, oxlint, knip, ruff). Same code in, same score out. No API calls, no LLMs, no network dependency (except optional dependency audits). The name refers to what it *catches*.
|
|
27
|
-
|
|
28
|
-
## See it in action
|
|
29
|
-
|
|
30
|
-
### Scan
|
|
31
|
-
|
|
32
|
-

|
|
33
|
-
|
|
34
|
-
### Fix
|
|
35
|
-
|
|
36
|
-

|
|
12
|
+
Catches the slop AI agents leave behind: dead code, oversized functions and files, unused imports, `as any` casts, swallowed errors, hallucinated imports, todo stubs, narrative comments. Scores 0–100. Deterministic (regex + AST, no LLMs). 8+ languages.
|
|
37
13
|
|
|
38
14
|
## Quick start
|
|
39
15
|
|
|
40
16
|
```bash
|
|
41
|
-
# scan your project
|
|
42
17
|
npx aislop scan
|
|
43
|
-
|
|
44
|
-
# auto-fix what can be fixed safely
|
|
45
|
-
npx aislop fix
|
|
46
|
-
|
|
47
|
-
# CI mode (JSON output + quality gate)
|
|
48
|
-
npx aislop ci
|
|
49
|
-
|
|
50
|
-
# wire aislop into your agent so it runs on every edit
|
|
51
|
-
npx aislop hook install --claude
|
|
52
18
|
```
|
|
53
19
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```text
|
|
57
|
-
[ok] Formatting: done (0 issues, 426ms)
|
|
58
|
-
[ok] Linting: done (0 issues, 396ms)
|
|
59
|
-
[!] Code Quality: done (2 warnings, 812ms)
|
|
60
|
-
[!] AI Slop: done (4 warnings, 455ms)
|
|
61
|
-
[ok] Security: done (0 issues, 1.3s)
|
|
62
|
-
aislop 0.8.0 · the quality gate for agentic coding
|
|
63
|
-
|
|
64
|
-
scan · my-app · typescript · 142 files
|
|
20
|
+
No install needed. Works on any project. Get your score in seconds.
|
|
65
21
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
> AI Slop
|
|
72
|
-
[WARN] [auto] Narrative comment block (2)
|
|
73
|
-
src/lib/auth.ts:86
|
|
74
|
-
[WARN] 'as any' bypasses type safety
|
|
75
|
-
src/api/normalize.ts:47
|
|
76
|
-
|
|
77
|
-
87 / 100 Healthy 0 errors · 6 warnings · 4 fixable
|
|
78
|
-
142 files · 5 engines · 1.9s
|
|
79
|
-
|
|
80
|
-
→ Run npx aislop fix to auto-fix 4 issues
|
|
81
|
-
→ Run npx aislop fix --claude to hand off the rest to an agent
|
|
22
|
+
```bash
|
|
23
|
+
npx aislop fix # auto-fix issues
|
|
24
|
+
npx aislop fix -f # aggressive fixes (deps, unused files)
|
|
25
|
+
npx aislop ci # CI mode (JSON + gate)
|
|
26
|
+
npx aislop hook install --claude # per-edit hook
|
|
82
27
|
```
|
|
83
28
|
|
|
84
|
-
|
|
29
|
+
**Public badge**: Show your score on your README
|
|
85
30
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`aislop` gives you one view and one score. Fully deterministic, no AI in the loop.
|
|
91
|
-
|
|
92
|
-
- **One score, one gate**: a 0-100 number you can enforce in CI with `aislop ci`. Weighted so sloppy patterns (dead code, `as any`, swallowed errors) hit harder than style noise.
|
|
93
|
-
- **Auto-fix first, agent second**: `aislop fix` clears what's mechanically safe (formatters, unused imports, trivial comments, dead patterns). For the rest, one flag hands off to Claude Code, Codex, Cursor, Gemini, Windsurf, Amp, Aider, Goose, and 7 more, with full diagnostic context pre-filled.
|
|
94
|
-
- **Wire it into your agent**: `aislop hook install` plugs aislop into Claude Code, Cursor, Gemini CLI (runtime), plus Codex, Windsurf, Cline, Kilo Code, Antigravity, and Copilot (rules-only). The agent gets score + findings on the turn it wrote the code, not after.
|
|
95
|
-
- **Deterministic**: regex, AST, and standard tooling. No LLMs, no API keys, no network dependency. Same repo in, same score out.
|
|
96
|
-
- **Zero-config start**: `npx aislop scan` works on any repo. Add `.aislop/config.yml` when you want to tune thresholds or enable the architecture engine.
|
|
97
|
-
- **Works across stacks**: TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP, Expo / React Native.
|
|
31
|
+
```markdown
|
|
32
|
+
[](https://scanaislop.com)
|
|
33
|
+
```
|
|
98
34
|
|
|
99
|
-
|
|
35
|
+
Run `npx aislop badge` to auto-generate. Free at [scanaislop.com](https://scanaislop.com).
|
|
100
36
|
|
|
101
|
-
|
|
37
|
+
## See it in action
|
|
102
38
|
|
|
103
|
-
|
|
104
|
-
|---|---|---|
|
|
105
|
-
| **Formatting** | Code style consistency | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
|
|
106
|
-
| **Linting** | Language-specific issues | oxlint, ruff, golangci-lint, clippy, expo-doctor |
|
|
107
|
-
| **Code Quality** | Complexity and dead code | Function/file size limits, deep nesting, unused files/deps (knip), AST-based unused-declaration removal |
|
|
108
|
-
| **AI Slop** | AI-authored code patterns | Narrative comments, trivial comments, dead patterns, unused imports, `as any`, `console.log` leftovers, TODO stubs, generic names |
|
|
109
|
-
| **Security** | Vulnerabilities and risky code | eval, innerHTML, SQL/shell injection, dependency audits (npm/pip/cargo/govulncheck) |
|
|
110
|
-
| **Architecture** | Structural rules (opt-in) | Custom import bans, layering rules, required patterns |
|
|
39
|
+
### Scan
|
|
111
40
|
|
|
112
|
-
|
|
41
|
+

|
|
113
42
|
|
|
114
43
|
---
|
|
115
44
|
|
|
@@ -138,111 +67,89 @@ Also available as [`@scanaislop/aislop`](docs/installation.md) on GitHub Package
|
|
|
138
67
|
|
|
139
68
|
## Usage
|
|
140
69
|
|
|
141
|
-
### Scan
|
|
70
|
+
### Scan
|
|
142
71
|
|
|
143
72
|
```bash
|
|
144
|
-
aislop scan
|
|
145
|
-
aislop scan ./src
|
|
146
|
-
aislop scan --changes
|
|
147
|
-
aislop scan --staged
|
|
148
|
-
aislop scan --json
|
|
73
|
+
npx aislop scan # current directory
|
|
74
|
+
npx aislop scan ./src # specific directory
|
|
75
|
+
npx aislop scan --changes # changed files from HEAD
|
|
76
|
+
npx aislop scan --staged # staged files only
|
|
77
|
+
npx aislop scan --json # JSON output
|
|
149
78
|
```
|
|
150
79
|
|
|
151
|
-
**Exclude files
|
|
80
|
+
**Exclude files**: `node_modules`, `.git`, `dist`, `build`, `coverage` excluded by default. Add more in `.aislop/config.yml`:
|
|
152
81
|
|
|
153
82
|
```yaml
|
|
154
83
|
exclude:
|
|
155
|
-
- "**/*.test.ts"
|
|
84
|
+
- "**/*.test.ts"
|
|
156
85
|
- src/generated
|
|
157
|
-
- legacy
|
|
158
86
|
```
|
|
159
87
|
|
|
160
|
-
Or
|
|
88
|
+
Or via CLI: `npx aislop scan --exclude "**/*.test.ts,dist"`
|
|
161
89
|
|
|
162
|
-
|
|
163
|
-
aislop scan --exclude "**/*.test.ts"
|
|
164
|
-
aislop scan --exclude node_modules,dist,logs
|
|
165
|
-
aislop scan --exclude "src/generated" --exclude "**/*.spec.*"
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
CLI flags beat config; config beats defaults.
|
|
169
|
-
|
|
170
|
-
**Extend a shared config.** A project config can extend a parent and override specific keys. Useful for org-wide baselines: ship one strict config, let each repo soften or tighten as needed.
|
|
90
|
+
**Extend config**: Project config can extend a parent:
|
|
171
91
|
|
|
172
92
|
```yaml
|
|
173
93
|
# .aislop/config.yml
|
|
174
|
-
extends: ../../.aislop/base.yml
|
|
175
|
-
|
|
94
|
+
extends: ../../.aislop/base.yml
|
|
176
95
|
ci:
|
|
177
|
-
failBelow: 80
|
|
96
|
+
failBelow: 80 # override specific keys
|
|
178
97
|
```
|
|
179
98
|
|
|
180
|
-
|
|
99
|
+
### Fix
|
|
181
100
|
|
|
182
|
-
|
|
101
|
+
Auto-fix what's mechanical (formatters, unused imports, dead code). For issues that need context, hand off to your agent with full diagnostic info.
|
|
183
102
|
|
|
184
103
|
```bash
|
|
185
|
-
aislop fix # safe auto-fixes
|
|
186
|
-
aislop fix -f # aggressive:
|
|
104
|
+
npx aislop fix # safe auto-fixes
|
|
105
|
+
npx aislop fix -f # aggressive: deps, unused files
|
|
187
106
|
```
|
|
188
107
|
|
|
189
|
-
### Hand off to
|
|
108
|
+
### Hand off to agent
|
|
190
109
|
|
|
191
|
-
When auto-fix can't solve it,
|
|
110
|
+
When auto-fix can't solve it, pass the remaining issues to your coding agent with full context:
|
|
192
111
|
|
|
193
112
|
```bash
|
|
194
|
-
aislop fix --claude # Claude Code
|
|
195
|
-
aislop fix --
|
|
196
|
-
aislop fix --
|
|
197
|
-
aislop fix --
|
|
198
|
-
|
|
199
|
-
aislop fix --
|
|
200
|
-
aislop fix --aider # Aider
|
|
201
|
-
aislop fix --goose # Goose
|
|
202
|
-
aislop fix --opencode # OpenCode
|
|
203
|
-
aislop fix --warp # Warp
|
|
204
|
-
aislop fix --kimi # Kimi Code CLI
|
|
205
|
-
aislop fix --antigravity # Antigravity
|
|
206
|
-
aislop fix --deep-agents # Deep Agents
|
|
207
|
-
aislop fix --vscode # VS Code Copilot (copies prompt to clipboard)
|
|
208
|
-
aislop fix --prompt # print the prompt (agent-agnostic)
|
|
113
|
+
npx aislop fix --claude # Claude Code
|
|
114
|
+
npx aislop fix --cursor # Cursor (copies to clipboard)
|
|
115
|
+
npx aislop fix --gemini # Gemini CLI
|
|
116
|
+
npx aislop fix --codex # Codex CLI
|
|
117
|
+
# Also: --windsurf, --amp, --aider, --goose, --opencode, --warp, --kimi, --antigravity, --deep-agents, --vscode
|
|
118
|
+
npx aislop fix --prompt # print prompt (agent-agnostic)
|
|
209
119
|
```
|
|
210
120
|
|
|
211
|
-
### Install
|
|
121
|
+
### Install hook
|
|
212
122
|
|
|
213
|
-
|
|
123
|
+
Runs after every agent edit. Feedback flows back immediately.
|
|
214
124
|
|
|
215
125
|
```bash
|
|
216
|
-
aislop hook install --claude # Claude Code
|
|
217
|
-
aislop hook install --cursor # Cursor
|
|
218
|
-
aislop hook install --gemini # Gemini CLI
|
|
219
|
-
aislop hook install #
|
|
220
|
-
aislop hook install claude cursor #
|
|
221
|
-
aislop hook install --agent claude,cursor # comma-list if you prefer one flag
|
|
126
|
+
npx aislop hook install --claude # Claude Code
|
|
127
|
+
npx aislop hook install --cursor # Cursor
|
|
128
|
+
npx aislop hook install --gemini # Gemini CLI
|
|
129
|
+
npx aislop hook install # all supported agents
|
|
130
|
+
npx aislop hook install claude cursor # specific agents
|
|
222
131
|
```
|
|
223
132
|
|
|
224
|
-
Runtime adapters (scan + feedback
|
|
225
|
-
|
|
226
|
-
Rules-only installers (agent reads rules on every turn): `codex`, `windsurf`, `cline`, `kilocode`, `antigravity`, `copilot`.
|
|
133
|
+
**Runtime adapters** (scan + feedback): `claude`, `cursor`, `gemini`.
|
|
134
|
+
**Rules-only** (agent reads rules): `codex`, `windsurf`, `cline`, `kilocode`, `antigravity`, `copilot`.
|
|
227
135
|
|
|
228
|
-
|
|
136
|
+
**Quality-gate mode**: Blocks if score regresses below baseline.
|
|
229
137
|
|
|
230
138
|
```bash
|
|
231
|
-
aislop hook install --claude --quality-gate
|
|
232
|
-
aislop hook baseline # re-capture baseline
|
|
233
|
-
aislop hook status # list installed
|
|
234
|
-
aislop hook uninstall --claude # remove
|
|
235
|
-
aislop hook uninstall # remove every aislop entry, sentinel-verified
|
|
139
|
+
npx aislop hook install --claude --quality-gate
|
|
140
|
+
npx aislop hook baseline # re-capture baseline
|
|
141
|
+
npx aislop hook status # list installed
|
|
142
|
+
npx aislop hook uninstall --claude # remove
|
|
236
143
|
```
|
|
237
144
|
|
|
238
|
-
|
|
145
|
+
Docs: [`/docs/hooks`](https://scanaislop.com/docs/hooks)
|
|
239
146
|
|
|
240
|
-
###
|
|
147
|
+
### MCP server
|
|
241
148
|
|
|
242
|
-
aislop
|
|
149
|
+
Expose aislop as MCP tools for Claude Desktop, Cursor, Codex:
|
|
243
150
|
|
|
244
151
|
```jsonc
|
|
245
|
-
// ~/.cursor/mcp.json
|
|
152
|
+
// ~/.cursor/mcp.json or Claude Desktop config
|
|
246
153
|
{
|
|
247
154
|
"mcpServers": {
|
|
248
155
|
"aislop": {
|
|
@@ -253,51 +160,30 @@ aislop ships an MCP (Model Context Protocol) server so any agent that speaks MCP
|
|
|
253
160
|
}
|
|
254
161
|
```
|
|
255
162
|
|
|
256
|
-
Tools
|
|
257
|
-
- `aislop_scan({ path? })` — score + counts + top findings
|
|
258
|
-
- `aislop_fix({ path?, force? })` — apply mechanical fixes; returns before/after delta
|
|
259
|
-
- `aislop_why({ rule_id })` — engine + docs link for a rule
|
|
260
|
-
- `aislop_baseline({ path? })` — read the per-edit-hook baseline (score, lastScanAt, fileCount)
|
|
261
|
-
|
|
262
|
-
Same engines as the CLI; calling these from inside an agent session lets the model self-check before claiming work is done.
|
|
263
|
-
|
|
264
|
-
### Use in CI pipelines
|
|
265
|
-
|
|
266
|
-
```bash
|
|
267
|
-
aislop ci # JSON output, exits 1 if score < threshold
|
|
268
|
-
```
|
|
163
|
+
**Tools**: `aislop_scan`, `aislop_fix`, `aislop_why`, `aislop_baseline`
|
|
269
164
|
|
|
270
|
-
###
|
|
165
|
+
### CI
|
|
271
166
|
|
|
272
167
|
```bash
|
|
273
|
-
#
|
|
274
|
-
aislop scan --staged
|
|
275
|
-
|
|
276
|
-
# during local cleanup
|
|
277
|
-
aislop fix
|
|
278
|
-
|
|
279
|
-
# full project check
|
|
280
|
-
aislop scan
|
|
168
|
+
npx aislop ci # JSON output, exits 1 if score < threshold
|
|
281
169
|
```
|
|
282
170
|
|
|
283
171
|
### Other commands
|
|
284
172
|
|
|
285
173
|
```bash
|
|
286
|
-
aislop init # create .aislop/config.yml
|
|
287
|
-
aislop
|
|
288
|
-
aislop
|
|
289
|
-
aislop
|
|
290
|
-
aislop hook install # wire aislop into your coding agent
|
|
291
|
-
aislop # interactive menu
|
|
174
|
+
npx aislop init # create .aislop/config.yml
|
|
175
|
+
npx aislop rules # list rules
|
|
176
|
+
npx aislop badge # print badge URL
|
|
177
|
+
npx aislop # interactive menu
|
|
292
178
|
```
|
|
293
179
|
|
|
294
|
-
|
|
180
|
+
Docs: [commands](docs/commands.md)
|
|
295
181
|
|
|
296
182
|
---
|
|
297
183
|
|
|
298
|
-
##
|
|
184
|
+
## CI integration
|
|
299
185
|
|
|
300
|
-
### Pre-commit
|
|
186
|
+
### Pre-commit
|
|
301
187
|
|
|
302
188
|
```bash
|
|
303
189
|
npx aislop scan --staged
|
|
@@ -305,9 +191,7 @@ npx aislop scan --staged
|
|
|
305
191
|
|
|
306
192
|
### GitHub Actions
|
|
307
193
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
Manual form:
|
|
194
|
+
Run `npx aislop init` and accept the workflow prompt, or add manually:
|
|
311
195
|
|
|
312
196
|
```yaml
|
|
313
197
|
- uses: actions/checkout@v4
|
|
@@ -317,90 +201,86 @@ Manual form:
|
|
|
317
201
|
- run: npx aislop@latest ci .
|
|
318
202
|
```
|
|
319
203
|
|
|
320
|
-
|
|
204
|
+
**Composite action**:
|
|
321
205
|
|
|
322
206
|
```yaml
|
|
323
207
|
- uses: actions/checkout@v4
|
|
324
|
-
- uses: scanaislop/aislop@v0.
|
|
208
|
+
- uses: scanaislop/aislop@v0.8
|
|
325
209
|
```
|
|
326
210
|
|
|
327
211
|
### Quality gate
|
|
328
212
|
|
|
329
|
-
Set
|
|
213
|
+
Set minimum score in `.aislop/config.yml`:
|
|
330
214
|
|
|
331
215
|
```yaml
|
|
332
216
|
ci:
|
|
333
217
|
failBelow: 70
|
|
334
218
|
```
|
|
335
219
|
|
|
336
|
-
`aislop ci` exits
|
|
220
|
+
`aislop ci` exits 1 when score < threshold. Docs: [CI/CD](docs/ci.md)
|
|
337
221
|
|
|
338
222
|
---
|
|
339
223
|
|
|
340
|
-
##
|
|
224
|
+
## For teams
|
|
341
225
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
| Telemetry | [docs/telemetry.md](docs/telemetry.md) |
|
|
226
|
+
[scanaislop](https://scanaislop.com) is the hosted platform for teams:
|
|
227
|
+
|
|
228
|
+
- PR gates with score thresholds
|
|
229
|
+
- Standards hierarchy (org → team → project)
|
|
230
|
+
- Dashboards and agent attribution
|
|
231
|
+
- Visual rules manager
|
|
232
|
+
|
|
233
|
+
Same engines, same scores. CLI is MIT-licensed. [Learn more →](https://scanaislop.com)
|
|
351
234
|
|
|
352
235
|
---
|
|
353
236
|
|
|
354
|
-
##
|
|
237
|
+
## Why aislop
|
|
355
238
|
|
|
356
|
-
|
|
239
|
+
AI coding tools generate code that compiles and passes tests but ships with patterns no engineer would write. `aislop` gives you one score, one gate, and auto-fixes what it can.
|
|
357
240
|
|
|
358
|
-
- **
|
|
359
|
-
- **
|
|
360
|
-
- **
|
|
361
|
-
- **
|
|
362
|
-
- **Same engines, same rule IDs, same score**. The CLI remains the source of truth.
|
|
241
|
+
- **One score**: 0-100, enforced in CI. Weighted so sloppy patterns hit harder than style noise.
|
|
242
|
+
- **Auto-fix first**: Clears formatters, unused imports, dead code mechanically. Hands off the rest to your agent with full context.
|
|
243
|
+
- **Deterministic**: Regex + AST + standard tooling. No LLMs, no API calls. Same code in, same score out.
|
|
244
|
+
- **Zero-config start**: `npx aislop scan` works on any repo. Add `.aislop/config.yml` to tune.
|
|
363
245
|
|
|
364
|
-
|
|
246
|
+
## What it catches
|
|
365
247
|
|
|
366
|
-
|
|
248
|
+
Six deterministic engines run in parallel:
|
|
367
249
|
|
|
368
|
-
|
|
250
|
+
| Engine | What it checks | How |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| **Formatting** | Code style consistency | Biome, ruff, gofmt, cargo fmt, rubocop, php-cs-fixer |
|
|
253
|
+
| **Linting** | Language-specific issues | oxlint, ruff, golangci-lint, clippy, expo-doctor |
|
|
254
|
+
| **Code Quality** | Complexity and dead code | Function/file size limits, deep nesting, unused files/deps (knip), AST-based unused-declaration removal |
|
|
255
|
+
| **AI Slop** | AI-authored code patterns | Narrative comments, trivial comments, dead patterns, unused imports, `as any`, `console.log` leftovers, TODO stubs, generic names |
|
|
256
|
+
| **Security** | Vulnerabilities and risky code | eval, innerHTML, SQL/shell injection, dependency audits (npm/pip/cargo/govulncheck) |
|
|
257
|
+
| **Architecture** | Structural rules (opt-in) | Custom import bans, layering rules, required patterns |
|
|
369
258
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
259
|
+
See the full [rules reference](docs/rules.md).
|
|
260
|
+
|
|
261
|
+
---
|
|
373
262
|
|
|
374
|
-
|
|
263
|
+
## Docs
|
|
375
264
|
|
|
376
|
-
|
|
265
|
+
[Installation](docs/installation.md) · [Commands](docs/commands.md) · [Rules](docs/rules.md) · [Config](docs/configuration.md) · [Scoring](docs/scoring.md) · [CI/CD](docs/ci.md) · [Telemetry](docs/telemetry.md)
|
|
377
266
|
|
|
378
267
|
## Contributing
|
|
379
268
|
|
|
380
|
-
See [CONTRIBUTING.md](CONTRIBUTING.md)
|
|
269
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). AI assistants: [AGENTS.md](AGENTS.md).
|
|
381
270
|
|
|
382
271
|
## Acknowledgments
|
|
383
272
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
- [Biome](https://biomejs.dev/) for formatting and linting JS/TS
|
|
387
|
-
- [oxlint](https://oxc.rs/) for fast JS/TS linting
|
|
388
|
-
- [knip](https://knip.dev/) for unused files, exports, and dependencies
|
|
389
|
-
- [ruff](https://docs.astral.sh/ruff/) for Python linting and formatting
|
|
390
|
-
- [golangci-lint](https://golangci-lint.run/) for Go linting
|
|
391
|
-
- [expo-doctor](https://docs.expo.dev/) for Expo/React Native project health
|
|
273
|
+
Built on: [Biome](https://biomejs.dev/), [oxlint](https://oxc.rs/), [knip](https://knip.dev/), [ruff](https://docs.astral.sh/ruff/), [golangci-lint](https://golangci-lint.run/), [expo-doctor](https://docs.expo.dev/)
|
|
392
274
|
|
|
393
275
|
## Contributors
|
|
394
276
|
|
|
395
|
-
Thanks to everyone who has shipped code, ideas, docs, or bug reports.
|
|
396
|
-
|
|
397
277
|
<!-- CONTRIBUTORS-START -->
|
|
398
278
|
- [@heavykenny](https://github.com/heavykenny)
|
|
399
279
|
- [@myke-awoniran](https://github.com/myke-awoniran)
|
|
400
280
|
- [@yashrajoria](https://github.com/yashrajoria)
|
|
401
281
|
<!-- CONTRIBUTORS-END -->
|
|
402
282
|
|
|
403
|
-
|
|
283
|
+
Auto-updated by `.github/workflows/contributors.yml`. [Link commit email](https://github.com/settings/emails) or add to [`.github/contributors-overrides.json`](.github/contributors-overrides.json).
|
|
404
284
|
|
|
405
285
|
## License
|
|
406
286
|
|
package/dist/cli.js
CHANGED
|
@@ -730,9 +730,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
730
730
|
return diagnostics;
|
|
731
731
|
};
|
|
732
732
|
|
|
733
|
+
//#endregion
|
|
734
|
+
//#region src/engines/ai-slop/non-production-paths.ts
|
|
735
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
736
|
+
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
737
|
+
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
738
|
+
|
|
733
739
|
//#endregion
|
|
734
740
|
//#region src/engines/ai-slop/comments.ts
|
|
735
|
-
const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
736
741
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
737
742
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
738
743
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
@@ -789,7 +794,7 @@ const detectTrivialComments = async (context) => {
|
|
|
789
794
|
for (const filePath of files) {
|
|
790
795
|
if (isAutoGenerated(filePath)) continue;
|
|
791
796
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
792
|
-
if (
|
|
797
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
793
798
|
let content;
|
|
794
799
|
try {
|
|
795
800
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -825,11 +830,10 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
825
830
|
fixable
|
|
826
831
|
});
|
|
827
832
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
828
|
-
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
829
833
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
830
834
|
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
831
835
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
832
|
-
if (
|
|
836
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
833
837
|
const diagnostics = [];
|
|
834
838
|
const lines = content.split("\n");
|
|
835
839
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -879,7 +883,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
879
883
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
880
884
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
881
885
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
882
|
-
if (
|
|
886
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
883
887
|
const diagnostics = [];
|
|
884
888
|
const lines = content.split("\n");
|
|
885
889
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1784,7 +1788,6 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
1784
1788
|
|
|
1785
1789
|
//#endregion
|
|
1786
1790
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1787
|
-
const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1788
1791
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1789
1792
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1790
1793
|
const getCommentSyntax = (ext) => {
|
|
@@ -1935,13 +1938,21 @@ const looksLikeGoDocComment = (block, ext) => {
|
|
|
1935
1938
|
if (!declMatch) return false;
|
|
1936
1939
|
return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
|
|
1937
1940
|
};
|
|
1938
|
-
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see)
|
|
1941
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
|
|
1939
1942
|
const hasDocIndicator = (block) => {
|
|
1940
1943
|
const joined = block.prose.join(" ");
|
|
1941
1944
|
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
1942
1945
|
for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
|
|
1943
1946
|
return false;
|
|
1944
1947
|
};
|
|
1948
|
+
const hasPreambleSlopSignal = (block) => {
|
|
1949
|
+
const joined = block.prose.join(" ");
|
|
1950
|
+
for (const l of block.prose) {
|
|
1951
|
+
if (EXPLANATORY_OPENERS.test(l)) return true;
|
|
1952
|
+
if (JUSTIFICATION_OPENERS.some((re) => re.test(l))) return true;
|
|
1953
|
+
}
|
|
1954
|
+
return CROSS_REFERENCE_PHRASES.some((re) => re.test(joined));
|
|
1955
|
+
};
|
|
1945
1956
|
const detectNarrativeInBlock = (block, ext) => {
|
|
1946
1957
|
if (looksLikeLicenseHeader(block)) return {
|
|
1947
1958
|
matched: false,
|
|
@@ -1981,9 +1992,13 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1981
1992
|
matched: false,
|
|
1982
1993
|
reason: ""
|
|
1983
1994
|
};
|
|
1984
|
-
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1995
|
+
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1996
|
+
matched: true,
|
|
1997
|
+
reason: "multi-line preamble before declaration"
|
|
1998
|
+
};
|
|
1999
|
+
if (block.kind === "jsdoc" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
|
|
1985
2000
|
matched: true,
|
|
1986
|
-
reason:
|
|
2001
|
+
reason: "JSDoc preamble with slop signal"
|
|
1987
2002
|
};
|
|
1988
2003
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
1989
2004
|
matched: true,
|
|
@@ -2023,7 +2038,7 @@ const detectNarrativeComments = async (context) => {
|
|
|
2023
2038
|
const syntax = getCommentSyntax(ext);
|
|
2024
2039
|
if (!syntax) continue;
|
|
2025
2040
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2026
|
-
if (
|
|
2041
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
2027
2042
|
let content;
|
|
2028
2043
|
try {
|
|
2029
2044
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -7556,7 +7571,7 @@ const renderCleanRun = (input, deps = {}) => {
|
|
|
7556
7571
|
|
|
7557
7572
|
//#endregion
|
|
7558
7573
|
//#region src/version.ts
|
|
7559
|
-
const APP_VERSION = "0.8.
|
|
7574
|
+
const APP_VERSION = "0.8.2";
|
|
7560
7575
|
|
|
7561
7576
|
//#endregion
|
|
7562
7577
|
//#region src/utils/telemetry.ts
|
|
@@ -9382,13 +9397,77 @@ const fixDependencyAudit = async (context, onProgress) => {
|
|
|
9382
9397
|
}
|
|
9383
9398
|
onProgress?.("Dependency audit fixes · skipping (pnpm audit unavailable and no package-lock.json for npm fallback)");
|
|
9384
9399
|
};
|
|
9400
|
+
const SEMVER_PREFIX_RE = /^[~^]?/;
|
|
9401
|
+
const parseSemverMin = (spec) => {
|
|
9402
|
+
const match = spec.replace(SEMVER_PREFIX_RE, "").match(/^(\d+|x|X|\*)(?:\.(\d+|x|X|\*))?(?:\.(\d+|x|X|\*))?/);
|
|
9403
|
+
if (!match) return null;
|
|
9404
|
+
const head = match[1];
|
|
9405
|
+
if (!/^\d+$/.test(head)) return null;
|
|
9406
|
+
const toNum = (part) => {
|
|
9407
|
+
if (!part) return 0;
|
|
9408
|
+
return /^\d+$/.test(part) ? Number(part) : 0;
|
|
9409
|
+
};
|
|
9410
|
+
return [
|
|
9411
|
+
Number(head),
|
|
9412
|
+
toNum(match[2]),
|
|
9413
|
+
toNum(match[3])
|
|
9414
|
+
];
|
|
9415
|
+
};
|
|
9416
|
+
const isDowngrade = (oldSpec, newSpec) => {
|
|
9417
|
+
const oldV = parseSemverMin(oldSpec);
|
|
9418
|
+
const newV = parseSemverMin(newSpec);
|
|
9419
|
+
if (!oldV || !newV) return false;
|
|
9420
|
+
for (let i = 0; i < 3; i++) {
|
|
9421
|
+
if ((newV[i] ?? 0) < (oldV[i] ?? 0)) return true;
|
|
9422
|
+
if ((newV[i] ?? 0) > (oldV[i] ?? 0)) return false;
|
|
9423
|
+
}
|
|
9424
|
+
return false;
|
|
9425
|
+
};
|
|
9426
|
+
const DEP_BUCKETS = [
|
|
9427
|
+
"dependencies",
|
|
9428
|
+
"devDependencies",
|
|
9429
|
+
"peerDependencies",
|
|
9430
|
+
"optionalDependencies"
|
|
9431
|
+
];
|
|
9432
|
+
const snapshotPackageVersions = (pkg) => {
|
|
9433
|
+
const map = /* @__PURE__ */ new Map();
|
|
9434
|
+
for (const bucket of DEP_BUCKETS) {
|
|
9435
|
+
const deps = pkg[bucket];
|
|
9436
|
+
if (!deps || typeof deps !== "object") continue;
|
|
9437
|
+
for (const [name, version] of Object.entries(deps)) if (typeof version === "string") map.set(`${bucket}:${name}`, version);
|
|
9438
|
+
}
|
|
9439
|
+
return map;
|
|
9440
|
+
};
|
|
9441
|
+
const revertDowngrades = (rootDir, before) => {
|
|
9442
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
9443
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
9444
|
+
const reverted = [];
|
|
9445
|
+
for (const bucket of DEP_BUCKETS) {
|
|
9446
|
+
const deps = pkg[bucket];
|
|
9447
|
+
if (!deps) continue;
|
|
9448
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
9449
|
+
const prior = before.get(`${bucket}:${name}`);
|
|
9450
|
+
if (!prior) continue;
|
|
9451
|
+
if (isDowngrade(prior, version)) {
|
|
9452
|
+
deps[name] = prior;
|
|
9453
|
+
reverted.push(`${name} ${version} → ${prior}`);
|
|
9454
|
+
}
|
|
9455
|
+
}
|
|
9456
|
+
}
|
|
9457
|
+
if (reverted.length > 0) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
9458
|
+
return reverted;
|
|
9459
|
+
};
|
|
9385
9460
|
const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
9461
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
9462
|
+
const before = snapshotPackageVersions(JSON.parse(fs.readFileSync(pkgPath, "utf-8")));
|
|
9386
9463
|
onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
|
|
9387
9464
|
const result = await runSubprocess("npm", ["audit", "fix"], {
|
|
9388
9465
|
cwd: rootDir,
|
|
9389
9466
|
timeout: INSTALL_TIMEOUT
|
|
9390
9467
|
});
|
|
9391
9468
|
if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error("npm audit fix failed");
|
|
9469
|
+
const reverted = revertDowngrades(rootDir, before);
|
|
9470
|
+
if (reverted.length > 0) onProgress?.(`Dependency audit fixes · reverted ${reverted.length} downgrade(s): ${reverted.join(", ")}`);
|
|
9392
9471
|
onProgress?.("Dependency audit fixes · running npm install");
|
|
9393
9472
|
const installResult = await runSubprocess("npm", ["install"], {
|
|
9394
9473
|
cwd: rootDir,
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-
|
|
1
|
+
import { n as ENGINE_INFO, r as getEngineLabel, t as APP_VERSION } from "./version-G3ekYjY1.js";
|
|
2
2
|
import { n as runSubprocess, t as isToolInstalled } from "./subprocess-CQUJDGgn.js";
|
|
3
3
|
import { r as runGenericLinter, t as fixRubyLint } from "./generic-BrcWMW7E.js";
|
|
4
4
|
import { n as runExpoDoctor } from "./expo-doctor-Bz0LZhQ6.js";
|
|
@@ -1272,9 +1272,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
1272
1272
|
return diagnostics;
|
|
1273
1273
|
};
|
|
1274
1274
|
|
|
1275
|
+
//#endregion
|
|
1276
|
+
//#region src/engines/ai-slop/non-production-paths.ts
|
|
1277
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
1278
|
+
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
1279
|
+
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
1280
|
+
|
|
1275
1281
|
//#endregion
|
|
1276
1282
|
//#region src/engines/ai-slop/comments.ts
|
|
1277
|
-
const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1278
1283
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
1279
1284
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
1280
1285
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
@@ -1331,7 +1336,7 @@ const detectTrivialComments = async (context) => {
|
|
|
1331
1336
|
for (const filePath of files) {
|
|
1332
1337
|
if (isAutoGenerated(filePath)) continue;
|
|
1333
1338
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1334
|
-
if (
|
|
1339
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
1335
1340
|
let content;
|
|
1336
1341
|
try {
|
|
1337
1342
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -1367,11 +1372,10 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
1367
1372
|
fixable
|
|
1368
1373
|
});
|
|
1369
1374
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
1370
|
-
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
1371
1375
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
1372
1376
|
if (!JS_EXTENSIONS$4.has(ext)) return [];
|
|
1373
1377
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
1374
|
-
if (
|
|
1378
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
1375
1379
|
const diagnostics = [];
|
|
1376
1380
|
const lines = content.split("\n");
|
|
1377
1381
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1421,7 +1425,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
1421
1425
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
1422
1426
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
1423
1427
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
1424
|
-
if (
|
|
1428
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
1425
1429
|
const diagnostics = [];
|
|
1426
1430
|
const lines = content.split("\n");
|
|
1427
1431
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -2326,7 +2330,6 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
2326
2330
|
|
|
2327
2331
|
//#endregion
|
|
2328
2332
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
2329
|
-
const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
2330
2333
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
2331
2334
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
2332
2335
|
const getCommentSyntax = (ext) => {
|
|
@@ -2477,13 +2480,21 @@ const looksLikeGoDocComment = (block, ext) => {
|
|
|
2477
2480
|
if (!declMatch) return false;
|
|
2478
2481
|
return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
|
|
2479
2482
|
};
|
|
2480
|
-
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see)
|
|
2483
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
|
|
2481
2484
|
const hasDocIndicator = (block) => {
|
|
2482
2485
|
const joined = block.prose.join(" ");
|
|
2483
2486
|
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
2484
2487
|
for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
|
|
2485
2488
|
return false;
|
|
2486
2489
|
};
|
|
2490
|
+
const hasPreambleSlopSignal = (block) => {
|
|
2491
|
+
const joined = block.prose.join(" ");
|
|
2492
|
+
for (const l of block.prose) {
|
|
2493
|
+
if (EXPLANATORY_OPENERS.test(l)) return true;
|
|
2494
|
+
if (JUSTIFICATION_OPENERS.some((re) => re.test(l))) return true;
|
|
2495
|
+
}
|
|
2496
|
+
return CROSS_REFERENCE_PHRASES.some((re) => re.test(joined));
|
|
2497
|
+
};
|
|
2487
2498
|
const detectNarrativeInBlock = (block, ext) => {
|
|
2488
2499
|
if (looksLikeLicenseHeader(block)) return {
|
|
2489
2500
|
matched: false,
|
|
@@ -2523,9 +2534,13 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
2523
2534
|
matched: false,
|
|
2524
2535
|
reason: ""
|
|
2525
2536
|
};
|
|
2526
|
-
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2537
|
+
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
2538
|
+
matched: true,
|
|
2539
|
+
reason: "multi-line preamble before declaration"
|
|
2540
|
+
};
|
|
2541
|
+
if (block.kind === "jsdoc" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
|
|
2527
2542
|
matched: true,
|
|
2528
|
-
reason:
|
|
2543
|
+
reason: "JSDoc preamble with slop signal"
|
|
2529
2544
|
};
|
|
2530
2545
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
2531
2546
|
matched: true,
|
|
@@ -2565,7 +2580,7 @@ const detectNarrativeComments = async (context) => {
|
|
|
2565
2580
|
const syntax = getCommentSyntax(ext);
|
|
2566
2581
|
if (!syntax) continue;
|
|
2567
2582
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
2568
|
-
if (
|
|
2583
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
2569
2584
|
let content;
|
|
2570
2585
|
try {
|
|
2571
2586
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -6361,7 +6376,7 @@ const scanCommand = async (directory, config, options) => {
|
|
|
6361
6376
|
});
|
|
6362
6377
|
}
|
|
6363
6378
|
if (options.json) {
|
|
6364
|
-
const { buildJsonOutput } = await import("./json-
|
|
6379
|
+
const { buildJsonOutput } = await import("./json-DxLkV8n2.js");
|
|
6365
6380
|
const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
|
|
6366
6381
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
6367
6382
|
return { exitCode };
|
|
@@ -7361,13 +7376,77 @@ const fixDependencyAudit = async (context, onProgress) => {
|
|
|
7361
7376
|
}
|
|
7362
7377
|
onProgress?.("Dependency audit fixes · skipping (pnpm audit unavailable and no package-lock.json for npm fallback)");
|
|
7363
7378
|
};
|
|
7379
|
+
const SEMVER_PREFIX_RE = /^[~^]?/;
|
|
7380
|
+
const parseSemverMin = (spec) => {
|
|
7381
|
+
const match = spec.replace(SEMVER_PREFIX_RE, "").match(/^(\d+|x|X|\*)(?:\.(\d+|x|X|\*))?(?:\.(\d+|x|X|\*))?/);
|
|
7382
|
+
if (!match) return null;
|
|
7383
|
+
const head = match[1];
|
|
7384
|
+
if (!/^\d+$/.test(head)) return null;
|
|
7385
|
+
const toNum = (part) => {
|
|
7386
|
+
if (!part) return 0;
|
|
7387
|
+
return /^\d+$/.test(part) ? Number(part) : 0;
|
|
7388
|
+
};
|
|
7389
|
+
return [
|
|
7390
|
+
Number(head),
|
|
7391
|
+
toNum(match[2]),
|
|
7392
|
+
toNum(match[3])
|
|
7393
|
+
];
|
|
7394
|
+
};
|
|
7395
|
+
const isDowngrade = (oldSpec, newSpec) => {
|
|
7396
|
+
const oldV = parseSemverMin(oldSpec);
|
|
7397
|
+
const newV = parseSemverMin(newSpec);
|
|
7398
|
+
if (!oldV || !newV) return false;
|
|
7399
|
+
for (let i = 0; i < 3; i++) {
|
|
7400
|
+
if ((newV[i] ?? 0) < (oldV[i] ?? 0)) return true;
|
|
7401
|
+
if ((newV[i] ?? 0) > (oldV[i] ?? 0)) return false;
|
|
7402
|
+
}
|
|
7403
|
+
return false;
|
|
7404
|
+
};
|
|
7405
|
+
const DEP_BUCKETS = [
|
|
7406
|
+
"dependencies",
|
|
7407
|
+
"devDependencies",
|
|
7408
|
+
"peerDependencies",
|
|
7409
|
+
"optionalDependencies"
|
|
7410
|
+
];
|
|
7411
|
+
const snapshotPackageVersions = (pkg) => {
|
|
7412
|
+
const map = /* @__PURE__ */ new Map();
|
|
7413
|
+
for (const bucket of DEP_BUCKETS) {
|
|
7414
|
+
const deps = pkg[bucket];
|
|
7415
|
+
if (!deps || typeof deps !== "object") continue;
|
|
7416
|
+
for (const [name, version] of Object.entries(deps)) if (typeof version === "string") map.set(`${bucket}:${name}`, version);
|
|
7417
|
+
}
|
|
7418
|
+
return map;
|
|
7419
|
+
};
|
|
7420
|
+
const revertDowngrades = (rootDir, before) => {
|
|
7421
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
7422
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
7423
|
+
const reverted = [];
|
|
7424
|
+
for (const bucket of DEP_BUCKETS) {
|
|
7425
|
+
const deps = pkg[bucket];
|
|
7426
|
+
if (!deps) continue;
|
|
7427
|
+
for (const [name, version] of Object.entries(deps)) {
|
|
7428
|
+
const prior = before.get(`${bucket}:${name}`);
|
|
7429
|
+
if (!prior) continue;
|
|
7430
|
+
if (isDowngrade(prior, version)) {
|
|
7431
|
+
deps[name] = prior;
|
|
7432
|
+
reverted.push(`${name} ${version} → ${prior}`);
|
|
7433
|
+
}
|
|
7434
|
+
}
|
|
7435
|
+
}
|
|
7436
|
+
if (reverted.length > 0) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
7437
|
+
return reverted;
|
|
7438
|
+
};
|
|
7364
7439
|
const runNpmAuditFix = async (rootDir, onProgress) => {
|
|
7440
|
+
const pkgPath = path.join(rootDir, "package.json");
|
|
7441
|
+
const before = snapshotPackageVersions(JSON.parse(fs.readFileSync(pkgPath, "utf-8")));
|
|
7365
7442
|
onProgress?.("Dependency audit fixes · running npm audit fix (can take a few minutes)");
|
|
7366
7443
|
const result = await runSubprocess("npm", ["audit", "fix"], {
|
|
7367
7444
|
cwd: rootDir,
|
|
7368
7445
|
timeout: INSTALL_TIMEOUT
|
|
7369
7446
|
});
|
|
7370
7447
|
if (result.exitCode !== 0 && !result.stdout && !result.stderr) throw new Error("npm audit fix failed");
|
|
7448
|
+
const reverted = revertDowngrades(rootDir, before);
|
|
7449
|
+
if (reverted.length > 0) onProgress?.(`Dependency audit fixes · reverted ${reverted.length} downgrade(s): ${reverted.join(", ")}`);
|
|
7371
7450
|
onProgress?.("Dependency audit fixes · running npm install");
|
|
7372
7451
|
const installResult = await runSubprocess("npm", ["install"], {
|
|
7373
7452
|
cwd: rootDir,
|
package/dist/mcp.js
CHANGED
|
@@ -531,9 +531,14 @@ const detectOverAbstraction = async (context) => {
|
|
|
531
531
|
return diagnostics;
|
|
532
532
|
};
|
|
533
533
|
|
|
534
|
+
//#endregion
|
|
535
|
+
//#region src/engines/ai-slop/non-production-paths.ts
|
|
536
|
+
const DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3|cli|cli-[\w-]+|[\w-]+-cli)\//i;
|
|
537
|
+
const BASENAME_PATTERN = /(?:^|\/)(?:benchmark|bench|demo|example|script|seed|migrate|profile|smoke|stress|load|debug|repro)[-_.][^/]*\.[mc]?[jt]sx?$|(?:^|\/)[^/]+[-_](?:benchmark|bench|demo|example)\.[mc]?[jt]sx?$/i;
|
|
538
|
+
const isNonProductionPath = (relativePath) => DIR_PATTERN.test(relativePath) || BASENAME_PATTERN.test(relativePath);
|
|
539
|
+
|
|
534
540
|
//#endregion
|
|
535
541
|
//#region src/engines/ai-slop/comments.ts
|
|
536
|
-
const NON_PRODUCTION_DIR_PATTERN$2 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
537
542
|
const TRIVIAL_VERB_STEMS = "Import|Defin|Initializ|Setting|Set\\s+up|Setup|Return|Check|Loop|Iterat|Creat|Updat|Delet|Remov|Handl|Get|Fetch|Increment|Decrement|Writ|Runn|Run|Pars|Execut|Extract|Sav|Load|Build|Start|Stopp|Stop|Clean(?:up|\\s+up)?|Configur|Validat|Process|Queue|Fire|Emit|Dispatch|Log|Print|Render";
|
|
538
543
|
const TRIVIAL_JS_COMMENT_PATTERNS = [/\/\/\s*This (?:function|method|class|variable|constant) (?:will |is used to |is responsible for )?/i, new RegExp(`\\/\\/\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
539
544
|
const TRIVIAL_PYTHON_COMMENT_PATTERNS = [/^#\s*This (?:function|method|class) (?:will |is used to )?/i, new RegExp(`^#\\s*(?:${TRIVIAL_VERB_STEMS})(?:e|es|ing|s)?\\b`, "i")];
|
|
@@ -590,7 +595,7 @@ const detectTrivialComments = async (context) => {
|
|
|
590
595
|
for (const filePath of files) {
|
|
591
596
|
if (isAutoGenerated(filePath)) continue;
|
|
592
597
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
593
|
-
if (
|
|
598
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
594
599
|
let content;
|
|
595
600
|
try {
|
|
596
601
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -626,11 +631,10 @@ const slop = (filePath, line, rule, severity, message, help, fixable) => ({
|
|
|
626
631
|
fixable
|
|
627
632
|
});
|
|
628
633
|
const LOGGER_FILE_PATTERN = /(?:^|\/)(?:logger|logging|log)\.[^/]+$/i;
|
|
629
|
-
const NON_PRODUCTION_DIR_PATTERN$1 = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|cli|cli-[\w-]+|[\w-]+-cli)\//;
|
|
630
634
|
const detectConsoleLeftovers = (content, relativePath, ext) => {
|
|
631
635
|
if (!JS_EXTENSIONS$3.has(ext)) return [];
|
|
632
636
|
if (LOGGER_FILE_PATTERN.test(relativePath)) return [];
|
|
633
|
-
if (
|
|
637
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
634
638
|
const diagnostics = [];
|
|
635
639
|
const lines = content.split("\n");
|
|
636
640
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -680,7 +684,7 @@ const asAnyPattern = new RegExp(`\\bas\\s+any\\b`);
|
|
|
680
684
|
const doubleAssertPattern = new RegExp(`\\bas\\s+unknown\\s+as\\s+`);
|
|
681
685
|
const detectUnsafeTypePatterns = (content, relativePath, ext) => {
|
|
682
686
|
if (ext !== ".ts" && ext !== ".tsx") return [];
|
|
683
|
-
if (
|
|
687
|
+
if (isNonProductionPath(relativePath)) return [];
|
|
684
688
|
const diagnostics = [];
|
|
685
689
|
const lines = content.split("\n");
|
|
686
690
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -1585,7 +1589,6 @@ const PHP_DECL_START = /^\s*(?:public|private|protected|static|final|abstract|re
|
|
|
1585
1589
|
|
|
1586
1590
|
//#endregion
|
|
1587
1591
|
//#region src/engines/ai-slop/narrative-comments.ts
|
|
1588
|
-
const NON_PRODUCTION_DIR_PATTERN = /(?:^|\/)(?:scripts|bin|examples?|demos?|bench|benches|benchmarks?|fixtures?|__fixtures__|__mocks__|__tests__|vendor|_vendor|vendored|third_party|blib2to3|lib2to3)\//i;
|
|
1589
1592
|
const stripJsdocLine = (line) => line.replace(/^\s*\/\*\*+\s?/, "").replace(/\s*\*+\/\s*$/, "").replace(/^\s*\*\s?/, "").trim();
|
|
1590
1593
|
const stripLineComment = (line) => line.replace(/^\s*(?:(?:\/\/)|#)\s?/, "");
|
|
1591
1594
|
const getCommentSyntax = (ext) => {
|
|
@@ -1736,13 +1739,21 @@ const looksLikeGoDocComment = (block, ext) => {
|
|
|
1736
1739
|
if (!declMatch) return false;
|
|
1737
1740
|
return ((block.prose.find((l) => l.length > 0) ?? "").split(/\s+/)[0] ?? "") === declMatch[1];
|
|
1738
1741
|
};
|
|
1739
|
-
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see)
|
|
1742
|
+
const DOC_INDICATOR_RE = /`[^`]+`|\|\s*[-:]+\s*\||```|\b(?:note|warning|warn|caveat|example|caution|see):|\(e\.g\.[^)]+\)|\(i\.e\.[^)]+\)/i;
|
|
1740
1743
|
const hasDocIndicator = (block) => {
|
|
1741
1744
|
const joined = block.prose.join(" ");
|
|
1742
1745
|
if (DOC_INDICATOR_RE.test(joined)) return true;
|
|
1743
1746
|
for (const l of block.prose) if (/^[-]\s/.test(l)) return true;
|
|
1744
1747
|
return false;
|
|
1745
1748
|
};
|
|
1749
|
+
const hasPreambleSlopSignal = (block) => {
|
|
1750
|
+
const joined = block.prose.join(" ");
|
|
1751
|
+
for (const l of block.prose) {
|
|
1752
|
+
if (EXPLANATORY_OPENERS.test(l)) return true;
|
|
1753
|
+
if (JUSTIFICATION_OPENERS.some((re) => re.test(l))) return true;
|
|
1754
|
+
}
|
|
1755
|
+
return CROSS_REFERENCE_PHRASES.some((re) => re.test(joined));
|
|
1756
|
+
};
|
|
1746
1757
|
const detectNarrativeInBlock = (block, ext) => {
|
|
1747
1758
|
if (looksLikeLicenseHeader(block)) return {
|
|
1748
1759
|
matched: false,
|
|
@@ -1782,9 +1793,13 @@ const detectNarrativeInBlock = (block, ext) => {
|
|
|
1782
1793
|
matched: false,
|
|
1783
1794
|
reason: ""
|
|
1784
1795
|
};
|
|
1785
|
-
if (block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1796
|
+
if (block.kind === "line" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext)) return {
|
|
1797
|
+
matched: true,
|
|
1798
|
+
reason: "multi-line preamble before declaration"
|
|
1799
|
+
};
|
|
1800
|
+
if (block.kind === "jsdoc" && block.prose.length >= 3 && looksLikeDeclarationPreamble(block.nextNonBlankLine, ext) && hasPreambleSlopSignal(block)) return {
|
|
1786
1801
|
matched: true,
|
|
1787
|
-
reason:
|
|
1802
|
+
reason: "JSDoc preamble with slop signal"
|
|
1788
1803
|
};
|
|
1789
1804
|
if (CROSS_REFERENCE_PHRASES.some((re) => re.test(joined))) return {
|
|
1790
1805
|
matched: true,
|
|
@@ -1824,7 +1839,7 @@ const detectNarrativeComments = async (context) => {
|
|
|
1824
1839
|
const syntax = getCommentSyntax(ext);
|
|
1825
1840
|
if (!syntax) continue;
|
|
1826
1841
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
1827
|
-
if (
|
|
1842
|
+
if (isNonProductionPath(relativePath)) continue;
|
|
1828
1843
|
let content;
|
|
1829
1844
|
try {
|
|
1830
1845
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -5056,7 +5071,7 @@ const handleAislopBaseline = (input) => {
|
|
|
5056
5071
|
|
|
5057
5072
|
//#endregion
|
|
5058
5073
|
//#region src/version.ts
|
|
5059
|
-
const APP_VERSION = "0.8.
|
|
5074
|
+
const APP_VERSION = "0.8.2";
|
|
5060
5075
|
|
|
5061
5076
|
//#endregion
|
|
5062
5077
|
//#region src/mcp.ts
|
|
@@ -29,7 +29,7 @@ const getEngineLabel = (engine) => ENGINE_INFO[engine].label;
|
|
|
29
29
|
|
|
30
30
|
//#endregion
|
|
31
31
|
//#region src/version.ts
|
|
32
|
-
const APP_VERSION = "0.8.
|
|
32
|
+
const APP_VERSION = "0.8.2";
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
export { ENGINE_INFO as n, getEngineLabel as r, APP_VERSION as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aislop",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "The engineering standards layer and quality gate for AI-written code. Define your standard once. Every agent — Claude Code, Cursor, Codex — is held to it automatically, on every edit and every PR. Catches the slop they leave behind, enforces the rules your team sets. 8+ languages. Deterministic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|