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 +190 -252
- package/dist/cli.js +17 -19
- package/dist/index.d.ts +5 -6
- package/dist/src.js +0 -0
- package/package.json +11 -3
- package/schema.json +16 -0
package/README.md
CHANGED
|
@@ -1,329 +1,267 @@
|
|
|
1
|
-
#
|
|
1
|
+
# StopSlop
|
|
2
2
|
|
|
3
|
-
[](https://github.com/BaryshevRS/stopslop)
|
|
3
|
+
[](https://github.com/BaryshevRS/stopslop)
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
pnpm exec stopslop .
|
|
51
|
+
npx stopslop .
|
|
25
52
|
```
|
|
26
53
|
|
|
27
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
65
|
+
```text
|
|
66
|
+
stopslop 27 files 4.3 KLOC 980ms
|
|
69
67
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
[](https://github.com/OWNER/REPO)
|
|
133
|
+
[](https://github.com/OWNER/REPO)
|
|
120
134
|
```
|
|
121
135
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
143
|
-
|
|
149
|
+
- name: Reject new AI slop
|
|
150
|
+
run: npx stopslop . --baseline .stopslop-baseline.json
|
|
144
151
|
```
|
|
145
152
|
|
|
146
|
-
|
|
153
|
+
Or compare the working tree directly with the target branch and keep no baseline
|
|
154
|
+
file:
|
|
147
155
|
|
|
148
|
-
```
|
|
149
|
-
stopslop
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
For GitHub Code Scanning, emit SARIF before propagating the gate failure:
|
|
163
165
|
|
|
164
166
|
```bash
|
|
165
|
-
stopslop
|
|
167
|
+
npx stopslop . --base origin/main --format sarif > stopslop.sarif
|
|
166
168
|
```
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
## Send the findings back to the agent
|
|
169
171
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
```
|
|
174
|
-
stopslop . --
|
|
175
|
-
stopslop . --base origin/main --format sarif > stopslop.sarif
|
|
175
|
+
```bash
|
|
176
|
+
npx stopslop . --json > slop-report.json
|
|
176
177
|
```
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
229
|
-
|
|
230
|
-
stopslop . --baseline .stopslop-baseline.json --update-baseline
|
|
201
|
+
```text
|
|
202
|
+
stopslop [path] analyze a directory or file (default: .)
|
|
231
203
|
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
##
|
|
218
|
+
## Configuration
|
|
255
219
|
|
|
256
|
-
|
|
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":
|
|
263
|
-
"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
326
|
-
pnpm release:check
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
303
|
-
|
|
304
|
-
const payload = {
|
|
295
|
+
const clear = result.findings.length === 0;
|
|
296
|
+
return `${JSON.stringify({
|
|
305
297
|
schemaVersion: 1,
|
|
306
|
-
label: "slop",
|
|
307
|
-
message:
|
|
308
|
-
color:
|
|
309
|
-
}
|
|
310
|
-
|
|
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(
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
|
193
|
-
* docs/thresholds.md)
|
|
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.
|
|
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": "
|
|
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"
|