Documentation

Everything the CLI accepts, everything it emits, and how to wire it into an autonomous coding agent's loop. SlopGuard is alpha (v0.2). The JSON schemas are versioned and stable (shared across languages), but flags may still move before v1.0.

Overview

slopguard-swift measures complex, undertested code in Swift sources. It parses every method with SwiftSyntax, computes cyclomatic and cognitive complexity, drives xcodebuild test to gather line coverage, and combines them into a weighted CRAP (Change Risk Anti-Patterns) score per method and per type:

wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog)
  • cyc: cyclomatic complexity (McCabe), parsed via SwiftSyntax.
  • cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; ignores early-exit shapes (guard, ??, plain return). Recursion increment is currently deferred (known undercount vs Sonar parity).
  • cov: line coverage gathered by slopguard-swift itself via xcodebuild test. Never user-supplied.
  • Default crappy threshold: 30 (on wCRAP), matching the original CRAP paper.

slopguard-ts measures complex, undertested code in TypeScript and JavaScript sources. It parses every declaration with the TypeScript compiler API, computes cyclomatic and cognitive complexity, drives your project's own test runner (vitest or jest) to gather line coverage, and combines them into a weighted CRAP (Change Risk Anti-Patterns) score per method and per type. Same formula, same schema, same UX as slopguard-swift:

wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog)
  • cyc: cyclomatic complexity (McCabe), parsed via the TypeScript compiler API.
  • cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; ignores early-exit shapes (??, plain return); charges a whole switch once.
  • cov: line coverage gathered by slopguard-ts itself by driving vitest/jest. Never user-supplied.
  • Default crappy threshold: 30 (on wCRAP), matching the original CRAP paper.

slopguard-go measures complex, undertested code in Go modules. It parses every function and method with the standard library go/ast, computes cyclomatic and cognitive complexity, drives the module's own go test to gather line coverage, and combines them into a weighted CRAP (Change Risk Anti-Patterns) score per function and per type. Same formula, same schema, same UX as slopguard-swift:

wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog)
  • cyc: cyclomatic complexity (McCabe), parsed via go/ast. Counts if, for, range, each non-default case, and each && / || — comparable to gocyclo.
  • cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; charges a whole switch/select once; ignores early-exit shapes (plain return/break/continue).
  • cov: line coverage gathered by slopguard-go itself by driving go test. Never user-supplied.
  • Default crappy threshold: 30 (on wCRAP), matching the original CRAP paper.

slopguard-kotlin measures complex, undertested code in Kotlin and Android projects. It parses every declaration with the Kotlin compiler's PSI front-end, computes cyclomatic and cognitive complexity, drives your project's own Gradle test run (Kover or JaCoCo) to gather line coverage, and combines them into a weighted CRAP (Change Risk Anti-Patterns) score per method and per type. Same formula, same schema, same UX as slopguard-swift:

wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog)
  • cyc: cyclomatic complexity (McCabe), parsed via the Kotlin compiler PSI. Counts if, for, while, do, each non-else when branch, catch, the elvis ?:, and each && / ||.
  • cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; charges a whole when once; ignores early-exit shapes (plain return/break/continue).
  • cov: line coverage gathered by slopguard-kotlin itself by driving Gradle + Kover/JaCoCo. Never user-supplied.
  • Default crappy threshold: 30 (on wCRAP), matching the original CRAP paper.

slopguard-python measures complex, undertested code in Python projects. It parses every declaration with the standard-library ast module, computes cyclomatic and cognitive complexity, drives your project's own test suite (pytest or unittest, under coverage.py) to gather line coverage, and combines them into a weighted CRAP (Change Risk Anti-Patterns) score per method and per type. Same formula, same schema, same UX as slopguard-swift:

wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog)
  • cyc: cyclomatic complexity (McCabe), parsed via ast. Counts if/elif, for/while, each except, the ternary, each and/or, comprehension clauses, and match cases — comparable to mccabe.
  • cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; charges a whole match once; ignores early-exit shapes (plain return/break/continue).
  • cov: line coverage gathered by slopguard-python itself by driving pytest/unittest under coverage.py. Never user-supplied.
  • Default crappy threshold: 30 (on wCRAP), matching the original CRAP paper.

The mutate subcommand checks the tests themselves. It makes one small change to the source at a time, runs the tests against each change, and reports every change that no test catches. See How mutation testing works.

Install

git clone https://github.com/JeevanThandi/SlopGuard-Swift.git
cd SlopGuard-Swift
swift build -c release
cp .build/release/slopguard-swift /usr/local/bin

Requirements: Xcode 16 / Swift 6.0+ on macOS 13+.

v0.2.x release binaries are SBOM'd and SHA-256-checksummed but not yet code-signed or notarized. Verify the checksum, or build from source. Signing and notarization are planned before v0.3.

git clone https://github.com/JeevanThandi/slopguard-typescript.git
cd slopguard-typescript
npm install && npm run build
npm link        # exposes slopguard-ts on your PATH

Requirements: Node 18.17+.

v0.2.x is alpha. The analyzer is stable and self-tested (CI keeps the suite green on Node 18.17/20/22 at 100% coverage), but the CLI surface and JSON schema may still change before v1.0.

go install github.com/JeevanThandi/slopguard-go/cmd/slopguard-go@latest

…or build from source:

git clone https://github.com/JeevanThandi/slopguard-go.git
cd slopguard-go
go build -o slopguard-go ./cmd/slopguard-go
cp slopguard-go /usr/local/bin

Requirements: Go 1.23+. Zero third-party dependencies — only the Go standard library.

v0.2.x is alpha. The analyzer is stable and self-tested (CI holds coverage ≥95% across the core, coverage, cli, mutation and internal packages), but the CLI surface and JSON schema may still change before v1.0.

git clone https://github.com/JeevanThandi/slopguard-kotlin.git
cd slopguard-kotlin
./gradlew :app:installDist
# launcher: app/build/install/slopguard-kotlin/bin/slopguard-kotlin
export PATH="$PWD/app/build/install/slopguard-kotlin/bin:$PATH"

Requirements: JDK 17+. One runtime dependency: kotlin-compiler-embeddable (the PSI parser).

v0.2.x is alpha. The analyzer is stable and self-tested, but the CLI surface and JSON schema may still change before v1.0.

pip install slopguard-python

…or from source:

git clone https://github.com/JeevanThandi/slopguard-python.git
cd slopguard-python
pip install .

Requirements: Python 3.9+. Zero runtime dependencies (standard library only). Run it from your project's own environment (the one your tests run in) so coverage gathering can import your package and its tests.

v0.2.x is alpha. The analyzer is stable and self-tested (CI holds coverage ≥95%), but the CLI surface and JSON schema may still change before v1.0.

Quickstart

# Zero-config: analyze the current directory (drives xcodebuild test for coverage)
slopguard-swift

# Scan a specific directory and print the top crappy methods
slopguard-swift analyze --path Sources --threshold 30

# iOS app: pick a scheme and destination
slopguard-swift analyze --path . --scheme MyApp --destination 'platform=iOS Simulator,name=iPhone 16'

# CocoaPods-style project: point xcodebuild at the workspace explicitly
slopguard-swift analyze --path . --workspace MyApp.xcworkspace --scheme MyApp

# Full JSON for CI / downstream tooling
slopguard-swift analyze --path Sources --json | jq '.methods | sort_by(-.crap)[:10]'

# Fail CI when any method's CRAP exceeds 50
slopguard-swift analyze --path Sources --fail-over 50

# Complexity only (skip the test build; every method shows 0% coverage)
slopguard-swift analyze --path Sources --no-coverage

# Mutation testing: change the code, run the tests, list the changes no test caught
slopguard-swift mutate --path Sources/MyLib/Store.swift
# Zero-config: analyze the current directory (detects vitest/jest, runs it for coverage)
slopguard-ts

# Scan a specific directory and print the top crappy methods
slopguard-ts analyze --path src --threshold 30

# Skip runner auto-detection: name the framework to drive
slopguard-ts analyze --path src --runner vitest
slopguard-ts analyze --path src --runner jest

# Monorepo: run the tests of a specific package
slopguard-ts analyze --path packages/core/src --project-dir packages/core

# Full JSON for CI / downstream tooling
slopguard-ts analyze --path src --json | jq '.methods | sort_by(-.crap)[:10]'

# Fail CI when any method's CRAP exceeds 50
slopguard-ts analyze --path src --fail-over 50

# Complexity only (skip the test run; every method shows 0% coverage)
slopguard-ts analyze --path src --no-coverage

# Join coverage CI already produced (nyc, c8, mocha, or a prior run)
slopguard-ts analyze --path src --coverage-file coverage/coverage-final.json

# Mutation testing: change the code, run the tests, list the changes no test caught
slopguard-ts mutate --path src/store.ts
# Zero-config: analyze the current module (drives go test for coverage)
slopguard-go

# Scan a specific directory and print the top crappy methods
slopguard-go analyze --path ./internal --threshold 30

# Scope the test run to specific packages (anything they miss reads 0%)
slopguard-go analyze --path ./internal/auth --packages ./internal/auth/...

# Full JSON for CI / downstream tooling
slopguard-go analyze --path . --json | jq '.methods | sort_by(-.crap)[:10]'

# Fail CI when any method's CRAP exceeds 50
slopguard-go analyze --path . --fail-over 50

# Complexity only (skip the test run; every method shows 0% coverage)
slopguard-go analyze --path ./pkg --no-coverage

# Join coverage CI already produced (a go test -coverprofile file)
slopguard-go analyze --path . --coverage-file cover.out

# Mutation testing: change the code, run the tests, list the changes no test caught
slopguard-go mutate --path ./internal/auth
# Zero-config: analyze the current module (drives gradle test + Kover for coverage)
slopguard-kotlin analyze --path src/main/kotlin

# Scan a directory and print the top crappy methods
slopguard-kotlin analyze --path src/main/kotlin --threshold 30

# Android with Kover (the default): scope to a variant report task
slopguard-kotlin analyze --path app/src/main/kotlin \
  --gradle-test-task testDebugUnitTest --gradle-report-task koverXmlReportDebug

# Prefer JaCoCo? Opt in with --coverage-tool jacoco
slopguard-kotlin analyze --path src/main/kotlin --coverage-tool jacoco

# Multi-module: point at the module whose tests should run
slopguard-kotlin analyze --path feature/login/src/main/kotlin --project-dir feature/login

