# SlopGuard (Go): complete interface reference (agent-ingestible) slopguard-go measures complex, undertested code in Go modules. It computes a weighted CRAP (Change Risk Anti-Patterns) score combining cyclomatic and cognitive complexity with line coverage, and prints a structured report you can pipe into jq or fail CI on. It is the Go sibling of slopguard-swift and slopguard-typescript: same formula, same JSON schema, same UX. Status: alpha (v0.1.x). The analyzer is stable and self-tested; the CLI surface and JSON schema may move before v1.0. License: MIT. Repo: https://github.com/JeevanThandi/slopguard-go ## Why this matters for coding agents Autonomous/background agents iterate by changing code and running tests. Two codebase properties decide how well that loop works: 1. Coverage: an agent editing untested code cannot tell a fix from a regression. Coverage is the agent's only feedback signal. 2. Complexity: deeply nested, many-branched methods are where agents (like humans) make mistakes. wCRAP captures both in one number per method. Use it as a CI gate (agents cannot merge new slop) or as a work queue (feed the JSON to an agent: "get every method under 30"). ## The formula wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog) - cyc: cyclomatic complexity (McCabe), parsed via the standard library go/ast. Counts if, for, range, each non-default case, and each && / || — comparable to gocyclo. - cog: cognitive complexity (SonarSource 2023 spec): penalises nesting, charges a whole switch/select once, ignores early-exit shapes (plain return/break/continue). - cov: line coverage percentage in [0, 100], gathered by slopguard-go itself by driving the module's own `go test` and parsing the coverage profile. Never user-supplied. It cannot be hand-fed or faked; the only way to lower a score without refactoring is to write tests that actually execute the code. - Semantics: cov = 100 → score collapses to sqrt(cyc × cog) (complexity floor). cov = 0 → full quadratic penalty (cyc × cog) + sqrt(cyc × cog). The cubed factor sharply rewards partial coverage: 50% coverage removes 87.5% of the risk term. - Inputs are clamped (complexity ≥ 0, coverage in [0,100]); score is always ≥ 0. - Default "crappy" threshold: 30 (matches the original CRAP paper). - Weighting rationale: a flat 50-case switch (cyc=50, cog=1) scores like a small method; a deeply nested 3-branch tangle (cyc=3, cog=12) scores like medium-complex code. ## Install 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 (go/ast, go/parser, encoding/json, flag). ## Subcommands | Command | Purpose | |---|---| | analyze | Walk a directory of Go sources, drive `go test` for coverage, emit a wCRAP report (text or JSON). Default subcommand. | | version | Print version metadata as JSON. | `analyze` is the default and `--path` defaults to the current directory; a bare `slopguard-go` in a module root just works. ## analyze: all flags | Flag | Default | Meaning | |---|---|---| | -p, --path | . | Directory of Go sources, or a single .go file. | | -t, --threshold | 30 | wCRAP score above which a method/type is flagged isCrappy. | | --packages | ./... | Go package pattern for the test run and coverage instrumentation (-coverpkg). Anything the selected packages don't exercise reads 0%. | | --project-dir | nearest go.mod | Module root the `go test` run executes in. Walked up from --path when omitted. | | --no-coverage | off | Skip the test run; complexity only. Every method reads 0% coverage (worst-case scores). | | --coverage-file | — | A `go test -coverprofile` file to join instead of running tests. For CI that already produced one. | | --include | — | Glob(s) of files to include. Repeat or pass space-separated. | | --exclude | — | Extra exclude glob(s), combined with 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 | — | Exit 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 stderr progress chatter. Wins over --verbose. Stdout unaffected. | ## Exit codes and streams | Code | Meaning | |---|---| | 0 | Analysis completed; nothing exceeded --fail-over (or flag not passed). | | 1 | Analysis error (bad path, test/coverage failure, parse error). With --json a structured error envelope goes 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 go test with coverage…") go to stderr. Piped stdout is always clean. ## How coverage works Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives `xcodebuild test` itself. slopguard-go drives the module's own `go test`: 1. Module discovery. Walk up from --path to the nearest go.mod (override with --project-dir). 2. Test run. Run `go test -coverprofile=/cover.out -covermode=count -coverpkg= ` (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). A build failure with only a header-only profile aborts with the `go test` output tail. 3. Join. Parse the coverage profile into a per-line index, resolve its import-path file names to disk via the module path from go.mod (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 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. ## 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. Abstract declarations with no body are skipped. Go attaches methods to a type by receiver, not by lexical nesting, so a type's members are gathered package-wide: `func (p *Parser) Parse()` in parse.go and `func (p *Parser) reset()` in reset.go both roll up into the Parser type entry. This receiver-based aggregation is the one structural divergence from the Swift/TypeScript/Kotlin ports (which aggregate by lexical nesting). Default excludes: vendor/, testdata/, *_test.go, generated code (*.pb.go, *_gen.go, mock_*.go, *_string.go, and anything carrying the `// Code generated … DO NOT EDIT.` header), and build dirs. Analyze excluded code with --no-default-excludes. ## JSON report schema (schemaVersion "2", shared with the sibling ports) Top level: schemaVersion, generatedAt, coverageAvailable, summary, methods[], types[]. JSON object keys are emitted in a stable order for diff-stable CI output. summary: - files, types, methods counts + crappy counts - avgCrap, maxCrap, average cyclomatic/cognitive/weighted complexity - weighted coverage across analyzed methods (when gathered) - coverageAvailable: false under --no-coverage or when no coverage data was found methods[], every analyzed function/method: - id: STABLE identifier "file#qualifiedName@line". Track methods across runs with this. - qualifiedName: "Type.method(signature)" for methods, the function name for free functions - file, line, endLine: location relative to analyzed root - kind: function | method (method = function with a receiver) - typeName: receiver type name for methods, null for free functions - complexity: cyclomatic (McCabe) - cognitiveComplexity: SonarSource 2023 - weightedComplexity: sqrt(cyc × cog) - coverage: line coverage percent 0–100 - crap: the wCRAP score - isCrappy: true when crap > threshold types[], per-struct / interface / method-bearing named-type aggregation (rolled up by receiver): - aggregatedCrap: formula applied to the type's totals (total burden, comparable across types) - maxCrap: worst single-method offender ("biggest fire") - isCrappy, weighted coverage, method counts Example (real output from slopguard-go analyzing its own sources, --no-coverage): { "qualifiedName": "enumerate", "file": "core/diranalyzer.go", "line": 95, "kind": "function", "typeName": null, "complexity": 10, "cognitiveComplexity": 26, "weightedComplexity": 16.12, "coverage": 0, "crap": 276.12, "isCrappy": true, "id": "core/diranalyzer.go#enumerate@95" } ## Quickstart commands # 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 don't exercise 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) 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 ## Agent integration ### CLAUDE.md / AGENTS.md drop-in ## 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 ### Recipes # Agent work queue: worst methods first, with stable IDs slopguard-go analyze --json --quiet \ | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap) | map({id, crap, coverage, file, line})' # Coverage gaps: complex code no test touches slopguard-go analyze --json --quiet \ | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)' # Regression check around an agent change before=$(slopguard-go analyze --json --quiet | jq '.summary.maxCrap') # … agent edits code … after=$(slopguard-go analyze --json --quiet | jq '.summary.maxCrap') # Fast pre-commit sweep, no test run (use a high fail-over: coverage reads 0%) slopguard-go analyze --no-coverage --quiet --fail-over 200 ### CI gate (GitHub Actions) name: slop-gate on: [pull_request] jobs: slopguard: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: { go-version: '1.23' } - name: Install slopguard-go run: go install github.com/JeevanThandi/slopguard-go/cmd/slopguard-go@latest - name: Gate on wCRAP run: slopguard-go analyze --path . --fail-over 50 Threshold guidance: flag at 30 (classic CRAP paper threshold); hard-fail CI at 50. Under --no-coverage all methods read 0% coverage, so use a much higher fail-over (e.g. 200) for complexity-only sweeps. ## Security / supply-chain posture - Zero third-party dependencies — only the Go standard library (go/ast, go/parser, encoding/json, flag). - The only subprocess slopguard-go spawns is the module's own `go test`. - No network, no telemetry, no source mutation. - MIT licensed. - Dogfooded: CI holds test coverage ≥95% across all three packages and asserts the analyzer stays under its own threshold against its own sources and the sampleapps/ fixtures. ## Library use Everything the CLI does is exported: import ( "github.com/JeevanThandi/slopguard-go/core" "github.com/JeevanThandi/slopguard-go/coverage" ) report, err := coverage.Run(coverage.PipelineArgs{ SourcePath: "./internal", Coverage: coverage.CoverageSource{Mode: coverage.CoverageAuto}, Options: core.DefaultAnalysisOptions(), }) For complexity-only analysis with no I/O, core.AnalyzeSource([]byte, "file.go") returns the per-file metrics directly.