stopslop 0.2.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # Compatibility
2
+
3
+ Last updated: 1.1.0
4
+
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
+
9
+ | Surface | Tier | Compatibility policy |
10
+ | --- | --- | --- |
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
+
17
+ Findings and scores are not a compatibility surface. Detector fixes, threshold
18
+ calibration, and Knip or Clone Alert updates can change what a release reports
19
+ for the same code; such changes are called out in the changelog. Pin the
20
+ StopSlop version in CI when a gate must not move without a commit.
21
+
22
+ ## Runtime support
23
+
24
+ - Node.js 20.19+, 22.12+, and 24.
25
+ - ESM only.
26
+ - Git-base gating requires the `git` executable and a non-shallow checkout that
27
+ contains the requested base revision.
28
+
29
+ ## Deprecations
30
+
31
+ None.
package/README.md CHANGED
@@ -1,6 +1,7 @@
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)
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
+ <!-- clone-alert badge: pnpm exec clone-alert --minimum-tokens 100 --files src --format shields --no-fail-on-violation -->
4
5
 
5
6
  **Stop AI-generated code from becoming instant legacy.**
6
7
 
@@ -8,9 +9,10 @@ Coding agents are good at making code compile. They are also good at making too
8
9
  much of it: tangled functions, god files, copied blocks, dead exports, and entire
9
10
  features that pass their own tests but are never connected to the application.
10
11
 
11
- StopSlop catches that structural overproduction before it becomes permanent.
12
- It complements ESLint, Biome, and Oxlint rather than repeating their style and
13
- type rules.
12
+ StopSlop is a static analyzer for JavaScript and TypeScript codebases under
13
+ heavy AI-agent change. It catches that structural overproduction before it
14
+ becomes permanent. It complements ESLint, Biome, Oxlint, SonarQube, and
15
+ SonarCloud rather than repeating their style and type rules.
14
16
 
15
17
  ```bash
16
18
  npx stopslop .
@@ -19,9 +21,6 @@ npx stopslop .
19
21
  It runs locally against JavaScript and TypeScript. No account. No source-code
20
22
  upload.
21
23
 
22
- > **0.1 preview:** Requires Node.js 20.19+ or 22.12+. Public surfaces and the
23
- > pre-1.0 change policy are documented in [COMPATIBILITY.md](COMPATIBILITY.md).
24
-
25
24
  ## What it catches
26
25
 
27
26
  - **Tangled functions** using cognitive complexity from the SonarSource
@@ -56,6 +55,14 @@ Or pin it in the project so local runs and CI use the same version:
56
55
  ```bash
57
56
  npm install --save-dev stopslop
58
57
  npx stopslop .
58
+
59
+ # pnpm
60
+ pnpm add -D stopslop
61
+ pnpm exec stopslop .
62
+
63
+ # Yarn
64
+ yarn add --dev stopslop
65
+ yarn stopslop .
59
66
  ```
60
67
 
61
68
  The default report gives you the full repository score, the signal densities,
@@ -130,9 +137,12 @@ npx stopslop . \
130
137
  Commit or publish `stopslop-badge.json`, then add this Markdown:
131
138
 
132
139
  ```md