# Full JSON for CI / downstream tooling
slopguard-kotlin analyze --path src/main/kotlin --json | jq '.methods | sort_by(-.crap)[:10]'

# Fail CI when any method's CRAP exceeds 50
slopguard-kotlin analyze --path src/main/kotlin --fail-over 50

# Complexity only (skip the test run; every method shows 0% coverage)
slopguard-kotlin analyze --path src/main/kotlin --no-coverage

# Join a Kover/JaCoCo report CI already produced
slopguard-kotlin analyze --path src/main/kotlin \
  --coverage-file build/reports/kover/report.xml

# Mutation testing: change the code, run the tests, list the changes no test caught
slopguard-kotlin mutate --path src/main/kotlin/com/example/Store.kt
# Zero-config: analyze the current project (auto-finds and runs its tests for coverage)
slopguard-python

# Scan a specific directory and print the top crappy methods
slopguard-python analyze --path ./src --threshold 30

# Name the test runner instead of auto-detecting it
slopguard-python analyze --path ./src --runner pytest

# Full JSON for CI / downstream tooling
slopguard-python analyze --path . --json | jq '.methods | sort_by(-.crap)[:10]'

# Fail CI when any method's CRAP exceeds 50
slopguard-python analyze --path . --fail-over 50

# Complexity only (skip the test run; every method shows 0% coverage)
slopguard-python analyze --path ./src --no-coverage

# Join coverage CI already produced (a coverage json report)
slopguard-python analyze --path . --coverage-file coverage.json

# Mutation testing: change the code, run the tests, list the changes no test caught
slopguard-python mutate --path src/pkg/store.py

# Or run it as a module
python -m slopguard analyze --path ./src

Subcommands

CommandPurpose
analyze Walk a directory of Swift sources, drive xcodebuild test for coverage, emit a wCRAP report (text or JSON). Walk a directory of TS/JS sources, drive the test runner (vitest/jest) for coverage, emit a wCRAP report (text or JSON). Walk a directory of Go sources, drive go test for coverage, emit a wCRAP report (text or JSON). Walk a directory of Kotlin sources, drive gradle test + Kover/JaCoCo for coverage, emit a wCRAP report (text or JSON). Walk a directory of Python sources, drive the test suite (pytest/unittest under coverage.py) for coverage, emit a wCRAP report (text or JSON). This is the default subcommand: a bare slopguard-swiftslopguard-tsslopguard-goslopguard-kotlinslopguard-python runs it against the current directory.
mutate Mutate the Swift sources one small change at a time and run swift test or xcodebuild against each mutant. Report the mutants no test catches (text or JSON). Mutate the TS/JS sources one small change at a time and run the test runner (vitest/jest) against each mutant. Report the mutants no test catches (text or JSON). Mutate the Go sources one small change at a time and run go test against each mutant. Report the mutants no test catches (text or JSON). The source tree is never written: each mutant reaches the compiler through go test -overlay. Mutate the Kotlin sources one small change at a time and run the Gradle tests against each mutant. Report the mutants no test catches (text or JSON). Mutate the Python sources one small change at a time and run the test suite (pytest/unittest) against each mutant. Report the mutants no test catches (text or JSON). See How mutation testing works.
versionPrint version metadata as JSON (name, version, …).

analyze: all flags

FlagDefaultMeaning
-p, --path <path>.Directory of Swift sources, or a single .swift file.
-t, --threshold <n>30wCRAP score above which a method/type is flagged isCrappy.
--scheme <name>autoxcodebuild scheme to test for coverage. Auto-discovered when omitted.
--workspace <path>—Path to an .xcworkspace passed as -workspace to xcodebuild. Use when the project directory holds multiple containers (e.g. CocoaPods) and xcodebuild picks the wrong one.
--destination <str>platform=macOSxcodebuild destination string, e.g. 'platform=iOS Simulator,name=iPhone 16'.
--project-dir <path>cwdDirectory passed as cwd to xcodebuild.
--no-coverageoffSkip the xcodebuild step; report complexity only. Every method shows 0% coverage, so scores are worst-case.
--include <glob…>—Glob(s) of files to include. Repeat the flag or pass space-separated.
--exclude <glob…>—Extra glob(s) to exclude, combined with the built-in defaults (.build, Pods, Carthage, Generated, *Tests, *Spec, …).
--no-default-excludesoffStart with an empty exclude list, e.g. to analyze test code itself.
--jsonoffEmit JSON to stdout (default is pretty text).
--fail-over <n>—Exit with code 2 if any method's wCRAP exceeds this value. The CI gate.
-v, --verboseoffStream xcodebuild output and subprocess chatter to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of TypeScript/JavaScript sources, or a single source file.
-t, --threshold <n>30wCRAP score above which a method/type is flagged isCrappy.
--runner <vitest|jest>autoTest runner to drive for coverage. Auto-detected from the project when omitted.
--project-dir <dir>nearest package.jsonDirectory the test runner executes in. Defaults to the nearest package.json above --path.
--no-coverageoffSkip the test run; report complexity only. Every method shows 0% coverage, so scores are worst-case.
--coverage-file <path>—Pre-built istanbul coverage-final.json to join instead of running tests. Escape hatch for runners slopguard can't drive (nyc, c8, mocha) or CI that already produced coverage.
--include <glob…>—Glob(s) of files to include. Repeat the flag or pass space-separated.
--exclude <glob…>—Extra glob(s) to exclude, combined with the built-in defaults (node_modules, dist, *.test.*, *.spec.*, __tests__, …).
--no-default-excludesoffStart with an empty exclude list, e.g. to analyze test code itself.
--jsonoffEmit JSON to stdout (default is pretty text).
--fail-over <n>—Exit with code 2 if any method's wCRAP exceeds this value. The CI gate.
-v, --verboseoffStream test-runner output and subprocess chatter to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of Go sources, or a single .go file.
-t, --threshold <n>30wCRAP score above which a method/type is flagged isCrappy.
--packages <pattern>./...Go package pattern for the test run and coverage instrumentation (-coverpkg). Anything the selected packages don't exercise reads 0%.
--project-dir <path>nearest go.modModule root the go test run executes in. Walked up from --path when omitted.
--no-coverageoffSkip the go test step; report complexity only. Every method shows 0% coverage, so scores are worst-case.
--coverage-file <path>—A go test -coverprofile file to join instead of running tests. For CI that already produced one.
--include <glob>—Glob of files to include. Repeatable: pass the flag once per glob.
--exclude <glob>—Extra glob to exclude, combined with the built-in defaults (vendor, testdata, *_test.go, generated code, …). Repeatable.
--no-default-excludesoffStart with an empty exclude list, e.g. to analyze test code itself.
--jsonoffEmit JSON to stdout (default is pretty text).
--fail-over <n>—Exit with code 2 if any method's wCRAP exceeds this value. The CI gate.
-v, --verboseoffStream go test output and subprocess chatter to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of Kotlin sources, or a single .kt file.
-t, --threshold <n>30wCRAP score above which a method/type is flagged isCrappy.
--coverage-tool <kover|jacoco>koverCoverage tool to drive. Both emit the same JaCoCo-format XML, so either is joined by one parser.
--gradle-test-task <task>testGradle test task to run, e.g. testDebugUnitTest for an Android variant.
--gradle-report-task <task>koverXmlReportGradle task that emits the coverage XML (jacocoTestReport under --coverage-tool jacoco).
--project-dir <path>nearest Gradle rootDirectory the Gradle run executes in. Walked up from --path when omitted.
--no-coverageoffSkip the Gradle run; report complexity only. Every method shows 0% coverage, so scores are worst-case.
--coverage-file <path>—A JaCoCo/Kover report XML to join instead of running Gradle. For CI that already produced one.
--include <glob…>—Glob(s) of files to include. Repeat the flag or pass space-separated.
--exclude <glob…>—Extra glob(s) to exclude, combined with the built-in defaults (build, .gradle, *Test.kt, src/test, generated code, …).
--no-default-excludesoffStart with an empty exclude list, e.g. to analyze test code itself.
--jsonoffEmit JSON to stdout (default is pretty text).
--fail-over <n>—Exit with code 2 if any method's wCRAP exceeds this value. The CI gate.
-v, --verboseoffStream Gradle output and subprocess chatter to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of Python sources, or a single .py file.
-t, --threshold <n>30wCRAP score above which a method/type is flagged isCrappy.
--runner <pytest|unittest>autoTest runner to drive for coverage. Auto-detected from the project when omitted.
--project-dir <path>nearest project rootDirectory the test run executes in. Walked up from --path (pyproject.toml / setup.py / setup.cfg / tox.ini / .git) when omitted.
--no-coverageoffSkip the test run; report complexity only. Every method shows 0% coverage, so scores are worst-case.
--coverage-file <path>—A coverage json report to join instead of running tests. For CI that already produced one.
--include <glob…>—Glob(s) of files to include. Repeat the flag or pass space-separated.
--exclude <glob…>—Extra glob(s) to exclude, combined with the built-in defaults (.venv, __pycache__, test_*.py, *_test.py, tests/, conftest.py, …).
--no-default-excludesoffStart with an empty exclude list, e.g. to analyze test code itself.
--jsonoffEmit JSON to stdout (default is pretty text).
--fail-over <n>—Exit with code 2 if any method's wCRAP exceeds this value. The CI gate.
-v, --verboseoffStream test-runner output and subprocess chatter to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.

mutate: all flags

