stopslop 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/COMPATIBILITY.md CHANGED
@@ -1,18 +1,24 @@
1
1
  # Compatibility
2
2
 
3
- Last updated: 0.2.0 (2026-08-05)
3
+ Last updated: 1.1.0
4
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.
5
+ StopSlop follows Semantic Versioning. Stable surfaces change incompatibly only
6
+ in a major release. Experimental surfaces may change in a minor release; such
7
+ changes are called out in the changelog.
8
8
 
9
9
  | Surface | Tier | Compatibility policy |
10
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. |
11
+ | CLI flags and exit codes | Stable | New flags may be added; existing flags and exit code meanings change only in a major release. |
12
+ | Versioned JSON report | Stable | Fields may be added within a `schemaVersion`; removing or changing one increments it and requires a major release. |
13
+ | SARIF 2.1.0 report | Stable | New rules and optional properties may be added; existing required fields remain compatible. |
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
+ | 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. |
17
+
18
+ Findings and scores are not a compatibility surface. Detector fixes, threshold
19
+ calibration, and Knip or Clone Alert updates can change what a release reports
20
+ for the same code; such changes are called out in the changelog. Pin the
21
+ StopSlop version in CI when a gate must not move without a commit.
16
22
 
17
23
  ## Runtime support
18
24
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # StopSlop
2
2
 
