Documentation

Everything the CLI accepts, everything it emits, and how to wire it into an autonomous coding agent's loop. SlopGuard is alpha (v0.1). The JSON schema is 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.

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.1.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 land before v0.2.

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.1.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.1.x is alpha. The analyzer is stable and self-tested (CI holds coverage ≥95% across all three 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.1.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.1.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
# 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
# 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
# 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
# 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

# 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.
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(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 (vendor, testdata, *_test.go, 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 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 / .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.

Exit codes & streams

CodeMeaning
0Analysis completed; nothing exceeded --fail-over (or the flag wasn't passed).
1Analysis error: bad path, test/coverage failure, parse error. With --json, a structured error envelope is written to stderr.
2Quality gate tripped: at least one method's wCRAP exceeds --fail-over.

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.

JSON report schema

Stable, versioned, and shared byte-for-byte across slopguard-swift, slopguard-ts, slopguard-go, and slopguard-kotlin. The same downstream tooling reads all four. 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
}

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 / .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%).

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).

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.
- 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.
- 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.
- 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.
- 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.
- 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.

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.