FlagDefaultMeaning
-p, --path <path>.Directory of Swift sources, or a single .swift file, to mutate.
--include <glob…>—Only mutate files matching the glob(s), relative to --path. Repeat the flag or pass space-separated.
--exclude <glob…>—Extra glob(s) to skip, combined with the built-in defaults (.build, Pods, Carthage, Generated, *Tests, *Spec, …).
--no-default-excludesoffStart with an empty exclude list.
--operators <ids>allOperator ids to apply, comma-separated or repeated. An unknown id is an invalid_argument error. See Operators.
--runner <swift-test|xcodebuild>autoTest runner to drive. Without the flag, mutate picks xcodebuild when --scheme or --workspace is passed or the project directory holds an .xcodeproj or .xcworkspace. Otherwise it picks swift test when the project directory holds a Package.swift.
--project-dir <path>nearest Package.swift, .xcodeproj or .xcworkspaceDirectory the tests run in. Walked up from --path when omitted.
--scheme <name>autoxcodebuild scheme. Auto-discovered when omitted. Passing it selects the xcodebuild runner.
--workspace <path>—Path to an .xcworkspace passed as -workspace to xcodebuild. Passing it selects the xcodebuild runner.
--destination <str>platform=macOSxcodebuild destination string, e.g. 'platform=iOS Simulator,name=iPhone 16'.
--only-testing <id>—Test identifier(s) such as MyAppTests/LoginTests. xcodebuild gets -only-testing:MyAppTests/LoginTests, and swift test gets --filter MyAppTests.LoginTests.
--no-coverageoffSkip the coverage baseline and run every mutant, including mutants on lines no test executes.
--timeout <seconds>ceil(3 × baseline) + 60Per-mutant timeout for the build and the test run together. The default comes from the run time of the plain baseline.
--dry-runoffList the mutants. Run no tests and change no files.
--jsonoffEmit the JSON mutation report to stdout (default is pretty text).
--fail-under <score>—Exit with code 2 if the mutation score (0–100) is below this value. A null score never fails. Ignored with --dry-run.
-v, --verboseoffStream test-runner output to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of TypeScript/JavaScript sources, or a single source file, to mutate.
--include <glob…>—Only mutate files matching the glob(s), relative to --path. Repeat the flag or pass space-separated.
--exclude <glob…>—Extra glob(s) to skip, combined with the built-in defaults (node_modules, dist, *.test.*, *.spec.*, __tests__, …).
--no-default-excludesoffStart with an empty exclude list.
--operators <ids…>allOperator ids to apply, comma-separated or repeated. An unknown id is an invalid_argument error. See Operators.
--runner <vitest|jest>autoTest runner to drive. Auto-detected from the project when omitted.
--project-dir <dir>nearest package.jsonDirectory the test runner executes in. Defaults to the nearest package.json above --path.
--no-coverageoffSkip the coverage baseline and run every mutant, including mutants on lines no test executes.
--timeout <seconds>ceil(3 × baseline) + 10Per-mutant test timeout. The default comes from the run time of the plain baseline.
--dry-runoffList the mutants. Run no tests and touch no files.
--jsonoffEmit the JSON mutation report to stdout (default is pretty text).
--fail-under <score>—Exit with code 2 if the mutation score (0–100) is below this value. A null score never fails. Ignored with --dry-run.
-v, --verboseoffStream test-runner output to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of Go sources, or a single .go file, to mutate.
--include <glob>—Only mutate files matching the glob, relative to --path. Repeatable.
--exclude <glob>—Extra glob to skip, combined with the built-in defaults (vendor, testdata, *_test.go, generated code, …). Repeatable.
--no-default-excludesoffStart with an empty exclude list.
--operators <ids>allComma-separated operator ids to apply. Repeatable. An unknown id is an invalid_argument error. See Operators.
--packages <pattern>./...Go package pattern for every test run, and for -coverpkg in the coverage baseline.
--project-dir <path>nearest go.modModule root that go test runs in. Walked up from --path when omitted.
--no-coverageoffSkip the coverage baseline and run every mutant, including mutants on lines no test executes.
--timeout <seconds>ceil(3 × baseline) + 30Per-mutant test timeout. The default comes from the run time of the plain baseline.
--dry-runoffList the mutants. Run no tests.
--jsonoffEmit the JSON mutation report to stdout (default is pretty text).
--fail-under <score>—Exit with code 2 if the mutation score (0–100) is below this value. A null score never fails. Ignored with --dry-run.
-v, --verboseoffStream go test output to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of Kotlin sources, or a single .kt file, to mutate.
--include <glob>—Only mutate files matching the glob, relative to --path. Repeatable.
--exclude <glob>—Extra glob to skip, combined with the built-in defaults (build, .gradle, *Test.kt, src/test, generated code, …). Repeatable.
--no-default-excludesoffStart with an empty exclude list.
--operators <ids>allComma-separated operator ids to apply. Repeatable. An unknown id is an invalid_argument error. See Operators.
--project-dir <path>nearest Gradle rootGradle project the tests run in. Walked up from --path when omitted.
--coverage-tool <kover|jacoco>koverCoverage tool for the coverage baseline.
--gradle-test-task <task>testGradle test task for both baselines and every mutant run, e.g. :core:test in a multi-module build.
--gradle-report-task <task>koverXmlReportGradle task that emits the coverage XML in the coverage baseline (jacocoTestReport under --coverage-tool jacoco).
--no-coverageoffSkip the coverage baseline and run every mutant, including mutants on lines no test executes.
--timeout <seconds>ceil(3 × baseline) + 60Per-mutant timeout. Each mutant run includes a compile. The default comes from the run time of the plain baseline.
--dry-runoffList the mutants. Run no tests and change no files.
--jsonoffEmit the JSON mutation report to stdout (default is pretty text).
--fail-under <score>—Exit with code 2 if the mutation score (0–100) is below this value. A null score never fails. Ignored with --dry-run.
-v, --verboseoffStream Gradle output to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.
FlagDefaultMeaning
-p, --path <path>.Directory of Python sources, or a single .py file, to mutate.
--include <glob>—Only mutate files matching the glob, relative to --path. Repeatable.
--exclude <glob>—Extra glob to skip, combined with the built-in defaults (.venv, __pycache__, test_*.py, *_test.py, tests/, conftest.py, …). Repeatable.
--no-default-excludesoffStart with an empty exclude list.
--operators <ids>allComma-separated operator ids to apply. Repeatable. An unknown id is an invalid_argument error. See Operators.
--runner <pytest|unittest>autoTest runner to drive. Auto-detected from the project when omitted.
--project-dir <path>nearest project rootDirectory the tests run in. Walked up from --path (pyproject.toml / setup.py / setup.cfg / tox.ini / .git) when omitted.
--no-coverageoffSkip the coverage baseline and run every mutant, including mutants on lines no test executes.
--timeout <seconds>ceil(3 × baseline) + 10Per-mutant test timeout. The default comes from the run time of the plain baseline.
--dry-runoffList the mutants. Run no tests and touch no files.
--jsonoffEmit the JSON mutation report to stdout (default is pretty text).
--fail-under <score>—Exit with code 2 if the mutation score (0–100) is below this value. A null score never fails. Ignored with --dry-run.
-v, --verboseoffStream test-runner output to stderr.
--quietoffSilence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected.

Flags that only analyze uses, such as --threshold and --fail-over, are rejected as unknown flags. Each mutant costs one run of the test suite, so start with one file or one package. --dry-run shows how many mutants a path yields.

Exit codes & streams

CodeMeaning
0The run completed and no quality gate tripped: nothing exceeded --fail-over (analyze), the mutation score is not below --fail-under (mutate), or no gate flag was passed.
1Error: bad path or flag value, test/coverage failure, parse error. mutate adds its own error codes, such as baseline_failed when the tests fail before any mutation (see Errors). With --json, a structured error envelope is written to stderr. An error that the argument parser reports, such as an unknown flag, is plain text instead. An error that the argument parser reports, such as an unknown flag, is plain text instead. An error that the argument parser reports, such as an unknown flag, is plain text instead.
2Quality gate tripped. For analyze, at least one method's wCRAP exceeds --fail-over. For mutate, the mutation score is below --fail-under, and one line on stderr names the score and the threshold.
130, 143, 129 mutate only: SIGINT, SIGTERM or SIGHUP stopped the run. The running test process group is killed and the file under mutation is restored before the exit. The running test process group is killed and the file under mutation is restored before the exit. The running go test is killed. There is nothing to restore, because slopguard-go never writes to the source tree. The running Gradle build is killed and the file under mutation is restored before the exit. The running test process group is killed and the file under mutation is restored before the exit.
64Usage error from swift-argument-parser: an unknown flag, a flag with no value, or a bad value for a typed analyze option such as --threshold. It is plain text, even with --json. No report is written.

Stream discipline: the report (text or JSON) goes to stdout; progress markers (slopguard: running tests…) go to stderr. Piped stdout is always clean; analyze --json | jq .summary just works, and so does mutate --json | jq .summary.

JSON report schema

Stable, versioned, and shared as one schema across slopguard-swift, slopguard-ts, slopguard-go, slopguard-kotlin, and slopguard-python. The same downstream tooling reads all five. Three top-level sections:

summary

methods[]

Every analyzed function, method, constructor, and accessor:

FieldMeaning
idStable identifier: file#qualifiedName@line. Track a method across runs with this.
qualifiedNameType.method(signature).
file, line, endLineLocation relative to the analyzed root.
kind function, initializer, getter, setter, subscript, … function, method, constructor, getter, setter, named arrow, … function (free function), method (function with a receiver). function, method, constructor, initializer, getter, setter. function, method, constructor (__init__), getter, setter.
complexityCyclomatic complexity (McCabe).
cognitiveComplexityCognitive complexity (SonarSource 2023).
weightedComplexitysqrt(cyc × cog): the input the formula squares.
coverageLine coverage percentage, 0–100.
crapThe wCRAP score.
isCrappytrue when crap > threshold.

types[]

Example output from SlopGuard analyzing its own sources:

{
  "qualifiedName": "CrapAggregator.aggregate(fileReports:sourceRootURL:…)",
  "file": "slopguard-core/Aggregation/CrapAggregator.swift",
  "line": 19,
  "kind": "function",
  "complexity": 17,
  "cognitiveComplexity": 15,
  "weightedComplexity": 15.97,
  "coverage": 0,
  "crap": 270.97,
  "isCrappy": true
}
{
  "qualifiedName": "CrapAggregator.aggregate",
  "file": "core/aggregation/crapAggregator.ts",
  "line": 48,
  "kind": "method",
  "complexity": 20,
  "cognitiveComplexity": 16,
  "weightedComplexity": 17.89,
  "coverage": 0,
  "crap": 337.89,
  "isCrappy": true
}
{
  "qualifiedName": "enumerate",
  "file": "core/diranalyzer.go",
  "line": 95,
  "kind": "function",
  "complexity": 10,
  "cognitiveComplexity": 26,
  "weightedComplexity": 16.12,
  "coverage": 0,
  "crap": 276.12,
  "isCrappy": true
}
{
  "qualifiedName": "ComplexityCalculator.walk",
  "file": "core/src/main/kotlin/.../ComplexityVisitor.kt",
  "line": 81,
  "kind": "method",
  "complexity": 19,
  "cognitiveComplexity": 12,
  "weightedComplexity": 15.10,
  "coverage": 0,
  "crap": 243.10,
  "isCrappy": true
}
{
  "qualifiedName": "_run_analyze",
  "file": "src/slopguard/cli.py",
  "line": 116,
  "kind": "function",
  "complexity": 14,
  "cognitiveComplexity": 18,
  "weightedComplexity": 15.87,
  "coverage": 0,
  "crap": 267.87,
  "isCrappy": true
}