3
- [![AI slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/BaryshevRS/stopslop/main/stopslop-badge.json)](https://github.com/BaryshevRS/stopslop) [![clone-alert: 0 clones](https://img.shields.io/badge/clone--alert-0%20clones-brightgreen)](https://github.com/BaryshevRS/clone-alert) [![CI](https://github.com/BaryshevRS/stopslop/actions/workflows/ci.yml/badge.svg)](https://github.com/BaryshevRS/stopslop/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/stopslop.svg)](https://www.npmjs.com/package/stopslop)
3
+ [![StopSlop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/BaryshevRS/stopslop/main/stopslop-badge.json)](https://github.com/BaryshevRS/stopslop) [![clone-alert: 0 clones](https://img.shields.io/badge/clone--alert-0%20clones-brightgreen)](https://github.com/BaryshevRS/clone-alert) [![CI](https://github.com/BaryshevRS/stopslop/actions/workflows/ci.yml/badge.svg)](https://github.com/BaryshevRS/stopslop/actions/workflows/ci.yml) [![npm version](https://img.shields.io/npm/v/stopslop.svg)](https://www.npmjs.com/package/stopslop)
4
4
  <!-- clone-alert badge: pnpm exec clone-alert --minimum-tokens 100 --files src --format shields --no-fail-on-violation -->
5
5
 
6
6
  **Stop AI-generated code from becoming instant legacy.**
@@ -177,18 +177,54 @@ For GitHub Code Scanning, emit SARIF before propagating the gate failure:
177
177
  npx stopslop . --base origin/main --format sarif > stopslop.sarif
178
178
  ```
179
179
 
180
+ Every rule in the SARIF report carries its help, so the alert says why the
181
+ finding matters and how to fix it, not only what failed.
182
+
180
183
  See [CI and automation](docs/ci-and-automation.md) for a complete workflow that
181
184
  uploads SARIF and still preserves StopSlop's exit code.
182
185
 
183
- ## Send the findings back to the agent
184
-
185
- The JSON report is stable and versioned. Feed it into the next coding-agent turn
186
- instead of paraphrasing terminal output:
186
+ ## Catch slop while the agent is still working
187
+
188
+ A check that first runs on the pull request reports code nobody remembers
189
+ writing. StopSlop can run inside the coding session instead, while the code is
190
+ still in front of the agent.
191
+
192
+ This repository is a plugin for Claude Code, Codex, and Cursor and an extension
193
+ for Gemini CLI. Each agent gets the `stopslop` skill, which covers how to run the
194
+ check, read the report, and fix each kind of finding without gaming it: no
195
+ raised thresholds, no ignore globs, no baseline edits on the agent's own
196
+ initiative. Where the agent supports it, the install also adds a stop hook.
197
+
198
+ | Agent | Install | Stop hook |
199
+ | --- | --- | --- |
200
+ | Claude Code | `/plugin marketplace add BaryshevRS/stopslop`<br>`/plugin install stopslop@stopslop` | included |
201
+ | Codex | `codex plugin marketplace add BaryshevRS/stopslop`<br>`codex plugin add stopslop@stopslop` | included; trust it when Codex asks at session start |
202
+ | Cursor | Customize → import `BaryshevRS/stopslop` from a GitHub repository | included |
203
+ | Gemini CLI | `gemini extensions install https://github.com/BaryshevRS/stopslop` | [in project settings](docs/ci-and-automation.md#commit-the-hook-to-the-project) |
204
+ | Others | `npx skills add BaryshevRS/stopslop` (skill only) | — |
205
+
206
+ Before the agent ends a turn that changed JS/TS source, the hook checks the
207
+ uncommitted changes against `HEAD`, or against `.stopslop-baseline.json` when
208
+ the project has one, and hands any new findings back with how to fix each kind.
209
+ Legacy never blocks a turn, a turn that changed no source costs one
210
+ `git status`, and the agent gets one nudge per turn: accepting a finding stays
211
+ your decision. The hook runs the project's pinned `stopslop` from `node_modules`
212
+ and falls back to `npx stopslop@1`.
213
+
214
+ The hook also ships in the npm package, so a project that pins `stopslop` can
215
+ commit it to its own agent settings and give it to every contributor; see
216
+ [Commit the hook to the project](docs/ci-and-automation.md#commit-the-hook-to-the-project).
217
+
218
+ **Any agent or script.** The JSON report is stable and versioned. Feed it into
219
+ the next turn instead of paraphrasing terminal output:
187
220
 
188
221
  ```bash
189
- npx stopslop . --json > slop-report.json
222
+ npx stopslop . --base HEAD --json > slop-report.json
190
223
  ```
191
224
 
225
+ Each finding carries `kind`, `file`, `line`, and `message`, and `rules` says for
226
+ each reported kind why it matters and what resolves it: the same text as
227
+ [docs/rules.md](docs/rules.md) and the rule help in GitHub Code Scanning.
192
228
  God-unit findings include member groups that serve as concrete refactoring
193
229
  boundaries. Clone findings include every occurrence. Baseline and Git-base runs
194
230
  retain the complete score while returning only new findings.
@@ -252,6 +288,31 @@ See [docs/configuration.md](docs/configuration.md) for every field, default, and
252
288
  Knip workspace example. See [stopslop.example.json](stopslop.example.json) for a
253
289
  complete configuration.
254
290
 
291
+ ### Dead-code configuration
292
+
293
+ Knip settings go under `knip` in `stopslop.json`. A project's own `knip.json`,
294
+ `.knip.json`, `knip.config.*` and `package.json#knip` are never read, so an
295
+ analysis does not change with whatever happens to sit in the directory.
296
+ Everything in [Knip's configuration reference](https://knip.dev/reference/configuration)
297
+ works there unchanged.
298
+
299
+ You do not have to work out what belongs there. Run the analysis and Knip
300
+ reports what its configuration is missing — which workspace it never reached,
301
+ which declared entry point resolved to nothing — under **config hints**, with
302
+ the file count each one accounts for:
303
+
304
+ ```
305
+ config hints (from Knip)
306
+ packages/core · 12 files
307
+ Add entry and/or refine project files in workspaces["packages/core"] (12 unused files)
308
+ → Knip settings live under `knip` in stopslop.json (knip.json is not read)
309
+ reference: https://knip.dev/reference/configuration
310
+ ```
311
+
312
+ Address them, run again, repeat until they are gone. Until then dead code is
313
+ reported but left out of the score, because a run without entry points reports
314
+ live files as unreachable. The same list is in `configHints` of `--format json`.
315
+
255
316
  ## Node.js API
256
317
 
257
318
  StopSlop ships ESM JavaScript and TypeScript declarations for programmatic use:
@@ -268,7 +329,8 @@ console.log(result.slop.score, result.findings);
268
329
  The API also exports the resolved defaults, scoring helpers, baseline helpers,
269
330
  and report types. See the [Node.js API guide](docs/node-api.md) for Git-base
270
331
  analysis, typed configuration, result semantics, and custom analyzers. The API
271
- is experimental before 1.0; see [COMPATIBILITY.md](COMPATIBILITY.md).
332
+ is experimental and may change in a minor release; see
333
+ [COMPATIBILITY.md](COMPATIBILITY.md).
272
334
 
273
335
  ## Design and limitations
274
336
 
@@ -277,7 +339,8 @@ is experimental before 1.0; see [COMPATIBILITY.md](COMPATIBILITY.md).
277
339
  directories are excluded by default.
278
340
  - A high score does not prove poor engineering, and a low score does not prove
279
341
  correctness. StopSlop measures structural overproduction, not total quality.
280
- - Dead-code accuracy depends on Knip knowing the project's entry points.
342
+ - Dead-code accuracy depends on Knip knowing the project's entry points; the
343
+ run says so through config hints when it does not.
281
344
  - A baseline accepts finding identities. If an accepted function becomes more
282
345
  complex without changing identity, the score rises but the gate stays clear.
283
346
 
package/context7.json CHANGED
@@ -4,7 +4,7 @@
4
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
5
  "folders": ["docs"],
6
6
  "excludeFolders": [],
7
- "excludeFiles": ["CHANGELOG.md", "ISSUE_KNIP.md", "RELEASING.md"],
7
+ "excludeFiles": ["CHANGELOG.md", "RELEASING.md"],
8
8
  "rules": [
9
9
  "StopSlop detects structural code patterns; it does not determine whether code was written by a human or AI.",
10
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.",
@@ -16,6 +16,9 @@
16
16
  "StopSlop works without configuration. Put optional overrides in `stopslop.json` and use `$schema: https://unpkg.com/stopslop/schema.json` for editor validation.",
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
- "Clone detection is powered by Clone Alert and dead-code analysis by Knip. Configure Knip only under the `knip` property in stopslop.json."
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.",
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`."
20
23
  ]
21
24
  }
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as loadConfig, d as readBaseline, f as writeBaseline, l as applyBaseline, n as analyzeGitBase, t as analyze, u as fingerprint } from "./src.js";
3
- import { posix } from "node:path";
2
+ import { c as loadConfig, d as fingerprint, f as readBaseline, n as analyzeGitBase, p as writeBaseline, s as configFilePath, t as analyze, u as applyBaseline } from "./src.js";
3
+ import { posix, relative, sep } from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import pc from "picocolors";
6
6
  //#region src/report/terminal.ts
@@ -69,8 +69,29 @@ const LEVEL_COLOR = {
69
69
  high: pc.red,
70
70
  "legacy-grade": pc.red
71
71
  };
72
+ /**
73
+ * What Knip says is missing from the configuration. This block is the whole
74
+ * setup story: a dead-code run is only as good as its entry points, and Knip
75
+ * is the one that knows which of them came up empty. The last line names the
76
+ * file to edit — the one thing Knip cannot know, and the thing an agent
77
+ * otherwise guesses wrong by writing knip.json, which StopSlop never reads.
78
+ */
79
+ function renderConfigHints(result) {
80
+ if (result.configHints.length === 0) return [];
81
+ const lines = ["", " " + pc.bold("config hints") + pc.dim(" (from Knip)")];
82
+ for (const hint of result.configHints) {
83
+ const where = hint.workspace && hint.workspace !== hint.identifier ? ` in ${hint.workspace}` : "";
84
+ const size = hint.size === void 0 ? "" : ` · ${hint.size} files`;
85
+ lines.push(` ${pc.yellow(hint.identifier)}${pc.dim(where + size)}`);
86
+ lines.push(" " + pc.dim(hint.message));
87
+ }
88
+ const file = relative(result.root, configFilePath(result.root, result.config)).split(sep).join("/");
89
+ lines.push(" " + pc.dim(`→ Knip settings live under \`knip\` in ${file} (knip.json is not read)`));
90
+ lines.push(" " + pc.dim(" reference: https://knip.dev/reference/configuration"));
91
+ return lines;
92
+ }
72
93
  function renderFooter(result) {
73
- const lines = [""];
94
+ const lines = [...renderConfigHints(result), ""];
74
95
  for (const note of result.notes) lines.push(" " + pc.dim(note));
75
96
  if (result.config.preliminary) lines.push(" " + pc.yellow("thresholds are preliminary (pre-calibration) — see docs/thresholds.md"));
76
97
  if (result.errors.length > 0) lines.push(" " + pc.dim(`${result.errors.length} file(s) skipped on error`));
@@ -141,6 +162,85 @@ function countByKind(findings) {
141
162
  for (const f of findings) counts[f.kind]++;
142
163
  return counts;
143
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. That is a feature written together with its test and never wired in, and the green test makes it look finished.",
220
+ fix: "Delete the symbol and its test together: deleting only the source breaks the test, and deleting only the test leaves dead code. If the feature is meant to exist, wire it into the production code that needs it.",
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
+ }
144
244
  const KINDS = [
145
245
  "god-module",
146
246
  "god-class",
@@ -188,7 +288,14 @@ function toJson(result) {
188
288
  },
189
289
  summary: summarize(result),
190
290
  findings: result.findings,
291
+ rules: rulesFor(result.findings),
191
292
  notes: result.notes,
293
+ configHints: result.configHints,
294
+ /** Where Knip settings are read from. Nowhere else is consulted. */
295
+ configFile: {
296
+ path: relative(result.root, configFilePath(result.root, result.config)).split(sep).join("/"),
297
+ property: "knip"
298
+ },
192
299
  errors: result.errors
193
300
  }, null, 2);
194
301
  }
@@ -206,9 +313,17 @@ function toSarif(result) {
206
313
  for (const finding of result.findings) {
207
314
  if (seenRules.has(finding.kind)) continue;
208
315
  seenRules.add(finding.kind);
316
+ const rule = ruleHelp(finding.kind);
209
317
  rules.push({
210
318
  id: finding.kind,
211
- shortDescription: { text: describeKind(finding.kind) },
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,
212
327
  defaultConfiguration: { level: sarifLevel(finding.severity) }
213
328
  });
214
329
  }
@@ -234,9 +349,9 @@ function toSarif(result) {
234
349
  function sarifLevel(severity) {
235
350
  return severity === "warn" ? "warning" : "note";
236
351
  }
237
- function describeKind(kind) {
238
- const description = kind.replaceAll("-", " ");
239
- return `${description.charAt(0).toUpperCase()}${description.slice(1)}`;
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("");
240
355
  }
241
356
  function locationsFor(root, finding) {
242
357
  const primaryUri = artifactUri(root, finding.file);
package/dist/index.d.ts CHANGED
@@ -44,12 +44,35 @@ interface Analyzer {
44
44
  interface AnalyzeContext {
45
45
  config: ResolvedConfig;
46
46
  }
47
+ /**
48
+ * What Knip reports as missing from its configuration, computed from the run it
49
+ * just did. Knip owns this judgement — it knows which entry pattern matched
50
+ * nothing and which workspace it never reached, and it counts the files that
51
+ * hang off each. StopSlop adds one thing Knip cannot know: where the
52
+ * configuration lives.
53
+ */
54
+ interface ConfigHint {
55
+ /** Knip's hint type: `workspace-unconfigured`, `package-entry`, `ignore`, … */
56
+ type: string;
57
+ /** What it is about — a workspace key, a path, a dependency name. */
58
+ identifier: string;
59
+ /** Knip's own wording. */
60
+ message: string;
61
+ /** Workspace the hint belongs to — what tells two identical ones apart. */
62
+ workspace?: string;
63
+ /** File the hint is about, relative to the analyzed root. */
64
+ filePath?: string;
65
+ /** Unused files attributable to this hint, where Knip counted them. */
66
+ size?: number;
67
+ }
47
68
  interface ProjectContext {
48
69
  root: string;
49
70
  files: ParsedFile[];
50
71
  config: ResolvedConfig;
51
72
  /** Non-fatal notes from an engine (e.g. "knip skipped: no package.json"). */
52
73
  notes: string[];
74
+ /** Configuration Knip found missing. Empty when it has nothing to say. */
75
+ configHints: ConfigHint[];
53
76
  /**
54
77
  * What the Stage-2 engines actually measured. An engine that was disabled —
55
78
  * or that bailed out at runtime — leaves its entry unset, and the score then
@@ -147,6 +170,12 @@ interface ResolvedConfig {
147
170
  ignore: string[];
148
171
  /** Whether current thresholds are provisional. False for the calibrated defaults. */
149
172
  preliminary: boolean;
173
+ /**
174
+ * The stopslop.json `loadConfig` read — or looked for, when there is none.
175
+ * Unset for an in-memory `resolveConfig()`; the analyzed root's own
176
+ * stopslop.json stands in then.
177
+ */
178
+ configFile?: string;
150
179
  }
151
180
  //#endregion
152
181
  //#region src/score.d.ts
@@ -267,6 +296,8 @@ interface AnalyzeResult {
267
296
  errors: string[];
268
297
  /** Non-fatal notes from the Stage-2 engines (skipped checks, and why). */
269
298
  notes: string[];
299
+ /** What Knip reports as missing from `stopslop.json#knip`, from this run. */
300
+ configHints: ConfigHint[];
270
301
  config: ResolvedConfig;
271
302
  elapsedMs: number;
272
303
  /**
@@ -288,4 +319,4 @@ declare function analyze(root: string, config: ResolvedConfig, analyzers?: Analy
288
319
  /** Gate the current working tree against findings present at a Git revision. */
289
320
  declare function analyzeGitBase(root: string, baseRef: string, config: ResolvedConfig, analyzers?: Analyzer[]): Promise<AnalyzeResult>;
290
321
  //#endregion
291
- export { AnalyzeResult, type Analyzer, type BaselineRecord, DEFAULT_CONFIG, type Finding, type ParsedFile, type ResolvedConfig, type SlopLevel, type SlopScore, type StopSlopConfig, analyze, analyzeGitBase, applyBaseline, computeScore, defaultAnalyzers, fingerprint, levelFor, loadConfig, readBaseline, resolveConfig, writeBaseline };
322
+ export { AnalyzeResult, type Analyzer, type BaselineRecord, type ConfigHint, DEFAULT_CONFIG, type Finding, type ParsedFile, type ResolvedConfig, type SlopLevel, type SlopScore, type StopSlopConfig, analyze, analyzeGitBase, applyBaseline, computeScore, defaultAnalyzers, fingerprint, levelFor, loadConfig, readBaseline, resolveConfig, writeBaseline };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as loadConfig, c as levelFor, d as readBaseline, f as writeBaseline, i as DEFAULT_CONFIG, l as applyBaseline, n as analyzeGitBase, o as resolveConfig, r as defaultAnalyzers, s as computeScore, t as analyze, u as fingerprint } from "./src.js";
1
+ import { a as levelFor, c as loadConfig, d as fingerprint, f as readBaseline, i as computeScore, l as resolveConfig, n as analyzeGitBase, o as DEFAULT_CONFIG, p as writeBaseline, r as defaultAnalyzers, t as analyze, u as applyBaseline } from "./src.js";
2
2
  export { DEFAULT_CONFIG, analyze, analyzeGitBase, applyBaseline, computeScore, defaultAnalyzers, fingerprint, levelFor, loadConfig, readBaseline, resolveConfig, writeBaseline };
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
 
@@ -183,9 +183,14 @@ and never merged. Move any settings StopSlop should use into this property.
183
183
 
184
184
  Workspace keys and overrides follow Knip unchanged: `.` configures the root,
185
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.
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.
189
194
 
190
195
  In a single-package project, `entry` and `project` belong directly under
191
196
  `knip`. In a monorepo they belong under `knip.workspaces`; Knip ignores the
package/docs/node-api.md CHANGED
@@ -26,7 +26,8 @@ console.log({
26
26
 
27
27
  `findings` contains the issues that should fail the current gate. `slop` always
28
28
  describes the complete analyzed repository. Check `errors` before trusting a
29
- result; `notes` explains non-fatal skips by project-level engines.
29
+ result; `notes` explains non-fatal skips by project-level engines, and
30
+ `configHints` lists what Knip reports as missing from `stopslop.json#knip`.
30
31
 
31
32
  ## Load `stopslop.json`
32
33
 
@@ -110,5 +111,5 @@ const result = await analyze(
110
111
  ```
111
112
 
112
113
  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).
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/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. That is a feature written together with its test and never wired in, and the green test makes it look finished.
99
+
100
+ **How to fix.** Delete the symbol and its test together: deleting only the source breaks the test, and deleting only the test leaves dead code. If the feature is meant to exist, wire it into the production code that needs it.
101
+
102
+ **Tune or disable:** `deadCode.orphans` in `stopslop.json`.
package/docs/score.md CHANGED
@@ -46,9 +46,10 @@ Three properties are deliberate:
46
46
  axis, and the other axes still matter. Without the cap, one pathological file
47
47
  would drown out everything else.
48
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
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
52
53
  denominator instead of contributing zero. Not measuring something must never
53
54
  read as a clean bill of health. The terminal report shows only the signals
54
55
  that were actually measured, and says in a note why one is missing.
@@ -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
@@ -4,11 +4,16 @@
4
4
 
5
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
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
+ 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
+
7
11
  ## Start here
8
12
 
9
13
  - [Overview, installation, CLI and badge](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/README.md)
10
14
  - [Configuration reference](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/configuration.md)
11
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)
12
17
  - [Typed Node.js API](https://raw.githubusercontent.com/BaryshevRS/stopslop/main/docs/node-api.md)
13
18
 
14
19
  ## How it works
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stopslop",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
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
  },
@@ -67,7 +70,7 @@
67
70
  },
68
71
  "dependencies": {
69
72
  "clone-alert": "^1.1.3",
70
- "knip": "6.31.0",
73
+ "knip": "6.39.0",
71
74
  "oxc-parser": "^0.139.0",
72
75
  "picocolors": "^1.1.1"
73
76
  },
package/schema.json CHANGED
@@ -343,6 +343,9 @@
343
343
  "ignoreBinaries": {
344
344
  "$ref": "#/definitions/knip/definitions/ignoreBinaries"
345
345
  },
346
+ "ignoreGlobalBinaries": {
347
+ "$ref": "#/definitions/knip/definitions/ignoreGlobalBinaries"
348
+ },
346
349
  "ignoreDependencies": {
347
350
  "$ref": "#/definitions/knip/definitions/ignoreDependencies"
348
351
  },
@@ -513,6 +516,21 @@
513
516
  }
514
517
  }
515
518
  },
519
+ "preprocessor": {
520
+ "title": "Preprocess the results before providing them to the reporter(s)",
521
+ "oneOf": [
522
+ {
523
+ "type": "string"
524
+ },
525
+ {
526
+ "$ref": "#/definitions/knip/definitions/list"
527
+ }
528
+ ]
529
+ },
530
+ "preprocessorOptions": {
531
+ "title": "Extra options to pass to the preprocessor",
532
+ "type": "object"
533
+ },
516
534
  "treatConfigHintsAsErrors": {
517
535
  "title": "Exit with non-zero code (1) if there are any configuration hints",
518
536
  "type": "boolean"
@@ -539,6 +557,12 @@
539
557
  ],
540
558
  "$ref": "#/definitions/knip/definitions/list"
541
559
  },
560
+ "ignoreGlobalBinaries": {
561
+ "title": "Ignore common global binaries",
562
+ "description": "Set to false to report required binaries otherwise excluded by Knip's default list. Explicit ignoreBinaries entries still apply.",
563
+ "type": "boolean",
564
+ "default": true
565
+ },
542
566
  "ignoreDependencies": {
543
567
  "title": "Dependencies to exclude from the report (regex allowed)",
544
568
  "examples": [
@@ -710,6 +734,9 @@
710
734
  "ignoreBinaries": {
711
735
  "$ref": "#/definitions/knip/definitions/ignoreBinaries"
712
736
  },
737
+ "ignoreGlobalBinaries": {
738
+ "$ref": "#/definitions/knip/definitions/ignoreGlobalBinaries"
739
+ },
713
740
  "ignoreDependencies": {
714
741
  "$ref": "#/definitions/knip/definitions/ignoreDependencies"
715
742
  },
@@ -811,6 +838,10 @@
811
838
  "title": "biome plugin configuration (https://knip.dev/reference/plugins/biome)",
812
839
  "$ref": "#/definitions/knip/definitions/plugin"
813
840
  },
841
+ "borp": {
842
+ "title": "borp plugin configuration (https://knip.dev/reference/plugins/borp)",
843
+ "$ref": "#/definitions/knip/definitions/plugin"
844
+ },
814
845
  "bumpp": {
815
846
  "title": "bumpp plugin configuration (https://knip.dev/reference/plugins/bumpp)",
816
847
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1027,6 +1058,10 @@
1027
1058
  "title": "metro plugin configuration (https://knip.dev/reference/plugins/metro)",
1028
1059
  "$ref": "#/definitions/knip/definitions/plugin"
1029
1060
  },
1061
+ "mise": {
1062
+ "title": "mise plugin configuration (https://knip.dev/reference/plugins/mise)",
1063
+ "$ref": "#/definitions/knip/definitions/plugin"
1064
+ },
1030
1065
  "mocha": {
1031
1066
  "title": "Mocha plugin configuration (https://knip.dev/reference/plugins/mocha)",
1032
1067
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1167,6 +1202,10 @@
1167
1202
  "title": "PostCSS plugin configuration (https://knip.dev/reference/plugins/postcss)",
1168
1203
  "$ref": "#/definitions/knip/definitions/plugin"
1169
1204
  },
1205
+ "pre-commit": {
1206
+ "title": "pre-commit plugin configuration (https://knip.dev/reference/plugins/pre-commit)",
1207
+ "$ref": "#/definitions/knip/definitions/plugin"
1208
+ },
1170
1209
  "preconstruct": {
1171
1210
  "title": "preconstruct plugin configuration (https://knip.dev/reference/plugins/preconstruct)",
1172
1211
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1187,6 +1226,10 @@
1187
1226
  "title": "qwik plugin configuration (https://knip.dev/reference/plugins/qwik)",
1188
1227
  "$ref": "#/definitions/knip/definitions/plugin"
1189
1228
  },
1229
+ "railway": {
1230
+ "title": "railway plugin configuration (https://knip.dev/reference/plugins/railway)",
1231
+ "$ref": "#/definitions/knip/definitions/plugin"
1232
+ },
1190
1233
  "react-cosmos": {
1191
1234
  "title": "react-cosmos plugin configuration (https://knip.dev/reference/plugins/react-cosmos)",
1192
1235
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1339,6 +1382,10 @@
1339
1382
  "title": "temporal plugin configuration (https://knip.dev/reference/plugins/temporal)",
1340
1383
  "$ref": "#/definitions/knip/definitions/plugin"
1341
1384
  },
1385
+ "textlint": {
1386
+ "title": "textlint plugin configuration (https://knip.dev/reference/plugins/textlint)",
1387
+ "$ref": "#/definitions/knip/definitions/plugin"
1388
+ },
1342
1389
  "travis": {
1343
1390
  "title": "travis plugin configuration (https://knip.dev/reference/plugins/travis)",
1344
1391
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1347,6 +1394,10 @@
1347
1394
  "title": "ts-node plugin configuration (https://knip.dev/reference/plugins/ts-node)",
1348
1395
  "$ref": "#/definitions/knip/definitions/plugin"
1349
1396
  },
1397
+ "tsd": {
1398
+ "title": "tsd plugin configuration (https://knip.dev/reference/plugins/tsd)",
1399
+ "$ref": "#/definitions/knip/definitions/plugin"
1400
+ },
1350
1401
  "tsdown": {
1351
1402
  "title": "tsdown plugin configuration (https://knip.dev/reference/plugins/tsdown)",
1352
1403
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1359,6 +1410,10 @@
1359
1410
  "title": "tsx plugin configuration (https://knip.dev/reference/plugins/tsx)",
1360
1411
  "$ref": "#/definitions/knip/definitions/plugin"
1361
1412
  },
1413
+ "turbo": {
1414
+ "title": "turbo plugin configuration (https://knip.dev/reference/plugins/turbo)",
1415
+ "$ref": "#/definitions/knip/definitions/plugin"
1416
+ },
1362
1417
  "typedoc": {
1363
1418
  "title": "typedoc plugin configuration (https://knip.dev/reference/plugins/typedoc)",
1364
1419
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1367,6 +1422,10 @@
1367
1422
  "title": "TypeScript plugin configuration (https://knip.dev/reference/plugins/typescript)",
1368
1423
  "$ref": "#/definitions/knip/definitions/plugin"
1369
1424
  },
1425
+ "typescript-content-mapper": {
1426
+ "title": "typescript-content-mapper plugin configuration (https://knip.dev/reference/plugins/typescript-content-mapper)",
1427
+ "$ref": "#/definitions/knip/definitions/plugin"
1428
+ },
1370
1429
  "unbuild": {
1371
1430
  "title": "unbuild plugin configuration (https://knip.dev/reference/plugins/unbuild)",
1372
1431
  "$ref": "#/definitions/knip/definitions/plugin"
@@ -1399,6 +1458,10 @@
1399
1458
  "title": "unplugin-vue-router plugin configuration (https://knip.dev/reference/plugins/unplugin-vue-router)",
1400
1459
  "$ref": "#/definitions/knip/definitions/plugin"
1401
1460
  },
1461
+ "varlock": {
1462
+ "title": "varlock plugin configuration (https://knip.dev/reference/plugins/varlock)",
1463
+ "$ref": "#/definitions/knip/definitions/plugin"
1464
+ },
1402
1465
  "vercel": {
1403
1466
  "title": "vercel plugin configuration (https://knip.dev/reference/plugins/vercel)",
1404
1467
  "$ref": "#/definitions/knip/definitions/plugin"