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,??, plainreturn). 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 (
??, plainreturn); charges a wholeswitchonce. - 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. Countsif,for,range, each non-defaultcase, and each&&/||— comparable togocyclo. - cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; charges a whole
switch/selectonce; ignores early-exit shapes (plainreturn/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-elsewhenbranch,catch, the elvis?:, and each&&/||. - cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; charges a whole
whenonce; ignores early-exit shapes (plainreturn/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. Countsif/elif,for/while, eachexcept, the ternary, eachand/or, comprehension clauses, andmatchcases — comparable tomccabe. - cog: cognitive complexity per the SonarSource 2023 spec. Penalises nesting; charges a whole
matchonce; ignores early-exit shapes (plainreturn/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
| Command | Purpose |
|---|---|
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.
|
version | Print version metadata as JSON (name, version, …). |
analyze: all flags
| Flag | Default | Meaning |
|---|---|---|
-p, --path <path> | . | Directory of Swift sources, or a single .swift file. |
-t, --threshold <n> | 30 | wCRAP score above which a method/type is flagged isCrappy. |
--scheme <name> | auto | xcodebuild 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=macOS | xcodebuild destination string, e.g. 'platform=iOS Simulator,name=iPhone 16'. |
--project-dir <path> | cwd | Directory passed as cwd to xcodebuild. |
--no-coverage | off | Skip 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-excludes | off | Start with an empty exclude list, e.g. to analyze test code itself. |
--json | off | Emit 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, --verbose | off | Stream xcodebuild output and subprocess chatter to stderr. |
--quiet | off | Silence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected. |
| Flag | Default | Meaning |
|---|---|---|
-p, --path <path> | . | Directory of TypeScript/JavaScript sources, or a single source file. |
-t, --threshold <n> | 30 | wCRAP score above which a method/type is flagged isCrappy. |
--runner <vitest|jest> | auto | Test runner to drive for coverage. Auto-detected from the project when omitted. |
--project-dir <dir> | nearest package.json | Directory the test runner executes in. Defaults to the nearest package.json above --path. |
--no-coverage | off | Skip 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-excludes | off | Start with an empty exclude list, e.g. to analyze test code itself. |
--json | off | Emit 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, --verbose | off | Stream test-runner output and subprocess chatter to stderr. |
--quiet | off | Silence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected. |
| Flag | Default | Meaning |
|---|---|---|
-p, --path <path> | . | Directory of Go sources, or a single .go file. |
-t, --threshold <n> | 30 | wCRAP 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.mod | Module root the go test run executes in. Walked up from --path when omitted. |
--no-coverage | off | Skip 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-excludes | off | Start with an empty exclude list, e.g. to analyze test code itself. |
--json | off | Emit 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, --verbose | off | Stream go test output and subprocess chatter to stderr. |
--quiet | off | Silence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected. |
| Flag | Default | Meaning |
|---|---|---|
-p, --path <path> | . | Directory of Kotlin sources, or a single .kt file. |
-t, --threshold <n> | 30 | wCRAP score above which a method/type is flagged isCrappy. |
--coverage-tool <kover|jacoco> | kover | Coverage tool to drive. Both emit the same JaCoCo-format XML, so either is joined by one parser. |
--gradle-test-task <task> | test | Gradle test task to run, e.g. testDebugUnitTest for an Android variant. |
--gradle-report-task <task> | koverXmlReport | Gradle task that emits the coverage XML (jacocoTestReport under --coverage-tool jacoco). |
--project-dir <path> | nearest Gradle root | Directory the Gradle run executes in. Walked up from --path when omitted. |
--no-coverage | off | Skip 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-excludes | off | Start with an empty exclude list, e.g. to analyze test code itself. |
--json | off | Emit 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, --verbose | off | Stream Gradle output and subprocess chatter to stderr. |
--quiet | off | Silence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected. |
| Flag | Default | Meaning |
|---|---|---|
-p, --path <path> | . | Directory of Python sources, or a single .py file. |
-t, --threshold <n> | 30 | wCRAP score above which a method/type is flagged isCrappy. |
--runner <pytest|unittest> | auto | Test runner to drive for coverage. Auto-detected from the project when omitted. |
--project-dir <path> | nearest project root | Directory the test run executes in. Walked up from --path (pyproject.toml / setup.py / setup.cfg / .git) when omitted. |
--no-coverage | off | Skip 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-excludes | off | Start with an empty exclude list, e.g. to analyze test code itself. |
--json | off | Emit 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, --verbose | off | Stream test-runner output and subprocess chatter to stderr. |
--quiet | off | Silence all progress chatter on stderr. Wins over --verbose. Stdout output is unaffected. |
Exit codes & streams
| Code | Meaning |
|---|---|
0 | Analysis completed; nothing exceeded --fail-over (or the flag wasn't passed). |
1 | Analysis error: bad path, test/coverage failure, parse error. With --json, a structured error envelope is written to stderr. |
2 | Quality 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
files,types,methods: counts, plus crappy counts.avgCrap,maxCrap: distribution of wCRAP across all methods.- weighted coverage across analyzed methods, when coverage was gathered.
coverageAvailable:falseunder--no-coverageor when no coverage data was found.
methods[]
Every analyzed function, method, constructor, and accessor:
| Field | Meaning |
|---|---|
id | Stable identifier: file#qualifiedName@line. Track a method across runs with this. |
qualifiedName | Type.method(signature). |
file, line, endLine | Location 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.
|
complexity | Cyclomatic complexity (McCabe). |
cognitiveComplexity | Cognitive complexity (SonarSource 2023). |
weightedComplexity | sqrt(cyc × cog): the input the formula squares. |
coverage | Line coverage percentage, 0–100. |
crap | The wCRAP score. |
isCrappy | true when crap > threshold. |
types[]
aggregatedCrap: the formula applied to the type's totals (total burden, comparable across types).maxCrap: the worst single-method offender in the type (the "biggest fire" metric).isCrappy, weighted coverage, method counts.
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)
- cov = 100 → the risk term vanishes; the score collapses to
sqrt(cyc × cog), complexity alone. - cov = 0 → full quadratic penalty:
(cyc × cog) + sqrt(cyc × cog). - The cube sharply rewards even partial coverage: at 50% coverage the untested factor is
0.5³ = 0.125, so 87.5% of the risk term is gone. - Inputs are clamped: negative complexity → 0; coverage → [0, 100]. The score is always ≥ 0.
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
--workspaceso xcodebuild builds the right thing. --no-coverageskips 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:
- Project discovery. Walk up from
--pathto the nearestpackage.json(override with--project-dir). - Runner detection. Look for vitest or jest signals: a config file, the dependency itself, a
jestkey in package.json, or the runner named in thetestscript. Both detected →runner_ambiguous(pass--runner); neither →runner_not_detected(pass--runner,--coverage-file, or--no-coverage). - Test run. Spawn the project-local binary (
node_modules/.bin/<runner>) with flags that force an istanbulcoverage-final.jsoninto 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). - 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:
- Module discovery. Walk up from
--pathto the nearestgo.mod(override with--project-dir). - Test run. Run
go test -coverprofile=… -covermode=count -coverpkg=<packages>(default./...) into a slopguard-owned temp directory.-coverpkginstruments every selected package, so coverage from cross-package tests counts. Failing tests don't abort — partial coverage is still useful (a note is attached). - 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:
- Project discovery. Walk up from
--pathto the nearest Gradle root (settings.gradle[.kts],gradlew, orbuild.gradle[.kts]). Override with--project-dir. - Test run. Run the project's own
./gradlew <test-task> <report-task> --continue. Pick the tool with--coverage-tool: Kover (default, report taskkoverXmlReport) or JaCoCo (jacocoTestReportwith XML output enabled). Override either default with--gradle-test-task/--gradle-report-taskfor Android variant tasks. Failing tests don't abort (--continue). - 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:
- Project discovery. Walk up from
--pathto the nearest project root (pyproject.toml/setup.py/setup.cfg/.git). Override with--project-dir. - 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. - 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, thencoverage json. Failing tests don't abort; partial coverage is still useful (a note is attached). - Join. Parse the
coverage jsonreport 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:
- As a gate: agents can't merge new slop. Run
--fail-overin CI; exit code 2 blocks the PR. - As a work queue: feed the JSON to an agent and have it burn the leaderboard down: write tests for crappy methods, refactor the tangles, re-run to verify the score dropped.
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:
| URL | Contents |
|---|---|
/llms.txt | Short index: what SlopGuard is, the supported languages, and where each full reference lives. |
/llms-full.txt | The complete slopguard-swift reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context. |
/llms-full.ts.txt | The complete slopguard-ts reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context. |
/llms-full.go.txt | The complete slopguard-go reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context. |
/llms-full.kotlin.txt | The complete slopguard-kotlin reference as a single markdown document: flags, schema, exit codes, agent recipes. One fetch, fully in context. |
/llms-full.python.txt | The 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.