Mutation report schema

mutate --json emits its own report, separate from the analyze report. Its reportType is "mutation" and its schemaVersion is "1". Every port emits the same shape, with keys in alphabetical order at every level.

Top-level fields

FieldMeaning
coverageAvailabletrue when the coverage baseline produced usable line coverage. false in a dry run, under --no-coverage, and when the coverage baseline produced no usable data.
generatedAtUTC timestamp with milliseconds and a trailing Z, e.g. "2026-09-30T14:26:27.222Z".
mutantsEvery mutant, sorted by file, line, column and operator id.
notesPlain sentences about the run. The standard notes are listed below.
operatorsThe enabled operator ids, sorted.
projectRootAbsolute path of the directory the tests ran in.
reportTypeAlways "mutation".
runner "swift test" or "xcodebuild". "vitest" or "jest". "go test". "gradle". "pytest" or "unittest".
schemaVersion"1", as a string.
sourceRoot Absolute path of --path. When --path is a directory, each mutant's file is relative to it. When --path is a single file, sourceRoot is that file itself and each file is its name. Absolute path of --path. When --path is a directory, each mutant's file is relative to it. When --path is a single file, sourceRoot is that file itself and each file is its name. Absolute path of --path. When --path is a directory, each mutant's file is relative to it. When --path is a single file, sourceRoot is that file itself and each file is its name. Absolute path of the mutated directory. Each mutant's file is relative to it. When --path is a single file, sourceRoot is the file's parent directory. Absolute path of --path. When --path is a directory, each mutant's file is relative to it. When --path is a single file, sourceRoot is that file itself and each file is its name.
summaryThe counts and the mutation score, described below.
timeoutSecondsThe per-mutant timeout that was used: an integer when computed from the baseline, or the number passed to --timeout.
tool, toolVersion "slopguard-swift" and its version, e.g. "0.2.0". "slopguard-typescript" (the package name; the CLI is slopguard-ts) and its version, e.g. "0.2.0". "slopguard-go" and its version, e.g. "0.2.0". "slopguard-kotlin" and its version, e.g. "0.2.0". "slopguard-python" and its version, e.g. "0.2.0".

projectRoot, runner and timeoutSeconds are null when no test ran. That happens in a dry run, and when no mutant is left to run because none was generated or all are ignored.

mutants[]

FieldMeaning
id<file>:<line>:<column>:<operator>. Unique within a report.
filePath relative to the --path directory, with forward slashes. When --path is a single file, it is the file name. It is the same relative path the analyze report uses.
line, columnStart of the replaced text. Both are 1-based. column counts Unicode code points, not bytes.
methodThe qualifiedName of the innermost method around the mutant, the same name the analyze report uses. null for code outside any method.
operatorThe operator id. See Operators.
originalThe exact text that was replaced. For remove_call it can span several lines.
replacementThe exact text written in its place. It can be empty.
statusOne of killed, survived, timeout, no_coverage, compile_error, ignored and pending. See Statuses and score.

summary

FieldMeaning
mutantCountAll mutants. The seven status counts below always add up to it.
killedMutants with status killed.
timedOutMutants with status timeout.
survivedMutants with status survived.
noCoverageMutants with status no_coverage.
compileErrorsMutants with status compile_error.
ignoredMutants with status ignored.
pendingMutants with status pending.
fileCountFiles scanned, including files that produced no mutants.
mutationScore0–100, unrounded. null when no mutant counts toward the score, which is always the case in a dry run. The formula is under Statuses and score.

notes[]

Each note is a plain sentence. The standard notes are:

slopguard-go never writes to the source tree, so it never adds the last two notes.

Errors

On failure, mutate exits with code 1 and writes the same error envelope as analyze to stderr: JSON with --json, text without it. The argument parser reports some errors itself, such as an unknown flag. Those errors are plain text, even with --json, and exit with code 64. An unknown flag also exits with code 1, but the argument parser reports it as plain text, even with --json. An unknown flag also exits with code 1, but Go's flag parser reports it as plain text, followed by the usage, even with --json. An unknown flag is an invalid_argument error in the same envelope. An unknown flag or an invalid --runner choice also exits with code 1, but Python's argparse reports it as plain text, after the usage, even with --json. These codes are new:

CodeWhen
baseline_failedThe tests fail before any mutation. Fix them first. The message includes the tail of the test output.
mutation_in_progressAnother live mutate run holds the workspace guard for the same project.
restore_failedWriting the original file back failed. The message names the file and the backup path.
runner_unavailableThe test runner cannot be launched.
runner_unavailableThe test runner cannot be launched.
runner_unavailableThe test runner cannot be launched.
runner_not_detectedNo --runner, --scheme or --workspace was passed, and the project directory holds no Package.swift, .xcodeproj or .xcworkspace.

slopguard-go never writes to the source tree, so it never returns mutation_in_progress or restore_failed.

Existing codes are reused where they fit, such as invalid_argument for a bad flag value like an unknown operator id. mutate also returns runner_unavailable, the code that analyze already uses when the test runner cannot be launched. mutate also returns runner_unavailable, the code that analyze already uses when the test runner cannot be launched.

An excerpt from a run on the sample app, with the other top-level keys and mutants left out:

{
  "mutants" : [
    {
      "column" : 59,
      "file" : "Sources/TodoList/TodoStore.swift",
      "id" : "Sources/TodoList/TodoStore.swift:19:59:negate_conditional",
      "line" : 19,
      "method" : "TodoStore.toggle(id:)",
      "operator" : "negate_conditional",
      "original" : "==",
      "replacement" : "!=",
      "status" : "killed"
    }
  ],
  "summary" : {
    "compileErrors" : 0,
    "fileCount" : 3,
    "ignored" : 0,
    "killed" : 11,
    "mutantCount" : 11,
    "mutationScore" : 100,
    "noCoverage" : 0,
    "pending" : 0,
    "survived" : 0,
    "timedOut" : 0
  }
}
{
  "mutants": [
    {
      "column": 37,
      "file": "todoStore.ts",
      "id": "todoStore.ts:27:37:remove_not",
      "line": 27,
      "method": "TodoStore.remaining.get",
      "operator": "remove_not",
      "original": "!",
      "replacement": "",
      "status": "survived"
    }
  ],
  "summary": {
    "compileErrors": 0,
    "fileCount": 3,
    "ignored": 0,
    "killed": 6,
    "mutantCount": 7,
    "mutationScore": 85.71428571428571,
    "noCoverage": 0,
    "pending": 0,
    "survived": 1,
    "timedOut": 0
  }
}
{
  "mutants": [
    {
      "column": 32,
      "file": "store.go",
      "id": "store.go:55:32:increment",
      "line": 55,
      "method": "Store.All",
      "operator": "increment",
      "original": "++",
      "replacement": "--",
      "status": "timeout"
    }
  ],
  "summary": {
    "compileErrors": 0,
    "fileCount": 3,
    "ignored": 1,
    "killed": 15,
    "mutantCount": 17,
    "mutationScore": 100,
    "noCoverage": 0,
    "pending": 0,
    "survived": 0,
    "timedOut": 1
  }
}
{
  "mutants": [
    {
      "column": 19,
      "file": "com/example/todo/TodoStore.kt",
      "id": "com/example/todo/TodoStore.kt:19:19:boundary",
      "line": 19,
      "method": "TodoStore.toggle",
      "operator": "boundary",
      "original": "<",
      "replacement": "<=",
      "status": "survived"
    }
  ],
  "summary": {
    "compileErrors": 0,
    "fileCount": 2,
    "ignored": 0,
    "killed": 13,
    "mutantCount": 15,
    "mutationScore": 86.66666666666667,
    "noCoverage": 0,
    "pending": 0,
    "survived": 2,
    "timedOut": 0
  }
}
{
  "mutants": [
    {
      "column": 23,
      "file": "store.py",
      "id": "store.py:14:23:arithmetic",
      "line": 14,
      "method": "TodoStore.add",
      "operator": "arithmetic",
      "original": "+=",
      "replacement": "-=",
      "status": "survived"
    }
  ],
  "summary": {
    "compileErrors": 0,
    "fileCount": 4,
    "ignored": 0,
    "killed": 11,
    "mutantCount": 12,
    "mutationScore": 91.66666666666666,
    "noCoverage": 0,
    "pending": 0,
    "survived": 1,
    "timedOut": 0
  }
}

The wCRAP formula, precisely

wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog)

The exact shipping implementation is Sources/slopguard-core/CRAP.swift: 18 lines, fully covered. Play with the curve in the interactive formula lab.

The exact shipping implementation is src/core/crap.ts: a handful of lines, fully covered, identical in behaviour to the Swift version. Play with the curve in the interactive formula lab.

The exact shipping implementation is core/crap.go: a handful of lines, fully covered, identical in behaviour to the other ports. Play with the curve in the interactive formula lab.

The exact shipping implementation is core/src/main/kotlin/dev/slopguard/core/Crap.kt: a handful of lines, fully covered, identical in behaviour to the other ports. Play with the curve in the interactive formula lab.

The exact shipping implementation is src/slopguard/crap.py: a handful of lines, fully covered, identical in behaviour to the other ports. Play with the curve in the interactive formula lab.

How coverage is gathered