133
- [![AI slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/stopslop-badge.json)](https://github.com/OWNER/REPO)
140
+ [![AI slop](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/stopslop-badge.json)](https://github.com/BaryshevRS/stopslop)
134
141
  ```
135
142
 
143
+ Replace `OWNER/REPO` in the image URL with your repository. Keep the outer link
144
+ unchanged: clicking the badge takes readers to StopSlop.
145
+
136
146
  Without `--baseline` or `--base`, `clear` requires zero findings across the
137
147
  entire scan. With a baseline, it requires zero findings outside the committed
138
148
  baseline. The full score is still present in the terminal and JSON reports.
@@ -167,6 +177,9 @@ For GitHub Code Scanning, emit SARIF before propagating the gate failure:
167
177
  npx stopslop . --base origin/main --format sarif > stopslop.sarif
168
178
  ```
169
179
 
180
+ See [CI and automation](docs/ci-and-automation.md) for a complete workflow that
181
+ uploads SARIF and still preserves StopSlop's exit code.
182
+
170
183
  ## Send the findings back to the agent
171
184
 
172
185
  The JSON report is stable and versioned. Feed it into the next coding-agent turn
@@ -239,6 +252,50 @@ See [docs/configuration.md](docs/configuration.md) for every field, default, and
239
252
  Knip workspace example. See [stopslop.example.json](stopslop.example.json) for a
240
253
  complete configuration.
241
254
 
255
+ ### Dead-code configuration
256
+
257
+ Knip settings go under `knip` in `stopslop.json`. A project's own `knip.json`,
258
+ `.knip.json`, `knip.config.*` and `package.json#knip` are never read, so an
259
+ analysis does not change with whatever happens to sit in the directory.
260
+ Everything in [Knip's configuration reference](https://knip.dev/reference/configuration)
261
+ works there unchanged.
262
+
263
+ You do not have to work out what belongs there. Run the analysis and Knip
264
+ reports what its configuration is missing — which workspace it never reached,
265
+ which declared entry point resolved to nothing — under **config hints**, with
266
+ the file count each one accounts for:
267
+
268
+ ```
269
+ config hints (from Knip)
270
+ packages/core · 12 files
271
+ Add entry and/or refine project files in workspaces["packages/core"] (12 unused files)
272
+ → Knip settings live under `knip` in stopslop.json (knip.json is not read)
273
+ reference: https://knip.dev/reference/configuration
274
+ ```
275
+
276
+ Address them, run again, repeat until they are gone. Until then dead code is
277
+ reported but left out of the score, because a run without entry points reports
278
+ live files as unreachable. The same list is in `configHints` of `--format json`.
279
+
280
+ ## Node.js API
281
+
282
+ StopSlop ships ESM JavaScript and TypeScript declarations for programmatic use:
283
+
284
+ ```ts
285
+ import { analyze, loadConfig } from 'stopslop';
286
+
287
+ const root = process.cwd();
288
+ const result = await analyze(root, loadConfig(root));
289
+
290
+ console.log(result.slop.score, result.findings);
291
+ ```
292
+
293
+ The API also exports the resolved defaults, scoring helpers, baseline helpers,
294
+ and report types. See the [Node.js API guide](docs/node-api.md) for Git-base
295
+ analysis, typed configuration, result semantics, and custom analyzers. The API
296
+ is experimental and may change in a minor release; see
297
+ [COMPATIBILITY.md](COMPATIBILITY.md).
298
+
242
299
  ## Design and limitations
243
300
 
244
301
  - StopSlop supports `.js`, `.jsx`, `.mjs`, `.cjs`, `.ts`, and `.tsx` source.
@@ -246,19 +303,53 @@ complete configuration.
246
303
  directories are excluded by default.
247
304
  - A high score does not prove poor engineering, and a low score does not prove
248
305
  correctness. StopSlop measures structural overproduction, not total quality.
249
- - Dead-code accuracy depends on Knip knowing the project's entry points.
306
+ - Dead-code accuracy depends on Knip knowing the project's entry points; the
307
+ run says so through config hints when it does not.
250
308
  - A baseline accepts finding identities. If an accepted function becomes more
251
309
  complex without changing identity, the score rises but the gate stays clear.
252
310
 
253
311
  Architecture, research references, and the boundary between published methods
254
312
  and StopSlop heuristics are documented in [ARCH.md](ARCH.md).
255
313
 
314
+ ## FAQ
315
+
316
+ ### Is StopSlop an AI-generated code detector?
317
+
318
+ No. It does not guess who wrote a line of code. It finds structural waste that
319
+ coding agents often produce—duplicate blocks, over-complex functions, god
320
+ classes and modules, dead code, and orphan features—and reports the same issue
321
+ when a human wrote it.
322
+
323
+ ### How is StopSlop different from ESLint, SonarQube, SonarCloud, or Knip?
324
+
325
+ ESLint and similar linters focus on local rules. SonarQube and SonarCloud are
326
+ broad quality platforms. Knip specializes in unused code and dependencies.
327
+ StopSlop is a local, zero-account AI code quality gate focused on structural
328
+ overproduction. It combines several signals into one report and keeps accepted
329
+ legacy visible through its score while blocking only new findings through a
330
+ baseline or Git comparison. Knip and
331
+ [Clone Alert](https://www.npmjs.com/package/clone-alert) power two of those
332
+ signals.
333
+
334
+ ### Can I add StopSlop to a legacy repository?
335
+
336
+ Yes. Generate `.stopslop-baseline.json`, review and commit it, then use that same
337
+ file in CI. Existing findings remain in the score and report as accepted debt;
338
+ only unaccepted findings fail the gate.
339
+
340
+ ### Does StopSlop support monorepos and CI code scanning?
341
+
342
+ Yes. Configure Knip workspaces under `stopslop.json#knip.workspaces` for accurate
343
+ dead-code analysis in a monorepo. Use SARIF for GitHub Code Scanning, JSON for
344
+ automation and coding agents, or the Shields format for a README status badge.
345
+
256
346
  ## Development
257
347
 
258
348
  ```bash
259
349
  pnpm install
260
350
  pnpm test
261
351
  pnpm typecheck
352
+ pnpm check:clones
262
353
  pnpm build
263
354
  pnpm test:package
264
355
  pnpm release:check
package/context7.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "$schema": "https://context7.com/schema/context7.json",
3
+ "projectTitle": "StopSlop",
4
+ "description": "Static analysis and CI gate for AI code slop in JavaScript and TypeScript: cognitive complexity, god units, duplicate code, dead code, orphan features, SARIF and baselines.",
5
+ "folders": ["docs"],
6
+ "excludeFolders": [],
7
+ "excludeFiles": ["CHANGELOG.md", "RELEASING.md"],
8
+ "rules": [
9
+ "StopSlop detects structural code patterns; it does not determine whether code was written by a human or AI.",
10
+ "Run StopSlop without installing it with `npx stopslop .`. It requires Node.js ^20.19.0 or >=22.12.0 and analyzes JavaScript, JSX, TypeScript and TSX.",
11
+ "Exit code 0 means no unaccepted findings, 1 means findings were detected, and 2 means analysis or configuration failed.",
12
+ "Adopt an existing repository with `--baseline .stopslop-baseline.json --update-baseline`, commit that file, then use the same `--baseline` option in CI to reject only new findings.",
13
+ "A baseline changes the gate and reported findings but never lowers the full-repository slop score.",
14
+ "Use `--base <git-ref>` to compare against a Git revision without maintaining a baseline file. CI must fetch enough Git history to resolve that ref.",
15
+ "Report formats are text, json, sarif and shields. Use JSON for coding agents, SARIF for code scanning and shields for the binary `AI slop | clear` badge.",
16
+ "StopSlop works without configuration. Put optional overrides in `stopslop.json` and use `$schema: https://unpkg.com/stopslop/schema.json` for editor validation.",
17
+ "Use `--fast` only when intentionally skipping duplicate-code and dead-code analysis; it runs the AST checks for cognitive complexity and god units.",
18
+ "The Node API is ESM and typed: call `resolveConfig()` before `analyze()`, or call `loadConfig()` to read stopslop.json. The slop score always describes the complete analyzed repository.",
19
+ "Clone detection is powered by Clone Alert and dead-code analysis by Knip. Configure Knip only under the `knip` property in stopslop.json.",
20
+ "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
+ ]
22
+ }
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`));
@@ -189,6 +210,12 @@ function toJson(result) {
189
210
  summary: summarize(result),
190
211
  findings: result.findings,
191
212
  notes: result.notes,
213
+ configHints: result.configHints,
214
+ /** Where Knip settings are read from. Nowhere else is consulted. */
215
+ configFile: {
216
+ path: relative(result.root, configFilePath(result.root, result.config)).split(sep).join("/"),
217
+ property: "knip"
218
+ },
192
219
  errors: result.errors
193
220
  }, null, 2);
194
221
  }
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
@@ -0,0 +1,100 @@
1
+ # CI and automation
2
+
3
+ StopSlop has three stable exit codes: `0` means the gate is clear, `1` means
4
+ findings were detected, and `2` means analysis or configuration failed. Pin the
5
+ package version in `devDependencies` so local runs and CI use the same rules.
6
+
7
+ ## Reject new findings with a committed baseline
8
+
9
+ Create the baseline once and review it like source code:
10
+
11
+ ```bash
12
+ npx stopslop . \
13
+ --baseline .stopslop-baseline.json \
14
+ --update-baseline
15
+ git add .stopslop-baseline.json
16
+ ```
17
+
18
+ Then use the same file in CI:
19
+
20
+ ```yaml
21
+ - name: Reject new AI slop
22
+ run: npx stopslop . --baseline .stopslop-baseline.json
23
+ ```
24
+
25
+ The baseline suppresses accepted findings from the gate. It never removes them
26
+ from the full-repository score.
27
+
28
+ ## Compare a pull request with its Git base
29
+
30
+ This mode needs the base revision to exist locally:
31
+
32
+ ```yaml
33
+ - uses: actions/checkout@v7
34
+ with:
35
+ fetch-depth: 0
36
+
37
+ - name: Reject AI slop added by this change
38
+ run: npx stopslop . --base origin/main
39
+ ```
40
+
41
+ `--base` and `--baseline` are alternative gates. Do not pass them together.
42
+
43
+ ## Publish SARIF without hiding the gate failure
44
+
45
+ The scan may exit `1` after writing valid SARIF. Let the upload step run, then
46
+ propagate the original result. The workflow or job needs permission to publish
47
+ code-scanning results:
48
+
49
+ ```yaml
50
+ permissions:
51
+ contents: read
52
+ security-events: write
53
+ ```
54
+
55
+ Then add the scan, upload, and enforcement steps:
56
+
57
+ ```yaml
58
+ - name: Scan with StopSlop
59
+ id: stopslop
60
+ continue-on-error: true
61
+ run: npx stopslop . --base origin/main --format sarif > stopslop.sarif
62
+
63
+ - name: Upload StopSlop SARIF
64
+ if: always() && hashFiles('stopslop.sarif') != ''
65
+ uses: github/codeql-action/upload-sarif@v4
66
+ with:
67
+ sarif_file: stopslop.sarif
68
+
69
+ - name: Enforce StopSlop gate
70
+ if: steps.stopslop.outcome == 'failure'
71
+ run: exit 1
72
+ ```
73
+
74
+ An exit code of `2` does not emit a report, preventing an incomplete analysis
75
+ from being published as a trustworthy result.
76
+
77
+ ## Give structured findings to a coding agent
78
+
79
+ ```bash
80
+ npx stopslop . --format json > slop-report.json
81
+ ```
82
+
83
+ The JSON schema version is included in the document. Findings carry stable
84
+ `kind`, `file`, `symbol`, `line`, `message`, and optional structured `data`
85
+ fields. God-unit data includes responsibility groups; duplicate-code data
86
+ includes clone locations.
87
+
88
+ ## Generate a binary Shields badge
89
+
90
+ Use the same baseline as CI:
91
+
92
+ ```bash
93
+ npx stopslop . \
94
+ --baseline .stopslop-baseline.json \
95
+ --format shields > stopslop-badge.json
96
+ ```
97
+
98
+ The badge says `AI slop | clear` when the gate has no unaccepted findings and
99
+ `AI slop | detected` otherwise. Accepted legacy remains visible in the terminal
100
+ and JSON score.