stopslop 0.1.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/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +427 -0
- package/dist/index.d.ts +292 -0
- package/dist/index.js +2 -0
- package/dist/src.js +0 -0
- package/package.json +77 -0
- package/schema.json +1488 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ruslan Baryshev
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# stopslop
|
|
2
|
+
|
|
3
|
+
[](https://github.com/BaryshevRS/stopslop)
|
|
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+.
|
|
8
|
+
|
|
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.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx stopslop .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or install it in the project so CI uses the version pinned in your lockfile:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm add --save-dev stopslop
|
|
24
|
+
pnpm exec stopslop .
|
|
25
|
+
```
|
|
26
|
+
|
|
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.
|
|
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
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Slop level
|
|
66
|
+
|
|
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.
|
|
69
|
+
|
|
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
|
|
73
|
+
```
|
|
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:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
score = 100 · Σ wᵢ · sat(xᵢ) / Σ wᵢ (i = enabled signals)
|
|
82
|
+
sat(x) = clamp((x − floorᵢ) / (budgetᵢ − floorᵢ), 0, 1)
|
|
83
|
+
```
|
|
84
|
+
|
|
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 |
|
|
91
|
+
|
|
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:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
slop level: moderate (43.0/100) · worse than 60% of reference repos
|
|
100
|
+
```
|
|
101
|
+
|
|
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.**
|
|
107
|
+
|
|
108
|
+
## Slop badge
|
|
109
|
+
|
|
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:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
stopslop . --format shields > stopslop-badge.json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```md
|
|
119
|
+
[](https://github.com/OWNER/REPO)
|
|
120
|
+
```
|
|
121
|
+
|
|
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:
|
|
140
|
+
|
|
141
|
+
```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
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Usage
|
|
147
|
+
|
|
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
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Exit codes: `0` clean · `1` findings · `2` error.
|
|
161
|
+
|
|
162
|
+
### For an agent loop
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
stopslop --json > slop.json # feed to the agent: clusters are the split boundaries
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Git-base gate
|
|
169
|
+
|
|
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
|
+
|
|
173
|
+
```sh
|
|
174
|
+
stopslop . --base origin/main
|
|
175
|
+
stopslop . --base origin/main --format sarif > stopslop.sarif
|
|
176
|
+
```
|
|
177
|
+
|
|
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.
|
|
184
|
+
|
|
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`.
|
|
189
|
+
|
|
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:
|
|
193
|
+
|
|
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
|
+
```
|
|
220
|
+
|
|
221
|
+
## Baseline (adopting an existing project)
|
|
222
|
+
|
|
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*."
|
|
227
|
+
|
|
228
|
+
```sh
|
|
229
|
+
# 1. Record today's findings (writes the file, exits 0)
|
|
230
|
+
stopslop . --baseline .stopslop-baseline.json --update-baseline
|
|
231
|
+
|
|
232
|
+
# 2. In CI: fail only on findings not in the baseline
|
|
233
|
+
stopslop . --baseline .stopslop-baseline.json
|
|
234
|
+
```
|
|
235
|
+
|
|
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.
|
|
253
|
+
|
|
254
|
+
## Config (`stopslop.json`)
|
|
255
|
+
|
|
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.
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"$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
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"score": { "weights": { "god": 0.35 }, "budgets": { "duplication": 0.3 }, "floors": { "duplication": 0.05 } },
|
|
275
|
+
"hubFanInRatio": 0.5,
|
|
276
|
+
"ignore": ["**/*.generated.ts"]
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
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).
|
|
318
|
+
|
|
319
|
+
## Development
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
pnpm install
|
|
323
|
+
pnpm test
|
|
324
|
+
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
|
|
329
|
+
```
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|