Coverage is an artifact of the analysis, not an input. SlopGuard drives xcodebuild test -enableCodeCoverage YES itself, then reads the resulting .xcresult via xcrun xccov. There is no flag to feed it a coverage number, which means a coding agent (or a hurried human) cannot satisfy the gate without writing tests that actually execute the code.

  • Scheme is auto-discovered; override with --scheme.
  • Default destination is platform=macOS; iOS projects pass a simulator destination.
  • CocoaPods-style multi-container directories: pass --workspace so xcodebuild builds the right thing.
  • --no-coverage skips the build entirely for fast, complexity-only sweeps (all methods read 0%).

Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives xcodebuild test itself. slopguard-ts drives your project's own test runner, so a coding agent can't satisfy the gate without writing tests that actually execute the code:

  1. Project discovery. Walk up from --path to the nearest package.json (override with --project-dir).
  2. Runner detection. Look for vitest or jest signals: a config file, the dependency itself, a jest key in package.json, or the runner named in the test script. Both detected → runner_ambiguous (pass --runner); neither → runner_not_detected (pass --runner, --coverage-file, or --no-coverage).
  3. Test run. Spawn the project-local binary (node_modules/.bin/<runner>) with flags that force an istanbul coverage-final.json into a slopguard-owned temp directory. Your own coverage config is untouched. Failing tests don't abort; partial coverage is still useful (a note is attached).
  4. Join. Parse the istanbul map, join per-method line coverage onto the parsed declarations (basename + longest-suffix fallback for CI-vs-local path mismatches), then delete the temp dir.

The istanbul coverage-final.json is the universal interchange format (jest, vitest, nyc, and c8 all emit it), so any runner slopguard can't drive directly is still supported via --coverage-file. --no-coverage skips the run entirely for fast, complexity-only sweeps (all methods read 0%).

Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives xcodebuild test. slopguard-go drives the module's own go test, so a coding agent can't satisfy the gate without writing tests that actually execute the code:

  1. Module discovery. Walk up from --path to the nearest go.mod (override with --project-dir).
  2. Test run. Run go test -coverprofile=… -covermode=count -coverpkg=<packages> (default ./...) into a slopguard-owned temp directory. -coverpkg instruments every selected package, so coverage from cross-package tests counts. Failing tests don't abort — partial coverage is still useful (a note is attached).
  3. Join. Parse the profile into a per-line index, resolve its import-path file names to disk via the module path from go.mod, join per-method line coverage onto the parsed declarations, then delete the temp dir.

A Go coverage profile is the universal interchange format — anything that runs go test -coverprofile produces one — so a profile your CI already generated is supported via --coverage-file. --no-coverage skips the run entirely for fast, complexity-only sweeps (all methods read 0%).

Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives xcodebuild test. slopguard-kotlin drives your project's own Gradle test run, so a coding agent can't satisfy the gate without writing tests that actually execute the code:

  1. Project discovery. Walk up from --path to the nearest Gradle root (settings.gradle[.kts], gradlew, or build.gradle[.kts]). Override with --project-dir.
  2. Test run. Run the project's own ./gradlew <test-task> <report-task> --continue. Pick the tool with --coverage-tool: Kover (default, report task koverXmlReport) or JaCoCo (jacocoTestReport with XML output enabled). Override either default with --gradle-test-task / --gradle-report-task for Android variant tasks. Failing tests don't abort (--continue).
  3. Join. Parse the XML report into a per-line index, resolve its package-qualified file names against the analyzed paths, and join per-method line coverage onto the parsed declarations.

Both JaCoCo and Kover emit the same JaCoCo-format XML, so one parser handles either, and a report your CI already generated is supported via --coverage-file regardless of which tool produced it. --no-coverage skips the run entirely for fast, complexity-only sweeps (all methods read 0%).

Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives xcodebuild test. slopguard-python drives your project's own test suite under coverage.py, so a coding agent can't satisfy the gate without writing tests that actually execute the code:

  1. Project discovery. Walk up from --path to the nearest project root (pyproject.toml / setup.py / setup.cfg / tox.ini / .git). Override with --project-dir.
  2. Runner detection. Prefer pytest when the project shows pytest signals (a pytest.ini/conftest.py, a [tool.pytest.ini_options] block, or pytest in the deps); otherwise fall back to stdlib unittest. Override with --runner.
  3. Test run. Drive the suite under coverage.py — python -m coverage run --source=<root> -m pytest (or -m unittest discover) — into a slopguard-owned temp directory, then coverage json. Failing tests don't abort; partial coverage is still useful (a note is attached).
  4. Join. Parse the coverage json report into a per-line index, resolve its file paths to disk (basename + longest-suffix fallback for CI-vs-local path mismatches), join per-method line coverage onto the parsed declarations, then delete the temp dir.

A coverage json report is the universal Python interchange format — anything you can run under coverage produces one — so a report your CI already generated is supported via --coverage-file. slopguard-python itself has zero runtime dependencies; coverage.py lives in your project's environment (like pytest), not in the tool. --no-coverage skips the run entirely for fast, complexity-only sweeps (all methods read 0%).

How mutation testing works

Coverage shows that a line ran during a test. It does not show that a test would fail if the line were wrong. mutate checks that. It makes one small change to the source, called a mutant: > becomes >=, + becomes -, or a call statement is deleted. Then it runs the tests against the mutant.

A mutant that makes a test fail is killed: a test checks that behaviour. A mutant that every test passes with has survived: no test checks that behaviour. The report lists each survivor with its file, line, column, original text, replacement text and enclosing method, so a developer or an agent can write the test that kills it.

analyze and mutate find different gaps. analyze finds complex code that the tests do not run. mutate finds code that the tests run but do not check.

What a run does

  1. Walk --path with the same include and exclude rules as analyze, so test files are not mutated by default. Parse each file, generate its mutants, and name the enclosing method of each one. Apply the ignore markers and --operators, then sort the mutants by file, line, column and operator. A dry run stops here, and so does a run with no mutant left to run.
  2. Run the plain baseline: the exact command used for mutants, once, with no mutation and no timeout. It must pass, or the run stops with baseline_failed. A broken command would otherwise fail every mutant and fake a perfect score. The run time of this baseline sets the default timeout.
  3. Run the coverage baseline, the same coverage run that analyze uses. The swift test runner is the exception: it runs swift test --enable-code-coverage and reads the line counts with llvm-cov. A mutant on a line that no test executes gets no_coverage and does not run. This step never stops the run: without usable coverage data, every mutant runs and a note says so. --no-coverage skips it.
  4. For each remaining mutant, in order: write it into the file, build and test with the timeout, write the original back, and classify the result.
  5. For each remaining mutant, in order: write it into the file, run the tests with the timeout, write the original back, and classify the result.
  6. For each remaining mutant, in order: write the mutated file to a temporary directory, run go test with an overlay that swaps it in, and classify the result. The source tree is never written.
  7. For each remaining mutant, in order: write it into the file, run the Gradle tests with the timeout, write the original back, and classify the result.
  8. For each remaining mutant, in order: write it into the file, run the tests with the timeout, write the original back, and classify the result.

With the swift test runner, each mutant runs swift build --build-tests and then swift test --skip-build --disable-code-coverage. Its coverage baseline reads per-line counts through xcrun llvm-cov export. With the xcodebuild runner, each mutant runs build-for-testing and then test-without-building, both with -enableCodeCoverage NO. xccov reports coverage per function, so under the xcodebuild runner a mutant is no_coverage when its enclosing method never ran.

The mutant command is vitest run --bail=1 --coverage.enabled=false --reporter=dot or jest --bail=1 --coverage=false --watchAll=false --ci. Coverage stays off in mutant runs, so a coverage threshold in your config cannot fail a run and fake a kill.

The mutant command is go test -overlay=<json> -vet=off -failfast <packages>, run in the module root. The test cache is keyed on the compiled binary, so packages that a mutant does not change come back (cached). Go records coverage per block of statements, so a mutant in a case expression can get no_coverage even though the expression was evaluated.

The mutant command is ./gradlew <test-task> --fail-fast --console=plain with an init script. The script switches off every Kover and JaCoCo report and verification task and the JaCoCo agent, so a coverage threshold in your build cannot fail a mutant run and fake a kill.

The mutant command is python -m pytest -x -q -p no:cacheprovider (plus --no-cov when pytest-cov is installed) or python -m unittest discover -f. Coverage stays off in mutant runs, so a coverage threshold in your config cannot fail a run and fake a kill.

The text report

The text report opens with a summary. Then it lists the mutants that survived, the mutants on lines that no test runs, and the mutants that timed out. Each line gives the location, the operator, the original and replacement text, and the enclosing method.

This dry run of the sample app lists every mutant without running a test:

slopguard-swift 0.2.0 — mutation report (schema 1)
source:    /work/TodoList
project:   (not run)
runner:    (not run)
timeout:   (not run)

Summary
  files:          3
  mutants:        11
  killed:         0
  timed out:      0
  survived:       0
  no coverage:    0
  compile errors: 0
  ignored:        0
  pending:        11
  score:          n/a

Mutants (11, not run)
  Sources/TodoList/Todo.swift:12:29  boolean_literal  `false` → `true`  Todo.init(id:title:isCompleted:createdAt:)
  Sources/TodoList/TodoFilter.swift:10:33  boolean_literal  `true` → `false`  TodoFilter.matches(_:)
  Sources/TodoList/TodoFilter.swift:11:33  remove_not  `!` → ``  TodoFilter.matches(_:)
  Sources/TodoList/TodoStore.swift:11:9  remove_call  `todos.append(todo)` → ``  TodoStore.add(_:)
  Sources/TodoList/TodoStore.swift:15:9  remove_call  `todos.removeAll { $0.id == id }` → ``  TodoStore.remove(id:)
  Sources/TodoList/TodoStore.swift:15:33  negate_conditional  `==` → `!=`  TodoStore.remove(id:)
  Sources/TodoList/TodoStore.swift:19:59  negate_conditional  `==` → `!=`  TodoStore.toggle(id:)
  Sources/TodoList/TodoStore.swift:20:9  remove_call  `todos[index].isCompleted.toggle()` → ``  TodoStore.toggle(id:)
  Sources/TodoList/TodoStore.swift:24:59  negate_conditional  `==` → `!=`  TodoStore.rename(id:to:)
  Sources/TodoList/TodoStore.swift:33:29  remove_not  `!` → ``  TodoStore.activeCount.get
  Sources/TodoList/TodoStore.swift:41:9  remove_call  `todos.removeAll(where: \.isCompleted)` → ``  TodoStore.clearCompleted()

This run is on the sample app, before its tests were strengthened. One mutant survives:

slopguard-typescript 0.2.0 — mutation report (schema 1)
source:    /work/todo-list/src
project:   /work/todo-list
runner:    vitest
timeout:   21s per mutant

Summary
  files:          3
  mutants:        7
  killed:         6
  timed out:      0
  survived:       1
  no coverage:    0
  compile errors: 0
  ignored:        0
  score:          85.71%

Survived (1) — tests still pass with these changes
  todoStore.ts:27:37  remove_not  `!` → ``  TodoStore.remaining.get

