stopslop 1.1.0 → 1.2.1
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 +6 -1
- package/README.md +79 -7
- package/context7.json +3 -1
- package/dist/cli.js +92 -4
- package/dist/src.js +0 -0
- package/docs/ci-and-automation.md +115 -1
- package/docs/configuration.md +2 -1
- package/docs/rules.md +102 -0
- package/hooks/stop-gate.mjs +156 -0
- package/llms.txt +3 -1
- package/package.json +17 -14
- package/schema.json +56 -0
package/COMPATIBILITY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Compatibility
|
|
2
2
|
|
|
3
|
-
Last updated: 1.1
|
|
3
|
+
Last updated: 1.2.1 (2026-11-02)
|
|
4
4
|
|
|
5
5
|
StopSlop follows Semantic Versioning. Stable surfaces change incompatibly only
|
|
6
6
|
in a major release. Experimental surfaces may change in a minor release; such
|
|
@@ -13,12 +13,17 @@ changes are called out in the changelog.
|
|
|
13
13
|
| SARIF 2.1.0 report | Stable | New rules and optional properties may be added; existing required fields remain compatible. |
|
|
14
14
|
| `stopslop.json` schema | Stable | New settings may be added; configuration valid in a 1.x release stays valid in every later 1.x release. |
|
|
15
15
|
| Programmatic Node.js API | Experimental | Exported functions and TypeScript types may change in a minor release. |
|
|
16
|
+
| Agent plugins (`stopslop` skill and stop hook for Claude Code, Codex, Cursor, Gemini CLI) | Experimental | When the hook runs, what it reports, and the skill's instructions may change in a minor release. |
|
|
16
17
|
|
|
17
18
|
Findings and scores are not a compatibility surface. Detector fixes, threshold
|
|
18
19
|
calibration, and Knip or Clone Alert updates can change what a release reports
|
|
19
20
|
for the same code; such changes are called out in the changelog. Pin the
|
|
20
21
|
StopSlop version in CI when a gate must not move without a commit.
|
|
21
22
|
|
|
23
|
+
Version 1.2.1 preserves all stable surfaces and adds no deprecations. Anonymous
|
|
24
|
+
function detection and analysis-engine updates can produce new findings or
|
|
25
|
+
different scores for existing code.
|
|
26
|
+
|
|
22
27
|
## Runtime support
|
|
23
28
|
|
|
24
29
|
- Node.js 20.19+, 22.12+, and 24.
|
package/README.md
CHANGED
|
@@ -42,6 +42,41 @@ uses [Clone Alert](https://www.npmjs.com/package/clone-alert); dead-code analysi
|
|
|
42
42
|
uses [Knip](https://knip.dev). StopSlop detects code patterns, not whether a human
|
|
43
43
|
or an AI wrote them.
|
|
44
44
|
|
|
45
|
+
## A passing test does not mean the feature is connected
|
|
46
|
+
|
|
47
|
+
An agent adds a discount function and a test:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/discount.ts
|
|
51
|
+
export function applyDiscount(total: number): number {
|
|
52
|
+
return total * 0.9;
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// test/discount.test.ts
|
|
58
|
+
import { strictEqual } from 'node:assert';
|
|
59
|
+
import { test } from 'node:test';
|
|
60
|
+
import { applyDiscount } from '../src/discount.js';
|
|
61
|
+
|
|
62
|
+
test('applies a 10% discount', () => {
|
|
63
|
+
strictEqual(applyDiscount(100), 90);
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The test passes, but checkout still calculates the total without calling
|
|
68
|
+
`applyDiscount`. StopSlop reports:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
1 orphan feature
|
|
72
|
+
|
|
73
|
+
orphan feature `applyDiscount` is only used by the test `test/discount.test.ts`; connect it to production code, or remove it and its test if unneeded
|
|
74
|
+
src/discount.ts:1
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
If the discount belongs in checkout, connect it and keep the test. If the feature
|
|
78
|
+
was abandoned, remove the unused function and its test.
|
|
79
|
+
|
|
45
80
|
## Start with one command
|
|
46
81
|
|
|
47
82
|
Run without installing:
|
|
@@ -177,18 +212,54 @@ For GitHub Code Scanning, emit SARIF before propagating the gate failure:
|
|
|
177
212
|
npx stopslop . --base origin/main --format sarif > stopslop.sarif
|
|
178
213
|
```
|
|
179
214
|
|
|
215
|
+
Every rule in the SARIF report carries its help, so the alert says why the
|
|
216
|
+
finding matters and how to fix it, not only what failed.
|
|
217
|
+
|
|
180
218
|
See [CI and automation](docs/ci-and-automation.md) for a complete workflow that
|
|
181
219
|
uploads SARIF and still preserves StopSlop's exit code.
|
|
182
220
|
|
|
183
|
-
##
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
instead
|
|
221
|
+
## Catch slop while the agent is still working
|
|
222
|
+
|
|
223
|
+
A check that first runs on the pull request reports code nobody remembers
|
|
224
|
+
writing. StopSlop can run inside the coding session instead, while the code is
|
|
225
|
+
still in front of the agent.
|
|
226
|
+
|
|
227
|
+
This repository is a plugin for Claude Code, Codex, and Cursor and an extension
|
|
228
|
+
for Gemini CLI. Each agent gets the `stopslop` skill, which covers how to run the
|
|
229
|
+
check, read the report, and fix each kind of finding without gaming it: no
|
|
230
|
+
raised thresholds, no ignore globs, no baseline edits on the agent's own
|
|
231
|
+
initiative. Where the agent supports it, the install also adds a stop hook.
|
|
232
|
+
|
|
233
|
+
| Agent | Install | Stop hook |
|
|
234
|
+
| --- | --- | --- |
|
|
235
|
+
| Claude Code | `/plugin marketplace add BaryshevRS/stopslop`<br>`/plugin install stopslop@stopslop` | included |
|
|
236
|
+
| Codex | `codex plugin marketplace add BaryshevRS/stopslop`<br>`codex plugin add stopslop@stopslop` | included; trust it when Codex asks at session start |
|
|
237
|
+
| Cursor | Customize → import `BaryshevRS/stopslop` from a GitHub repository | included |
|
|
238
|
+
| Gemini CLI | `gemini extensions install https://github.com/BaryshevRS/stopslop` | [in project settings](docs/ci-and-automation.md#commit-the-hook-to-the-project) |
|
|
239
|
+
| Others | `npx skills add BaryshevRS/stopslop` (skill only) | — |
|
|
240
|
+
|
|
241
|
+
Before the agent ends a turn that changed JS/TS source, the hook checks the
|
|
242
|
+
uncommitted changes against `HEAD`, or against `.stopslop-baseline.json` when
|
|
243
|
+
the project has one, and hands any new findings back with how to fix each kind.
|
|
244
|
+
Legacy never blocks a turn, a turn that changed no source costs one
|
|
245
|
+
`git status`, and the agent gets one nudge per turn: accepting a finding stays
|
|
246
|
+
your decision. The hook runs the project's pinned `stopslop` from `node_modules`
|
|
247
|
+
and falls back to `npx stopslop@1`.
|
|
248
|
+
|
|
249
|
+
The hook also ships in the npm package, so a project that pins `stopslop` can
|
|
250
|
+
commit it to its own agent settings and give it to every contributor; see
|
|
251
|
+
[Commit the hook to the project](docs/ci-and-automation.md#commit-the-hook-to-the-project).
|
|
252
|
+
|
|
253
|
+
**Any agent or script.** The JSON report is stable and versioned. Feed it into
|
|
254
|
+
the next turn instead of paraphrasing terminal output:
|
|
187
255
|
|
|
188
256
|
```bash
|
|
189
|
-
npx stopslop . --json > slop-report.json
|
|
257
|
+
npx stopslop . --base HEAD --json > slop-report.json
|
|
190
258
|
```
|
|
191
259
|
|
|
260
|
+
Each finding carries `kind`, `file`, `line`, and `message`, and `rules` says for
|
|
261
|
+
each reported kind why it matters and what resolves it: the same text as
|
|
262
|
+
[docs/rules.md](docs/rules.md) and the rule help in GitHub Code Scanning.
|
|
192
263
|
God-unit findings include member groups that serve as concrete refactoring
|
|
193
264
|
boundaries. Clone findings include every occurrence. Baseline and Git-base runs
|
|
194
265
|
retain the complete score while returning only new findings.
|
|
@@ -308,8 +379,9 @@ is experimental and may change in a minor release; see
|
|
|
308
379
|
- A baseline accepts finding identities. If an accepted function becomes more
|
|
309
380
|
complex without changing identity, the score rises but the gate stays clear.
|
|
310
381
|
|
|
311
|
-
|
|
312
|
-
|
|
382
|
+
The score formula and its limits are in [docs/score.md](docs/score.md), how
|
|
383
|
+
the default thresholds were calibrated in [docs/thresholds.md](docs/thresholds.md),
|
|
384
|
+
and what each finding means in [docs/rules.md](docs/rules.md).
|
|
313
385
|
|
|
314
386
|
## FAQ
|
|
315
387
|
|
package/context7.json
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
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
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
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
|
-
"Knip settings go under `knip` in stopslop.json; a project's own knip.json or package.json#knip is never read. Do not write one up front — run the analysis and follow the `config hints` it prints, which come from Knip and name what is missing."
|
|
20
|
+
"Knip settings go under `knip` in stopslop.json; a project's own knip.json or package.json#knip is never read. Do not write one up front — run the analysis and follow the `config hints` it prints, which come from Knip and name what is missing.",
|
|
21
|
+
"Every reported kind has help: the JSON report's `rules` block and SARIF rule help give `why` and `fix`, the same text as docs/rules.md. Fix the code; do not raise thresholds or edit the baseline to get a clear run.",
|
|
22
|
+
"The repo is a plugin for Claude Code, Codex and Cursor and a Gemini CLI extension: a stopslop skill plus a stop hook reporting findings the uncommitted changes added. Other agents: `npx skills add BaryshevRS/stopslop`."
|
|
21
23
|
]
|
|
22
24
|
}
|
package/dist/cli.js
CHANGED
|
@@ -162,6 +162,85 @@ function countByKind(findings) {
|
|
|
162
162
|
for (const f of findings) counts[f.kind]++;
|
|
163
163
|
return counts;
|
|
164
164
|
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/report/rules.ts
|
|
167
|
+
const RULES_DOC = "https://github.com/BaryshevRS/stopslop/blob/main/docs/rules.md";
|
|
168
|
+
const RULES = {
|
|
169
|
+
"cognitive-complexity": {
|
|
170
|
+
summary: "Function is harder to follow than the threshold allows",
|
|
171
|
+
why: "Cognitive complexity counts what a reader has to hold in mind to follow a function: branches, loops, nesting, and boolean chains (SonarSource specification). Above the threshold a function is hard to review and easy to break, and it keeps growing the same way: each new case lands beside the existing ones instead of in a place of its own.",
|
|
172
|
+
fix: "Return early instead of nesting, and move each independent decision into a function whose name says what it decides. Replace a long if/else or switch over one value with a lookup table. Cutting the body into arbitrary part1/part2 helpers moves the complexity without removing it.",
|
|
173
|
+
config: "cognitiveComplexity"
|
|
174
|
+
},
|
|
175
|
+
"god-module": {
|
|
176
|
+
summary: "Module mixes unrelated responsibilities or has outgrown one file",
|
|
177
|
+
why: "Its top-level definitions fall into groups that do not use each other, or the file is too large and too complex to be one unit. Every change touches a file that does several jobs, and new code keeps landing in it because it is the file already open.",
|
|
178
|
+
fix: "Move each responsibility group to its own module; `--details` and the JSON report list the groups found in the reference graph, the ready-made split boundaries. Members listed as holding cohesion together are shared helpers: move them where every group can import them. A size-axis finding has no clean boundaries, so split it by responsibility by hand.",
|
|
179
|
+
config: "godModule"
|
|
180
|
+
},
|
|
181
|
+
"god-class": {
|
|
182
|
+
summary: "Class mixes unrelated responsibilities or has outgrown one unit",
|
|
183
|
+
why: "Its methods fall into groups that do not share state or call each other, or the class is too large and too complex to be one unit. Every change risks the unrelated behaviour next to it, and the class cannot be tested one responsibility at a time.",
|
|
184
|
+
fix: "Extract each responsibility group into its own class or set of functions, and let the original delegate to them or disappear; `--details` and the JSON report list the groups. A size-axis finding has no clean boundaries, so split it by responsibility by hand.",
|
|
185
|
+
config: "godClass"
|
|
186
|
+
},
|
|
187
|
+
"multiple-exported-classes": {
|
|
188
|
+
summary: "File exports more than one class",
|
|
189
|
+
why: "Several exported classes in one file usually mean code was appended where it was convenient instead of placed where it belongs. Readers cannot find a class by its file name, and the file drifts toward a god module.",
|
|
190
|
+
fix: "Give each exported class its own file named after it. A class that only serves another can stay in the same file without being exported.",
|
|
191
|
+
config: "multipleExportedClasses"
|
|
192
|
+
},
|
|
193
|
+
"duplicate-code": {
|
|
194
|
+
summary: "Block of code is copied in more than one place",
|
|
195
|
+
why: "Copies drift. A bug fixed in one stays in the others, and the copies start to disagree about the same computation. It is usually cheaper to write a block again than to find the helper that already does it, which is how copies accumulate.",
|
|
196
|
+
fix: "Extract the block into one function and call it from every occurrence; the finding lists all of them. Where the copies differ on purpose, make the difference a parameter. Renaming variables does not hide a copy when `duplicates.ignoreIdentifiers` is on.",
|
|
197
|
+
config: "duplicates"
|
|
198
|
+
},
|
|
199
|
+
"unused-export": {
|
|
200
|
+
summary: "Export is not imported anywhere",
|
|
201
|
+
why: "Code nothing uses still has to be read, compiled, kept type-correct, and migrated. Code is added readily and deleted reluctantly, so unused exports only accumulate.",
|
|
202
|
+
fix: "Delete it, or drop the `export` keyword when the file uses it internally. If something Knip does not know about uses it, such as a framework convention or a public package entry, declare that entry under `knip` in stopslop.json instead.",
|
|
203
|
+
config: "deadCode.exports"
|
|
204
|
+
},
|
|
205
|
+
"unused-dependency": {
|
|
206
|
+
summary: "Dependency in package.json is not imported anywhere",
|
|
207
|
+
why: "An unused package still installs, still gets audited and updated, and still widens the supply chain the project trusts.",
|
|
208
|
+
fix: "Remove it from package.json. If it is used in a way Knip cannot see, such as a binary run from a script or a plugin loaded by name, list it in `ignoreDependencies` under `knip` in stopslop.json.",
|
|
209
|
+
config: "deadCode.dependencies"
|
|
210
|
+
},
|
|
211
|
+
"unused-file": {
|
|
212
|
+
summary: "File is not reachable from any entry point",
|
|
213
|
+
why: "Often a superseded version of a module, left next to the one that replaced it. Reported as info and not scored: the result is only as good as the entry points Knip knows.",
|
|
214
|
+
fix: "Resolve the config hints in the report first; a missing entry point makes live files look unreachable. Then delete the file.",
|
|
215
|
+
config: "deadCode.files"
|
|
216
|
+
},
|
|
217
|
+
"orphan-feature": {
|
|
218
|
+
summary: "Function or class is used only by its own test",
|
|
219
|
+
why: "No production code calls it; one test is the only thing that imports it. This can be a feature written together with its test and never wired in, and the green test makes it look finished.",
|
|
220
|
+
fix: "Check whether the feature is meant to exist. If so, wire it into the production code that needs it. Otherwise, remove the unused symbol and its test together: deleting only the source breaks the test, and deleting only the test leaves dead code.",
|
|
221
|
+
config: "deadCode.orphans"
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
function ruleHelp(kind) {
|
|
225
|
+
return {
|
|
226
|
+
...RULES[kind],
|
|
227
|
+
helpUri: `${RULES_DOC}#${kind}`
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** Help for the kinds present in a set of findings, keyed by kind. */
|
|
231
|
+
function rulesFor(findings) {
|
|
232
|
+
const rules = {};
|
|
233
|
+
for (const { kind } of findings) rules[kind] ??= ruleHelp(kind);
|
|
234
|
+
return rules;
|
|
235
|
+
}
|
|
236
|
+
/** The rule's help as Markdown: the body of a SARIF rule help and of docs/rules.md. */
|
|
237
|
+
function ruleMarkdown(rule) {
|
|
238
|
+
return [
|
|
239
|
+
`**Why it matters.** ${rule.why}`,
|
|
240
|
+
`**How to fix.** ${rule.fix}`,
|
|
241
|
+
`**Tune or disable:** \`${rule.config}\` in \`stopslop.json\`.`
|
|
242
|
+
].join("\n\n");
|
|
243
|
+
}
|
|
165
244
|
const KINDS = [
|
|
166
245
|
"god-module",
|
|
167
246
|
"god-class",
|
|
@@ -209,6 +288,7 @@ function toJson(result) {
|
|
|
209
288
|
},
|
|
210
289
|
summary: summarize(result),
|
|
211
290
|
findings: result.findings,
|
|
291
|
+
rules: rulesFor(result.findings),
|
|
212
292
|
notes: result.notes,
|
|
213
293
|
configHints: result.configHints,
|
|
214
294
|
/** Where Knip settings are read from. Nowhere else is consulted. */
|
|
@@ -233,9 +313,17 @@ function toSarif(result) {
|
|
|
233
313
|
for (const finding of result.findings) {
|
|
234
314
|
if (seenRules.has(finding.kind)) continue;
|
|
235
315
|
seenRules.add(finding.kind);
|
|
316
|
+
const rule = ruleHelp(finding.kind);
|
|
236
317
|
rules.push({
|
|
237
318
|
id: finding.kind,
|
|
238
|
-
|
|
319
|
+
name: ruleName(finding.kind),
|
|
320
|
+
shortDescription: { text: rule.summary },
|
|
321
|
+
fullDescription: { text: rule.why },
|
|
322
|
+
help: {
|
|
323
|
+
text: `${rule.why}\n\n${rule.fix}\n\nTune or disable: ${rule.config} in stopslop.json.`,
|
|
324
|
+
markdown: ruleMarkdown(rule)
|
|
325
|
+
},
|
|
326
|
+
helpUri: rule.helpUri,
|
|
239
327
|
defaultConfiguration: { level: sarifLevel(finding.severity) }
|
|
240
328
|
});
|
|
241
329
|
}
|
|
@@ -261,9 +349,9 @@ function toSarif(result) {
|
|
|
261
349
|
function sarifLevel(severity) {
|
|
262
350
|
return severity === "warn" ? "warning" : "note";
|
|
263
351
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
return `${
|
|
352
|
+
/** SARIF rule name, the identifier Code Scanning filters by: `god-class` → `GodClass`. */
|
|
353
|
+
function ruleName(kind) {
|
|
354
|
+
return kind.split("-").map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1)}`).join("");
|
|
267
355
|
}
|
|
268
356
|
function locationsFor(root, finding) {
|
|
269
357
|
const primaryUri = artifactUri(root, finding.file);
|
package/dist/src.js
CHANGED
|
Binary file
|
|
@@ -83,7 +83,121 @@ npx stopslop . --format json > slop-report.json
|
|
|
83
83
|
The JSON schema version is included in the document. Findings carry stable
|
|
84
84
|
`kind`, `file`, `symbol`, `line`, `message`, and optional structured `data`
|
|
85
85
|
fields. God-unit data includes responsibility groups; duplicate-code data
|
|
86
|
-
includes clone locations.
|
|
86
|
+
includes clone locations. `rules` holds, for each reported kind, its `summary`,
|
|
87
|
+
`why`, `fix`, the `config` setting that tunes it, and a `helpUri` into
|
|
88
|
+
[rules.md](rules.md), so the agent does not have to guess what a rule wants.
|
|
89
|
+
|
|
90
|
+
## Run the check inside the agent session
|
|
91
|
+
|
|
92
|
+
The repository is a plugin for Claude Code, Codex, and Cursor and an extension
|
|
93
|
+
for Gemini CLI; the [README](../README.md#catch-slop-while-the-agent-is-still-working)
|
|
94
|
+
lists the install command for each. All of them get the `stopslop` skill. The
|
|
95
|
+
stop hook, `hooks/stop-gate.mjs`, runs when the agent is about to end a turn. It
|
|
96
|
+
asks Git whether any JS/TS source or `package.json` changed, and if something
|
|
97
|
+
did, it runs
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx stopslop . --baseline .stopslop-baseline.json --format json # when the file exists
|
|
101
|
+
npx stopslop . --base HEAD --format json # otherwise
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
and hands the new findings back to the agent with each kind's fix, which the
|
|
105
|
+
agent receives as its next instruction. It does not block on analysis errors,
|
|
106
|
+
and it nudges once per turn, so a finding you decide to keep cannot loop the
|
|
107
|
+
session. It runs the project's own `node_modules/stopslop` when installed,
|
|
108
|
+
otherwise `npx stopslop@1`.
|
|
109
|
+
|
|
110
|
+
| Agent | Hook event | Hook answer |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| Claude Code, Codex | `Stop` | `decision: "block"` with the findings as `reason` |
|
|
113
|
+
| Gemini CLI | `AfterAgent` | the same |
|
|
114
|
+
| Cursor | `stop` (`--agent cursor`) | `followup_message` |
|
|
115
|
+
|
|
116
|
+
Codex runs plugin hooks only after you trust them; it asks at the start of the
|
|
117
|
+
first session after install.
|
|
118
|
+
|
|
119
|
+
## Commit the hook to the project
|
|
120
|
+
|
|
121
|
+
`hooks/stop-gate.mjs` ships in the npm package. A project that pins `stopslop`
|
|
122
|
+
in `devDependencies` can commit the hook to its agent settings, so every
|
|
123
|
+
contributor gets it without installing a plugin. This is also the way to the
|
|
124
|
+
hook for Gemini CLI: an extension reads hooks only from `hooks/hooks.json`,
|
|
125
|
+
which in this repository holds the Claude Code and Codex hook.
|
|
126
|
+
|
|
127
|
+
Gemini CLI, `.gemini/settings.json`:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"hooks": {
|
|
132
|
+
"AfterAgent": [
|
|
133
|
+
{
|
|
134
|
+
"hooks": [
|
|
135
|
+
{
|
|
136
|
+
"type": "command",
|
|
137
|
+
"command": "node \"$GEMINI_PROJECT_DIR/node_modules/stopslop/hooks/stop-gate.mjs\"",
|
|
138
|
+
"timeout": 300000
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Claude Code, `.claude/settings.json`:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"hooks": {
|
|
152
|
+
"Stop": [
|
|
153
|
+
{
|
|
154
|
+
"hooks": [
|
|
155
|
+
{
|
|
156
|
+
"type": "command",
|
|
157
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/node_modules/stopslop/hooks/stop-gate.mjs\"",
|
|
158
|
+
"timeout": 300
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Codex, `.codex/hooks.json`; Codex runs the hook in the session's working
|
|
168
|
+
directory:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"hooks": {
|
|
173
|
+
"Stop": [
|
|
174
|
+
{
|
|
175
|
+
"hooks": [
|
|
176
|
+
{
|
|
177
|
+
"type": "command",
|
|
178
|
+
"command": "node node_modules/stopslop/hooks/stop-gate.mjs",
|
|
179
|
+
"timeout": 300
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Cursor, `.cursor/hooks.json`; project hooks run from the project root:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"version": 1,
|
|
193
|
+
"hooks": {
|
|
194
|
+
"stop": [{ "command": "node node_modules/stopslop/hooks/stop-gate.mjs --agent cursor" }]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Agents without a plugin system or hooks get the skill alone with
|
|
200
|
+
`npx skills add BaryshevRS/stopslop`.
|
|
87
201
|
|
|
88
202
|
## Generate a binary Shields badge
|
|
89
203
|
|
package/docs/configuration.md
CHANGED
|
@@ -62,7 +62,8 @@ the corpus it sits between count p95 ≈ 13 and p99 ≈ 38). Lower = stricter.
|
|
|
62
62
|
### `godClass` / `godModule` — object | false
|
|
63
63
|
|
|
64
64
|
A class (or a module's top-level definitions) is flagged on **either** of two
|
|
65
|
-
axes
|
|
65
|
+
axes, described below; [thresholds.md](thresholds.md) has the calibration behind
|
|
66
|
+
the defaults.
|
|
66
67
|
|
|
67
68
|
| field | meaning | default (class / module) |
|
|
68
69
|
|---|---|---|
|
package/docs/rules.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
<!-- Generated from src/report/rules.ts by `pnpm rules:generate`. Do not edit by hand. -->
|
|
2
|
+
|
|
3
|
+
# Rules
|
|
4
|
+
|
|
5
|
+
Every StopSlop finding has a `kind`. This page says, for each one, why it is a
|
|
6
|
+
finding and what resolves it. The same text is the rule help in GitHub Code
|
|
7
|
+
Scanning (`--format sarif`) and the `rules` block of `--format json`, so a
|
|
8
|
+
reviewer and a coding agent read the same instructions.
|
|
9
|
+
|
|
10
|
+
Every setting named below lives in `stopslop.json`; see the
|
|
11
|
+
[configuration reference](configuration.md). Setting a check to `false`
|
|
12
|
+
disables it.
|
|
13
|
+
|
|
14
|
+
## cognitive-complexity
|
|
15
|
+
|
|
16
|
+
Function is harder to follow than the threshold allows.
|
|
17
|
+
|
|
18
|
+
**Why it matters.** Cognitive complexity counts what a reader has to hold in mind to follow a function: branches, loops, nesting, and boolean chains (SonarSource specification). Above the threshold a function is hard to review and easy to break, and it keeps growing the same way: each new case lands beside the existing ones instead of in a place of its own.
|
|
19
|
+
|
|
20
|
+
**How to fix.** Return early instead of nesting, and move each independent decision into a function whose name says what it decides. Replace a long if/else or switch over one value with a lookup table. Cutting the body into arbitrary part1/part2 helpers moves the complexity without removing it.
|
|
21
|
+
|
|
22
|
+
**Tune or disable:** `cognitiveComplexity` in `stopslop.json`.
|
|
23
|
+
|
|
24
|
+
## god-module
|
|
25
|
+
|
|
26
|
+
Module mixes unrelated responsibilities or has outgrown one file.
|
|
27
|
+
|
|
28
|
+
**Why it matters.** Its top-level definitions fall into groups that do not use each other, or the file is too large and too complex to be one unit. Every change touches a file that does several jobs, and new code keeps landing in it because it is the file already open.
|
|
29
|
+
|
|
30
|
+
**How to fix.** Move each responsibility group to its own module; `--details` and the JSON report list the groups found in the reference graph, the ready-made split boundaries. Members listed as holding cohesion together are shared helpers: move them where every group can import them. A size-axis finding has no clean boundaries, so split it by responsibility by hand.
|
|
31
|
+
|
|
32
|
+
**Tune or disable:** `godModule` in `stopslop.json`.
|
|
33
|
+
|
|
34
|
+
## god-class
|
|
35
|
+
|
|
36
|
+
Class mixes unrelated responsibilities or has outgrown one unit.
|
|
37
|
+
|
|
38
|
+
**Why it matters.** Its methods fall into groups that do not share state or call each other, or the class is too large and too complex to be one unit. Every change risks the unrelated behaviour next to it, and the class cannot be tested one responsibility at a time.
|
|
39
|
+
|
|
40
|
+
**How to fix.** Extract each responsibility group into its own class or set of functions, and let the original delegate to them or disappear; `--details` and the JSON report list the groups. A size-axis finding has no clean boundaries, so split it by responsibility by hand.
|
|
41
|
+
|
|
42
|
+
**Tune or disable:** `godClass` in `stopslop.json`.
|
|
43
|
+
|
|
44
|
+
## multiple-exported-classes
|
|
45
|
+
|
|
46
|
+
File exports more than one class.
|
|
47
|
+
|
|
48
|
+
**Why it matters.** Several exported classes in one file usually mean code was appended where it was convenient instead of placed where it belongs. Readers cannot find a class by its file name, and the file drifts toward a god module.
|
|
49
|
+
|
|
50
|
+
**How to fix.** Give each exported class its own file named after it. A class that only serves another can stay in the same file without being exported.
|
|
51
|
+
|
|
52
|
+
**Tune or disable:** `multipleExportedClasses` in `stopslop.json`.
|
|
53
|
+
|
|
54
|
+
## duplicate-code
|
|
55
|
+
|
|
56
|
+
Block of code is copied in more than one place.
|
|
57
|
+
|
|
58
|
+
**Why it matters.** Copies drift. A bug fixed in one stays in the others, and the copies start to disagree about the same computation. It is usually cheaper to write a block again than to find the helper that already does it, which is how copies accumulate.
|
|
59
|
+
|
|
60
|
+
**How to fix.** Extract the block into one function and call it from every occurrence; the finding lists all of them. Where the copies differ on purpose, make the difference a parameter. Renaming variables does not hide a copy when `duplicates.ignoreIdentifiers` is on.
|
|
61
|
+
|
|
62
|
+
**Tune or disable:** `duplicates` in `stopslop.json`.
|
|
63
|
+
|
|
64
|
+
## unused-export
|
|
65
|
+
|
|
66
|
+
Export is not imported anywhere.
|
|
67
|
+
|
|
68
|
+
**Why it matters.** Code nothing uses still has to be read, compiled, kept type-correct, and migrated. Code is added readily and deleted reluctantly, so unused exports only accumulate.
|
|
69
|
+
|
|
70
|
+
**How to fix.** Delete it, or drop the `export` keyword when the file uses it internally. If something Knip does not know about uses it, such as a framework convention or a public package entry, declare that entry under `knip` in stopslop.json instead.
|
|
71
|
+
|
|
72
|
+
**Tune or disable:** `deadCode.exports` in `stopslop.json`.
|
|
73
|
+
|
|
74
|
+
## unused-dependency
|
|
75
|
+
|
|
76
|
+
Dependency in package.json is not imported anywhere.
|
|
77
|
+
|
|
78
|
+
**Why it matters.** An unused package still installs, still gets audited and updated, and still widens the supply chain the project trusts.
|
|
79
|
+
|
|
80
|
+
**How to fix.** Remove it from package.json. If it is used in a way Knip cannot see, such as a binary run from a script or a plugin loaded by name, list it in `ignoreDependencies` under `knip` in stopslop.json.
|
|
81
|
+
|
|
82
|
+
**Tune or disable:** `deadCode.dependencies` in `stopslop.json`.
|
|
83
|
+
|
|
84
|
+
## unused-file
|
|
85
|
+
|
|
86
|
+
File is not reachable from any entry point.
|
|
87
|
+
|
|
88
|
+
**Why it matters.** Often a superseded version of a module, left next to the one that replaced it. Reported as info and not scored: the result is only as good as the entry points Knip knows.
|
|
89
|
+
|
|
90
|
+
**How to fix.** Resolve the config hints in the report first; a missing entry point makes live files look unreachable. Then delete the file.
|
|
91
|
+
|
|
92
|
+
**Tune or disable:** `deadCode.files` in `stopslop.json`.
|
|
93
|
+
|
|
94
|
+
## orphan-feature
|
|
95
|
+
|
|
96
|
+
Function or class is used only by its own test.
|
|
97
|
+
|
|
98
|
+
**Why it matters.** No production code calls it; one test is the only thing that imports it. This can be a feature written together with its test and never wired in, and the green test makes it look finished.
|
|
99
|
+
|
|
100
|
+
**How to fix.** Check whether the feature is meant to exist. If so, wire it into the production code that needs it. Otherwise, remove the unused symbol and its test together: deleting only the source breaks the test, and deleting only the test leaves dead code.
|
|
101
|
+
|
|
102
|
+
**Tune or disable:** `deadCode.orphans` in `stopslop.json`.
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Coding-agent hook, run when the agent is about to end its turn. Check whether
|
|
3
|
+
// the uncommitted changes added slop, and if they did, hand the findings back
|
|
4
|
+
// while the code is still in front of the agent rather than in a review later.
|
|
5
|
+
//
|
|
6
|
+
// It reports only what the changes added — findings absent from
|
|
7
|
+
// .stopslop-baseline.json when the project gates on one, otherwise absent from
|
|
8
|
+
// HEAD — so legacy never blocks a turn. Turns that changed no JS/TS source cost
|
|
9
|
+
// one `git status`. Analysis errors never block: the gate is CI's job, this
|
|
10
|
+
// hook is feedback.
|
|
11
|
+
//
|
|
12
|
+
// node stop-gate.mjs Claude Code (Stop), Codex (Stop), Gemini CLI (AfterAgent)
|
|
13
|
+
// node stop-gate.mjs --agent cursor Cursor (stop)
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from 'node:child_process';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
const BASELINE = '.stopslop-baseline.json';
|
|
20
|
+
const RULES_DOC = 'https://github.com/BaryshevRS/stopslop/blob/main/docs/rules.md';
|
|
21
|
+
const SOURCE = /\.[cm]?[jt]sx?$|(?:^|\/)package\.json$/;
|
|
22
|
+
/** Findings listed in full; the rest are counted. Keeps the feedback short. */
|
|
23
|
+
const MAX_LISTED = 20;
|
|
24
|
+
|
|
25
|
+
/** How each agent hands the hook its turn and takes the feedback back. */
|
|
26
|
+
const AGENTS = {
|
|
27
|
+
// Claude Code, Codex, and Gemini CLI share one protocol: the project in `cwd`,
|
|
28
|
+
// a flag set once a hook already sent the agent back, and a block decision
|
|
29
|
+
// whose reason the agent receives as its next instruction. Codex rejects any
|
|
30
|
+
// other output field, so this is the whole answer.
|
|
31
|
+
default: {
|
|
32
|
+
root: (input) => input.cwd || process.cwd(),
|
|
33
|
+
resumed: (input) => input.stop_hook_active === true,
|
|
34
|
+
answer: (feedback) => ({ decision: 'block', reason: feedback }),
|
|
35
|
+
},
|
|
36
|
+
// Cursor runs plugin hooks from the plugin directory and names the project in
|
|
37
|
+
// the environment. Only a turn that completed is checked: an aborted one was
|
|
38
|
+
// the user's call, a failed one has nothing finished to review.
|
|
39
|
+
cursor: {
|
|
40
|
+
root: (input) => process.env.CURSOR_PROJECT_DIR ?? input.workspace_roots?.[0],
|
|
41
|
+
resumed: (input) => input.status !== 'completed' || input.loop_count > 0,
|
|
42
|
+
answer: (feedback) => ({ followup_message: feedback }),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The feedback for the agent, or undefined to let it end the turn. */
|
|
47
|
+
function gate(root) {
|
|
48
|
+
if (!changesSource(root)) return undefined;
|
|
49
|
+
|
|
50
|
+
const mode = existsSync(join(root, BASELINE))
|
|
51
|
+
? { args: ['--baseline', BASELINE], what: `not in ${BASELINE}` }
|
|
52
|
+
: { args: ['--base', 'HEAD'], what: 'added by the uncommitted changes' };
|
|
53
|
+
const { status, stdout } = runStopslop(root, ['.', ...mode.args, '--format', 'json']);
|
|
54
|
+
if (status !== 1) return undefined;
|
|
55
|
+
|
|
56
|
+
let report;
|
|
57
|
+
try {
|
|
58
|
+
report = JSON.parse(stdout);
|
|
59
|
+
} catch {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(report.findings) || report.findings.length === 0) return undefined;
|
|
63
|
+
return feedback(report, mode);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** True when Git shows a changed or new JS/TS source file or package.json. */
|
|
67
|
+
function changesSource(root) {
|
|
68
|
+
const status = spawnSync('git', ['status', '--porcelain', '-z', '--untracked-files=all'], {
|
|
69
|
+
cwd: root,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
});
|
|
72
|
+
if (status.status !== 0) return false; // not a Git work tree: nothing to compare with
|
|
73
|
+
// Each entry is "XY path"; a rename adds its old path as the next entry. Only
|
|
74
|
+
// the extension matters, so the status prefix needs no parsing.
|
|
75
|
+
return status.stdout.split('\0').some((entry) => SOURCE.test(entry));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function runStopslop(root, args) {
|
|
79
|
+
// The project's own pinned version first — the one its CI runs.
|
|
80
|
+
const local = join(root, 'node_modules', 'stopslop', 'dist', 'cli.js');
|
|
81
|
+
const [command, prefix] = existsSync(local)
|
|
82
|
+
? [process.execPath, [local]]
|
|
83
|
+
: ['npx', ['--yes', 'stopslop@1']];
|
|
84
|
+
const result = spawnSync(command, [...prefix, ...args], {
|
|
85
|
+
cwd: root,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
88
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
89
|
+
shell: command === 'npx' && process.platform === 'win32',
|
|
90
|
+
timeout: 280_000,
|
|
91
|
+
});
|
|
92
|
+
return { status: result.status, stdout: result.stdout ?? '' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function feedback(report, mode) {
|
|
96
|
+
const { findings } = report;
|
|
97
|
+
const rules = report.rules ?? {};
|
|
98
|
+
const command = `npx stopslop . ${mode.args.join(' ')}`;
|
|
99
|
+
const lines = [`StopSlop: ${plural(findings.length, 'finding')} ${mode.what} (\`${command}\`).`];
|
|
100
|
+
|
|
101
|
+
const byKind = new Map();
|
|
102
|
+
for (const finding of findings.slice(0, MAX_LISTED)) {
|
|
103
|
+
if (!byKind.has(finding.kind)) byKind.set(finding.kind, []);
|
|
104
|
+
byKind.get(finding.kind).push(finding);
|
|
105
|
+
}
|
|
106
|
+
for (const [kind, group] of byKind) {
|
|
107
|
+
const rule = rules[kind];
|
|
108
|
+
lines.push('', rule ? `${kind} — ${rule.summary}` : kind);
|
|
109
|
+
for (const finding of group) {
|
|
110
|
+
lines.push(` ${finding.file}:${finding.line} ${finding.message}${alsoAt(finding)}`);
|
|
111
|
+
}
|
|
112
|
+
if (rule) lines.push(` How to fix: ${rule.fix}`);
|
|
113
|
+
}
|
|
114
|
+
if (findings.length > MAX_LISTED) {
|
|
115
|
+
lines.push('', `${findings.length - MAX_LISTED} more not listed; \`${command} --json\` has all.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push(
|
|
119
|
+
'',
|
|
120
|
+
'Fix these before ending the turn. If one should stay, tell the user which and why: ' +
|
|
121
|
+
`accepting it into the reviewed ${BASELINE} is their decision, and raised thresholds ` +
|
|
122
|
+
'or ignore globs are not a fix.',
|
|
123
|
+
`Rule reference: ${RULES_DOC}`,
|
|
124
|
+
);
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Other places a cloned block occurs: the finding's own line is the first. */
|
|
129
|
+
function alsoAt(finding) {
|
|
130
|
+
const occurrences = finding.data?.occurrences;
|
|
131
|
+
if (!Array.isArray(occurrences) || occurrences.length < 2) return '';
|
|
132
|
+
const rest = occurrences.slice(1);
|
|
133
|
+
const shown = rest.slice(0, 2).map((o) => `${o.file}:${o.startLine}-${o.endLine}`);
|
|
134
|
+
const more = rest.length > shown.length ? ` +${rest.length - shown.length} more` : '';
|
|
135
|
+
return ` (also ${shown.join(', ')}${more})`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function plural(n, word) {
|
|
139
|
+
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const flag = process.argv.indexOf('--agent');
|
|
143
|
+
const agent = AGENTS[flag === -1 ? 'default' : process.argv[flag + 1]] ?? AGENTS.default;
|
|
144
|
+
let input = {};
|
|
145
|
+
try {
|
|
146
|
+
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
147
|
+
} catch {
|
|
148
|
+
// No readable input: behave as a turn with nothing to check.
|
|
149
|
+
}
|
|
150
|
+
// Already sent back once this turn: one nudge, so a finding the user decides to
|
|
151
|
+
// keep cannot loop the session.
|
|
152
|
+
if (!agent.resumed(input)) {
|
|
153
|
+
const root = agent.root(input);
|
|
154
|
+
const context = typeof root === 'string' && root !== '' ? gate(root) : undefined;
|
|
155
|
+
if (context !== undefined) process.stdout.write(`${JSON.stringify(agent.answer(context))}\n`);
|
|
156
|
+
}
|
package/llms.txt
CHANGED
|
@@ -6,18 +6,20 @@ Run without installing: `npx stopslop .`. Exit codes are `0` for a clear gate, `
|
|
|
6
6
|
|
|
7
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
8
|
|
|
9
|
+
Coding agents: this repository is a plugin for Claude Code (`/plugin marketplace add BaryshevRS/stopslop`), Codex (`codex plugin marketplace add BaryshevRS/stopslop`), and Cursor, and an extension for Gemini CLI. It adds the `stopslop` skill and a stop hook that hands the agent what its uncommitted changes added before it ends a turn. Other agents get the skill with `npx skills add BaryshevRS/stopslop`.
|
|
10
|
+
|
|
9
11
|
## Start here
|
|
10
12
|
|
|
11
13
|
- [Overview, installation, CLI and badge](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/README.md)
|
|
12
14
|
- [Configuration reference](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/configuration.md)
|
|
13
15
|
- [CI baselines, Git comparison, SARIF and agent reports](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/ci-and-automation.md)
|
|
16
|
+
- [What each finding means and how to fix it](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/rules.md)
|
|
14
17
|
- [Typed Node.js API](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/node-api.md)
|
|
15
18
|
|
|
16
19
|
## How it works
|
|
17
20
|
|
|
18
21
|
- [Score formula and interpretation](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/score.md)
|
|
19
22
|
- [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
23
|
- [Compatibility policy](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/COMPATIBILITY.md)
|
|
22
24
|
|
|
23
25
|
## Package
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stopslop",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist/**/*",
|
|
31
31
|
"docs/**/*.md",
|
|
32
|
+
"hooks/stop-gate.mjs",
|
|
32
33
|
"README.md",
|
|
33
34
|
"COMPATIBILITY.md",
|
|
34
35
|
"LICENSE",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"access": "public"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
|
-
"prebuild": "pnpm schema:check",
|
|
48
|
+
"prebuild": "pnpm schema:check && pnpm rules:check",
|
|
48
49
|
"build": "tsdown",
|
|
49
50
|
"dev": "tsdown --watch",
|
|
50
51
|
"test": "vitest run",
|
|
@@ -59,6 +60,8 @@
|
|
|
59
60
|
"typecheck": "tsc --noEmit",
|
|
60
61
|
"schema:generate": "tsx scripts/generate-config-schema.ts",
|
|
61
62
|
"schema:check": "tsx scripts/generate-config-schema.ts --check",
|
|
63
|
+
"rules:generate": "tsx scripts/generate-rules-doc.ts",
|
|
64
|
+
"rules:check": "tsx scripts/generate-rules-doc.ts --check",
|
|
62
65
|
"commitlint": "commitlint",
|
|
63
66
|
"prepare": "simple-git-hooks 1>&2"
|
|
64
67
|
},
|
|
@@ -66,22 +69,22 @@
|
|
|
66
69
|
"commit-msg": "pnpm exec commitlint --edit \"$1\""
|
|
67
70
|
},
|
|
68
71
|
"dependencies": {
|
|
69
|
-
"clone-alert": "^1.1.
|
|
70
|
-
"knip": "6.
|
|
71
|
-
"oxc-parser": "^0.
|
|
72
|
+
"clone-alert": "^1.1.5",
|
|
73
|
+
"knip": "6.44.1",
|
|
74
|
+
"oxc-parser": "^0.156.0",
|
|
72
75
|
"picocolors": "^1.1.1"
|
|
73
76
|
},
|
|
74
77
|
"devDependencies": {
|
|
75
|
-
"@arethetypeswrong/core": "^0.18.
|
|
76
|
-
"@commitlint/cli": "^21.
|
|
77
|
-
"@commitlint/config-conventional": "^21.
|
|
78
|
-
"@types/node": "^22.
|
|
79
|
-
"publint": "^0.3.
|
|
80
|
-
"simple-git-hooks": "^2.
|
|
78
|
+
"@arethetypeswrong/core": "^0.18.5",
|
|
79
|
+
"@commitlint/cli": "^21.2.4",
|
|
80
|
+
"@commitlint/config-conventional": "^21.2.4",
|
|
81
|
+
"@types/node": "^22.20.5",
|
|
82
|
+
"publint": "^0.3.25",
|
|
83
|
+
"simple-git-hooks": "^2.14.0",
|
|
81
84
|
"tsdown": "^0.21.10",
|
|
82
|
-
"tsx": "^4.
|
|
83
|
-
"typescript": "^5.
|
|
84
|
-
"vitest": "^3.
|
|
85
|
+
"tsx": "^4.23.15",
|
|
86
|
+
"typescript": "^5.9.3",
|
|
87
|
+
"vitest": "^3.2.7"
|
|
85
88
|
},
|
|
86
89
|
"keywords": [
|
|
87
90
|
"ai",
|
package/schema.json
CHANGED
|
@@ -842,6 +842,10 @@
|
|
|
842
842
|
"title": "borp plugin configuration (https://knip.dev/reference/plugins/borp)",
|
|
843
843
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
844
844
|
},
|
|
845
|
+
"buf": {
|
|
846
|
+
"title": "buf plugin configuration (https://knip.dev/reference/plugins/buf)",
|
|
847
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
848
|
+
},
|
|
845
849
|
"bumpp": {
|
|
846
850
|
"title": "bumpp plugin configuration (https://knip.dev/reference/plugins/bumpp)",
|
|
847
851
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -922,6 +926,10 @@
|
|
|
922
926
|
"title": "Drizzle plugin configuration (https://knip.dev/reference/plugins/drizzle)",
|
|
923
927
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
924
928
|
},
|
|
929
|
+
"electron-forge": {
|
|
930
|
+
"title": "Electron Forge plugin configuration (https://knip.dev/reference/plugins/electron-forge)",
|
|
931
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
932
|
+
},
|
|
925
933
|
"electron-vite": {
|
|
926
934
|
"title": "electron-vite plugin configuration (https://knip.dev/reference/plugins/electron-vite)",
|
|
927
935
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -950,6 +958,10 @@
|
|
|
950
958
|
"title": "Expo plugin configuration (https://knip.dev/reference/plugins/expo)",
|
|
951
959
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
952
960
|
},
|
|
961
|
+
"expo-router": {
|
|
962
|
+
"title": "Expo Router plugin configuration (https://knip.dev/reference/plugins/expo-router)",
|
|
963
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
964
|
+
},
|
|
953
965
|
"expressive-code": {
|
|
954
966
|
"title": "expressive-code plugin configuration (https://knip.dev/reference/plugins/expressive-code)",
|
|
955
967
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -958,6 +970,14 @@
|
|
|
958
970
|
"title": "fast plugin configuration (https://knip.dev/reference/plugins/fast)",
|
|
959
971
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
960
972
|
},
|
|
973
|
+
"filesystem-routing": {
|
|
974
|
+
"title": "filesystem-routing plugin configuration (https://knip.dev/reference/plugins/filesystem-routing)",
|
|
975
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
976
|
+
},
|
|
977
|
+
"flue": {
|
|
978
|
+
"title": "flue plugin configuration (https://knip.dev/reference/plugins/flue)",
|
|
979
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
980
|
+
},
|
|
961
981
|
"fumadocs": {
|
|
962
982
|
"title": "fumadocs plugin configuration (https://knip.dev/reference/plugins/fumadocs)",
|
|
963
983
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -990,6 +1010,10 @@
|
|
|
990
1010
|
"title": "i18next-parser plugin configuration (https://knip.dev/reference/plugins/i18next-parser)",
|
|
991
1011
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
992
1012
|
},
|
|
1013
|
+
"jasmine": {
|
|
1014
|
+
"title": "jasmine plugin configuration (https://knip.dev/reference/plugins/jasmine)",
|
|
1015
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1016
|
+
},
|
|
993
1017
|
"jest": {
|
|
994
1018
|
"title": "Jest plugin configuration (https://knip.dev/reference/plugins/jest)",
|
|
995
1019
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1002,6 +1026,10 @@
|
|
|
1002
1026
|
"title": "knex plugin configuration (https://knip.dev/reference/plugins/knex)",
|
|
1003
1027
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1004
1028
|
},
|
|
1029
|
+
"kysely-codegen": {
|
|
1030
|
+
"title": "kysely-codegen plugin configuration (https://knip.dev/reference/plugins/kysely-codegen)",
|
|
1031
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1032
|
+
},
|
|
1005
1033
|
"ladle": {
|
|
1006
1034
|
"title": "ladle plugin configuration (https://knip.dev/reference/plugins/ladle)",
|
|
1007
1035
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1014,6 +1042,10 @@
|
|
|
1014
1042
|
"title": "lefthook plugin configuration (https://knip.dev/reference/plugins/lefthook)",
|
|
1015
1043
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1016
1044
|
},
|
|
1045
|
+
"lingui": {
|
|
1046
|
+
"title": "lingui plugin configuration (https://knip.dev/reference/plugins/lingui)",
|
|
1047
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1048
|
+
},
|
|
1017
1049
|
"lint-staged": {
|
|
1018
1050
|
"title": "lint-staged plugin configuration (https://knip.dev/reference/plugins/lint-staged)",
|
|
1019
1051
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1046,6 +1078,14 @@
|
|
|
1046
1078
|
"title": "marko plugin configuration (https://knip.dev/reference/plugins/marko)",
|
|
1047
1079
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1048
1080
|
},
|
|
1081
|
+
"mastra": {
|
|
1082
|
+
"title": "mastra plugin configuration (https://knip.dev/reference/plugins/mastra)",
|
|
1083
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1084
|
+
},
|
|
1085
|
+
"mcp": {
|
|
1086
|
+
"title": "mcp plugin configuration (https://knip.dev/reference/plugins/mcp)",
|
|
1087
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1088
|
+
},
|
|
1049
1089
|
"mdx": {
|
|
1050
1090
|
"title": "mdx plugin configuration (https://knip.dev/reference/plugins/mdx)",
|
|
1051
1091
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1158,6 +1198,10 @@
|
|
|
1158
1198
|
"title": "orval plugin configuration (https://knip.dev/reference/plugins/orval)",
|
|
1159
1199
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1160
1200
|
},
|
|
1201
|
+
"osls": {
|
|
1202
|
+
"title": "osls plugin configuration (https://knip.dev/reference/plugins/osls)",
|
|
1203
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1204
|
+
},
|
|
1161
1205
|
"oxfmt": {
|
|
1162
1206
|
"title": "oxfmt plugin configuration (https://knip.dev/reference/plugins/oxfmt)",
|
|
1163
1207
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1218,6 +1262,10 @@
|
|
|
1218
1262
|
"title": "Prisma plugin configuration (https://knip.dev/reference/plugins/prisma)",
|
|
1219
1263
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1220
1264
|
},
|
|
1265
|
+
"purgecss": {
|
|
1266
|
+
"title": "purgecss plugin configuration (https://knip.dev/reference/plugins/purgecss)",
|
|
1267
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1268
|
+
},
|
|
1221
1269
|
"quasar": {
|
|
1222
1270
|
"title": "quasar plugin configuration (https://knip.dev/reference/plugins/quasar)",
|
|
1223
1271
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1310,6 +1358,10 @@
|
|
|
1310
1358
|
"title": "size-limit plugin configuration (https://knip.dev/reference/plugins/size-limit)",
|
|
1311
1359
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1312
1360
|
},
|
|
1361
|
+
"solidjs-vite-plugin": {
|
|
1362
|
+
"title": "solidjs-vite-plugin plugin configuration (https://knip.dev/reference/plugins/solidjs-vite-plugin)",
|
|
1363
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1364
|
+
},
|
|
1313
1365
|
"sst": {
|
|
1314
1366
|
"title": "sst plugin configuration (https://knip.dev/reference/plugins/sst)",
|
|
1315
1367
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
@@ -1402,6 +1454,10 @@
|
|
|
1402
1454
|
"title": "tsdown plugin configuration (https://knip.dev/reference/plugins/tsdown)",
|
|
1403
1455
|
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1404
1456
|
},
|
|
1457
|
+
"tsrs": {
|
|
1458
|
+
"title": "tsrs plugin configuration (https://knip.dev/reference/plugins/tsrs)",
|
|
1459
|
+
"$ref": "#/definitions/knip/definitions/plugin"
|
|
1460
|
+
},
|
|
1405
1461
|
"tsup": {
|
|
1406
1462
|
"title": "tsup plugin configuration (https://knip.dev/reference/plugins/tsup)",
|
|
1407
1463
|
"$ref": "#/definitions/knip/definitions/plugin"
|