stopslop 0.2.0 → 1.0.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/COMPATIBILITY.md +26 -0
- package/README.md +72 -8
- package/context7.json +21 -0
- package/docs/ci-and-automation.md +100 -0
- package/docs/configuration.md +252 -0
- package/docs/node-api.md +114 -0
- package/docs/score.md +133 -0
- package/docs/thresholds.md +105 -0
- package/llms.txt +25 -0
- package/package.json +47 -7
package/COMPATIBILITY.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Compatibility
|
|
2
|
+
|
|
3
|
+
Last updated: 0.2.0 (2026-08-05)
|
|
4
|
+
|
|
5
|
+
StopSlop is pre-1.0. Public surfaces are usable, but none are declared stable
|
|
6
|
+
yet. Breaking preview changes require a minor release and are called out in the
|
|
7
|
+
changelog. Experimental surfaces may change in any release.
|
|
8
|
+
|
|
9
|
+
| Surface | Tier | Compatibility policy |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| CLI flags and exit codes | Preview | Existing flags and exit meanings are preserved within a minor line. |
|
|
12
|
+
| Versioned JSON report | Preview | Breaking payload changes increment `schemaVersion`. |
|
|
13
|
+
| SARIF 2.1.0 report | Preview | New rules and optional properties may be added; existing required fields remain compatible. |
|
|
14
|
+
| `stopslop.json` schema | Preview | Existing valid configuration remains valid within a minor line. |
|
|
15
|
+
| Programmatic Node.js API | Experimental | Exported functions and TypeScript types may change before 1.0. |
|
|
16
|
+
|
|
17
|
+
## Runtime support
|
|
18
|
+
|
|
19
|
+
- Node.js 20.19+, 22.12+, and 24.
|
|
20
|
+
- ESM only.
|
|
21
|
+
- Git-base gating requires the `git` executable and a non-shallow checkout that
|
|
22
|
+
contains the requested base revision.
|
|
23
|
+
|
|
24
|
+
## Deprecations
|
|
25
|
+
|
|
26
|
+
None.
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# StopSlop
|
|
2
2
|
|
|
3
|
-
[](https://github.com/BaryshevRS/stopslop)
|
|
3
|
+
[](https://github.com/BaryshevRS/stopslop) [](https://github.com/BaryshevRS/clone-alert) [](https://github.com/BaryshevRS/stopslop/actions/workflows/ci.yml) [](https://www.npmjs.com/package/stopslop)
|
|
4
|
+
<!-- clone-alert badge: pnpm exec clone-alert --minimum-tokens 100 --files src --format shields --no-fail-on-violation -->
|
|
4
5
|
|
|
5
6
|
**Stop AI-generated code from becoming instant legacy.**
|
|
6
7
|
|
|
@@ -8,9 +9,10 @@ Coding agents are good at making code compile. They are also good at making too
|
|
|
8
9
|
much of it: tangled functions, god files, copied blocks, dead exports, and entire
|
|
9
10
|
features that pass their own tests but are never connected to the application.
|
|
10
11
|
|
|
11
|
-
StopSlop
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
StopSlop is a static analyzer for JavaScript and TypeScript codebases under
|
|
13
|
+
heavy AI-agent change. It catches that structural overproduction before it
|
|
14
|
+
becomes permanent. It complements ESLint, Biome, Oxlint, SonarQube, and
|
|
15
|
+
SonarCloud rather than repeating their style and type rules.
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
npx stopslop .
|
|
@@ -19,9 +21,6 @@ npx stopslop .
|
|
|
19
21
|
It runs locally against JavaScript and TypeScript. No account. No source-code
|
|
20
22
|
upload.
|
|
21
23
|
|
|
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
24
|
## What it catches
|
|
26
25
|
|
|
27
26
|
- **Tangled functions** using cognitive complexity from the SonarSource
|
|
@@ -56,6 +55,14 @@ Or pin it in the project so local runs and CI use the same version:
|
|
|
56
55
|
```bash
|
|
57
56
|
npm install --save-dev stopslop
|
|
58
57
|
npx stopslop .
|
|
58
|
+
|
|
59
|
+
# pnpm
|
|
60
|
+
pnpm add -D stopslop
|
|
61
|
+
pnpm exec stopslop .
|
|
62
|
+
|
|
63
|
+
# Yarn
|
|
64
|
+
yarn add --dev stopslop
|
|
65
|
+
yarn stopslop .
|
|
59
66
|
```
|
|
60
67
|
|
|
61
68
|
The default report gives you the full repository score, the signal densities,
|
|
@@ -130,9 +137,12 @@ npx stopslop . \
|
|
|
130
137
|
Commit or publish `stopslop-badge.json`, then add this Markdown:
|
|
131
138
|
|
|
132
139
|
```md
|
|
133
|
-
[](https://github.com/
|
|
140
|
+
[](https://github.com/BaryshevRS/stopslop)
|
|
134
141
|
```
|
|
135
142
|
|
|
143
|
+
Replace `OWNER/REPO` in the image URL with your repository. Keep the outer link
|
|
144
|
+
unchanged: clicking the badge takes readers to StopSlop.
|
|
145
|
+
|
|
136
146
|
Without `--baseline` or `--base`, `clear` requires zero findings across the
|
|
137
147
|
entire scan. With a baseline, it requires zero findings outside the committed
|
|
138
148
|
baseline. The full score is still present in the terminal and JSON reports.
|
|
@@ -167,6 +177,9 @@ For GitHub Code Scanning, emit SARIF before propagating the gate failure:
|
|
|
167
177
|
npx stopslop . --base origin/main --format sarif > stopslop.sarif
|
|
168
178
|
```
|
|
169
179
|
|
|
180
|
+
See [CI and automation](docs/ci-and-automation.md) for a complete workflow that
|
|
181
|
+
uploads SARIF and still preserves StopSlop's exit code.
|
|
182
|
+
|
|
170
183
|
## Send the findings back to the agent
|
|
171
184
|
|
|
172
185
|
The JSON report is stable and versioned. Feed it into the next coding-agent turn
|
|
@@ -239,6 +252,24 @@ See [docs/configuration.md](docs/configuration.md) for every field, default, and
|
|
|
239
252
|
Knip workspace example. See [stopslop.example.json](stopslop.example.json) for a
|
|
240
253
|
complete configuration.
|
|
241
254
|
|
|
255
|
+
## Node.js API
|
|
256
|
+
|
|
257
|
+
StopSlop ships ESM JavaScript and TypeScript declarations for programmatic use:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { analyze, loadConfig } from 'stopslop';
|
|
261
|
+
|
|
262
|
+
const root = process.cwd();
|
|
263
|
+
const result = await analyze(root, loadConfig(root));
|
|
264
|
+
|
|
265
|
+
console.log(result.slop.score, result.findings);
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
The API also exports the resolved defaults, scoring helpers, baseline helpers,
|
|
269
|
+
and report types. See the [Node.js API guide](docs/node-api.md) for Git-base
|
|
270
|
+
analysis, typed configuration, result semantics, and custom analyzers. The API
|
|
271
|
+
is experimental before 1.0; see [COMPATIBILITY.md](COMPATIBILITY.md).
|
|
272
|
+
|
|
242
273
|
## Design and limitations
|
|
243
274
|
|
|
244
275
|
- StopSlop supports `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, and `.tsx` source.
|
|
@@ -253,12 +284,45 @@ complete configuration.
|
|
|
253
284
|
Architecture, research references, and the boundary between published methods
|
|
254
285
|
and StopSlop heuristics are documented in [ARCH.md](ARCH.md).
|
|
255
286
|
|
|
287
|
+
## FAQ
|
|
288
|
+
|
|
289
|
+
### Is StopSlop an AI-generated code detector?
|
|
290
|
+
|
|
291
|
+
No. It does not guess who wrote a line of code. It finds structural waste that
|
|
292
|
+
coding agents often produce—duplicate blocks, over-complex functions, god
|
|
293
|
+
classes and modules, dead code, and orphan features—and reports the same issue
|
|
294
|
+
when a human wrote it.
|
|
295
|
+
|
|
296
|
+
### How is StopSlop different from ESLint, SonarQube, SonarCloud, or Knip?
|
|
297
|
+
|
|
298
|
+
ESLint and similar linters focus on local rules. SonarQube and SonarCloud are
|
|
299
|
+
broad quality platforms. Knip specializes in unused code and dependencies.
|
|
300
|
+
StopSlop is a local, zero-account AI code quality gate focused on structural
|
|
301
|
+
overproduction. It combines several signals into one report and keeps accepted
|
|
302
|
+
legacy visible through its score while blocking only new findings through a
|
|
303
|
+
baseline or Git comparison. Knip and
|
|
304
|
+
[Clone Alert](https://www.npmjs.com/package/clone-alert) power two of those
|
|
305
|
+
signals.
|
|
306
|
+
|
|
307
|
+
### Can I add StopSlop to a legacy repository?
|
|
308
|
+
|
|
309
|
+
Yes. Generate `.stopslop-baseline.json`, review and commit it, then use that same
|
|
310
|
+
file in CI. Existing findings remain in the score and report as accepted debt;
|
|
311
|
+
only unaccepted findings fail the gate.
|
|
312
|
+
|
|
313
|
+
### Does StopSlop support monorepos and CI code scanning?
|
|
314
|
+
|
|
315
|
+
Yes. Configure Knip workspaces under `stopslop.json#knip.workspaces` for accurate
|
|
316
|
+
dead-code analysis in a monorepo. Use SARIF for GitHub Code Scanning, JSON for
|
|
317
|
+
automation and coding agents, or the Shields format for a README status badge.
|
|
318
|
+
|
|
256
319
|
## Development
|
|
257
320
|
|
|
258
321
|
```bash
|
|
259
322
|
pnpm install
|
|
260
323
|
pnpm test
|
|
261
324
|
pnpm typecheck
|
|
325
|
+
pnpm check:clones
|
|
262
326
|
pnpm build
|
|
263
327
|
pnpm test:package
|
|
264
328
|
pnpm release:check
|
package/context7.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://context7.com/schema/context7.json",
|
|
3
|
+
"projectTitle": "StopSlop",
|
|
4
|
+
"description": "Static analysis and CI gate for AI code slop in JavaScript and TypeScript: cognitive complexity, god units, duplicate code, dead code, orphan features, SARIF and baselines.",
|
|
5
|
+
"folders": ["docs"],
|
|
6
|
+
"excludeFolders": [],
|
|
7
|
+
"excludeFiles": ["CHANGELOG.md", "ISSUE_KNIP.md", "RELEASING.md"],
|
|
8
|
+
"rules": [
|
|
9
|
+
"StopSlop detects structural code patterns; it does not determine whether code was written by a human or AI.",
|
|
10
|
+
"Run StopSlop without installing it with `npx stopslop .`. It requires Node.js ^20.19.0 or >=22.12.0 and analyzes JavaScript, JSX, TypeScript and TSX.",
|
|
11
|
+
"Exit code 0 means no unaccepted findings, 1 means findings were detected, and 2 means analysis or configuration failed.",
|
|
12
|
+
"Adopt an existing repository with `--baseline .stopslop-baseline.json --update-baseline`, commit that file, then use the same `--baseline` option in CI to reject only new findings.",
|
|
13
|
+
"A baseline changes the gate and reported findings but never lowers the full-repository slop score.",
|
|
14
|
+
"Use `--base <git-ref>` to compare against a Git revision without maintaining a baseline file. CI must fetch enough Git history to resolve that ref.",
|
|
15
|
+
"Report formats are text, json, sarif and shields. Use JSON for coding agents, SARIF for code scanning and shields for the binary `AI slop | clear` badge.",
|
|
16
|
+
"StopSlop works without configuration. Put optional overrides in `stopslop.json` and use `$schema: https://unpkg.com/stopslop/schema.json` for editor validation.",
|
|
17
|
+
"Use `--fast` only when intentionally skipping duplicate-code and dead-code analysis; it runs the AST checks for cognitive complexity and god units.",
|
|
18
|
+
"The Node API is ESM and typed: call `resolveConfig()` before `analyze()`, or call `loadConfig()` to read stopslop.json. The slop score always describes the complete analyzed repository.",
|
|
19
|
+
"Clone detection is powered by Clone Alert and dead-code analysis by Knip. Configure Knip only under the `knip` property in stopslop.json."
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# CI and automation
|
|
2
|
+
|
|
3
|
+
StopSlop has three stable exit codes: `0` means the gate is clear, `1` means
|
|
4
|
+
findings were detected, and `2` means analysis or configuration failed. Pin the
|
|
5
|
+
package version in `devDependencies` so local runs and CI use the same rules.
|
|
6
|
+
|
|
7
|
+
## Reject new findings with a committed baseline
|
|
8
|
+
|
|
9
|
+
Create the baseline once and review it like source code:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx stopslop . \
|
|
13
|
+
--baseline .stopslop-baseline.json \
|
|
14
|
+
--update-baseline
|
|
15
|
+
git add .stopslop-baseline.json
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then use the same file in CI:
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
- name: Reject new AI slop
|
|
22
|
+
run: npx stopslop . --baseline .stopslop-baseline.json
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The baseline suppresses accepted findings from the gate. It never removes them
|
|
26
|
+
from the full-repository score.
|
|
27
|
+
|
|
28
|
+
## Compare a pull request with its Git base
|
|
29
|
+
|
|
30
|
+
This mode needs the base revision to exist locally:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- uses: actions/checkout@v7
|
|
34
|
+
with:
|
|
35
|
+
fetch-depth: 0
|
|
36
|
+
|
|
37
|
+
- name: Reject AI slop added by this change
|
|
38
|
+
run: npx stopslop . --base origin/main
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`--base` and `--baseline` are alternative gates. Do not pass them together.
|
|
42
|
+
|
|
43
|
+
## Publish SARIF without hiding the gate failure
|
|
44
|
+
|
|
45
|
+
The scan may exit `1` after writing valid SARIF. Let the upload step run, then
|
|
46
|
+
propagate the original result. The workflow or job needs permission to publish
|
|
47
|
+
code-scanning results:
|
|
48
|
+
|
|
49
|
+
```yaml
|
|
50
|
+
permissions:
|
|
51
|
+
contents: read
|
|
52
|
+
security-events: write
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Then add the scan, upload, and enforcement steps:
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
- name: Scan with StopSlop
|
|
59
|
+
id: stopslop
|
|
60
|
+
continue-on-error: true
|
|
61
|
+
run: npx stopslop . --base origin/main --format sarif > stopslop.sarif
|
|
62
|
+
|
|
63
|
+
- name: Upload StopSlop SARIF
|
|
64
|
+
if: always() && hashFiles('stopslop.sarif') != ''
|
|
65
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
66
|
+
with:
|
|
67
|
+
sarif_file: stopslop.sarif
|
|
68
|
+
|
|
69
|
+
- name: Enforce StopSlop gate
|
|
70
|
+
if: steps.stopslop.outcome == 'failure'
|
|
71
|
+
run: exit 1
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
An exit code of `2` does not emit a report, preventing an incomplete analysis
|
|
75
|
+
from being published as a trustworthy result.
|
|
76
|
+
|
|
77
|
+
## Give structured findings to a coding agent
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx stopslop . --format json > slop-report.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The JSON schema version is included in the document. Findings carry stable
|
|
84
|
+
`kind`, `file`, `symbol`, `line`, `message`, and optional structured `data`
|
|
85
|
+
fields. God-unit data includes responsibility groups; duplicate-code data
|
|
86
|
+
includes clone locations.
|
|
87
|
+
|
|
88
|
+
## Generate a binary Shields badge
|
|
89
|
+
|
|
90
|
+
Use the same baseline as CI:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx stopslop . \
|
|
94
|
+
--baseline .stopslop-baseline.json \
|
|
95
|
+
--format shields > stopslop-badge.json
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The badge says `AI slop | clear` when the gate has no unaccepted findings and
|
|
99
|
+
`AI slop | detected` otherwise. Accepted legacy remains visible in the terminal
|
|
100
|
+
and JSON score.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
stopslop reads `stopslop.json` from the analyzed directory (or `--config <path>`).
|
|
4
|
+
Every field is optional and merged over the defaults, so a config only needs the
|
|
5
|
+
knobs you want to change. Defaults are calibrated on a 22-repo TypeScript corpus
|
|
6
|
+
(see [thresholds.md](thresholds.md)).
|
|
7
|
+
|
|
8
|
+
The CLI validates StopSlop-owned fields at runtime as well as publishing the
|
|
9
|
+
schema: unknown properties, wrong types, and out-of-range values are errors
|
|
10
|
+
rather than silently ignored configuration.
|
|
11
|
+
|
|
12
|
+
## Full example (all defaults)
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"$schema": "https://unpkg.com/stopslop/schema.json",
|
|
17
|
+
"cognitiveComplexity": 15,
|
|
18
|
+
"godModule": { "minMembers": 15, "minClusters": 3, "minComplexity": 150, "sizeMembers": 37, "sizeComplexity": 350 },
|
|
19
|
+
"godClass": { "minMembers": 12, "minClusters": 3, "minComplexity": 90, "sizeMembers": 30, "sizeComplexity": 200 },
|
|
20
|
+
"multipleExportedClasses": { "maxPerFile": 1 },
|
|
21
|
+
"duplicates": { "minTokens": 50, "minLines": 5, "ignoreIdentifiers": false, "ignoreLiterals": false },
|
|
22
|
+
"deadCode": { "exports": true, "dependencies": true, "files": true, "orphans": true, "production": false },
|
|
23
|
+
"knip": {
|
|
24
|
+
"workspaces": {
|
|
25
|
+
".": { "entry": ["src/index.ts"] },
|
|
26
|
+
"packages/*": { "entry": ["src/index.ts"], "project": ["src/**/*.ts"] }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"score": {
|
|
30
|
+
"weights": { "god": 0.35, "duplication": 0.25, "complexity": 0.2, "deadCode": 0.2 },
|
|
31
|
+
"budgets": { "god": 0.1, "duplication": 0.3, "complexity": 4, "deadCode": 4 },
|
|
32
|
+
"floors": { "god": 0, "duplication": 0.05, "complexity": 0, "deadCode": 0 }
|
|
33
|
+
},
|
|
34
|
+
"hubFanInRatio": 0.5,
|
|
35
|
+
"ignore": []
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Disabling a check
|
|
40
|
+
|
|
41
|
+
Set any check to `false` to turn it off entirely:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"godModule": false,
|
|
46
|
+
"multipleExportedClasses": false
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Metrics
|
|
51
|
+
|
|
52
|
+
### `cognitiveComplexity` — number | false
|
|
53
|
+
|
|
54
|
+
Functions whose cognitive complexity (Campbell / SonarSource spec) is **strictly
|
|
55
|
+
above** this value are reported. Default **15** (the Sonar community default; on
|
|
56
|
+
the corpus it sits between count p95 ≈ 13 and p99 ≈ 38). Lower = stricter.
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "cognitiveComplexity": 20 }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `godClass` / `godModule` — object | false
|
|
63
|
+
|
|
64
|
+
A class (or a module's top-level definitions) is flagged on **either** of two
|
|
65
|
+
axes — see [ARCH.md](../ARCH.md) for the reasoning.
|
|
66
|
+
|
|
67
|
+
| field | meaning | default (class / module) |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| `minMembers` | dispersion axis: minimum methods / top-level definitions | 12 / 15 |
|
|
70
|
+
| `minClusters` | dispersion axis: minimum **substantive** responsibility groups (≥2 members) | 3 / 3 |
|
|
71
|
+
| `minComplexity` | dispersion axis: minimum total (Σ) cognitive complexity | 90 / 150 |
|
|
72
|
+
| `sizeMembers` | size/WMC axis: method count that alone signals a monolith | 30 / 37 |
|
|
73
|
+
| `sizeComplexity` | size/WMC axis: total complexity (WMC) that alone signals a monolith | 200 / 350 |
|
|
74
|
+
|
|
75
|
+
- **Dispersion** fires when `members ≥ minMembers AND groups ≥ minClusters AND
|
|
76
|
+
Σcomplexity ≥ minComplexity` — a class doing several unrelated things, with
|
|
77
|
+
clean split boundaries in the report.
|
|
78
|
+
- **Size (WMC)** fires when `members ≥ sizeMembers AND Σcomplexity ≥
|
|
79
|
+
sizeComplexity` — a densely-connected monolith too large to be one unit, even
|
|
80
|
+
if fully cohesive. Reported honestly as "no automatic split boundaries".
|
|
81
|
+
|
|
82
|
+
Raise `size*` for fewer, higher-confidence size findings; raise `minClusters` to
|
|
83
|
+
demand more distinct responsibilities before flagging dispersion.
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"godClass": { "sizeMembers": 45, "sizeComplexity": 300 },
|
|
88
|
+
"godModule": false
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `multipleExportedClasses` — object | false
|
|
93
|
+
|
|
94
|
+
Flags files exporting **more than** `maxPerFile` classes. Default `maxPerFile`
|
|
95
|
+
**1** (i.e. flag 2+). Informational severity.
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{ "multipleExportedClasses": { "maxPerFile": 2 } }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### `duplicates` — object | false
|
|
102
|
+
|
|
103
|
+
Copy-paste detection, powered by [Clone Alert](https://www.npmjs.com/package/clone-alert).
|
|
104
|
+
Only the files stopslop discovered are fed to it, so the ignore rules above apply
|
|
105
|
+
here too.
|
|
106
|
+
|
|
107
|
+
| field | meaning | default |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `minTokens` | minimum duplicated token span reported as a clone | 50 |
|
|
110
|
+
| `minLines` | minimum physical lines a clone must span (its tightest occurrence) | 5 |
|
|
111
|
+
| `ignoreIdentifiers` | normalize identifier names before matching (Type-2 clones) | `false` |
|
|
112
|
+
| `ignoreLiterals` | normalize literal values before matching | `false` |
|
|
113
|
+
|
|
114
|
+
The default is **exact matching (Type-1 clones)** — the reported percentage is
|
|
115
|
+
real copy-paste. Set `ignoreIdentifiers: true` to also catch Type-2 clones (an
|
|
116
|
+
agent copy-pasting a block and renaming the variables); the cost is that
|
|
117
|
+
structurally-similar boilerplate matches too, which inflates the duplication
|
|
118
|
+
ratio on component libraries by several points. `minLines` keeps dense
|
|
119
|
+
one-liners (config tables, long template strings) from matching on token count
|
|
120
|
+
alone; turn it up to only see substantial clones.
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{ "duplicates": { "minTokens": 100, "minLines": 10, "ignoreIdentifiers": true } }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `deadCode` — object | false
|
|
127
|
+
|
|
128
|
+
Unused code, powered by [Knip](https://knip.dev) via its programmatic session API
|
|
129
|
+
(no CLI spawn). This section controls which Knip findings StopSlop reports and
|
|
130
|
+
whether Knip runs in production mode. It is StopSlop policy, not Knip's own
|
|
131
|
+
configuration.
|
|
132
|
+
|
|
133
|
+
| field | meaning | default |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `exports` | report unused exports and unused exported types (warn) | `true` |
|
|
136
|
+
| `dependencies` | report unused (dev)dependencies from package.json (warn) | `true` |
|
|
137
|
+
| `files` | report files unreachable from any entry point (info) | `true` |
|
|
138
|
+
| `orphans` | report functions kept alive by nothing but their own paired test (warn) | `true` |
|
|
139
|
+
| `production` | analyze as production-only: usage from tests no longer counts | `false` |
|
|
140
|
+
|
|
141
|
+
Unused files are **info**, not warn, on purpose: they depend on entry-point
|
|
142
|
+
detection, which misfires on projects with unconventional entries. Verify before
|
|
143
|
+
deleting — or configure `entry` under `stopslop.json#knip`.
|
|
144
|
+
|
|
145
|
+
An **orphan feature** is a function or class no production code calls, imported
|
|
146
|
+
by exactly one file: its own paired test. The trace of a generated feature that
|
|
147
|
+
was never wired in — a plain unused export has an innocent explanation ready,
|
|
148
|
+
this one has almost none. The finding names both halves to delete, source symbol
|
|
149
|
+
and test file, because removing either alone breaks the build or leaves a green
|
|
150
|
+
test standing over nothing. It is reported and deliberately never scored: the
|
|
151
|
+
point is to get the pair deleted, and a weight in the number removes no code.
|
|
152
|
+
|
|
153
|
+
Symbols still used inside their own file are excluded, as are shared test
|
|
154
|
+
helpers, stories, and non-callable constants — every one of them a way for a
|
|
155
|
+
live symbol to look like an orphan. Orphans need Knip to see the test files at
|
|
156
|
+
all, which means either a test-runner plugin it recognises or `entry` patterns
|
|
157
|
+
covering the tests.
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{ "deadCode": { "files": false, "production": true } }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `knip` — object
|
|
164
|
+
|
|
165
|
+
The complete JSON-compatible [Knip configuration](https://knip.dev/reference/configuration).
|
|
166
|
+
This is the **only** Knip configuration StopSlop uses. It never reads, merges,
|
|
167
|
+
or executes project `knip.json`, `.knip.json`, or `knip.config.*` files. The
|
|
168
|
+
root `package.json` is still project metadata, but its `knip` field is discarded
|
|
169
|
+
and never merged. Move any settings StopSlop should use into this property.
|
|
170
|
+
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"knip": {
|
|
174
|
+
"ignoreDependencies": ["optional-adapter"],
|
|
175
|
+
"workspaces": {
|
|
176
|
+
".": { "entry": ["src/index.ts"] },
|
|
177
|
+
"packages/*": { "entry": ["src/index.ts"], "project": ["src/**/*.ts"] },
|
|
178
|
+
"packages/cli": { "entry": ["src/cli.ts"] }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Workspace keys and overrides follow Knip unchanged: `.` configures the root,
|
|
185
|
+
glob keys select groups of packages, and a more specific key can override a
|
|
186
|
+
group. In a detected monorepo without `knip.workspaces`, findings are still
|
|
187
|
+
shown but excluded from the dead-code score because entry-point detection is
|
|
188
|
+
not trustworthy enough to grade the repository.
|
|
189
|
+
|
|
190
|
+
In a single-package project, `entry` and `project` belong directly under
|
|
191
|
+
`knip`. In a monorepo they belong under `knip.workspaces`; Knip ignores the
|
|
192
|
+
top-level forms once multiple workspaces are present.
|
|
193
|
+
|
|
194
|
+
One Knip default is changed: StopSlop sets `ignoreExportsUsedInFile` to `true`.
|
|
195
|
+
Knip otherwise reports an export nothing imports even when the symbol is used
|
|
196
|
+
inside its own file — a `export` keyword wider than it needs to be, which no
|
|
197
|
+
behaviour depends on. That is a style nit rather than the structural waste
|
|
198
|
+
StopSlop reports. Set it back to `false` under `knip` to get Knip's own default;
|
|
199
|
+
the granular per-symbol-type form works as documented upstream.
|
|
200
|
+
|
|
201
|
+
The package ships `schema.json`. Add
|
|
202
|
+
`"$schema": "https://unpkg.com/stopslop/schema.json"` for editor completion,
|
|
203
|
+
field descriptions, and validation. StopSlop consumes configuration only; it
|
|
204
|
+
does not infer or generate project settings. That project inspection is kept as
|
|
205
|
+
a boundary for future external tooling (such as an MCP server).
|
|
206
|
+
|
|
207
|
+
### `score` — object
|
|
208
|
+
|
|
209
|
+
Weights, floors, and budgets of the slop level. A signal contributes nothing
|
|
210
|
+
below its floor, rises linearly, and saturates at its budget. Full formula and
|
|
211
|
+
rationale (all anchors measured on a 41-repo benchmark of mature TypeScript
|
|
212
|
+
OSS): [score.md](score.md).
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{
|
|
216
|
+
"score": {
|
|
217
|
+
"weights": { "god": 0.35, "duplication": 0.25, "complexity": 0.2, "deadCode": 0.2 },
|
|
218
|
+
"budgets": { "god": 0.1, "duplication": 0.3, "complexity": 4, "deadCode": 4 },
|
|
219
|
+
"floors": { "god": 0, "duplication": 0.05, "complexity": 0, "deadCode": 0 }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Weights need not sum to 1 — the score divides by the sum of the weights that
|
|
225
|
+
actually ran. Disabling a check removes its signal from the formula entirely
|
|
226
|
+
rather than scoring it as clean. The "worse than N% of reference repos" anchor
|
|
227
|
+
is recomputed with your weights/floors/budgets, so a custom config re-ranks the
|
|
228
|
+
corpus with the same yardstick it applies to your repo.
|
|
229
|
+
|
|
230
|
+
## Engine knobs
|
|
231
|
+
|
|
232
|
+
### `hubFanInRatio` — number (0..1)
|
|
233
|
+
|
|
234
|
+
A member referenced by more than this fraction of the unit's methods is treated
|
|
235
|
+
as a "hub" (shared glue), removed from the cohesion graph, and the responsibility
|
|
236
|
+
groups are recomputed — so a class held together by one shared field still splits
|
|
237
|
+
into its real parts. Default **0.5**. Lower = more aggressive splitting.
|
|
238
|
+
|
|
239
|
+
### `ignore` — string[]
|
|
240
|
+
|
|
241
|
+
Extra glob patterns (matched against POSIX-relative paths) to exclude, on top of
|
|
242
|
+
the built-in excludes (`node_modules`, `dist`/`build`, tests, fixtures, `.d.ts`,
|
|
243
|
+
minified/bundled files, and generator subtrees such as openapi-generator).
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{ "ignore": ["**/*.generated.ts", "src/legacy/**"] }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Reference
|
|
250
|
+
|
|
251
|
+
The JSON report (`--json`) echoes the fully-resolved config under `thresholds`,
|
|
252
|
+
so you can confirm exactly what ran.
|
package/docs/node-api.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Node.js API
|
|
2
|
+
|
|
3
|
+
StopSlop ships a typed ESM API for tools that need findings and metrics in
|
|
4
|
+
memory instead of a CLI report. It requires Node.js `^20.19.0 || >=22.12.0`.
|
|
5
|
+
|
|
6
|
+
## Analyze with defaults
|
|
7
|
+
|
|
8
|
+
Resolve configuration before calling `analyze`:
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { analyze, resolveConfig } from 'stopslop';
|
|
12
|
+
|
|
13
|
+
const result = await analyze(process.cwd(), resolveConfig(undefined));
|
|
14
|
+
|
|
15
|
+
for (const finding of result.findings) {
|
|
16
|
+
console.log(finding.kind, finding.file, finding.line, finding.message);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log({
|
|
20
|
+
score: result.slop.score,
|
|
21
|
+
level: result.slop.level,
|
|
22
|
+
errors: result.errors,
|
|
23
|
+
notes: result.notes,
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`findings` contains the issues that should fail the current gate. `slop` always
|
|
28
|
+
describes the complete analyzed repository. Check `errors` before trusting a
|
|
29
|
+
result; `notes` explains non-fatal skips by project-level engines.
|
|
30
|
+
|
|
31
|
+
## Load `stopslop.json`
|
|
32
|
+
|
|
33
|
+
`loadConfig` reads `stopslop.json` from the analysis root and returns the
|
|
34
|
+
resolved defaults plus overrides:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import { analyze, loadConfig } from 'stopslop';
|
|
38
|
+
|
|
39
|
+
const root = process.cwd();
|
|
40
|
+
const result = await analyze(root, loadConfig(root));
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
To build configuration in code, pass the user-facing shape to `resolveConfig`:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { analyze, resolveConfig, type StopSlopConfig } from 'stopslop';
|
|
47
|
+
|
|
48
|
+
const input: StopSlopConfig = {
|
|
49
|
+
cognitiveComplexity: 20,
|
|
50
|
+
duplicates: { minTokens: 100, minLines: 10 },
|
|
51
|
+
deadCode: { files: false },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const result = await analyze(process.cwd(), resolveConfig(input));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Unknown fields and invalid values throw instead of being ignored.
|
|
58
|
+
|
|
59
|
+
## Compare with a Git revision
|
|
60
|
+
|
|
61
|
+
`analyzeGitBase` returns only findings absent from the selected revision while
|
|
62
|
+
retaining the current repository's complete score:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { analyzeGitBase, loadConfig } from 'stopslop';
|
|
66
|
+
|
|
67
|
+
const root = process.cwd();
|
|
68
|
+
const result = await analyzeGitBase(root, 'origin/main', loadConfig(root));
|
|
69
|
+
|
|
70
|
+
if (result.errors.length > 0) process.exitCode = 2;
|
|
71
|
+
else if (result.findings.length > 0) process.exitCode = 1;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The ref must resolve in the local Git repository. StopSlop creates a temporary
|
|
75
|
+
detached worktree; it does not switch the current branch or modify the index.
|
|
76
|
+
|
|
77
|
+
## Extend the analyzer list
|
|
78
|
+
|
|
79
|
+
Pass a third argument to append a custom analyzer to the built-in set:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import {
|
|
83
|
+
analyze,
|
|
84
|
+
defaultAnalyzers,
|
|
85
|
+
resolveConfig,
|
|
86
|
+
type Analyzer,
|
|
87
|
+
} from 'stopslop';
|
|
88
|
+
|
|
89
|
+
const custom: Analyzer = {
|
|
90
|
+
name: 'project-policy',
|
|
91
|
+
analyzeFile(file) {
|
|
92
|
+
return file.source.includes('DO NOT SHIP')
|
|
93
|
+
? [{
|
|
94
|
+
kind: 'unused-file',
|
|
95
|
+
file: file.relPath,
|
|
96
|
+
symbol: 'DO NOT SHIP',
|
|
97
|
+
line: 1,
|
|
98
|
+
severity: 'warn',
|
|
99
|
+
message: 'Unresolved project marker',
|
|
100
|
+
}]
|
|
101
|
+
: [];
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = await analyze(
|
|
106
|
+
process.cwd(),
|
|
107
|
+
resolveConfig(undefined),
|
|
108
|
+
[...defaultAnalyzers(), custom],
|
|
109
|
+
);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Custom analyzers currently return the published `Finding` union, so use an
|
|
113
|
+
existing finding kind for integrations. Treat that surface as pre-1.0; see the
|
|
114
|
+
[compatibility policy](../COMPATIBILITY.md).
|
package/docs/score.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Slop level: the formula
|
|
2
|
+
|
|
3
|
+
The slop level is one number in `0..100` plus a band label. It exists so that a
|
|
4
|
+
repo can be compared **with itself over time** and **with other repos**, which
|
|
5
|
+
raw finding counts cannot do: a 200 KLOC monorepo will always "win" on absolute
|
|
6
|
+
counts without being any sloppier per line written.
|
|
7
|
+
|
|
8
|
+
Nothing here is a black box, and nothing here is science. The **densities are
|
|
9
|
+
the facts**; the score is a weighted aggregate over them, with heuristic weights
|
|
10
|
+
and budgets that you can change.
|
|
11
|
+
|
|
12
|
+
## The formula
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
score = 100 · Σ wᵢ · sat(xᵢ) / Σ wᵢ
|
|
16
|
+
sat(x) = clamp((x − floorᵢ) / (budgetᵢ − floorᵢ), 0, 1)
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
where `i` ranges over the **enabled** signals:
|
|
20
|
+
|
|
21
|
+
| signal | xᵢ (density) | weight wᵢ | floor | budget |
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
| `god` | (god modules + god classes) / KLOC | 0.35 | 0 | 0.1 |
|
|
24
|
+
| `duplication` | duplicated lines / total lines (0..1) | 0.25 | 0.05 | 0.30 |
|
|
25
|
+
| `complexity` | over-complex functions / KLOC | 0.20 | 0 | 4 |
|
|
26
|
+
| `deadCode` | (unused exports + deps) / KLOC | 0.20 | 0 | 4 |
|
|
27
|
+
|
|
28
|
+
Below the floor a signal contributes nothing — that density is the baseline of
|
|
29
|
+
any living codebase, not slop. At the budget the contribution saturates.
|
|
30
|
+
|
|
31
|
+
Only `warn`-severity findings feed the score. `info` findings — unused *files*,
|
|
32
|
+
which hang entirely on Knip's entry-point detection — are reported but never
|
|
33
|
+
scored: a misdetected entry point must not outweigh a real god class.
|
|
34
|
+
|
|
35
|
+
Bands: `clean` <10 · `low` <25 · `moderate` <50 · `high` <75 · `legacy-grade` ≥75.
|
|
36
|
+
The top band is `legacy-grade`, not "severe": a high score means the code reads
|
|
37
|
+
like accumulated legacy — true whether a team wrote it over a decade or an agent
|
|
38
|
+
produced it last week. It is a description, not a verdict on the engineers.
|
|
39
|
+
|
|
40
|
+
Three properties are deliberate:
|
|
41
|
+
|
|
42
|
+
- **Size independence.** Every signal is a rate, not a count. Four god classes in
|
|
43
|
+
20 KLOC score exactly like one in 5 KLOC.
|
|
44
|
+
- **Saturation.** `min(1, …)` caps each signal at its budget: a repo with 100 god
|
|
45
|
+
modules is not "20× worse" than one with 5 — it is simply maxed out on that
|
|
46
|
+
axis, and the other axes still matter. Without the cap, one pathological file
|
|
47
|
+
would drown out everything else.
|
|
48
|
+
- **A check that did not run leaves the formula.** If you turn off `deadCode` — or
|
|
49
|
+
if Knip bails out because the project's dependencies are not installed, or the
|
|
50
|
+
project is a monorepo without `stopslop.json#knip.workspaces` (where Knip's
|
|
51
|
+
entry-point detection produces a landslide of false "unused") — its weight drops out of the
|
|
52
|
+
denominator instead of contributing zero. Not measuring something must never
|
|
53
|
+
read as a clean bill of health. The terminal report shows only the signals
|
|
54
|
+
that were actually measured, and says in a note why one is missing.
|
|
55
|
+
|
|
56
|
+
`multiple-exported-classes` is reported but not scored: it is a structural hint,
|
|
57
|
+
not a quantity of slop, and it correlates with the god-module signal already.
|
|
58
|
+
|
|
59
|
+
## Where the numbers come from
|
|
60
|
+
|
|
61
|
+
Honestly: the **weights are a judgement call**; the **floors and budgets are
|
|
62
|
+
measured**, not invented. Every non-zero anchor comes from a 41-repo / 9.4 MLOC
|
|
63
|
+
benchmark run over mature TypeScript OSS (angular, react, playwright, nextjs,
|
|
64
|
+
svelte, vue-core, nest, vite, material-ui and the like — the corpus ships in
|
|
65
|
+
`src/corpus.ts`), by one rule: a *floor* is what ordinary repos carry (~p25), a
|
|
66
|
+
*budget* is "past anything in the reference corpus" (~max/p90). The calibrated
|
|
67
|
+
defaults remain configurable (`score.weights`, `score.budgets`, `score.floors`).
|
|
68
|
+
|
|
69
|
+
- **Weights** encode what we claim slop *is*. Misplaced structure (god units) and
|
|
70
|
+
copy-paste are the expensive kinds — they are what an agent produces when it
|
|
71
|
+
keeps appending instead of refactoring, and they are what a human has to undo
|
|
72
|
+
by hand. A single hairy function or a stale export is cheaper to fix, so
|
|
73
|
+
complexity and dead code weigh less. `god` (0.35) > `duplication` (0.25) >
|
|
74
|
+
`complexity` = `deadCode` (0.20).
|
|
75
|
+
- **`god` budget = 0.1/KLOC** — corpus median 0.013, half the repos at exactly 0,
|
|
76
|
+
worst repo (playwright) at 0.071. No floor: a single god unit is already slop.
|
|
77
|
+
- **`duplication` floor = 5%, budget = 30%** — corpus p25 ≈ 5% (unavoidable
|
|
78
|
+
boilerplate of a living TS codebase), p90 ≈ 31% (mantine). The first-guess
|
|
79
|
+
budget (10%, the jscpd convention) sat *below the corpus median of ~13%*: the
|
|
80
|
+
signal saturated on ¾ of the corpus, became a constant +25 points for nearly
|
|
81
|
+
everyone, and compressed all 42 benchmark scores into 27..56 — a scale with
|
|
82
|
+
only two live bands. Measured beats conventional.
|
|
83
|
+
- **`complexity` budget = 4/KLOC** — the detection threshold is already the count
|
|
84
|
+
p95 of the calibration corpus ([thresholds.md](thresholds.md)); the corpus
|
|
85
|
+
maximum of repos crossing it is 3.31/KLOC (svelte — a compiler, i.e. honestly
|
|
86
|
+
complex). 4 = past anything in the corpus.
|
|
87
|
+
- **`deadCode` budget = 4/KLOC** — ≈ p75 of the sub-corpus where dead code was
|
|
88
|
+
actually measurable; one unused export/dependency every 250 lines means the
|
|
89
|
+
project has stopped removing what it stopped using. This is the noisiest
|
|
90
|
+
signal (it leans on Knip config quality, and a library's public API reads as
|
|
91
|
+
"unused exports"), which is why it carries the smallest weight and never
|
|
92
|
+
enters the corpus percentile below.
|
|
93
|
+
|
|
94
|
+
## "Worse than N% of reference repos"
|
|
95
|
+
|
|
96
|
+
Alongside the absolute score, the report anchors the repo against the reference
|
|
97
|
+
corpus: the share of the 41 benchmark repos with a strictly lower **core score**
|
|
98
|
+
— the score computed from the three signals measured identically everywhere
|
|
99
|
+
(god, duplication, complexity; dead code is excluded as infrastructure-dependent).
|
|
100
|
+
The comparison uses *your* weights/floors/budgets, so a custom config re-ranks
|
|
101
|
+
the corpus with the same yardstick it applies to your repo. It appears only when
|
|
102
|
+
all three core signals were measured (`--fast` has no percentile).
|
|
103
|
+
|
|
104
|
+
This is context, not a second verdict: "43/100, worse than 60% of reference
|
|
105
|
+
repos" tells you where the number sits among codebases nobody calls slop.
|
|
106
|
+
|
|
107
|
+
## What the score means — and what it does *not*
|
|
108
|
+
|
|
109
|
+
The score is **"how much uncleaned, suspicious code is left"**, not a verdict on
|
|
110
|
+
engineering quality. The intended reading:
|
|
111
|
+
|
|
112
|
+
- Clean it up and the number goes to zero: no god units, no duplication, no dead
|
|
113
|
+
code → 0, whether the code was written by a human or an agent. Fix the god
|
|
114
|
+
class, deduplicate, remove dead exports — the score follows.
|
|
115
|
+
- Its primary use is comparing a repo **with itself over time** (did the agent's
|
|
116
|
+
last week of commits push it up?). Cross-repo comparison is meaningful but
|
|
117
|
+
needs the caveat below.
|
|
118
|
+
- A genuinely complex domain scores high honestly: playwright tops the corpus
|
|
119
|
+
ranking because it really does carry the corpus's highest god-unit density
|
|
120
|
+
(0.071/KLOC) and near-maximal density of over-complex functions. The score
|
|
121
|
+
reports that; it does not know (or claim to know) that the complexity is
|
|
122
|
+
well-earned. High score = "a lot here would need justification", not "bad
|
|
123
|
+
engineers".
|
|
124
|
+
|
|
125
|
+
Other non-goals:
|
|
126
|
+
|
|
127
|
+
- `deadCode` anchors are the weakest: the signal depends on Knip configuration
|
|
128
|
+
and misreads a library's public API as unused exports (see above).
|
|
129
|
+
- It does not weigh a finding by how bad it is individually (a complexity-80
|
|
130
|
+
function counts the same as a complexity-16 one). Severity lives in the
|
|
131
|
+
findings, not the score.
|
|
132
|
+
- It is not a quality score. Clean code with zero slop signals can still be bad
|
|
133
|
+
code — stopslop only claims to measure *overproduction*.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Threshold calibration
|
|
2
|
+
|
|
3
|
+
Two lenses on the same corpus:
|
|
4
|
+
|
|
5
|
+
- **Count percentiles** — each entity counts once. "pXX = N" means XX% of
|
|
6
|
+
entities score ≤ N. This is what we use for defaults: a god finding should be
|
|
7
|
+
the worst ~1–5% of entities.
|
|
8
|
+
- **LOC-weighted percentiles** — the Alves/Ypma/Visser (ICSM 2010) methodology:
|
|
9
|
+
each entity weighted by LOC, so "pXX = N" means XX% of *code volume* sits in
|
|
10
|
+
entities ≤ N. Kept for reference; it skews high because large entities are
|
|
11
|
+
both more complex and hold more volume.
|
|
12
|
+
|
|
13
|
+
Corpus: 22 TypeScript repositories, 65,817 analyzed files (tests, fixtures,
|
|
14
|
+
generated and bundled code excluded). The per-repository counts are listed
|
|
15
|
+
below.
|
|
16
|
+
|
|
17
|
+
## Count percentiles (basis for defaults)
|
|
18
|
+
|
|
19
|
+
### Function
|
|
20
|
+
| metric | n | p50 | p75 | p90 | p95 | p99 |
|
|
21
|
+
|---|---|---|---|---|---|---|
|
|
22
|
+
| cognitive complexity | 108102 | 0 | 2 | 7 | 13 | 38 |
|
|
23
|
+
|
|
24
|
+
### Class (≥3 methods)
|
|
25
|
+
| metric | n | p50 | p75 | p90 | p95 | p99 |
|
|
26
|
+
|---|---|---|---|---|---|---|
|
|
27
|
+
| methods | 2940 | 6 | 12 | 23 | 32 | 64 |
|
|
28
|
+
| responsibility groups (≥2) | 2940 | 1 | 1 | 2 | 2 | 4 |
|
|
29
|
+
| Σ cognitive complexity | 2940 | 8 | 24 | 58 | 93 | 207 |
|
|
30
|
+
|
|
31
|
+
### Module (≥5 top-level fns)
|
|
32
|
+
| metric | n | p50 | p75 | p90 | p95 | p99 |
|
|
33
|
+
|---|---|---|---|---|---|---|
|
|
34
|
+
| top-level definitions | 2819 | 8 | 11 | 16 | 22 | 37 |
|
|
35
|
+
| responsibility groups (≥2) | 2819 | 1 | 1 | 2 | 2 | 3 |
|
|
36
|
+
| Σ cognitive complexity | 2819 | 21 | 54 | 109 | 167 | 354 |
|
|
37
|
+
|
|
38
|
+
## LOC-weighted percentiles (Alves, reference)
|
|
39
|
+
|
|
40
|
+
### Function
|
|
41
|
+
| metric | n | p50 | p75 | p90 | p95 | p99 |
|
|
42
|
+
|---|---|---|---|---|---|---|
|
|
43
|
+
| cognitive complexity | 108102 | 4 | 16 | 45 | 95 | 309 |
|
|
44
|
+
|
|
45
|
+
### Class (≥3 methods)
|
|
46
|
+
| metric | n | p50 | p75 | p90 | p95 | p99 |
|
|
47
|
+
|---|---|---|---|---|---|---|
|
|
48
|
+
| methods | 2940 | 15 | 30 | 57 | 74 | 111 |
|
|
49
|
+
| responsibility groups (≥2) | 2940 | 1 | 1 | 2 | 3 | 5 |
|
|
50
|
+
| Σ cognitive complexity | 2940 | 41 | 104 | 205 | 341 | 825 |
|
|
51
|
+
|
|
52
|
+
### Module (≥5 top-level fns)
|
|
53
|
+
| metric | n | p50 | p75 | p90 | p95 | p99 |
|
|
54
|
+
|---|---|---|---|---|---|---|
|
|
55
|
+
| top-level definitions | 2819 | 10 | 17 | 29 | 38 | 72 |
|
|
56
|
+
| responsibility groups (≥2) | 2819 | 1 | 1 | 2 | 2 | 4 |
|
|
57
|
+
| Σ cognitive complexity | 2819 | 51 | 128 | 280 | 455 | 1175 |
|
|
58
|
+
|
|
59
|
+
## Final defaults
|
|
60
|
+
|
|
61
|
+
A god finding is rare by construction, but the detector does not compare each
|
|
62
|
+
metric with an independent p95 cutoff. It has two alternative axes:
|
|
63
|
+
|
|
64
|
+
- **dispersion** requires members **and** responsibility groups **and** total
|
|
65
|
+
complexity together, so its member floor can stay sensitive;
|
|
66
|
+
- **size/WMC** requires both a high member count and high total complexity, even
|
|
67
|
+
when the unit is cohesive.
|
|
68
|
+
|
|
69
|
+
| gate | members | groups | Σ complexity | final setting |
|
|
70
|
+
|---|---:|---:|---:|---|
|
|
71
|
+
| cognitive complexity | — | — | 15 | `cognitiveComplexity: 15` |
|
|
72
|
+
| class dispersion | 12 | 3 | 90 | `godClass: 12 / 3 / 90` |
|
|
73
|
+
| class size/WMC | 30 | — | 200 | `godClass: 30 / 200` |
|
|
74
|
+
| module dispersion | 15 | 3 | 150 | `godModule: 15 / 3 / 150` |
|
|
75
|
+
| module size/WMC | 37 | — | 350 | `godModule: 37 / 350` |
|
|
76
|
+
|
|
77
|
+
The size thresholds sit near the count p95–p99 tail. On the dispersion axis,
|
|
78
|
+
three substantive groups are already rare (between class p95 and p99; module
|
|
79
|
+
p99), and the conjunction with complexity supplies the precision. Raising the
|
|
80
|
+
member floor itself to p95 would make the conjunction unnecessarily blind to
|
|
81
|
+
smaller units that clearly contain three unrelated responsibilities.
|
|
82
|
+
|
|
83
|
+
## Per-repo file counts
|
|
84
|
+
- angular: 1971 files
|
|
85
|
+
- angular-components: 1971 files
|
|
86
|
+
- chakra-ui: 2359 files
|
|
87
|
+
- create-react-app: 122 files
|
|
88
|
+
- gatsby: 3086 files
|
|
89
|
+
- ionic-framework: 508 files
|
|
90
|
+
- material-ui: 26447 files
|
|
91
|
+
- nest: 1306 files
|
|
92
|
+
- nextjs: 4498 files
|
|
93
|
+
- ng-bootstrap: 527 files
|
|
94
|
+
- ng-bootstrap-fresh: 527 files
|
|
95
|
+
- nx: 3330 files
|
|
96
|
+
- playwright: 788 files
|
|
97
|
+
- primeng: 2743 files
|
|
98
|
+
- react: 1711 files
|
|
99
|
+
- react-bootstrap: 440 files
|
|
100
|
+
- remix: 805 files
|
|
101
|
+
- shadcn-ui: 3473 files
|
|
102
|
+
- storybook: 2617 files
|
|
103
|
+
- svelte: 2299 files
|
|
104
|
+
- taiga-ui: 2770 files
|
|
105
|
+
- tanstack-table: 1519 files
|
package/llms.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# StopSlop
|
|
2
|
+
|
|
3
|
+
> Static analysis and CI gating for structural AI code slop in JavaScript and TypeScript. StopSlop finds cognitive complexity, god classes and modules, duplicate code, dead code, orphan features, and responsibility boundaries. It reports code patterns, not authorship.
|
|
4
|
+
|
|
5
|
+
Run without installing: `npx stopslop .`. Exit codes are `0` for a clear gate, `1` for detected findings, and `2` for an analysis error. Requires Node.js 20.19+ or 22.12+.
|
|
6
|
+
|
|
7
|
+
## Start here
|
|
8
|
+
|
|
9
|
+
- [Overview, installation, CLI and badge](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/README.md)
|
|
10
|
+
- [Configuration reference](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/configuration.md)
|
|
11
|
+
- [CI baselines, Git comparison, SARIF and agent reports](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/ci-and-automation.md)
|
|
12
|
+
- [Typed Node.js API](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/node-api.md)
|
|
13
|
+
|
|
14
|
+
## How it works
|
|
15
|
+
|
|
16
|
+
- [Score formula and interpretation](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/score.md)
|
|
17
|
+
- [Default thresholds and calibration](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/thresholds.md)
|
|
18
|
+
- [Architecture and methodology](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/ARCH.md)
|
|
19
|
+
- [Compatibility policy](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/COMPATIBILITY.md)
|
|
20
|
+
|
|
21
|
+
## Package
|
|
22
|
+
|
|
23
|
+
- [npm package](https://www.npmjs.com/package/stopslop)
|
|
24
|
+
- [JSON Schema](https://unpkg.com/stopslop/schema.json)
|
|
25
|
+
- [GitHub repository](https://github.com/BaryshevRS/stopslop)
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stopslop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Static analysis for AI-generated JavaScript and TypeScript: find cognitive complexity, god classes/modules, duplicate code, dead code and orphan features.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
7
9
|
"repository": {
|
|
8
10
|
"type": "git",
|
|
9
11
|
"url": "git+https://github.com/BaryshevRS/stopslop.git"
|
|
@@ -18,12 +20,20 @@
|
|
|
18
20
|
"exports": {
|
|
19
21
|
".": {
|
|
20
22
|
"types": "./dist/index.d.ts",
|
|
21
|
-
"import": "./dist/index.js"
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
22
25
|
},
|
|
23
|
-
"./schema.json": "./schema.json"
|
|
26
|
+
"./schema.json": "./schema.json",
|
|
27
|
+
"./package.json": "./package.json"
|
|
24
28
|
},
|
|
25
29
|
"files": [
|
|
26
|
-
"dist",
|
|
30
|
+
"dist/**/*",
|
|
31
|
+
"docs/**/*.md",
|
|
32
|
+
"README.md",
|
|
33
|
+
"COMPATIBILITY.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"llms.txt",
|
|
36
|
+
"context7.json",
|
|
27
37
|
"schema.json"
|
|
28
38
|
],
|
|
29
39
|
"engines": {
|
|
@@ -40,8 +50,9 @@
|
|
|
40
50
|
"test": "vitest run",
|
|
41
51
|
"test:watch": "vitest",
|
|
42
52
|
"test:package": "node scripts/package-smoke.mjs",
|
|
53
|
+
"check:clones": "clone-alert --minimum-tokens 100 --files src --format text --fail-on-violation",
|
|
43
54
|
"audit:prod": "pnpm audit --prod",
|
|
44
|
-
"ci:check": "pnpm test && pnpm typecheck && pnpm build",
|
|
55
|
+
"ci:check": "pnpm test && pnpm typecheck && pnpm check:clones && pnpm build",
|
|
45
56
|
"release:check": "pnpm ci:check && pnpm audit:prod && pnpm test:package",
|
|
46
57
|
"prepublishOnly": "pnpm release:check",
|
|
47
58
|
"calibrate": "tsx scripts/calibrate.ts",
|
|
@@ -74,12 +85,41 @@
|
|
|
74
85
|
},
|
|
75
86
|
"keywords": [
|
|
76
87
|
"ai",
|
|
88
|
+
"ai-code",
|
|
89
|
+
"ai-generated-code",
|
|
90
|
+
"ai-code-quality",
|
|
91
|
+
"ai-slop",
|
|
77
92
|
"slop",
|
|
93
|
+
"slop-detector",
|
|
94
|
+
"vibe-coding",
|
|
95
|
+
"static-analysis",
|
|
96
|
+
"code-analysis",
|
|
97
|
+
"code-quality",
|
|
78
98
|
"code-smell",
|
|
99
|
+
"technical-debt",
|
|
100
|
+
"javascript",
|
|
101
|
+
"typescript",
|
|
102
|
+
"jsx",
|
|
103
|
+
"tsx",
|
|
104
|
+
"cli",
|
|
105
|
+
"lint",
|
|
106
|
+
"ci",
|
|
107
|
+
"quality-gate",
|
|
108
|
+
"code-scanning",
|
|
109
|
+
"github-actions",
|
|
110
|
+
"sarif",
|
|
111
|
+
"baseline",
|
|
79
112
|
"god-class",
|
|
80
113
|
"god-module",
|
|
81
114
|
"cognitive-complexity",
|
|
82
115
|
"lcom",
|
|
83
|
-
"
|
|
116
|
+
"cohesion",
|
|
117
|
+
"duplicate-code",
|
|
118
|
+
"copy-paste-detector",
|
|
119
|
+
"dead-code",
|
|
120
|
+
"unused-code",
|
|
121
|
+
"orphan-code",
|
|
122
|
+
"monorepo",
|
|
123
|
+
"code-review"
|
|
84
124
|
]
|
|
85
125
|
}
|