This run is on the sample app. One mutant timed out, which counts as killed, and an ignore marker switched off another:

slopguard-go 0.2.0 — mutation report (schema 1)
source:    /work/todolist
project:   /work/todolist
runner:    go test
timeout:   35s per mutant

Summary
  files:          3
  mutants:        17
  killed:         15
  timed out:      1
  survived:       0
  no coverage:    0
  compile errors: 0
  ignored:        1
  score:          100.00%

Timed out (1) — counted as killed
  store.go:55:32  increment  `++` → `--`  Store.All

This run is on the sample app, before its tests were strengthened. Two mutants survive:

slopguard-kotlin 0.2.0 — mutation report (schema 1)
source:    /work/todolist/src/main/kotlin
project:   /work/todolist
runner:    gradle
timeout:   70s per mutant

Summary
  files:          2
  mutants:        15
  killed:         13
  timed out:      0
  survived:       2
  no coverage:    0
  compile errors: 0
  ignored:        0
  score:          86.67%

Survived (2) — tests still pass with these changes
  com/example/todo/TodoStore.kt:19:19  boundary  `<` → `<=`  TodoStore.toggle
  com/example/todo/TodoStore.kt:29:23  arithmetic  `-` → `+`  TodoStore.clearCompleted

This run is on the sample app, before its tests were strengthened. One mutant survives:

slopguard-python 0.2.0 — mutation report (schema 1)
source:    /work/todolist/todolist
project:   /work/todolist
runner:    unittest
timeout:   11s per mutant

Summary
  files:          4
  mutants:        12
  killed:         11
  timed out:      0
  survived:       1
  no coverage:    0
  compile errors: 0
  ignored:        0
  score:          91.67%

Survived (1) — tests still pass with these changes
  store.py:14:23  arithmetic  `+=` → `-=`  TodoStore.add

Statuses and score

StatusMeaningScore
killedA test failed with the mutant in place.detected
timeoutThe run passed the timeout, usually because of an infinite loop, and was killed with all its child processes.detected
survivedEvery test passed with the mutant in place.missed
no_coverageNo test executes the line, so the mutant did not run.missed
compile_error The mutant did not build. The mutant did not type-check: jest reports Test suite failed to run with a ts-jest error TS… diagnostic. vitest strips types, so it never reports this status. The mutant did not compile: the go test output contains [build failed] or [setup failed]. The mutant did not compile: a compile… task failed, or Kotlin printed an e: diagnostic. The mutated file does not compile, so the mutant is not run. excluded
ignoredAn ignore marker switched the mutant off. It never runs.excluded
pending Not run. --dry-run lists every mutant that is not ignored as pending. A mutant whose file changed during the run also stays pending. A mutant whose file changed during the run also stays pending. A mutant whose file changed during the run also stays pending. A mutant whose file changed during the run also stays pending. excluded
mutationScore = (killed + timedOut) / (killed + timedOut + survived + noCoverage) × 100

Detected mutants count for the score and missed mutants count against it. Excluded mutants do not count at all. The JSON report gives the score unrounded, and the text report rounds it to two decimals. The score is null (n/a in text) when no mutant counts, which is always the case in a dry run.

Operators

The operator ids are the same in every port, and --operators takes any of them. Each mutant replaces one span of the source: an operator token, a literal, or a whole call statement for remove_call. A double arrow (↔) means the change goes both ways.

OperatorChange
arithmetic+↔-, *↔/, %→*, +=↔-=, *=↔/=, %=→*=. Binary operators only. A + or += with a string-literal operand is skipped, including every + in "a" + b + c.
boolean_literaltrue↔false
boundary<↔<=, >↔>=
incrementSwift has no ++ or --, so this id produces no mutants. It is still a valid id.
invert_negative-x → x, including negative literals.
logical&&↔||
negate_conditional==↔!=, ===↔!==, <→>=, <=→>, >→<=, >=→<
remove_callRemoves a statement that is only a call whose result is discarded, also under try or await. print, debugPrint, NSLog, os_log, logger.*, super.init(…) and self.init(…) are kept. So are a single-expression body whose value is returned, the only statement of a switch case, and the last statement of a guard … else body.
remove_not!x → x. The force unwrap x! is never touched.

No operator changes comments, string contents or #if conditions. Code inside #if blocks and inside string interpolations is mutated.

OperatorChange
arithmetic+↔-, *↔/, %→*, +=↔-=, *=↔/=, %=→*=. Binary operators only. A + or += with a string-literal or template-literal operand is skipped, including every + in "a" + b + c.
boolean_literaltrue↔false. Literal types are not mutated.
boundary<↔<=, >↔>=
increment++↔--, prefix and postfix.
invert_negative-x → x
logical&&↔||
negate_conditional==↔!=, ===↔!==, <→>=, <=→>, >→<=, >=→<
remove_callsave(x); → ; for a call statement in a block whose result is unused. console.*, super(...) and import(...) are skipped.
remove_not!x → x. The non-null assertion x! is never touched.

Type annotations, comments and strings are never mutated.

OperatorChange
arithmetic+↔-, *↔/, %→*, +=↔-=, *=↔/=, %=→*=. A + or += with a string-literal operand is skipped, including every + in "a" + b + c.
boolean_literaltrue↔false
boundary<↔<=, >↔>=
incrementi++↔i--
invert_negative-x → x. One space is written instead of nothing when the - sits between two identifier characters, as in return-x.
logical&&↔||
negate_conditional==↔!=, <→>=, <=→>, >→<=, >=→<
remove_callDeletes a statement that is only a call. fmt.Print*, log.*, t.Log* and b.Log* are skipped.
remove_not!x → x. One space is written instead of nothing when the ! sits between two identifier characters, as in return!x.

A mutant can fail to compile, for example a - b on two strings, or a deleted call that leaves a variable unused. It gets compile_error and does not count toward the score.

OperatorChange
arithmetic+↔-, *↔/, %→*, +=↔-=, *=↔/=, %=→*=. Binary operators only. A + or += with a string-literal operand is skipped, including every + in "a" + b + c.
boolean_literaltrue↔false
boundary<↔<=, >↔>=
increment++↔--, prefix and postfix.
invert_negative-x → x
logical&&↔||
negate_conditional==↔!=, ===↔!==, <→>=, <=→>, >→<=, >=→<. in and is checks are not mutated.
remove_callRemoves a plain call statement (f(), a.b(), a?.b()) directly inside a braced block. It keeps the last statement of a lambda, and of an if, when or try branch whose value is used. It also keeps println, print, System.out and System.err printing, and calls on logger, log or Log.
remove_not!x → x. The !! force unwrap is never touched.

Mutants come only from real syntax nodes of the Kotlin PSI tree. Comments, string text and annotation arguments are never mutated.

OperatorChange
arithmetic+↔-, *↔/, %→*, //→*, +=↔-=, *=↔/=, %=→*=, //=→*=. String concatenation ("a" + b, f-strings) is skipped.
boolean_literalTrue↔False
boundary<↔<=, >↔>=
incrementPython has no ++ or --, so this id produces no mutants. It is still a valid id.
invert_negative-x → x. One space stays where the removal would join two names: return-x → return x.
logicaland↔or
negate_conditional==↔!=, <→>=, <=→>, >→<=, >=→<, is↔is not, in↔not in
remove_callsave(x) → pass for a statement that is only a call, optionally awaited. print(...), logging calls (logging.*, logger.*, log.*, also as an attribute such as self.logger.info), super(...) and __init__ delegation are skipped.
remove_notnot x → x

Comments, strings, type annotations, type aliases and the inside of f-strings are never mutated.

Equivalent mutants and the ignore marker

Some mutants change nothing that a test can observe, so no test can kill them. These are equivalent mutants. For example, when code keeps the largest value seen so far, > and >= give the same result, because assigning an equal value changes nothing. Switch such a mutant off with a comment on its line, or on a comment line directly above it:

