stopslop 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,329 +1,267 @@
1
- # stopslop
1
+ # StopSlop
2
2
 
3
- [![slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/BaryshevRS/stopslop/main/stopslop-badge.json)](https://github.com/BaryshevRS/stopslop)
3
+ [![AI slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/BaryshevRS/stopslop/main/stopslop-badge.json)](https://github.com/BaryshevRS/stopslop)
4
4
 
5
- > **0.1 preview.** The CLI and report formats are usable in CI; compatibility
6
- > tiers and the pre-1.0 change policy are documented in
7
- > [COMPATIBILITY.md](COMPATIBILITY.md). Requires Node.js 20.19+ or 22.12+.
5
+ **Stop AI-generated code from becoming instant legacy.**
8
6
 
9
- **Slop is instant legacy.** A linter checks *how* code is written. **stopslop**
10
- checks whether the agent *overproduced junk* — over-complex functions, god
11
- modules/classes, and responsibilities dumped into one file. It measures how much
12
- uncleaned, unrefactored overproduction a repo carries — whether that took an
13
- agent a week or a team a decade. Zero style or type rules; it is not a competitor
14
- to ESLint/Biome/Oxlint.
7
+ Coding agents are good at making code compile. They are also good at making too
8
+ much of it: tangled functions, god files, copied blocks, dead exports, and entire
9
+ features that pass their own tests but are never connected to the application.
10
+
11
+ StopSlop catches that structural overproduction before it becomes permanent.
12
+ It complements ESLint, Biome, and Oxlint rather than repeating their style and
13
+ type rules.
15
14
 
16
15
  ```bash
17
16
  npx stopslop .
18
17
  ```
19
18
 
20
- Or install it in the project so CI uses the version pinned in your lockfile:
19
+ It runs locally against JavaScript and TypeScript. No account. No source-code
20
+ upload.
21
+
22
+ > **0.1 preview:** Requires Node.js 20.19+ or 22.12+. Public surfaces and the
23
+ > pre-1.0 change policy are documented in [COMPATIBILITY.md](COMPATIBILITY.md).
24
+
25
+ ## What it catches
26
+
27
+ - **Tangled functions** using cognitive complexity from the SonarSource
28
+ specification.
29
+ - **God classes and god modules** that mix unrelated responsibilities or have
30
+ grown into densely connected monoliths.
31
+ - **Useful split boundaries** for god units when cohesion exposes responsibility
32
+ groups that should move together.
33
+ - **Copy-pasted blocks**, with optional identifier normalization to catch copies
34
+ whose variables were renamed.
35
+ - **Dead exports, dependencies, and files.**
36
+ - **Orphan features** kept alive only by their own paired test but called by no
37
+ production code.
38
+ - **Files exporting multiple classes**, a common sign that generation kept
39
+ appending instead of designing.
40
+
41
+ Complexity and structural analysis are implemented by StopSlop. Clone detection
42
+ uses [Clone Alert](https://www.npmjs.com/package/clone-alert); dead-code analysis
43
+ uses [Knip](https://knip.dev). StopSlop detects code patterns, not whether a human
44
+ or an AI wrote them.
45
+
46
+ ## Start with one command
47
+
48
+ Run without installing:
21
49
 
22
50
  ```bash
23
- pnpm add --save-dev stopslop
24
- pnpm exec stopslop .
51
+ npx stopslop .
25
52
  ```
26
53
 
27
- ## What it finds
28
-
29
- - **Cognitive complexity** — functions that are hard to follow, scored by the
30
- G. Ann Campbell / SonarSource specification.
31
- - **God class** — flagged on either of two axes: **dispersion** (methods split
32
- into several unrelated responsibility groups — LCOM4 cohesion + hub removal,
33
- with clean split boundaries) or **size/WMC** (a densely-connected monolith too
34
- large to be one unit — the "everything is honestly connected but it's 2k lines"
35
- class an agent produces, which cohesion can't and shouldn't split).
36
- - **God module** — the same, applied to a file's top-level definitions: a
37
- "dump" of unrelated functions/classes/constants.
38
- - **Multiple exported classes** in one file.
39
- - **Clones** — copy-pasted blocks, *including ones whose variables were renamed*
40
- (identifiers are normalized before matching). Engine:
41
- [Clone Alert](https://www.npmjs.com/package/clone-alert).
42
- - **Dead code** — unused exports, unused dependencies, unreachable files. Engine:
43
- [Knip](https://knip.dev), driven through its programmatic session API and
44
- configured exclusively under `stopslop.json#knip`.
45
- - **Orphan features** — a function no production code calls, kept alive by
46
- nothing but its own paired test. A feature that was generated together with
47
- its test and never wired in: the test is green, the code does nothing. Read
48
- off Knip's module graph; the finding names both halves to delete.
49
-
50
- The last two checks are other people's engines, and we say so in the output.
51
- Everything above them is ours.
52
-
53
- Each god finding lists the responsibility groups by member name — ready-made
54
- refactor boundaries.
54
+ Or pin it in the project so local runs and CI use the same version:
55
55
 
56
- ```
57
- god class Class ExpressAdapter: 41 methods across 4 unrelated responsibility groups (+27 standalone)
58
- 4 responsibility groups (suggested split boundaries):
59
- 1. close, closeOpenConnections, initHttpServer, trackOpenConnections
60
- 2. applyStreamHeaders, reply, setHeaderIfNotExists
61
- 3. set, setBaseViewsDir, setViewEngine
62
- 4. isMiddlewareApplied, registerParserMiddleware
56
+ ```bash
57
+ npm install --save-dev stopslop
58
+ npx stopslop .
63
59
  ```
64
60
 
65
- ## Slop level
61
+ The default report gives you the full repository score, the signal densities,
62
+ and a grouped finding list. Add `--details` to show all clone locations and the
63
+ suggested split boundaries for god units; use `--json` for every finding.
66
64
 
67
- One number, so a repo can be compared with itself over time and with other repos
68
- regardless of size — absolute counts just track how big the codebase is.
65
+ ```text
66
+ stopslop 27 files 4.3 KLOC 980ms
69
67
 
70
- ```
71
- slop level: moderate (49.4/100)
72
- god units 0.00/KLOC · complex fns 5.81/KLOC · duplication 3.7% · dead code 5.09/KLOC
68
+ slop level: low (22.3/100) · worse than 49% of reference repos
69
+ god units 0.00/KLOC · complex fns 4.42/KLOC · duplication 0.8%
70
+ baseline: 23 accepted · 0 new
71
+
72
+ ✓ no new slop since baseline
73
73
  ```
74
74
 
75
- Every signal is a **density** (per KLOC; duplication is a share of lines). Each
76
- rises linearly from a *floor* (below it the density is the baseline of any
77
- living codebase — not slop) to a *budget* (the density at which the signal is as
78
- bad as it gets), then saturates, and is weighted:
75
+ Exit codes are made for CI: `0` means clear, `1` means findings, and `2` means
76
+ the analysis itself failed.
79
77
 
80
- ```
81
- score = 100 · Σ wᵢ · sat(xᵢ) / Σ wᵢ (i = enabled signals)
82
- sat(x) = clamp((x − floorᵢ) / (budgetᵢ − floorᵢ), 0, 1)
83
- ```
78
+ ## Adopt an existing codebase without cleaning it first
84
79
 
85
- | signal | xᵢ | weight | floor | budget |
86
- |---|---|---|---|---|
87
- | god units | (god modules + god classes) / KLOC | 0.35 | 0 | 0.1 |
88
- | duplication | duplicated lines / total lines | 0.25 | 0.05 | 0.30 |
89
- | complexity | over-complex functions / KLOC | 0.20 | 0 | 4 |
90
- | dead code | (unused exports + deps + files) / KLOC | 0.20 | 0 | 4 |
80
+ You do not need a perfect repository to start. Record today's findings once,
81
+ commit them, and fail only when a change adds an unaccepted finding:
91
82
 
92
- Floors and budgets are calibrated on a benchmark of **41 mature TypeScript OSS
93
- repos (9.4 MLOC)** — angular, react, playwright, nextjs, svelte, vue and the
94
- like: a floor is what ordinary repos carry (~p25), a budget is past anything in
95
- the reference corpus (~max/p90).
96
- The report also anchors the number against that corpus directly:
83
+ ```bash
84
+ # Record accepted legacy.
85
+ npx stopslop . \
86
+ --baseline .stopslop-baseline.json \
87
+ --update-baseline
97
88
 
89
+ git add .stopslop-baseline.json
90
+
91
+ # Use the same file as the CI gate.
92
+ npx stopslop . --baseline .stopslop-baseline.json
98
93
  ```
99
- slop level: moderate (43.0/100) · worse than 60% of reference repos
100
- ```
101
94
 
102
- Bands: `clean` <10 · `low` <25 · `moderate` <50 · `high` <75 · `legacy-grade` ≥75.
103
- A disabled check leaves the formula entirely (it is not scored as clean).
104
- Weights, floors, and budgets are configurable — details and rationale in
105
- [docs/score.md](docs/score.md). **The densities are the facts; the score is an
106
- aggregate.**
95
+ The baseline is a sorted, reviewable list of finding identities. Updating it is
96
+ an explicit acceptance of debt, so baseline changes should be reviewed like
97
+ source changes.
98
+
99
+ The baseline never lowers the score. These three surfaces answer different
100
+ questions:
101
+
102
+ | Surface | What it tells you |
103
+ | --- | --- |
104
+ | Findings | What needs attention in this run |
105
+ | Gate | Whether any finding remains unaccepted |
106
+ | Score | How much slop the complete repository carries, including accepted legacy |
107
107
 
108
- ## Slop badge
108
+ A baselined run therefore remains honest: the report can show
109
+ `moderate (43/100)`, `30 accepted`, and `0 new` while the gate is green.
109
110
 
110
- Show off a clean codebase with a [shields.io](https://shields.io/badges/endpoint-badge)
111
- badge. `--format shields` prints a shields **endpoint JSON** to stdout — host it
112
- (a committed file, a gist, anywhere reachable) and point shields at it:
111
+ ## Put the gate in your README
113
112
 
114
- ```sh
115
- stopslop . --format shields > stopslop-badge.json
113
+ The badge is deliberately binary. It is a public CI status, not a miniature
114
+ analytics report.
115
+
116
+ ```text
117
+ AI slop | clear 0 unaccepted findings
118
+ AI slop | detected 1 or more unaccepted findings
116
119
  ```
117
120
 
121
+ Generate the [Shields endpoint JSON](https://shields.io/badges/endpoint-badge)
122
+ with the same baseline used by CI:
123
+
124
+ ```bash
125
+ npx stopslop . \
126
+ --baseline .stopslop-baseline.json \
127
+ --format shields > stopslop-badge.json
128
+ ```
129
+
130
+ Commit or publish `stopslop-badge.json`, then add this Markdown:
131
+
118
132
  ```md
119
- [![slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/stopslop-badge.json)](https://github.com/OWNER/REPO)
133
+ [![AI slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/stopslop-badge.json)](https://github.com/OWNER/REPO)
120
134
  ```
121
135
 
122
- shields fetches the JSON and renders the badge, so it refreshes whenever you
123
- regenerate the file. The message is the level and score; the color comes from a
124
- fixed scale, tuned to reward near-zero slop:
125
-
126
- | Result | Color | |
127
- | --- | --- | --- |
128
- | **`0 slop`** | 🟢 bright green | the flex — nothing to clean up |
129
- | **`clean` <10** | 🟢 bright green | clean |
130
- | **`low` <25** | 🟢 green | a little debt |
131
- | **`moderate` <50** | 🟡 yellow | has debt |
132
- | **`high` <75** | 🟠 orange | needs attention |
133
- | **`legacy-grade` ≥75** | 🔴 red | reads like accumulated legacy |
134
-
135
- `--format shields` always exits `0` (it is badge generation, not a gate), so a CI
136
- step can regenerate it without failing the build. A score built from a subset of
137
- the signals (a check was disabled or bailed out) is inflated, so its message is
138
- marked with a `*` — run the full set for a comparable badge. Regenerate it in CI
139
- to keep it fresh:
136
+ Without `--baseline` or `--base`, `clear` requires zero findings across the
137
+ entire scan. With a baseline, it requires zero findings outside the committed
138
+ baseline. The full score is still present in the terminal and JSON reports.
139
+
140
+ `--format shields` exits `0` for both `clear` and `detected` so a publishing step
141
+ can write the current state. An incomplete or failed analysis exits `2` before
142
+ emitting a badge.
143
+
144
+ ## Gate pull requests
145
+
146
+ Use a committed baseline for a stable, explicitly reviewed allowlist:
140
147
 
141
148
  ```yaml
142
- - run: npx stopslop . --format shields > stopslop-badge.json
143
- # then commit the file (or push it to a gist) so shields serves the latest value
149
+ - name: Reject new AI slop
150
+ run: npx stopslop . --baseline .stopslop-baseline.json
144
151
  ```
145
152
 
146
- ## Usage
153
+ Or compare the working tree directly with the target branch and keep no baseline
154
+ file:
147
155
 
148
- ```
149
- stopslop [path] analyze a directory or file (default: .)
150
- --format <fmt> text (default) · json · sarif · shields (badge JSON)
151
- --json alias for --format json (stable, versioned schema)
152
- --details, -d show cluster composition, hubs, all clone sites
153
- --fast AST checks only: skip clones and dead code
154
- --config <file> path to a stopslop.json
155
- --base <ref> gate only on findings absent from this Git revision
156
- --baseline <file> gate only on findings not in this baseline
157
- --update-baseline write current findings to --baseline and exit 0
156
+ ```bash
157
+ npx stopslop . --base origin/main
158
158
  ```
159
159
 
160
- Exit codes: `0` clean · `1` findings · `2` error.
160
+ `--base` analyzes both revisions and reports findings that exist only in the
161
+ current tree. CI must check out enough Git history to resolve the base revision.
162
+ The command never switches your branch or changes the index.
161
163
 
162
- ### For an agent loop
164
+ For GitHub Code Scanning, emit SARIF before propagating the gate failure:
163
165
 
164
166
  ```bash
165
- stopslop --json > slop.json # feed to the agent: clusters are the split boundaries
167
+ npx stopslop . --base origin/main --format sarif > stopslop.sarif
166
168
  ```
167
169
 
168
- ### Git-base gate
170
+ ## Send the findings back to the agent
169
171
 
170
- For pull requests, `--base <ref>` analyzes both the current working tree and the
171
- given Git revision, then reports only findings absent from the base:
172
+ The JSON report is stable and versioned. Feed it into the next coding-agent turn
173
+ instead of paraphrasing terminal output:
172
174
 
173
- ```sh
174
- stopslop . --base origin/main
175
- stopslop . --base origin/main --format sarif > stopslop.sarif
175
+ ```bash
176
+ npx stopslop . --json > slop-report.json
176
177
  ```
177
178
 
178
- The current tree is analyzed as-is, including uncommitted changes. The base is
179
- checked out detached into a fresh directory under the operating system's temp
180
- directory. StopSlop links the installed dependency context (`node_modules` and
181
- supported generated directories) into that checkout, analyzes the same project
182
- subdirectory on both sides, and removes the temporary worktree in `finally`.
183
- It never switches the user's branch or edits the index or working files.
179
+ God-unit findings include member groups that serve as concrete refactoring
180
+ boundaries. Clone findings include every occurrence. Baseline and Git-base runs
181
+ retain the complete score while returning only new findings.
184
182
 
185
- This is an identity gate, not a metric diff: an existing finding with the same
186
- StopSlop fingerprint is accepted; a fingerprint seen only in the current tree is
187
- new. The full current slop score remains unchanged. `--base` is mutually
188
- exclusive with `--baseline` and `--update-baseline`.
183
+ ## Score: trend the repository, do not gate on the number
189
184
 
190
- `--format sarif` emits SARIF 2.1.0 for GitHub Code Scanning. It has the normal
191
- gate exit codes (`0` clean, `1` findings, `2` error), so upload the report before
192
- re-failing the job:
185
+ The `0..100` slop score normalizes findings by KLOC, so a repository can be
186
+ compared with itself over time instead of merely growing a larger raw count. It
187
+ combines four densities: god units, duplication, over-complex functions, and
188
+ dead code.
193
189
 
194
- ```yaml
195
- name: stopslop
196
- on: pull_request
197
-
198
- permissions:
199
- contents: read
200
- security-events: write
201
-
202
- jobs:
203
- stopslop:
204
- runs-on: ubuntu-latest
205
- steps:
206
- - uses: actions/checkout@v6
207
- with:
208
- fetch-depth: 0
209
- - id: scan
210
- continue-on-error: true
211
- run: npx stopslop . --base "origin/${{ github.base_ref }}" --format sarif > stopslop.sarif
212
- - if: always() && steps.scan.outcome != 'cancelled'
213
- uses: github/codeql-action/upload-sarif@v4
214
- with:
215
- sarif_file: stopslop.sarif
216
- category: stopslop
217
- - if: steps.scan.outcome == 'failure'
218
- run: exit 1
219
- ```
190
+ Floors and budgets are anchored in a benchmark of 41 mature TypeScript
191
+ repositories totaling 9.4 MLOC. The weights remain an explicit judgement call,
192
+ not a scientific truth. That is why the binary gate uses findings rather than an
193
+ arbitrary score cutoff.
220
194
 
221
- ## Baseline (adopting an existing project)
195
+ The complete formula, calibration rationale, limitations, and corpus comparison
196
+ are in [docs/score.md](docs/score.md) and
197
+ [docs/thresholds.md](docs/thresholds.md).
222
198
 
223
- A mature or already-sloppy repo can light up red on day one. A **baseline** lets
224
- you accept today's findings as legacy and gate CI only on what is added
225
- afterwards — the honest answer to "this codebase is old and complex, but I don't
226
- want the agent making it *worse*."
199
+ ## CLI
227
200
 
228
- ```sh
229
- # 1. Record today's findings (writes the file, exits 0)
230
- stopslop . --baseline .stopslop-baseline.json --update-baseline
201
+ ```text
202
+ stopslop [path] analyze a directory or file (default: .)
231
203
 
232
- # 2. In CI: fail only on findings not in the baseline
233
- stopslop . --baseline .stopslop-baseline.json
204
+ --format <fmt> text (default) · json · sarif · shields
205
+ --json alias for --format json
206
+ --details, -d show split groups, hubs, and all clone locations
207
+ --fast skip clone and dead-code analysis
208
+ --config <file> use a specific stopslop.json
209
+ --base <ref> gate on findings absent from a Git revision
210
+ --baseline <file> gate on findings absent from a baseline file
211
+ --update-baseline replace the baseline with current findings
212
+ --help, -h show help
234
213
  ```
235
214
 
236
- The baseline is a small, sorted JSON file you commit and review in pull requests.
237
- A finding's identity is `kind + file + symbol` — no line numbers, no messages — so
238
- a baselined god class stays accepted as it grows, and the file produces a stable,
239
- churn-free diff. Clones key on their content fingerprint alone, so they stay
240
- suppressed even when moved between files. Re-run `--update-baseline` to re-adopt
241
- after an intentional change.
242
-
243
- Use this committed file when you want a deliberately reviewed, stable allowlist.
244
- Use `--base` when the target branch itself should be the source of truth and no
245
- baseline artifact should be maintained. They share fingerprints but are separate
246
- CLI modes; one does not read or rewrite the other's state.
247
-
248
- **The baseline never touches the slop score or the badge.** The score is the
249
- honest state of the repo ("how much uncleaned code is here"); the baseline is
250
- only a CI gate ("don't add more"). A gated run reports the new findings and its
251
- score line reads e.g. `moderate (43.0/100)` with `baseline: 30 accepted · 1 new`
252
- below it — the number stays truthful while CI fails only on the one new finding.
215
+ Invalid configuration, no supported source files, and hard analysis errors exit
216
+ with code `2` before StopSlop emits a report, badge, or baseline.
253
217
 
254
- ## Config (`stopslop.json`)
218
+ ## Configuration
255
219
 
256
- Every metric is configurable — thresholds, and each check can be turned off with
257
- `false`. All fields are optional and merged over the defaults.
220
+ StopSlop works without configuration. To tune a check, add `stopslop.json`:
258
221
 
259
222
  ```json
260
223
  {
261
224
  "$schema": "https://unpkg.com/stopslop/schema.json",
262
- "cognitiveComplexity": 15,
263
- "godModule": { "minMembers": 15, "minClusters": 3, "minComplexity": 150, "sizeMembers": 37, "sizeComplexity": 350 },
264
- "godClass": { "minMembers": 12, "minClusters": 3, "minComplexity": 90, "sizeMembers": 30, "sizeComplexity": 200 },
265
- "multipleExportedClasses": { "maxPerFile": 1 },
266
- "duplicates": { "minTokens": 50, "minLines": 5, "ignoreIdentifiers": true, "ignoreLiterals": false },
267
- "deadCode": { "exports": true, "dependencies": true, "files": true, "production": false },
268
- "knip": {
269
- "workspaces": {
270
- ".": { "entry": ["src/index.ts"] },
271
- "packages/*": { "entry": ["src/index.ts"], "project": ["src/**/*.ts"] }
272
- }
225
+ "cognitiveComplexity": 20,
226
+ "duplicates": {
227
+ "minTokens": 100,
228
+ "minLines": 10,
229
+ "ignoreIdentifiers": true
273
230
  },
274
- "score": { "weights": { "god": 0.35 }, "budgets": { "duplication": 0.3 }, "floors": { "duplication": 0.05 } },
275
- "hubFanInRatio": 0.5,
276
231
  "ignore": ["**/*.generated.ts"]
277
232
  }
278
233
  ```
279
234
 
280
- | key | what it tunes |
281
- |---|---|
282
- | `cognitiveComplexity` | report functions above this score (or `false` to disable) |
283
- | `godClass` / `godModule` | `min*` = dispersion axis (unrelated groups); `size*` = size/WMC axis (a monolith too big even if fully connected); or `false` |
284
- | `multipleExportedClasses` | `maxPerFile` classes per file before flagging; or `false` |
285
- | `duplicates` | clone size floors and token normalization; or `false` |
286
- | `deadCode` | which Knip issue classes to report; or `false` |
287
- | `knip` | complete JSON-compatible Knip config, including `workspaces` |
288
- | `score` | slop-level weights, floors, and budgets |
289
- | `hubFanInRatio` | fan-in fraction that marks a shared "hub" member for removal |
290
- | `ignore` | extra exclude globs on top of the built-ins |
291
-
292
- Set a check to `false` to disable it, e.g. `"godModule": false`.
293
-
294
- `stopslop.json` is the only Knip configuration source. StopSlop does not load or
295
- execute `knip.json`, `.knip.json`, or `knip.config.*`; `package.json#knip` is
296
- discarded and never merged with `knip`. The published `schema.json` provides editor
297
- completion and validation for both StopSlop fields and the installed Knip
298
- configuration surface. StopSlop deliberately has no config generator or init
299
- wizard; project-aware generation belongs to separate tooling.
300
-
301
- For a single-package project, put `entry` and `project` directly under `knip`.
302
- For a monorepo, put them under `knip.workspaces`: Knip intentionally ignores
303
- top-level `entry` and `project` when multiple workspaces are present.
304
-
305
- **Full reference with every field, defaults, and examples:
306
- [docs/configuration.md](docs/configuration.md).** Defaults are calibrated on a
307
- 22-repo TypeScript corpus ([docs/thresholds.md](docs/thresholds.md)) and marked
308
- *preliminary* in output until validated further.
309
-
310
- ## How it works & why the numbers
311
-
312
- Architecture, literature references, and which parts are our heuristics:
313
- [ARCH.md](ARCH.md). Implementation plan: [PLAN.md](PLAN.md).
314
-
315
- Under the hood: **complexity and god module/class are ours**; clones are
316
- [Clone Alert](https://www.npmjs.com/package/clone-alert) and dead code is
317
- [Knip](https://knip.dev).
235
+ Every field is optional. Set a check to `false` to disable it. The published
236
+ schema provides editor completion and rejects unknown or invalid properties.
237
+
238
+ See [docs/configuration.md](docs/configuration.md) for every field, default, and
239
+ Knip workspace example. See [stopslop.example.json](stopslop.example.json) for a
240
+ complete configuration.
241
+
242
+ ## Design and limitations
243
+
244
+ - StopSlop supports `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, and `.tsx` source.
245
+ - Tests, generated code, build output, dependencies, and common framework output
246
+ directories are excluded by default.
247
+ - A high score does not prove poor engineering, and a low score does not prove
248
+ correctness. StopSlop measures structural overproduction, not total quality.
249
+ - Dead-code accuracy depends on Knip knowing the project's entry points.
250
+ - A baseline accepts finding identities. If an accepted function becomes more
251
+ complex without changing identity, the score rises but the gate stays clear.
252
+
253
+ Architecture, research references, and the boundary between published methods
254
+ and StopSlop heuristics are documented in [ARCH.md](ARCH.md).
318
255
 
319
256
  ## Development
320
257
 
321
258
  ```bash
322
259
  pnpm install
323
260
  pnpm test
261
+ pnpm typecheck
324
262
  pnpm build
325
- pnpm test:package # install and execute the packed npm artifact
326
- pnpm release:check # complete pre-publish gate
327
- pnpm schema:generate # refresh schema.json after a Knip/config change
328
- pnpm calibrate # regenerate docs/thresholds.md from a corpus
263
+ pnpm test:package
264
+ pnpm release:check
329
265
  ```
266
+
267
+ MIT licensed. See [LICENSE](LICENSE).
package/dist/cli.js CHANGED
@@ -51,7 +51,7 @@ function renderTerminal(result, details) {
51
51
  /** The headline: one comparable number, with the densities it came from. */
52
52
  function renderScore(result) {
53
53
  const { score, level, densities, signalCount, totalSignals, percentile } = result.slop;
54
- const paint = LEVEL_COLOR$1[level];
54
+ const paint = LEVEL_COLOR[level];
55
55
  const partial = signalCount < totalSignals ? pc.yellow(` — ${signalCount}/${totalSignals} signals, see notes`) : "";
56
56
  const anchor = percentile !== void 0 ? pc.dim(` · worse than ${percentile}% of reference repos`) : "";
57
57
  const head = ` slop level: ${paint(pc.bold(level))} ${pc.dim(`(${score.toFixed(1)}/100)`)}${anchor}${partial}`;
@@ -62,7 +62,7 @@ function renderScore(result) {
62
62
  if (densities.deadCode !== void 0) parts.push(`dead code ${densities.deadCode.toFixed(2)}/KLOC`);
63
63
  return parts.length > 0 ? head + "\n " + pc.dim(parts.join(" · ")) : head;
64
64
  }
65
- const LEVEL_COLOR$1 = {
65
+ const LEVEL_COLOR = {
66
66
  clean: pc.green,
67
67
  low: pc.green,
68
68
  moderate: pc.yellow,
@@ -290,24 +290,21 @@ function isLine(value) {
290
290
  }
291
291
  //#endregion
292
292
  //#region src/report/badge.ts
293
- const LEVEL_COLOR = {
294
- clean: "brightgreen",
295
- low: "green",
296
- moderate: "yellow",
297
- high: "orange",
298
- "legacy-grade": "red"
299
- };
300
- /** shields.io endpoint JSON for a slop badge. */
293
+ /** shields.io endpoint JSON for the AI-slop gate. */
301
294
  function toBadge(result) {
302
- const { score, level, signalCount, totalSignals } = result.slop;
303
- const partial = signalCount < totalSignals ? "*" : "";
304
- const payload = {
295
+ const clear = result.findings.length === 0;
296
+ return `${JSON.stringify({
305
297
  schemaVersion: 1,
306
- label: "slop",
307
- message: score === 0 ? "0 slop" : `${level} (${score.toFixed(0)}/100)${partial}`,
308
- color: LEVEL_COLOR[level]
309
- };
310
- return `${JSON.stringify(payload, null, 2)}\n`;
298
+ label: "AI slop",
299
+ message: clear ? "clear" : "detected",
300
+ color: clear ? "brightgreen" : "red"
301
+ }, null, 2)}\n`;
302
+ }
303
+ //#endregion
304
+ //#region src/analysis-completeness.ts
305
+ function assertAnalysisComplete(result) {
306
+ if (result.fileCount === 0) throw new Error("analysis incomplete: no supported source files found");
307
+ if (result.errors.length > 0) throw new Error(`analysis incomplete: ${result.errors.join("; ")}`);
311
308
  }
312
309
  //#endregion
313
310
  //#region src/cli.ts
@@ -389,6 +386,7 @@ async function main() {
389
386
  config.deadCode.enabled = false;
390
387
  }
391
388
  const result = args.base ? await analyzeGitBase(args.path, args.base, config) : await analyze(args.path, config);
389
+ assertAnalysisComplete(result);
392
390
  if (args.updateBaseline) {
393
391
  writeBaseline(args.baseline, result.findings);
394
392
  process.stderr.write(`stopslop: baseline written to ${args.baseline} (${result.findings.length} findings accepted)\n`);
@@ -406,7 +404,7 @@ async function main() {
406
404
  if (args.format === "json") process.stdout.write(toJson(reported) + "\n");
407
405
  else if (args.format === "sarif") process.stdout.write(toSarif(reported));
408
406
  else if (args.format === "shields") {
409
- process.stdout.write(toBadge(result));
407
+ process.stdout.write(toBadge(reported));
410
408
  process.exit(0);
411
409
  } else process.stdout.write(renderTerminal(reported, args.details) + "\n");
412
410
  process.exit(reported.findings.length > 0 ? 1 : 0);
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ interface Finding {
13
13
  message: string;
14
14
  /** Structured payload for --json consumers and --details rendering. */
15
15
  data?: Record<string, unknown>;
16
- /** True while thresholds are pre-calibration defaults. */
16
+ /** Compatibility marker for reports produced with provisional thresholds. */
17
17
  preliminary?: boolean;
18
18
  }
19
19
  /** A parsed source file with everything analyzers need, produced once per file. */
@@ -145,7 +145,7 @@ interface ResolvedConfig {
145
145
  /** fan-in fraction above which a member is treated as a hub (0..1). */
146
146
  hubFanInRatio: number;
147
147
  ignore: string[];
148
- /** Whether current thresholds are pre-calibration defaults. */
148
+ /** Whether current thresholds are provisional. False for the calibrated defaults. */
149
149
  preliminary: boolean;
150
150
  }
151
151
  //#endregion
@@ -184,14 +184,13 @@ interface ScoreInput {
184
184
  config: ResolvedConfig;
185
185
  }
186
186
  declare function computeScore(input: ScoreInput): SlopScore;
187
- /** Bands are labels for the score, not extra thresholds — same preliminary caveat. */
187
+ /** Bands are labels for the score, not extra detection thresholds. */
188
188
  declare function levelFor(score: number): SlopLevel;
189
189
  //#endregion
190
190
  //#region src/config.d.ts
191
191
  /**
192
- * Default thresholds. First-pass calibrated on the 22-repo corpus (see
193
- * docs/thresholds.md); still flagged PRELIMINARY until validated against the
194
- * Stage-4 victim repo. The god gates are a conjunction (members AND clusters AND
192
+ * Default thresholds calibrated on the 22-repo corpus (see
193
+ * docs/thresholds.md). The god gates are a conjunction (members AND clusters AND
195
194
  * complexity), so `minMembers` is kept sensitive — precision comes from the
196
195
  * cluster + complexity requirement, not from a high member floor.
197
196
  * Full reference: docs/configuration.md.
package/dist/src.js CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stopslop",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Detects AI-agent slop that linters miss: over-complex functions, god modules/classes, dumped responsibilities.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,18 +47,26 @@
47
47
  "calibrate": "tsx scripts/calibrate.ts",
48
48
  "typecheck": "tsc --noEmit",
49
49
  "schema:generate": "tsx scripts/generate-config-schema.ts",
50
- "schema:check": "tsx scripts/generate-config-schema.ts --check"
50
+ "schema:check": "tsx scripts/generate-config-schema.ts --check",
51
+ "commitlint": "commitlint",
52
+ "prepare": "simple-git-hooks 1>&2"
53
+ },
54
+ "simple-git-hooks": {
55
+ "commit-msg": "pnpm exec commitlint --edit \"$1\""
51
56
  },
52
57
  "dependencies": {
53
58
  "clone-alert": "^1.1.3",
54
- "knip": "^6.26.0",
59
+ "knip": "6.31.0",
55
60
  "oxc-parser": "^0.139.0",
56
61
  "picocolors": "^1.1.1"
57
62
  },
58
63
  "devDependencies": {
59
64
  "@arethetypeswrong/core": "^0.18.1",
65
+ "@commitlint/cli": "^21.1.0",
66
+ "@commitlint/config-conventional": "^21.1.0",
60
67
  "@types/node": "^22.10.0",
61
68
  "publint": "^0.3.8",
69
+ "simple-git-hooks": "^2.13.1",
62
70
  "tsdown": "^0.21.10",
63
71
  "tsx": "^4.19.0",
64
72
  "typescript": "^5.7.0",
package/schema.json CHANGED
@@ -466,6 +466,9 @@
466
466
  "catalog": {
467
467
  "$ref": "#/definitions/knip/definitions/ruleValue"
468
468
  },
469
+ "catalogReferences": {
470
+ "$ref": "#/definitions/knip/definitions/ruleValue"
471
+ },
469
472
  "cycles": {
470
473
  "$ref": "#/definitions/knip/definitions/ruleValue"
471
474
  },
@@ -639,6 +642,7 @@
639
642
  "enumMembers",
640
643
  "namespaceMembers",
641
644
  "catalog",
645
+ "catalogReferences",
642
646
  "cycles"
643
647
  ]
644
648
  }
@@ -1007,6 +1011,10 @@
1007
1011
  "title": "markdownlint plugin configuration (https://knip.dev/reference/plugins/markdownlint)",
1008
1012
  "$ref": "#/definitions/knip/definitions/plugin"
1009
1013
  },
1014
+ "marko": {
1015
+ "title": "marko plugin configuration (https://knip.dev/reference/plugins/marko)",
1016
+ "$ref": "#/definitions/knip/definitions/plugin"
1017
+ },
1010
1018
  "mdx": {
1011
1019
  "title": "mdx plugin configuration (https://knip.dev/reference/plugins/mdx)",
1012
1020
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1107,6 +1115,10 @@
1107
1115
  "title": "openapi-ts plugin configuration (https://knip.dev/reference/plugins/openapi-ts)",
1108
1116
  "$ref": "#/definitions/knip/definitions/plugin"
1109
1117
  },
1118
+ "openclaw": {
1119
+ "title": "openclaw plugin configuration (https://knip.dev/reference/plugins/openclaw)",
1120
+ "$ref": "#/definitions/knip/definitions/plugin"
1121
+ },
1110
1122
  "orval": {
1111
1123
  "title": "orval plugin configuration (https://knip.dev/reference/plugins/orval)",
1112
1124
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1323,6 +1335,10 @@
1323
1335
  "title": "tauri plugin configuration (https://knip.dev/reference/plugins/tauri)",
1324
1336
  "$ref": "#/definitions/knip/definitions/plugin"
1325
1337
  },
1338
+ "temporal": {
1339
+ "title": "temporal plugin configuration (https://knip.dev/reference/plugins/temporal)",
1340
+ "$ref": "#/definitions/knip/definitions/plugin"
1341
+ },
1326
1342
  "travis": {
1327
1343
  "title": "travis plugin configuration (https://knip.dev/reference/plugins/travis)",
1328
1344
  "$ref": "#/definitions/knip/definitions/plugin"