stopslop 0.2.0 → 1.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.
@@ -0,0 +1,257 @@
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.
187
+
188
+ Every run lists what Knip finds missing from this configuration under **config
189
+ hints** (`configHints` in `--format json`): a workspace it never reached, an
190
+ entry pattern that matched nothing, and how many unused files each accounts
191
+ for. While Knip reports the project or a workspace as unconfigured, dead-code
192
+ findings are shown but excluded from the score — without entry points, live
193
+ files come back unreachable.
194
+
195
+ In a single-package project, `entry` and `project` belong directly under
196
+ `knip`. In a monorepo they belong under `knip.workspaces`; Knip ignores the
197
+ top-level forms once multiple workspaces are present.
198
+
199
+ One Knip default is changed: StopSlop sets `ignoreExportsUsedInFile` to `true`.
200
+ Knip otherwise reports an export nothing imports even when the symbol is used
201
+ inside its own file — a `export` keyword wider than it needs to be, which no
202
+ behaviour depends on. That is a style nit rather than the structural waste
203
+ StopSlop reports. Set it back to `false` under `knip` to get Knip's own default;
204
+ the granular per-symbol-type form works as documented upstream.
205
+
206
+ The package ships `schema.json`. Add
207
+ `"$schema": "https://unpkg.com/stopslop/schema.json"` for editor completion,
208
+ field descriptions, and validation. StopSlop consumes configuration only; it
209
+ does not infer or generate project settings. That project inspection is kept as
210
+ a boundary for future external tooling (such as an MCP server).
211
+
212
+ ### `score` — object
213
+
214
+ Weights, floors, and budgets of the slop level. A signal contributes nothing
215
+ below its floor, rises linearly, and saturates at its budget. Full formula and
216
+ rationale (all anchors measured on a 41-repo benchmark of mature TypeScript
217
+ OSS): [score.md](score.md).
218
+
219
+ ```json
220
+ {
221
+ "score": {
222
+ "weights": { "god": 0.35, "duplication": 0.25, "complexity": 0.2, "deadCode": 0.2 },
223
+ "budgets": { "god": 0.1, "duplication": 0.3, "complexity": 4, "deadCode": 4 },
224
+ "floors": { "god": 0, "duplication": 0.05, "complexity": 0, "deadCode": 0 }
225
+ }
226
+ }
227
+ ```
228
+
229
+ Weights need not sum to 1 — the score divides by the sum of the weights that
230
+ actually ran. Disabling a check removes its signal from the formula entirely
231
+ rather than scoring it as clean. The "worse than N% of reference repos" anchor
232
+ is recomputed with your weights/floors/budgets, so a custom config re-ranks the
233
+ corpus with the same yardstick it applies to your repo.
234
+
235
+ ## Engine knobs
236
+
237
+ ### `hubFanInRatio` — number (0..1)
238
+
239
+ A member referenced by more than this fraction of the unit's methods is treated
240
+ as a "hub" (shared glue), removed from the cohesion graph, and the responsibility
241
+ groups are recomputed — so a class held together by one shared field still splits
242
+ into its real parts. Default **0.5**. Lower = more aggressive splitting.
243
+
244
+ ### `ignore` — string[]
245
+
246
+ Extra glob patterns (matched against POSIX-relative paths) to exclude, on top of
247
+ the built-in excludes (`node_modules`, `dist`/`build`, tests, fixtures, `.d.ts`,
248
+ minified/bundled files, and generator subtrees such as openapi-generator).
249
+
250
+ ```json
251
+ { "ignore": ["**/*.generated.ts", "src/legacy/**"] }
252
+ ```
253
+
254
+ ## Reference
255
+
256
+ The JSON report (`--json`) echoes the fully-resolved config under `thresholds`,
257
+ so you can confirm exactly what ran.
@@ -0,0 +1,115 @@
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, and
30
+ `configHints` lists what Knip reports as missing from `stopslop.json#knip`.
31
+
32
+ ## Load `stopslop.json`
33
+
34
+ `loadConfig` reads `stopslop.json` from the analysis root and returns the
35
+ resolved defaults plus overrides:
36
+
37
+ ```ts
38
+ import { analyze, loadConfig } from 'stopslop';
39
+
40
+ const root = process.cwd();
41
+ const result = await analyze(root, loadConfig(root));
42
+ ```
43
+
44
+ To build configuration in code, pass the user-facing shape to `resolveConfig`:
45
+
46
+ ```ts
47
+ import { analyze, resolveConfig, type StopSlopConfig } from 'stopslop';
48
+
49
+ const input: StopSlopConfig = {
50
+ cognitiveComplexity: 20,
51
+ duplicates: { minTokens: 100, minLines: 10 },
52
+ deadCode: { files: false },
53
+ };
54
+
55
+ const result = await analyze(process.cwd(), resolveConfig(input));
56
+ ```
57
+
58
+ Unknown fields and invalid values throw instead of being ignored.
59
+
60
+ ## Compare with a Git revision
61
+
62
+ `analyzeGitBase` returns only findings absent from the selected revision while
63
+ retaining the current repository's complete score:
64
+
65
+ ```ts
66
+ import { analyzeGitBase, loadConfig } from 'stopslop';
67
+
68
+ const root = process.cwd();
69
+ const result = await analyzeGitBase(root, 'origin/main', loadConfig(root));
70
+
71
+ if (result.errors.length > 0) process.exitCode = 2;
72
+ else if (result.findings.length > 0) process.exitCode = 1;
73
+ ```
74
+
75
+ The ref must resolve in the local Git repository. StopSlop creates a temporary
76
+ detached worktree; it does not switch the current branch or modify the index.
77
+
78
+ ## Extend the analyzer list
79
+
80
+ Pass a third argument to append a custom analyzer to the built-in set:
81
+
82
+ ```ts
83
+ import {
84
+ analyze,
85
+ defaultAnalyzers,
86
+ resolveConfig,
87
+ type Analyzer,
88
+ } from 'stopslop';
89
+
90
+ const custom: Analyzer = {
91
+ name: 'project-policy',
92
+ analyzeFile(file) {
93
+ return file.source.includes('DO NOT SHIP')
94
+ ? [{
95
+ kind: 'unused-file',
96
+ file: file.relPath,
97
+ symbol: 'DO NOT SHIP',
98
+ line: 1,
99
+ severity: 'warn',
100
+ message: 'Unresolved project marker',
101
+ }]
102
+ : [];
103
+ },
104
+ };
105
+
106
+ const result = await analyze(
107
+ process.cwd(),
108
+ resolveConfig(undefined),
109
+ [...defaultAnalyzers(), custom],
110
+ );
111
+ ```
112
+
113
+ Custom analyzers currently return the published `Finding` union, so use an
114
+ existing finding kind for integrations. That surface is experimental and may
115
+ change in a minor release; see the [compatibility policy](../COMPATIBILITY.md).
package/docs/score.md ADDED
@@ -0,0 +1,134 @@
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
50
+ Knip reports the run as unconfigured (its `top-level-unconfigured` or
51
+ `workspace-unconfigured` config hint: more files unreachable than anything but
52
+ missing entry points explains) — its weight drops out of the
53
+ denominator instead of contributing zero. Not measuring something must never
54
+ read as a clean bill of health. The terminal report shows only the signals
55
+ that were actually measured, and says in a note why one is missing.
56
+
57
+ `multiple-exported-classes` is reported but not scored: it is a structural hint,
58
+ not a quantity of slop, and it correlates with the god-module signal already.
59
+
60
+ ## Where the numbers come from
61
+
62
+ Honestly: the **weights are a judgement call**; the **floors and budgets are
63
+ measured**, not invented. Every non-zero anchor comes from a 41-repo / 9.4 MLOC
64
+ benchmark run over mature TypeScript OSS (angular, react, playwright, nextjs,
65
+ svelte, vue-core, nest, vite, material-ui and the like — the corpus ships in
66
+ `src/corpus.ts`), by one rule: a *floor* is what ordinary repos carry (~p25), a
67
+ *budget* is "past anything in the reference corpus" (~max/p90). The calibrated
68
+ defaults remain configurable (`score.weights`, `score.budgets`, `score.floors`).
69
+
70
+ - **Weights** encode what we claim slop *is*. Misplaced structure (god units) and
71
+ copy-paste are the expensive kinds — they are what an agent produces when it
72
+ keeps appending instead of refactoring, and they are what a human has to undo
73
+ by hand. A single hairy function or a stale export is cheaper to fix, so
74
+ complexity and dead code weigh less. `god` (0.35) > `duplication` (0.25) >
75
+ `complexity` = `deadCode` (0.20).
76
+ - **`god` budget = 0.1/KLOC** — corpus median 0.013, half the repos at exactly 0,
77
+ worst repo (playwright) at 0.071. No floor: a single god unit is already slop.
78
+ - **`duplication` floor = 5%, budget = 30%** — corpus p25 ≈ 5% (unavoidable
79
+ boilerplate of a living TS codebase), p90 ≈ 31% (mantine). The first-guess
80
+ budget (10%, the jscpd convention) sat *below the corpus median of ~13%*: the
81
+ signal saturated on ¾ of the corpus, became a constant +25 points for nearly
82
+ everyone, and compressed all 42 benchmark scores into 27..56 — a scale with
83
+ only two live bands. Measured beats conventional.
84
+ - **`complexity` budget = 4/KLOC** — the detection threshold is already the count
85
+ p95 of the calibration corpus ([thresholds.md](thresholds.md)); the corpus
86
+ maximum of repos crossing it is 3.31/KLOC (svelte — a compiler, i.e. honestly
87
+ complex). 4 = past anything in the corpus.
88
+ - **`deadCode` budget = 4/KLOC** — ≈ p75 of the sub-corpus where dead code was
89
+ actually measurable; one unused export/dependency every 250 lines means the
90
+ project has stopped removing what it stopped using. This is the noisiest
91
+ signal (it leans on Knip config quality, and a library's public API reads as
92
+ "unused exports"), which is why it carries the smallest weight and never
93
+ enters the corpus percentile below.
94
+
95
+ ## "Worse than N% of reference repos"
96
+
97
+ Alongside the absolute score, the report anchors the repo against the reference
98
+ corpus: the share of the 41 benchmark repos with a strictly lower **core score**
99
+ — the score computed from the three signals measured identically everywhere
100
+ (god, duplication, complexity; dead code is excluded as infrastructure-dependent).
101
+ The comparison uses *your* weights/floors/budgets, so a custom config re-ranks
102
+ the corpus with the same yardstick it applies to your repo. It appears only when
103
+ all three core signals were measured (`--fast` has no percentile).
104
+
105
+ This is context, not a second verdict: "43/100, worse than 60% of reference
106
+ repos" tells you where the number sits among codebases nobody calls slop.
107
+
108
+ ## What the score means — and what it does *not*
109
+
110
+ The score is **"how much uncleaned, suspicious code is left"**, not a verdict on
111
+ engineering quality. The intended reading:
112
+
113
+ - Clean it up and the number goes to zero: no god units, no duplication, no dead
114
+ code → 0, whether the code was written by a human or an agent. Fix the god
115
+ class, deduplicate, remove dead exports — the score follows.
116
+ - Its primary use is comparing a repo **with itself over time** (did the agent's
117
+ last week of commits push it up?). Cross-repo comparison is meaningful but
118
+ needs the caveat below.
119
+ - A genuinely complex domain scores high honestly: playwright tops the corpus
120
+ ranking because it really does carry the corpus's highest god-unit density
121
+ (0.071/KLOC) and near-maximal density of over-complex functions. The score
122
+ reports that; it does not know (or claim to know) that the complexity is
123
+ well-earned. High score = "a lot here would need justification", not "bad
124
+ engineers".
125
+
126
+ Other non-goals:
127
+
128
+ - `deadCode` anchors are the weakest: the signal depends on Knip configuration
129
+ and misreads a library's public API as unused exports (see above).
130
+ - It does not weigh a finding by how bad it is individually (a complexity-80
131
+ function counts the same as a complexity-16 one). Severity lives in the
132
+ findings, not the score.
133
+ - It is not a quality score. Clean code with zero slop signals can still be bad
134
+ 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,27 @@
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
+ Dead-code settings live under `knip` in `stopslop.json` — a project's own `knip.json` or `package.json#knip` is never read. Nothing needs to be written up front: the run prints `config hints` from Knip naming the workspace or entry point that is missing, and dead code stays out of the score until they are resolved.
8
+
9
+ ## Start here
10
+
11
+ - [Overview, installation, CLI and badge](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/README.md)
12
+ - [Configuration reference](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/configuration.md)
13
+ - [CI baselines, Git comparison, SARIF and agent reports](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/ci-and-automation.md)
14
+ - [Typed Node.js API](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/node-api.md)
15
+
16
+ ## How it works
17
+
18
+ - [Score formula and interpretation](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/score.md)
19
+ - [Default thresholds and calibration](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/thresholds.md)
20
+ - [Architecture and methodology](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/ARCH.md)
21
+ - [Compatibility policy](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/COMPATIBILITY.md)
22
+
23
+ ## Package
24
+
25
+ - [npm package](https://www.npmjs.com/package/stopslop)
26
+ - [JSON Schema](https://unpkg.com/stopslop/schema.json)
27
+ - [GitHub repository](https://github.com/BaryshevRS/stopslop)