if m.complexity > maxComplexity { // slopguard-ignore-mutant(boundary): equal values assign the same max
if (s > max) max = s; // slopguard-ignore-mutant(boundary): assigning an equal max changes nothing
if m.Complexity > maxComplexity { // slopguard-ignore-mutant(boundary): equal values assign the same max
if (value > max) max = value // slopguard-ignore-mutant(boundary): equal values give the same max

// slopguard-ignore-mutant
val limit = retries * 2
if score > best:  # slopguard-ignore-mutant(boundary): assigning an equal best changes nothing
    best = score

Write the reason after the marker, so a reviewer can check that the mutant really is equivalent.

Workspace safety

mutate writes each mutant into your source file, one file at a time, and always puts the original back.

  • It keeps the original bytes and timestamps in memory. It writes them back after every test run, when an error occurs, and on SIGINT, SIGTERM or SIGHUP.
  • A guard directory outside your project, <temp dir>/slopguard-mutate/<hash of the project path>/, holds a lock, a journal and a backup of the original file. The lock allows one run per project, so a second run stops with mutation_in_progress.
  • If a run is killed without warning, for example by SIGKILL, the next run restores the file from the backup. It does so only when the file still holds exactly the mutant named in the journal, so later edits are never overwritten. After a power loss, this works only if the temp directory survived the reboot. Many Linux systems clear it at boot, so check git status.
  • Before it writes each mutant, mutate checks that the file still holds the bytes the mutants were planned from. If you edit the file before its first mutant or between two mutants, it writes nothing more to that file, reports its remaining mutants as pending, and adds a note. That edit is kept. An edit saved while a mutant is in the file is not detected: the restore writes the original bytes over it. Do not edit files under --path while mutate runs.
  • It writes a mutant by truncating and rewriting the file, so the inode, the mode and hard links survive. SwiftPM's next build still sees the restored file and recompiles it.
  • --dry-run writes nothing.

mutate writes each mutant into your source file, one file at a time, and always puts the original back.

  • It keeps the original bytes and timestamps in memory. It writes them back after every test run, when an error occurs, and on SIGINT, SIGTERM or SIGHUP.
  • A guard directory outside your project, <temp dir>/slopguard-mutate/<hash of the project path>/, holds a lock, a journal and a backup of the original file. The lock allows one run per project, so a second run stops with mutation_in_progress.
  • If a run is killed without warning, for example by SIGKILL, the next run restores the file from the backup. It does so only when the file still holds exactly the mutant named in the journal, so later edits are never overwritten. After a power loss, this works only if the temp directory survived the reboot. Many Linux systems clear it at boot, so check git status.
  • Before it writes each mutant, mutate checks that the file still holds the bytes the mutants were planned from. If you edit the file before its first mutant or between two mutants, it writes nothing more to that file, reports its remaining mutants as pending, and adds a note. That edit is kept. An edit saved while a mutant is in the file is not detected: the restore writes the original bytes over it. Do not edit files under --path while mutate runs.
  • --dry-run writes nothing.

slopguard-go never writes to your source tree. It writes each mutant to a temporary directory that it owns, and go test -overlay hands the mutated file to the compiler in place of the original. The directory is removed when the run ends, including after an error or an interrupt.

Each go test run starts in its own process group. A timeout kills the whole group, so a test binary stuck in an infinite loop stops too. On Windows, only the go process itself is killed.

mutate writes each mutant into your source file, one file at a time, and always puts the original back.

  • It keeps the original bytes and timestamps in memory. It writes them back after every test run, when an error occurs, and on SIGINT, SIGTERM or SIGHUP.
  • A guard directory outside your project, <temp dir>/slopguard-mutate/<hash of the project path>/, holds a lock, a journal and a backup of the original file. The lock allows one run per project, so a second run stops with mutation_in_progress.
  • If a run is killed without warning, for example by SIGKILL, the next run restores the file from the backup. It does so only when the file still holds exactly the mutant named in the journal, so later edits are never overwritten. After a power loss, this works only if the temp directory survived the reboot. Many Linux systems clear it at boot, so check git status.
  • Before it writes each mutant, mutate checks that the file still holds the bytes the mutants were planned from. If you edit the file before its first mutant or between two mutants, it writes nothing more to that file, reports its remaining mutants as pending, and adds a note. That edit is kept. An edit saved while a mutant is in the file is not detected: the restore writes the original bytes over it. Do not edit files under --path while mutate runs.
  • On a signal, a JVM shutdown hook kills the test run and restores the file. A timeout kills the Gradle client and every process it started, and mutate also stops any test JVM of the project that is still running.
  • --dry-run writes nothing.

mutate writes each mutant into your source file, one file at a time, and always puts the original back.

  • It keeps the original bytes and timestamps in memory. It writes them back after every test run, when an error occurs, and on SIGINT, SIGTERM or SIGHUP.
  • A guard directory outside your project, <temp dir>/slopguard-mutate/<hash of the project path>/, holds a lock, a journal and a backup of the original file. The lock allows one run per project, so a second run stops with mutation_in_progress.
  • If a run is killed without warning, for example by SIGKILL, the next run restores the file from the backup. It does so only when the file still holds exactly the mutant named in the journal, so later edits are never overwritten. After a power loss, this works only if the temp directory survived the reboot. Many Linux systems clear it at boot, so check git status.
  • Before it writes each mutant, mutate checks that the file still holds the bytes the mutants were planned from. If you edit the file before its first mutant or between two mutants, it writes nothing more to that file, reports its remaining mutants as pending, and adds a note. That edit is kept. An edit saved while a mutant is in the file is not detected: the restore writes the original bytes over it. Do not edit files under --path while mutate runs.
  • Mutant runs set PYTHONDONTWRITEBYTECODE=1, and each mutant gets the original modification time plus one second, so Python never loads a stale cached .pyc for it. The restore puts the original time back, so your .pyc files stay valid.
  • A file keeps its own encoding (PEP 263), BOM and line endings. Only the mutated span changes.
  • --dry-run writes nothing.

Each mutant runs your tests, so a run executes your code once per mutant. This is the same trust boundary as analyze.

Includes & excludes

By default SlopGuard skips build products and test code: .build, Pods, Carthage, Generated, *Tests, *Spec, and friends. Add your own with --exclude (combined with the defaults) or take full manual control with --no-default-excludes. Useful when you want to score the test suite itself.

By default SlopGuard skips build products and test code: node_modules, dist, build, out, coverage, *.d.ts, *.min.js, generated code, and test files (*.test.*, *.spec.*, __tests__/, test(s)/). Add your own with --exclude (combined with the defaults) or take full manual control with --no-default-excludes. Useful when you want to score the test suite itself.

What counts as a method: functions, class methods, constructors, get/set accessors, static blocks, and named arrow functions / function expressions (const handler = () => {}, object properties, class fields). Anonymous inline callbacks don't get their own entry; their branches count toward the enclosing method, with a cognitive nesting bump for the callback body, per the Sonar spec.

By default SlopGuard skips build products and test code: vendor, testdata, *_test.go, and generated code (*.pb.go, *_gen.go, mock_*.go, and anything carrying the // Code generated … DO NOT EDIT. header). Add your own with --exclude (combined with the defaults) or take full manual control with --no-default-excludes.

What counts as a method: top-level functions and methods (functions with a receiver). Anonymous function literals don't get their own entry; their branches count toward the enclosing function, with a cognitive nesting bump for the closure body, per the Sonar spec. Go attaches methods to a type by receiver, not lexical nesting, so a type's members are gathered package-wide.

By default SlopGuard skips build products and test code: build, .gradle, generated code, *Test.kt / *Tests.kt, the src/test and src/androidTest source sets, and testFixtures/. Add your own with --exclude (combined with the defaults) or take full manual control with --no-default-excludes.

What counts as a method: top-level and member functions (in classes, objects, interfaces, enums), secondary constructors, init blocks, and custom get/set accessors with a body. Lambdas and local functions don't get their own entry; their branches count toward the enclosing function, with a cognitive nesting bump, per the Sonar spec. Types are gathered by lexical nesting.

By default SlopGuard skips environments, caches, and test code: .venv, __pycache__, .mypy_cache, test files and dirs (test_*.py, *_test.py, tests/, conftest.py), generated stubs (*_pb2.py), and anything carrying an @generated / DO NOT EDIT header. Add your own with --exclude (combined with the defaults) or take full manual control with --no-default-excludes.

What counts as a method: top-level functions, methods, constructors (__init__), and property get/set accessors. Named nested defs get their own entry; anonymous lambdas don't — their branches count toward the enclosing method, with a cognitive nesting bump for the lambda body, per the Sonar spec. Methods attach to their lexically enclosing class, so nested classes get qualified names (Outer.Inner.method).

mutate uses the same rules, so by default it never mutates test code.

Using with agents

SlopGuard's core claim: code quality and coverage decide agentic outcomes. Agents iterate by changing code and running tests. Where coverage is missing they fly blind, and where complexity is high they hallucinate. wCRAP is one number that captures both, so you can use it in two directions:

CLAUDE.md / AGENTS.md drop-in

## Quality gate: SlopGuard

Before declaring any Swift task complete, run:

    slopguard-swift analyze --path Sources --json --quiet

- The report is JSON on stdout; `.methods[]` entries with `isCrappy: true`
  exceed the wCRAP threshold (30).
- If your change ADDED a crappy method, refactor it or cover it with tests
  until its `crap` score is below 30. Coverage is gathered by the tool
  itself via `xcodebuild test`. Write real tests; the number cannot be
  hand-fed.
- CI runs `--fail-over 50` and will reject the PR at exit code 2.
- For each file you changed, run
  `slopguard-swift mutate --path <file> --json --quiet`. For every
  `.mutants[]` entry with status `survived` or `no_coverage`, add a test
  that passes on the real code and fails with that mutant in place.
  If no test can kill a mutant because the change has no observable
  effect, add `// slopguard-ignore-mutant(<operator>): <reason>`
  on its line instead. Re-run until none are left.
- Full interface reference: https://www.slopguard.dev/llms-full.txt
## Quality gate: SlopGuard

Before declaring any TypeScript task complete, run:

    slopguard-ts analyze --path src --json --quiet

- The report is JSON on stdout; `.methods[]` entries with `isCrappy: true`
  exceed the wCRAP threshold (30).
- If your change ADDED a crappy method, refactor it or cover it with tests
  until its `crap` score is below 30. Coverage is gathered by the tool
  itself by driving your test runner (vitest/jest). Write real tests; the
  number cannot be hand-fed.
- CI runs `--fail-over 50` and will reject the PR at exit code 2.
- For each file you changed, run
  `slopguard-ts mutate --path <file> --json --quiet`. For every
  `.mutants[]` entry with status `survived` or `no_coverage`, add a test
  that passes on the real code and fails with that mutant in place.
  If no test can kill a mutant because the change has no observable
  effect, add `// slopguard-ignore-mutant(<operator>): <reason>`
  on its line instead. Re-run until none are left.
- Full interface reference: https://www.slopguard.dev/llms-full.ts.txt
## Quality gate: SlopGuard

Before declaring any Go task complete, run:

    slopguard-go analyze --path . --json --quiet

- The report is JSON on stdout; `.methods[]` entries with `isCrappy: true`
  exceed the wCRAP threshold (30).
- If your change ADDED a crappy method, refactor it or cover it with tests
  until its `crap` score is below 30. Coverage is gathered by the tool
  itself by driving `go test`. Write real tests; the number cannot be
  hand-fed.
- CI runs `--fail-over 50` and will reject the PR at exit code 2.
- For each package you changed, run
  `slopguard-go mutate --path <package> --json --quiet`. For every
  `.mutants[]` entry with status `survived` or `no_coverage`, add a test
  that passes on the real code and fails with that mutant in place.
  If no test can kill a mutant because the change has no observable
  effect, add `// slopguard-ignore-mutant(<operator>): <reason>`
  on its line instead. Re-run until none are left.
- Full interface reference: https://www.slopguard.dev/llms-full.go.txt
## Quality gate: SlopGuard

Before declaring any Kotlin task complete, run:

    slopguard-kotlin analyze --path src/main/kotlin --json --quiet

- The report is JSON on stdout; `.methods[]` entries with `isCrappy: true`
  exceed the wCRAP threshold (30).
- If your change ADDED a crappy method, refactor it or cover it with tests
  until its `crap` score is below 30. Coverage is gathered by the tool
  itself by driving your Gradle test run (Kover/JaCoCo). Write real tests;
  the number cannot be hand-fed.
- CI runs `--fail-over 50` and will reject the PR at exit code 2.
- For each file you changed, run
  `slopguard-kotlin mutate --path <file> --json --quiet`. For every
  `.mutants[]` entry with status `survived` or `no_coverage`, add a test
  that passes on the real code and fails with that mutant in place.
  If no test can kill a mutant because the change has no observable
  effect, add `// slopguard-ignore-mutant(<operator>): <reason>`
  on its line instead. Re-run until none are left.
- Full interface reference: https://www.slopguard.dev/llms-full.kotlin.txt
## Quality gate: SlopGuard

Before declaring any Python task complete, run:

    slopguard-python analyze --path . --json --quiet

- The report is JSON on stdout; `.methods[]` entries with `isCrappy: true`
  exceed the wCRAP threshold (30).
- If your change ADDED a crappy method, refactor it or cover it with tests
  until its `crap` score is below 30. Coverage is gathered by the tool
  itself by driving your test suite (pytest/unittest under coverage.py).
  Write real tests; the number cannot be hand-fed.
- CI runs `--fail-over 50` and will reject the PR at exit code 2.
- For each file you changed, run
  `slopguard-python mutate --path <file> --json --quiet`. For every
  `.mutants[]` entry with status `survived` or `no_coverage`, add a test
  that passes on the real code and fails with that mutant in place.
  If no test can kill a mutant because the change has no observable
  effect, add `# slopguard-ignore-mutant(<operator>): <reason>`
  on its line instead. Re-run until none are left.
- Full interface reference: https://www.slopguard.dev/llms-full.python.txt

Agent recipes

# Build an agent work queue: worst methods first, stable IDs included
slopguard-swift analyze --json --quiet \
  | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap)
         | map({id, crap, coverage, file, line})'

# Coverage gaps only: complex code no test touches
slopguard-swift analyze --json --quiet \
  | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)'

# Diff-style check after an agent's change: did max crap go up?
before=$(slopguard-swift analyze --json --quiet | jq '.summary.maxCrap')
# … agent edits code …
after=$(slopguard-swift analyze --json --quiet | jq '.summary.maxCrap')
[ "$(echo "$after > $before" | bc)" = "0" ] || echo "quality regressed"

# Fast pre-commit sweep (no test build): catches new tangles instantly
slopguard-swift analyze --no-coverage --quiet --fail-over 200
# Build an agent work queue: worst methods first, stable IDs included
slopguard-ts analyze --json --quiet \
  | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap)
         | map({id, crap, coverage, file, line})'

