diff --git a/AGENTS.md b/AGENTS.md index 2688fc4..5c8a491 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,7 @@ These bullets are SOLID, DDD, clean architecture, clean code, and YAGNI applied - Add no knob nobody sets. A documented `const` beats a config field with one caller. - Delete before you add, and put the proof that a branch is dead in the commit message. - Ship the smallest correct diff. -- Every flag, subcommand and option carries clap help text, and anything reachable only through the README or a config file gets a pointer in the `--help` footer. A new command is not done until `--help` describes it. +- Every flag, subcommand and option carries clap help text, and anything reachable only through the docs or a config file gets a pointer in the `--help` footer. A new command is not done until `--help` describes it. - Speak the domain vocabulary in code, messages, docs, and commits: Rule, RuleDef, Diagnostic, Tier, Group, Panel, LintContext, NodeIndex, ProseDoc, Baseline, suppression directive, custom rule, Lang. - Coining a synonym for one of those names is a defect. SLOP034 flags the same habit in prose. @@ -64,7 +64,7 @@ These bullets are SOLID, DDD, clean architecture, clean code, and YAGNI applied - Evidence stays out of the source. Corpus rates, measurements, issue numbers, and rejected alternatives go in the commit message, the PR body, `bench/candidates.toml`, or the bench reports. - Delete a comment you come across that restates the code or carries evidence that belongs elsewhere. - Mark a deliberate shortcut with a comment naming its ceiling and upgrade path. -- A rule message may name an AI tell. It never claims a whole file is AI-written, and it never needs a model or a private key to say so. The README non-goals stay true. +- A rule message may name an AI tell. It never claims a whole file is AI-written, and it never needs a model or a private key to say so. The README and `docs/why.md` non-goals stay true. ## Testing @@ -105,5 +105,5 @@ These bullets are SOLID, DDD, clean architecture, clean code, and YAGNI applied - Write conventional commit subjects (`feat:`, `fix:`, `perf:`, `refactor:`, `docs:`, `test:`, `ci:`, `chore:`). - Self-review the whole patch and run the three CI commands plus the dogfood run before pushing. - Open a PR and let CI decide. Merge commits, no squash. -- Update the README rule table and `CHANGELOG.md` in the same PR that adds or retires a rule. +- Update the `docs/rules.md` rule table and `CHANGELOG.md` in the same PR that adds or retires a rule. - Touch `Cargo.toml` dependencies only with a PR-body line saying what std or an existing crate could not do. diff --git a/CHANGELOG.md b/CHANGELOG.md index 972d469..74db9a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,14 @@ migration notes live here. 300 lines" or "`__init__.py` must be empty" no longer need a Rust module. Existing entries are unchanged. +### Changed + +- **The README is a short pitch; reference material moved to `docs/`.** Usage, + configuration, the rule table, suppression, prose linting, benchmarks and + non-goals each have a page, indexed from `docs/README.md`. SARIF `helpUri` + now points every rule at `docs/rules.md`, and `bench/score_corpus.py` + renames `--readme` to `--rules-doc` (default `docs/rules.md`). + ### Fixed - **SLOP018 keeps ASCII `--` unflagged, now pinned by a fixture and a test** diff --git a/README.md b/README.md index 47c627e..c095517 100644 --- a/README.md +++ b/README.md @@ -4,47 +4,15 @@ # stopslop -Catch the junk AI leaves in your code. +A deterministic linter, written in Rust, for the tells AI leaves in code and prose: 51 rules, one static binary, no model at scan time. [](https://github.com/mgiovani/stopslop/actions/workflows/ci.yml) [](https://github.com/mgiovani/stopslop/blob/main/LICENSE) [](https://crates.io/crates/stopslop) - +[Docs](https://github.com/mgiovani/stopslop/blob/main/docs/README.md) · [Rules](https://github.com/mgiovani/stopslop/blob/main/docs/rules.md) · [Changelog](https://github.com/mgiovani/stopslop/blob/main/CHANGELOG.md) -Your linter can't see `// ... rest of code unchanged`. ESLint, Ruff, and -Clippy throw away comment and string content before analysis: exactly -where AI coding artifacts live. stopslop reads what they discard: leaked -chat preambles, elision comments that silently deleted code, stray markdown -fences, placeholder credentials, and package imports that don't resolve to -anything you declared. It reads your prose too: Markdown, MDX, plain text, -reST, and HTML get the same deterministic treatment. One static binary, no -LLM at scan time: same input, same output, every run. - -- One fast static binary, zero config to get started. -- Deterministic: no LLM calls, no API calls, no network access at scan time. -- Built for CI: exit codes, SARIF, JSON, and inline suppressions. - -## Why your existing linter misses this - -ESLint, Ruff, and Clippy parse comments and strings as trivia their default -rule sets don't inspect, which is exactly where copy-pasted chat output and -truncated edits land. A few examples: - -| Artifact | ESLint / Ruff / Clippy | stopslop | -|---|---|---| -| `// ... rest of code unchanged` | Not linted (comment content is ignored by default rules) | SLOP001 | -| Leaked chat preamble ("Certainly! Here's...") | Not linted (same reason) | SLOP002 | -| `x as unknown as T` type-escape chain | No stock rule (`as unknown` alone is valid, idiomatic TS) | SLOP007 | -| `YOUR_API_KEY`, `sk-...`-shaped secret | Not linted (string literal content is ignored by default rules) | SLOP009 | - -`stopslop` is a deterministic linter for TypeScript, Python, Go, and Rust. -Every rule is a tree-sitter AST match or a regex over extracted -comments/strings: same input, same output, every time. This is a -**quality gate that also names AI tells**: it flags patterns that are junk -regardless of who (or what) produced them, and patterns that are known -giveaways of machine-generated text or code, so you can rewrite either. It -never scores a file as "written by AI" and never needs a model to say so. + ## Install @@ -52,74 +20,46 @@ never scores a file as "written by AI" and never needs a model to say so. cargo install stopslop ``` -Once a day, an outdated install prints a one-line update notice to stderr -(never in CI, never for `--format json|sarif|markdown`, never when stderr is -not a terminal). Set `STOPSLOP_NO_UPDATE_CHECK=1` (or `NO_UPDATE_NOTIFIER=1`) -to turn it off. - -**Alternatives:** +From the latest main, or from a local checkout: ```bash -# latest main, for a merged-but-unreleased fix cargo install --git https://github.com/mgiovani/stopslop - -# local checkout, for hacking on stopslop itself -git clone https://github.com/mgiovani/stopslop -cd stopslop cargo install --path . ``` ## Usage ```bash -stopslop # lint the current directory -stopslop src/ lib/ # lint specific paths -stopslop --format json . # machine-readable output -stopslop --stats . # throughput and per-rule finding counts (stderr) -stopslop -j 1 . # single worker thread (perf work; 0 = automatic) -stopslop --select SLOP001 # run only the elision rule -stopslop --select rhetoric # run one rule group (see "Rule groups" below) -stopslop --select ALL # every rule (SLOP010 still needs --check-imports) -stopslop --ignore SLOP008 # run everything except stub detection -stopslop --select artifact --extend-select SLOP033 # add a rule on top of a narrower select -stopslop --list-rules # print every rule with its group, tier, default, and active state -stopslop --check-imports . # also run SLOP010 (unresolved import) — opt-in -stopslop --config path.toml # use a specific config file instead of the discovered one -stopslop --no-config # ignore any project or user-level stopslop.toml, CLI flags only -stopslop --write-baseline . # record today's findings (see "Baseline" below) -stopslop --baseline . # report only findings that aren't in the baseline -stopslop --staged # lint what is about to be committed (index content, not disk) -stopslop --changed # staged + unstaged changes against HEAD -stopslop --since origin/main # files changed on this branch, for PR checks +stopslop . # lint the current directory +stopslop --since origin/main # lint only the files changed on this branch ``` -Example output: +``` +tests/fixtures/typescript/slop_attribution.ts:1:1 SLOP004 AI attribution / chat-export artifact in comment +tests/fixtures/typescript/slop_elision.ts:3:3 SLOP001 elision comment may have replaced real code +tests/fixtures/markdown/slop_residue.md:3:1 SLOP011 unedited assistant-response residue in prose + fix: delete the sentence +```  -`--format json` emits a flat array of findings; `--format sarif` emits a -SARIF 2.1.0 document for GitHub code scanning and similar tools; -`--format markdown` emits the findings grouped under one heading per tier, for -pasting into a PR comment or a report. `--stats` prints a -files/lines/wall-time/throughput summary, followed by one row per rule with -findings (count, code, tier, name, distinct files), to stderr in text and -markdown modes; with `--format json` the output becomes -`{"findings": [...], "stats": {...}}`, and with `--format sarif` the same -object lands in `runs[0].properties.stats`. In both, `stats` also carries -`findings` (total) and `rules`, an array of `{code, tier, name, count, files}`. Wall time runs from startup -through the walk, so it lines up with an external `time`. "skipped" counts -files the walk reached but could not lint (unsupported extension or -unreadable); paths dropped by `.gitignore` or `exclude` are never walked and -are not counted. +Tier A findings exit 1 and fail CI. Tier B findings warn. See [usage](https://github.com/mgiovani/stopslop/blob/main/docs/usage.md) for formats (text, JSON, SARIF, Markdown), baselines and exit codes.  -## Rule groups +## Why stopslop + +- **Reads what other linters skip.** ESLint, Ruff, and Clippy treat comment and string content as trivia, which is where `// ... rest of code unchanged` and a leaked "Certainly! Here's..." preamble end up. See [why](https://github.com/mgiovani/stopslop/blob/main/docs/why.md). +- **Deterministic.** Every rule is a tree-sitter AST match or a regex over extracted comment, string or prose text. Same input, same output, with no LLM call, API call, or network access at scan time. +- **CI-ready tiers.** Tier A is mechanical and fails the run; Tier B warns. Tier C stays opt-in until a labelled corpus has scored it. +- **Code, prose, and images.** Source code and documents, plus the metadata of PNG, JPEG and WebP images. +- **English and Brazilian Portuguese.** Phrase panels for both run by default. +- **Measured.** `just corpus` scores every rule against labelled human and AI corpora; see [benchmarks](https://github.com/mgiovani/stopslop/blob/main/docs/benchmarks.md). +- **Fast and quiet to adopt.** One binary and no config to start. `--staged` and `--since` lint only a diff, and a baseline grandfathers old findings. + +## What it catches -`SLOP0NN` numbers are chronological, not thematic, so a numeric prefix can't -express "just the rhetoric rules". Named groups can. They work anywhere a rule -code or prefix does, including `--select`, `--ignore`, and the -`select`/`ignore` keys in `stopslop.toml`: +51 rules in 8 groups. `--list-rules` prints every rule with its group, tier, and state. | Group | What it covers | |-------|----------------| @@ -130,592 +70,54 @@ code or prefix does, including `--select`, `--ignore`, and the | `verbosity` | Words that cost the reader something and return nothing: hedging, filler, padding | | `sourcing` | Claims with no checkable source behind them | | `format` | Typographic and Markdown affectations | +| `provenance` | Embedded image metadata that names how or by what an image was made | -```bash -stopslop --select artifact,structure # the mechanical, high-confidence rules only -stopslop --select SLOP --ignore verbosity # everything except the density rules -``` - -Every rule belongs to exactly one group; a test enforces that the table stays -exhaustive, so a new rule can't quietly escape group selection. `--list-rules` -prints the full mapping. - -`ALL` is a reserved selector, not a group: `--select ALL` (or `select = -["ALL"]` in config) runs every registered rule plus any `[[custom-rule]]` -entries from your config. SLOP010 is the one exception: it stays off until -`--check-imports` is passed. - -## Baseline - -Turning stopslop on for an existing codebase usually surfaces findings nobody -plans to fix today. A baseline grandfathers them so CI only fails on findings -that are **new**: - -```bash -stopslop --write-baseline . # writes .stopslop-baseline.json, exits 0 -stopslop --baseline . # later runs report only what isn't in it -``` - -Use `--baseline=path.json` / `--write-baseline=path.json` for a different file -(the `=` is required, so a bare `--baseline` doesn't swallow your scan path), -or set `baseline = ".stopslop-baseline.json"` in `stopslop.toml`. - -Findings are matched by a fingerprint of `code + path + message` with digits -normalized out, not by line number, so editing above a finding doesn't -resurrect it and a density count drifting by one doesn't either. Paths are -normalized as well, with a leading `./` stripped, so a baseline written from -`git ls-files` (`foo.md`) still matches a `stopslop .` run (`./foo.md`). The file -stores a **count** per fingerprint: if a file has three accepted findings from -one rule, it absorbs exactly three, and a fourth is reported. Fix one and the -budget shrinks with it. The count ratchets down on its own; raising it takes a -deliberate `--write-baseline`. - -Commit the baseline file so CI and local runs agree. - -## Git-aware selection - -`--staged`, `--changed`, and `--since REF` (mutually exclusive) replace the walk -with a git-selected file list, so linting scales with the size of a diff -instead of the whole tree. Positional paths, if given, act as git pathspecs -(`stopslop --staged src/` lints only staged files under `src`). - -- `--staged` reads each file's **staged** content (`git show :./path`), so a - partially staged file is checked as it will be committed, not as it sits on - disk. -- `--changed` includes both staged and unstaged changes against `HEAD`; - untracked files are not included. -- `--since REF` diffs against the merge base of `REF` and `HEAD` (git 2.30+), - so `REF` advancing after the branch forked doesn't pull in its changes. - Built for `stopslop --since origin/main` on a PR. -- An empty selection exits 0. All three compose with `--baseline`, `exclude`, - and `[per-file-ignores]` exactly like a normal scan. - -Pre-commit hook: `stopslop --staged`. - -## Rules - -| Code | Group | Name | Tier | Langs | NL | Description | -|------|-------|------|------|-------|----|--------------| -| SLOP001 | artifact | Elision / "rest unchanged" comment | A, on | TS, TSX, Python, Go, Rust, HTML | en, pt-BR | A comment like `// ... rest unchanged` may mark code an AI dropped while truncating an edit | -| SLOP002 | artifact | Chat preamble leaked into code | A, on | TS, TSX, Python, Go, Rust | en, pt-BR | Chat-assistant preamble ("Certainly! Here's the updated...") pasted straight into a source comment | -| SLOP003 | artifact | Stray markdown code fence in source | A, on | TS, TSX, Python, Go, Rust | en, pt-BR | A bare ` ``` ` fence line left in source, usually from a whole file pasted out of a chat reply | -| SLOP004 | artifact | AI attribution / chat-share artifact | A, on | TS, TSX, Python, Go, Rust | en, pt-BR | "Generated by ChatGPT", a `claude.ai/share` link, or other chat-export junk in a comment | -| SLOP005 | structure | Empty / log-only catch | A, on | TS, TSX, Go, Rust | en, pt-BR | A `catch`/error branch that swallows the error with an empty body or just a log call | -| SLOP006 | structure | Broad / swallowing except | A, on | Python | en, pt-BR | A bare `except:` or an `except Exception`/`BaseException` whose body only passes or logs, swallowing the exception instead of handling it. A narrow named exception (`except KeyError: pass`) is a deliberate recovery and stays clean | -| SLOP007 | structure | Type-escape (`as any`/`as unknown`/`@ts-ignore`) | A, on | TS, TSX | en, pt-BR | `as any`, an `x as unknown as T` chain that fully escapes the type checker, or `@ts-ignore`/`@ts-nocheck` | -| SLOP008 | structure | Stub-only / unimplemented body | A, on | TS, TSX, Python, Go, Rust | en, pt-BR | A function whose entire body is `pass`/`...`/`throw new Error("not implemented")`/`todo!()`/empty. A Python body that documents itself with a docstring before its `pass`/`...` is a deliberate no-op hook and stays clean | -| SLOP009 | structure | Placeholder / sample credential value | A, on | TS, TSX, Python, Go, Rust, HTML | en, pt-BR | A hardcoded `YOUR_API_KEY`, `example.com`, `sk-...`-shaped secret, or other sample value; in HTML attributes also a placeholder-image host (`via.placeholder.com`, `placehold.co`) or an `alt` that is just `image` | -| SLOP010 | structure | Unresolved package import | B, off | TS, TSX, Python, Go, Rust | en, pt-BR | An imported package that isn't declared in the project's manifest or stdlib (opt-in, `--check-imports`) | -| SLOP011 | artifact | Assistant-response residue in prose | A, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A leftover chat-turn phrase (self-ID disclaimer, refusal boilerplate, a line-initial `Certainly!` opener, a trailing `let me know if you have` closer, reasoning-chain scaffolding like `let's think about this`, a mid-sentence `step 1:` that isn't heading a section or list item, or a paragraph-initial acknowledgment loop like `To answer your question, ...`) left unedited in prose | -| SLOP012 | artifact | LLM tool / citation artifact tokens | A, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A leftover search/citation-tool token (`turn0search0`, `:contentReference[oaicite:1]`, a `【12†L3】` marker, `utm_source=chatgpt.com`) left in text | -| SLOP013 | artifact | Unfilled template placeholder text | A, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | An unfilled placeholder (`[Your Name]`, `INSERT_SOURCE_URL_30`, a `date: 2025-XX-XX` stub) left in place of real content | -| SLOP014 | rhetoric | Formulaic cliché phrase | B, on | Markdown, MDX, Text, reST, HTML | en | A stock marketing/narrative cliché (`unlock the power of`, `in today's fast-paced world`, `a testament to`) | -| SLOP015 | verbosity | Hedging & filler-phrase density | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A document-wide density of hedging/filler phrases (`it's worth noting that`, `in conclusion`, `first and foremost`); an adjacent hedge stack like `might potentially` fires on its own, without waiting for the density threshold | -| SLOP016 | verbosity | Overused-vocabulary density | B, on | Markdown, MDX, Text, reST, HTML | en | A document-wide density of overused vocabulary (`delve`, `tapestry`, `robust`, `leverage`) across enough distinct terms to read as filler | -| SLOP017 | rhetoric | Rhetorical parallelism / false-depth scaffolding density | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A document-wide density of three-item rhetorical lists and `not only X but also Y` phrasing. Trailing `, underscoring its...` participles count toward the same threshold. Longer enumerations and lists of proper nouns are not counted | -| SLOP018 | format | Mid-prose em/en dash | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A mid-sentence em dash (`—`) or en dash (`–`) that should be rewritten out of the sentence (numeric ranges like `2020–2024` or `3 – 5`, and an attribution dash opening a block or blockquote like `— Oscar Wilde`, are exempt) | -| SLOP019 | format | Boldface & bold-lead-in list overuse | B, on | Markdown, MDX, HTML | en, pt-BR | Boldface overuse in body prose, or 3+ consecutive `- **Term**: ...` bold-lead-in list items | -| SLOP020 | format | Typographic (smart) quotes in source | B, off | Markdown, MDX, Text, reST, HTML | en, pt-BR | Curly quotes/apostrophes in source where straight ASCII quotes are expected | -| SLOP021 | format | Heading & marker formatting affectations | B, on | Markdown, MDX, HTML | en, pt-BR | Emoji or a decorative technical symbol used as a heading/list marker (counted and reported separately), headings written in Title Case against an otherwise sentence-case document, or headings stacked over two-sentence sections | -| SLOP022 | rhetoric | Formulaic opener / rhetorical setup | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A throat-clearing or faux-insight opener (`Here's the thing`, `What nobody tells you`, `Plot twist:`), or a self-answered `Question? Answer.` pair opening a line | -| SLOP023 | rhetoric | Binary contrast / negative listing | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | The `It's not X. It's Y.` / `The question isn't X, it's Y` shape, or a `Not a X. Not a Y.` fragment run | -| SLOP024 | rhetoric | Importance puffery / fake-strong verb | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | An inflated significance claim (`marks a pivotal moment`, `solidifies its position`), a `serves as a centralized hub`-style linking verb where plain `is` reads better, or a faux-scale range (`from the singularity of the Big Bang to the enigmatic dance of dark matter`) standing in for an actual magnitude | -| SLOP025 | sourcing | Unsourced weasel attribution | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | Anonymous authority (`experts agree`, `studies show`) with nothing citing it anywhere on the line, whether a link or a footnote. Also flags notability by name-dropping three-plus outlets (`cited in TechCrunch, Forbes, and Wired`) with no per-citation context | -| SLOP026 | rhetoric | Dramatic colon reveal | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A short noun phrase, a colon, then a lowercase dramatic reveal (`The best part: it learns`) | -| SLOP027 | verbosity | Empty filler phrase & adverb density | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A document-wide density of empty phrases (`when it comes to`, `at its core`) and filler adverbs (`simply`, `actually`) | -| SLOP028 | verbosity | Weak verb phrase / vague quantifier | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A document-wide density of nominalizations (`made a decision`, `has the ability to`) and vague quantifiers used where a number belongs (`significantly improves`) | -| SLOP029 | rhetoric | Summary-recap ending / fake-profound kicker | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A closing block that restates the piece (`In conclusion`, `Overall`) or lands a mic-drop line (`It's already here.`) | -| SLOP030 | rhetoric | Dramatic fragmentation / robotic rhythm | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | Stacked one-clause fragments (`That's it. That's the whole thing.`), consecutive `And`-initial sentences, or paragraph-wide repeated sentence shapes | -| SLOP031 | rhetoric | Promotional / advertisement language | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | Brochure register in technical prose (`boasts a`, `industry-leading`, `a hidden gem`) at document-wide density | -| SLOP032 | verbosity | Hyphenated-compound overuse | B, on | Markdown, MDX, Text, reST, HTML | en | Stacked hyphenated modifiers used as filler (`end-to-end`, `data-driven`, `battle-tested`) at document-wide density | -| SLOP033 | verbosity | Overlong sentence | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A sentence over 50 words, reported with its actual word count (URLs count as one word) | -| SLOP034 | verbosity | Synonym rotation across a closed concept set | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | One concept named two ways within 200 words of one section's running prose (`check` and `verify`, `config` and `settings`) where technical writing should fix one term. Bullet lists and tables are excluded, so a catalog of differently-named things doesn't count as rotation | -| SLOP035 | rhetoric | Outline-shaped filler section | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A `Challenges and Future Prospects`-style section heading, or `despite these challenges` boilerplate, standing in for specifics | -| SLOP036 | rhetoric | Diff-anchored documentation | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | Docs narrating a change (`was added to replace`, `no longer requires`) instead of describing current behavior; changelogs, migration guides and files under a `whatsnew/`, `changelog/` or `release-notes/` directory are exempt | -| SLOP037 | stdlib | Reinvented stdlib / native platform feature | B, on | TS, TSX, Python, Go, Rust | en, pt-BR | Hand-rolled code where a standard-library or platform primitive exists (`JSON.parse(JSON.stringify(x))`, `for i in range(len(xs))`, `ioutil.ReadFile`) | -| SLOP038 | stdlib | Dependency with a stdlib equivalent | B, on | TS, TSX | en, pt-BR | An import of a package the platform already covers (`moment`, `uuid`, `node-fetch`, `left-pad`) | -| SLOP039 | structure | Pass-through wrapper function | B, on | TS, TSX, Python, Go, Rust | en, pt-BR | A function whose whole body forwards its own parameters, unchanged, to another function. A Python method forwarding `self`/`cls` is a call-site adapter and stays clean | -| SLOP040 | structure | Single-implementation interface / abstract | B, on | TS, TSX, Python | en, pt-BR | An interface or abstract class with exactly one implementor in the same file: abstraction with no second user | -| SLOP041 | verbosity | Mechanical uniformity (templated prose) | B, on | Markdown, MDX, Text, reST, HTML | en, pt-BR | A document of 200+ words where flat sentence-length burstiness trips together with at least one of low type-token vocabulary ratio or repeated word-trigrams | -| SLOP042 | verbosity | Comment that restates the code | B, on | TS, TSX, Python, Go, Rust | en, pt-BR | A plain comment of 2 to 12 words whose content words all already appear in the one statement it sits on, or only name the construct that statement is (`// increment the counter` above `counter += 1`), so it adds nothing the code doesn't say. Doc comments (and the plain comments that serve as docs where a language has no doc syntax: Go file scope and struct fields, Python module and class attributes), pragmas, banners, questions, comments with code symbols or quotes in them, comments naming an identifier the statement lacks, and any comment carrying a *why* (`because`, `otherwise`, `workaround`, a URL, an issue number), a constraint (`not`, `only`, `unless`, `before`) or a warning (`careful`, `subtle`) are exempt. Inflection is ignored (`parsed` matches `parse_header`) but abbreviations are not (`max` is not `maximum`) | -| SLOP043 | verbosity | Comment that runs long | B, off | TS, TSX, Python, Go, Rust | en, pt-BR | A plain comment block (consecutive comment lines count as one) of more than 40 words. A reason fits in a sentence or two; a comment that needs three full lines is narrating the code or carrying a design note that belongs in a doc comment, the README or the commit message. Doc comments, godoc (any Go comment outside a function body), license headers, generated files and commented-out code are exempt | -| SLOP044 | artifact | Boilerplate or empty page title | B, on | HTML | en, pt-BR | A `