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 CHANGED
@@ -9,107 +9,36 @@
9
9
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
10
10
  [![Node >= 20](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
11
11
 
12
- Define your standard once in `.aislop/config.yml` + `.aislop/rules.yml`. Every change your agent makes is held to it automatically. `aislop` catches the slop they leave behind (narrative comments, `as any`, swallowed errors, hallucinated imports, todo stubs), enforces the rules your team sets, and scores every change 0–100. 8+ languages. Deterministic no LLM at runtime.
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
- ![aislop scan demo](assets/scan.gif)
33
-
34
- ### Fix
35
-
36
- ![aislop fix demo](assets/fix.gif)
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
- Sample output:
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
- > Code Quality
67
- [WARN] [auto] Unused export (2)
68
- src/lib/format-bytes.ts:12
69
- src/utils/retry.ts:8
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
- ## Why aislop
87
-
88
- AI coding tools generate code that compiles and passes tests but ships with patterns no engineer would write: trivial comments, swallowed exceptions, unused imports, `as any` casts, oversized functions, and leftover `console.log` calls. These problems are spread across many files and slip through review.
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
+ [![aislop](https://badges.scanaislop.com/score/<owner>/<repo>.svg)](https://scanaislop.com)
33
+ ```
98
34
 
99
- ## What it catches
35
+ Run `npx aislop badge` to auto-generate. Free at [scanaislop.com](https://scanaislop.com).
100
36
 
101
- Six deterministic engines run in parallel:
37
+ ## See it in action
102
38
 
103
- | Engine | What it checks | How |
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
- See the full [rules reference](docs/rules.md).
41
+ ![aislop scan demo](assets/scan.gif)
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 your project
70
+ ### Scan
142
71
 
143
72
  ```bash
144
- aislop scan # scan current directory
145
- aislop scan ./src # scan a specific directory
146
- aislop scan --changes # only files changed from HEAD
147
- aislop scan --staged # only staged files (pre-commit)
148
- aislop scan --json # output 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 and directories.** `node_modules`, `.git`, `dist`, `build`, and `coverage` are excluded by default. Add more via `.aislop/config.yml`:
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" # globs supported (micromatch)
84
+ - "**/*.test.ts"
156
85
  - src/generated
157
- - legacy
158
86
  ```
159
87
 
160
- Or override per-run with `--exclude` (comma-separated or repeatable, stacks on top of the config):
88
+ Or via CLI: `npx aislop scan --exclude "**/*.test.ts,dist"`
161
89
 
162
- ```bash
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 # relative path to a parent config
175
-
94
+ extends: ../../.aislop/base.yml
176
95
  ci:
177
- failBelow: 80 # override just this key, inherit the rest
96
+ failBelow: 80 # override specific keys
178
97
  ```
179
98
 
180
- `extends:` accepts a single path or an array of paths. Later entries win. Deep-merged: nested objects (`scoring.weights`, `engines`) are merged key-by-key; arrays are replaced. Circular references and depths beyond 5 are rejected with a clear error.
99
+ ### Fix
181
100
 
182
- ### Fix issues automatically
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: unused imports, formatting, lint
186
- aislop fix -f # aggressive: dependency audit, unused files, Expo alignment
104
+ npx aislop fix # safe auto-fixes
105
+ npx aislop fix -f # aggressive: deps, unused files
187
106
  ```
188
107
 
189
- ### Hand off to your coding agent
108
+ ### Hand off to agent
190
109
 
191
- When auto-fix can't solve it, aislop generates a prompt with full context and opens your agent. 14 supported:
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 --codex # Codex CLI
196
- aislop fix --cursor # Cursor (copies prompt to clipboard)
197
- aislop fix --gemini # Gemini CLI
198
- aislop fix --windsurf # Windsurf (copies prompt to clipboard)
199
- aislop fix --amp # Amp
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 as a native hook
121
+ ### Install hook
212
122
 
213
- One command and aislop runs automatically after every agent edit. Findings flow back to the agent as structured feedback (`aislop.hook.v1`) with score, counts, top-20 findings, and next steps, so the agent can self-correct on the same turn.
123
+ Runs after every agent edit. Feedback flows back immediately.
214
124
 
215
125
  ```bash
216
- aislop hook install --claude # Claude Code PostToolUse
217
- aislop hook install --cursor # Cursor afterFileEdit
218
- aislop hook install --gemini # Gemini CLI AfterTool
219
- aislop hook install # every supported agent at once
220
- aislop hook install claude cursor # pick any subset as positional args
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 on every edit): `claude`, `cursor`, `gemini`.
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
- Opt-in quality-gate mode captures `.aislop/baseline.json` at install time and blocks the Claude Stop hook if the score regresses:
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 hooks
234
- aislop hook uninstall --claude # remove a specific agent
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
- Every install is sentinel-guarded (SHA-256 hash fence) for idempotent re-runs and exact uninstall. Full guide: [`/docs/hooks`](https://scanaislop.com/docs/hooks).
145
+ Docs: [`/docs/hooks`](https://scanaislop.com/docs/hooks)
239
146
 
240
- ### Use as an MCP server
147
+ ### MCP server
241
148
 
242
- aislop ships an MCP (Model Context Protocol) server so any agent that speaks MCP — Claude Desktop, Claude Code, Cursor, Codex — can call it as a tool.
149
+ Expose aislop as MCP tools for Claude Desktop, Cursor, Codex:
243
150
 
244
151
  ```jsonc
245
- // ~/.cursor/mcp.json / Claude Desktop config / ~/.codex/config.toml equivalent
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 exposed:
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
- ### Common workflow
165
+ ### CI
271
166
 
272
167
  ```bash
273
- # before commit
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 doctor # check which tools are available
288
- aislop rules # list all built-in rules
289
- aislop badge # print the public score badge URL + README snippet
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
- See [all commands and flags](docs/commands.md).
180
+ Docs: [commands](docs/commands.md)
295
181
 
296
182
  ---
297
183
 
298
- ## Use in your project
184
+ ## CI integration
299
185
 
300
- ### Pre-commit hook
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
- Fastest path: run `npx aislop init` and say yes to "Add a GitHub Actions workflow?". It drops a working `.github/workflows/aislop.yml` for you.
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
- Or use the composite action (one-liner):
204
+ **Composite action**:
321
205
 
322
206
  ```yaml
323
207
  - uses: actions/checkout@v4
324
- - uses: scanaislop/aislop@v0.5
208
+ - uses: scanaislop/aislop@v0.8
325
209
  ```
326
210
 
327
211
  ### Quality gate
328
212
 
329
- Set a minimum score in `.aislop/config.yml`:
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 with code 1 when the score drops below the threshold. See [CI/CD docs](docs/ci.md) for more.
220
+ `aislop ci` exits 1 when score < threshold. Docs: [CI/CD](docs/ci.md)
337
221
 
338
222
  ---
339
223
 
340
- ## Documentation
224
+ ## For teams
341
225
 
342
- | Topic | Link |
343
- |---|---|
344
- | Installation | [docs/installation.md](docs/installation.md) |
345
- | Commands & flags | [docs/commands.md](docs/commands.md) |
346
- | Rules reference | [docs/rules.md](docs/rules.md) |
347
- | Configuration | [docs/configuration.md](docs/configuration.md) |
348
- | Scoring | [docs/scoring.md](docs/scoring.md) |
349
- | CI / CD | [docs/ci.md](docs/ci.md) |
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
- ## For engineering teams
237
+ ## Why aislop
355
238
 
356
- `aislop` runs locally and in your CI. [scanaislop](https://scanaislop.com) is the hosted platform built on top of it for teams that want enforcement without wiring every workflow themselves.
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
- - **PR gates on every repo** with a score threshold and block-to-merge
359
- - **Standards hierarchy**: org baseline, team overrides, project config
360
- - **Per-team dashboards** and agent attribution over time
361
- - **Visual rules manager** so engineering leads set standards without editing YAML
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
- The CLI is MIT-licensed and always will be. [Learn more about the platform →](https://scanaislop.com)
246
+ ## What it catches
365
247
 
366
- ## Public score badge
248
+ Six deterministic engines run in parallel:
367
249
 
368
- Show your aislop score on a README. Free for any project that opts in on [scanaislop.com](https://scanaislop.com).
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
- ```markdown
371
- [![aislop](https://badges.scanaislop.com/score/<owner>/<repo>.svg)](https://scanaislop.com)
372
- ```
259
+ See the full [rules reference](docs/rules.md).
260
+
261
+ ---
373
262
 
374
- Shields-compatible SVG, edge-cached on Cloudflare. Colour-coded: green ≥ 85, amber 70-84, red < 70, grey if no scans yet.
263
+ ## Docs
375
264
 
376
- Run `aislop badge` to print the snippet pre-filled with your repo's owner/name, auto-detected from `git remote get-url origin`.
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) for development setup and how to add new rules. AI coding assistants can find project context in [AGENTS.md](AGENTS.md).
269
+ See [CONTRIBUTING.md](CONTRIBUTING.md). AI assistants: [AGENTS.md](AGENTS.md).
381
270
 
382
271
  ## Acknowledgments
383
272
 
384
- `aislop` is built on top of excellent open-source projects:
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
- This list is regenerated by `.github/workflows/contributors.yml` after every push to `develop` or `main`. The workflow reads git log, resolves each author's GitHub login, and opens a PR with any diff. If your commits aren't being credited, either link your commit email under [GitHub Settings → Emails](https://github.com/settings/emails) or add a mapping to [`.github/contributors-overrides.json`](.github/contributors-overrides.json).
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 (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
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 (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
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 (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
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):/i;
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: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
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 (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
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.0";
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-B9ZchFMv.js";
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 (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
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 (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
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 (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
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):/i;
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: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
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 (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
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-BJGLCIK-.js");
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,
@@ -1,4 +1,4 @@
1
- import { n as ENGINE_INFO, t as APP_VERSION } from "./version-B9ZchFMv.js";
1
+ import { n as ENGINE_INFO, t as APP_VERSION } from "./version-G3ekYjY1.js";
2
2
 
3
3
  //#region src/output/json.ts
4
4
  const buildJsonOutput = (results, scoreResult, fileCount, elapsedMs) => {
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 (NON_PRODUCTION_DIR_PATTERN$2.test(relativePath)) continue;
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 (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
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 (NON_PRODUCTION_DIR_PATTERN$1.test(relativePath)) return [];
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):/i;
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: block.kind === "jsdoc" ? "JSDoc preamble before declaration" : "multi-line preamble before declaration"
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 (NON_PRODUCTION_DIR_PATTERN.test(relativePath)) continue;
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.0";
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.0";
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.0",
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": {