# Coverage gaps only: complex code no test touches
slopguard-ts analyze --json --quiet \
  | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)'

# Diff-style check after an agent's change: did max crap go up?
before=$(slopguard-ts analyze --json --quiet | jq '.summary.maxCrap')
# … agent edits code …
after=$(slopguard-ts analyze --json --quiet | jq '.summary.maxCrap')
[ "$(echo "$after > $before" | bc)" = "0" ] || echo "quality regressed"

# Fast pre-commit sweep (no test run): catches new tangles instantly
slopguard-ts analyze --no-coverage --quiet --fail-over 200
# Build an agent work queue: worst methods first, stable IDs included
slopguard-go analyze --json --quiet \
  | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap)
         | map({id, crap, coverage, file, line})'

# Coverage gaps only: complex code no test touches
slopguard-go analyze --json --quiet \
  | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)'

# Diff-style check after an agent's change: did max crap go up?
before=$(slopguard-go analyze --json --quiet | jq '.summary.maxCrap')
# … agent edits code …
after=$(slopguard-go analyze --json --quiet | jq '.summary.maxCrap')
[ "$(echo "$after > $before" | bc)" = "0" ] || echo "quality regressed"

# Fast pre-commit sweep (no test run): catches new tangles instantly
slopguard-go analyze --no-coverage --quiet --fail-over 200
# Build an agent work queue: worst methods first, stable IDs included
slopguard-kotlin analyze --json --quiet \
  | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap)
         | map({id, crap, coverage, file, line})'

# Coverage gaps only: complex code no test touches
slopguard-kotlin analyze --json --quiet \
  | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)'

# Diff-style check after an agent's change: did max crap go up?
before=$(slopguard-kotlin analyze --json --quiet | jq '.summary.maxCrap')
# … agent edits code …
after=$(slopguard-kotlin analyze --json --quiet | jq '.summary.maxCrap')
[ "$(echo "$after > $before" | bc)" = "0" ] || echo "quality regressed"

# Fast pre-commit sweep (no test run): catches new tangles instantly
slopguard-kotlin analyze --no-coverage --quiet --fail-over 200
# Build an agent work queue: worst methods first, stable IDs included
slopguard-python analyze --json --quiet \
  | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap)
         | map({id, crap, coverage, file, line})'

# Coverage gaps only: complex code no test touches
slopguard-python analyze --json --quiet \
  | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)'

# Diff-style check after an agent's change: did max crap go up?
before=$(slopguard-python analyze --json --quiet | jq '.summary.maxCrap')
# … agent edits code …
after=$(slopguard-python analyze --json --quiet | jq '.summary.maxCrap')
[ "$(echo "$after > $before" | bc)" = "0" ] || echo "quality regressed"

# Fast pre-commit sweep (no test run): catches new tangles instantly
slopguard-python analyze --no-coverage --quiet --fail-over 200

Threshold guidance: flag at 30 (the classic CRAP paper threshold), hard-fail CI at 50. With --no-coverage every method reads 0% coverage, so use a much higher fail-over (e.g. 200) for complexity-only sweeps.

Mutation testing recipe

Coverage alone lets an agent write tests that run code without checking it. mutate closes that gap. Its JSON report works as a queue: each mutant carries its exact location, its original and replacement text, and its enclosing method.

  1. Run mutate --json on the code the agent changed.
  2. For each mutant with status survived or no_coverage, the agent writes a test that passes on the real code and fails with that mutant in place.
  3. Re-run mutate until no such mutant is left. A mutant that no test can kill gets the ignore marker and a reason.
# 1. The work queue: every mutant no test catches, with what to test
slopguard-swift mutate --path Sources/MyLib/Store.swift --json --quiet \
  | jq '[.mutants[] | select(.status == "survived" or .status == "no_coverage")
         | {id, method, original, replacement}]'

# 2. The agent writes one test per entry. Each test must fail with its mutant in place.

# 3. Re-run until the queue is empty (jq -e exits 1 while entries remain)
slopguard-swift mutate --path Sources/MyLib/Store.swift --json --quiet \
  | jq -e '.summary.survived + .summary.noCoverage == 0'

# CI: fail the PR when the mutation score drops below 80
slopguard-swift mutate --path Sources --quiet --fail-under 80
# 1. The work queue: every mutant no test catches, with what to test
slopguard-ts mutate --path src/store.ts --json --quiet \
  | jq '[.mutants[] | select(.status == "survived" or .status == "no_coverage")
         | {id, method, original, replacement}]'

# 2. The agent writes one test per entry. Each test must fail with its mutant in place.

# 3. Re-run until the queue is empty (jq -e exits 1 while entries remain)
slopguard-ts mutate --path src/store.ts --json --quiet \
  | jq -e '.summary.survived + .summary.noCoverage == 0'

# CI: fail the PR when the mutation score drops below 80
slopguard-ts mutate --path src --quiet --fail-under 80
# 1. The work queue: every mutant no test catches, with what to test
slopguard-go mutate --path ./internal/auth --json --quiet \
  | jq '[.mutants[] | select(.status == "survived" or .status == "no_coverage")
         | {id, method, original, replacement}]'

# 2. The agent writes one test per entry. Each test must fail with its mutant in place.

# 3. Re-run until the queue is empty (jq -e exits 1 while entries remain)
slopguard-go mutate --path ./internal/auth --json --quiet \
  | jq -e '.summary.survived + .summary.noCoverage == 0'

# CI: fail the PR when the mutation score drops below 80
slopguard-go mutate --path . --quiet --fail-under 80
# 1. The work queue: every mutant no test catches, with what to test
slopguard-kotlin mutate --path src/main/kotlin/com/example/Store.kt --json --quiet \
  | jq '[.mutants[] | select(.status == "survived" or .status == "no_coverage")
         | {id, method, original, replacement}]'

# 2. The agent writes one test per entry. Each test must fail with its mutant in place.

# 3. Re-run until the queue is empty (jq -e exits 1 while entries remain)
slopguard-kotlin mutate --path src/main/kotlin/com/example/Store.kt --json --quiet \
  | jq -e '.summary.survived + .summary.noCoverage == 0'

# CI: fail the PR when the mutation score drops below 80
slopguard-kotlin mutate --path src/main/kotlin --quiet --fail-under 80
# 1. The work queue: every mutant no test catches, with what to test
slopguard-python mutate --path src/pkg/store.py --json --quiet \
  | jq '[.mutants[] | select(.status == "survived" or .status == "no_coverage")
         | {id, method, original, replacement}]'

# 2. The agent writes one test per entry. Each test must fail with its mutant in place.

# 3. Re-run until the queue is empty (jq -e exits 1 while entries remain)
slopguard-python mutate --path src/pkg/store.py --json --quiet \
  | jq -e '.summary.survived + .summary.noCoverage == 0'

# CI: fail the PR when the mutation score drops below 80
slopguard-python mutate --path src --quiet --fail-under 80

Each mutant costs one run of the test suite. Scope --path to the files a change touched, and use --dry-run to count the mutants before a long run.

Agent-ingestible docs

This site publishes its documentation in machine-friendly form, following the llms.txt convention:

URLContents
/llms.txtShort index: what SlopGuard is, the supported languages, and where each full reference lives.
/llms-full.txtThe complete slopguard-swift reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context.
/llms-full.ts.txtThe complete slopguard-ts reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context.
/llms-full.go.txtThe complete slopguard-go reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context.
/llms-full.kotlin.txtThe complete slopguard-kotlin reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context.
/llms-full.python.txtThe complete slopguard-python reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context.

Point your agent at https://www.slopguard.dev/llms-full.txt and it has the complete Swift interface. Both files are plain text/plain markdown with no markup to strip.

Point your agent at https://www.slopguard.dev/llms-full.ts.txt and it has the complete TypeScript interface. Both files are plain text/plain markdown with no markup to strip.

Point your agent at https://www.slopguard.dev/llms-full.go.txt and it has the complete Go interface. Both files are plain text/plain markdown with no markup to strip.

Point your agent at https://www.slopguard.dev/llms-full.kotlin.txt and it has the complete Kotlin interface. Both files are plain text/plain markdown with no markup to strip.

Point your agent at https://www.slopguard.dev/llms-full.python.txt and it has the complete Python interface. Both files are plain text/plain markdown with no markup to strip.