README
¶
crap
This repository contains two tools:
crapdeterministically calculates cyclomatic complexity and CRAP scores for C#, Go, Rust, TypeScript, and TSX callables.crap-mutateruns a language-native mutation engine and converts its output into one stable report for C#, Go, and TypeScript.
Both tools can run from the command line or as separate MCP stdio servers.
Changelog | Contributing | Security | Support | Releasing
The tools parse native reports or source and calculate every score. An AI caller can choose paths, coverage, a Git diff, and score thresholds, but it does not derive or reinterpret results.
Requirements
- Go 1.25 or newer
- A C compiler available to Go, required by Tree-sitter's CGO bindings
On Windows, install a GCC or Clang toolchain and ensure its compiler is on PATH. On macOS, install the Xcode command-line tools. Most Linux development environments provide GCC through their package manager.
Quick Start
Install both binaries and configure every detected supported MCP client with one command:
npx --yes github:hbaldwin98/crap
The npx bootstrap is not installed globally or written to GOBIN; npm and Go may retain normal cache artifacts. It requires Node.js 20+, Go 1.25+, and a C compiler available to Go for Tree-sitter's CGO bindings. It installs only crap and crap-mutate into GOBIN, or the first GOPATH entry's bin directory when GOBIN is empty. MCP configurations use absolute executable paths, so that directory does not need to be on the MCP client's PATH.
By default, the installer configures Claude Code and OpenCode when their executables are detected. It always writes a client-neutral MCP reference to ~/.config/crap/mcp.json and prints that path, including when no supported client is detected. Re-running the command updates the same binaries and managed config entries.
Select clients explicitly with repeatable or comma-separated --client flags:
npx --yes github:hbaldwin98/crap --client claude,opencode
npx --yes github:hbaldwin98/crap --client generic
npx --yes github:hbaldwin98/crap --client claude --client opencode --dry-run
Supported names are claude, opencode, and generic. Explicit claude selection requires the claude CLI. --dry-run prints commands and config paths without installing or writing files. --version VERSION installs both commands from the same validated module version and defaults to latest.
The short command executes the repository's current default branch and installs the latest Go module revision. For a reviewable, repeatable install, replace REVISION with one commit hash in both positions:
npx --yes github:hbaldwin98/crap#REVISION --version REVISION
OpenCode honors OPENCODE_CONFIG, OPENCODE_CONFIG_DIR, and XDG_CONFIG_HOME. OPENCODE_CONFIG names one exact file. Without that override, the installer updates both opencode.json and opencode.jsonc when both exist because OpenCode loads both in that order. If neither exists, it creates ~/.config/opencode/opencode.json. Existing unrelated settings, MCP servers, and JSONC comments are preserved. The two managed server definitions are replaced, including comments inside those definitions.
The generic config receives the same targeted update: unrelated top-level properties, MCP servers, and JSONC comments are preserved, while comments inside the two managed definitions are replaced. The installer rejects OpenCode and generic destination symlinks before installing anything. Safe replacement preserves ordinary existing file mode bits, but not ownership, ACLs, extended attributes, or other filesystem metadata. Newly created config files use mode 0600 where the operating system supports POSIX modes.
Claude Code is configured only through claude mcp add. If a user-scoped crap or crap-mutate server already exists, the installer leaves it unchanged rather than removing it. Because install paths are stable, this is normally the desired idempotent result. If an existing definition points elsewhere, remove it manually with claude mcp remove --scope user NAME, then rerun the installer.
Before installation, the command parses and renders every config target and checks go env GOOS and go env GOARCH against the running installer. This catches malformed configs, unsafe destinations, and persistent GOENV cross-compilation settings before go install. Immediately before writing a prepared config, the installer also verifies that the file is still byte-for-byte identical to its preflight state, or is still absent if it did not exist. Concurrent changes are preserved and reported with instructions to rerun. A transaction cannot span Go installation, filesystem writes, and external client CLIs. If a later step fails, rerun the command after correcting the error; completed earlier steps are idempotent and are printed as they finish.
To build manually instead, run these commands from the repository root:
go build -o crap ./cmd/crap
go build -o crap-mutate ./cmd/crap-mutate
For a shorter local build and install, use the included Makefile. The default
install location is ~/.local/bin:
make build # writes bin/crap and bin/crap-mutate
make install # installs both commands into ~/.local/bin
make gh-install # installs them and registers gh crap aliases
Override the destination with BINDIR=/path/to/bin make install. After
make gh-install, gh crap ... invokes crap and gh crap-mutate ...
invokes crap-mutate. The target requires GitHub CLI (gh) on PATH and
updates those two project-specific aliases.
On Windows, an explicit .exe name is convenient:
go build -o crap.exe ./cmd/crap
go build -o crap-mutate.exe ./cmd/crap-mutate
Analyze all supported source below the current directory:
./crap .
On Windows PowerShell:
.\crap.exe .
You can also run it from source without building a separate executable:
go run ./cmd/crap --format json .
If no path is supplied, crap analyzes the current directory. Go _test.go, Rust tests/ and benches/, and TypeScript .spec/.test files are excluded by default because tests produce coverage evidence but should not normally be scored as production callables.
CLI Usage
crap [options] [path ...]
crap scope actual --diff-base REVISION [options] [path ...]
crap compare --base REVISION [options] [path ...]
crap graph [options] [path ...]
crap arch [options] [path ...]
crap calls [options] [path ...]
crap mcp
Options:
| Option | Default | Purpose |
|---|---|---|
--format text|json|sarif |
text |
Select human-readable, versioned JSON, or SARIF 2.1.0 output. |
--output PATH |
stdout | Write the report using safe same-directory replacement instead of stdout. |
--coverage PATH |
none | Read Cobertura XML or a native Go coverprofile. |
--diff-base REVISION |
none | Return only callables touching lines changed from a Git revision. |
--threshold SCORE |
30 |
Mark scores strictly greater than this value as above threshold. |
--fail-on-threshold |
false |
Exit with code 2 when any returned callable is above threshold. |
--include-tests |
false |
Include Go _test.go, Rust tests/ and benches/, and TypeScript .spec/.test files. |
--include-generated |
false |
Include recognized generated C# and TypeScript files. |
--exclude PATTERN |
none | Exclude a root-relative gitignore-style pattern; repeat as needed. |
--strict-coverage |
false |
Fail when a supplied coverage report has unmatched or ambiguous source paths. |
--version |
Print the version. | |
-h, --help |
Print help and exit successfully. |
The standard Go flag rules apply: options must precede the first path, and an unknown option in that position is an error. Parsing stops at the first path, so later option-looking values are paths. Use -- to end option parsing explicitly, especially before a path that starts with -. Paths are resolved from the current working directory and can be individual files or directories.
--output creates a temporary file beside the destination and replaces the destination only after the complete report has been rendered, synced, and closed. Existing POSIX permission bits are copied where supported; ownership, ACLs, and other filesystem metadata are not preserved. Destination symlinks, including dangling symlinks, are rejected. Unix uses same-directory rename followed by directory sync; Windows uses MoveFileEx with replace-existing and write-through flags. A render, temporary-file sync, or close failure leaves an existing destination unchanged. Filesystem-specific replacement guarantees still apply. When --output is set, stdout remains empty; diagnostics still go to stderr.
Analyze selected paths with a maximum allowed score of 20:
./crap --threshold 20 --fail-on-threshold ./internal ./cmd
The threshold does not change or cap calculated scores. It sets aboveThreshold on each result and controls --fail-on-threshold. A score equal to the threshold passes; a score greater than it fails.
Source Discovery
Directory analysis honors repository .gitignore files and an optional root .crapignore, both with gitignore pattern semantics. --exclude adds non-negated root-relative exclusions. Explicitly named source files override .gitignore, .crapignore, generated-file, and test-file defaults, but not --exclude; explicitly named unsupported files fail instead of disappearing silently.
Built-in directory exclusions cover .git, node_modules, .next, bin, obj, and Go-style testdata directories at any depth, plus root vendor, dist, build, and coverage. An explicitly named source file remains selectable even inside one of these directories. Generated files such as *.g.cs, *.generated.cs, *.designer.cs, *.d.ts, and *.generated.ts are excluded unless --include-generated is set. JSON reports include a compact discovery section with the selected count and deterministic exclusion counts/examples.
Exit codes:
| Code | Meaning |
|---|---|
0 |
Analysis succeeded and no requested threshold failure occurred. |
1 |
Arguments, source parsing, coverage, Git, or output failed. |
2 |
Analysis succeeded, but at least one score exceeded the threshold while --fail-on-threshold was set. |
SARIF and GitHub Code Scanning
--format sarif emits deterministic SARIF 2.1.0 with one CRAP001 result per callable above the threshold. GitHub locations use root-relative escaped slash URIs and 1-based UTF-16 code-unit columns converted from the analysis report's UTF-8 byte columns. Every result has an explicit start and exclusive end. Result properties contain the CRAP score, complexity, coverage, and threshold. partialFingerprints.primaryLocationLineHash and a tool-specific fingerprint contain the stable callable ID so same-line callables remain distinct.
SARIF rendering validates each location against the current canonical root-relative source file and fails rather than emitting stale or invalid coordinates. GitHub accepts at most 25,000 results per upload; the command returns an output error instead of truncating when that limit would be exceeded.
Generate a file for GitHub code scanning with:
crap --format sarif --output crap.sarif --threshold 20 .
Upload crap.sarif with GitHub's github/codeql-action/upload-sarif action. SARIF is the supported GitHub integration; the CLI does not emit a separate annotation format.
Coverage
Coverage is optional. Without a coverage report, each callable has coveragePercent: null and is conservatively scored as if it had 0% coverage. Reports emit a coverage-not-provided warning so this assumption is visible in JSON, MCP, and text output. When a supplied report leaves one or more callables without attributable coverage, reports emit one coverage-callable-unknown warning per affected source file.
For a Go project, generate a coverprofile and analyze it from that project's root:
go test -coverprofile=coverage.out ./...
crap --coverage coverage.out .
To include coverage for packages that have no direct tests, generate the profile with -coverpkg:
go test -coverpkg=./... -coverprofile=coverage.out ./...
crap --coverage coverage.out .
For C# or TypeScript, export coverage in Cobertura XML format with the test runner or coverage tool used by the project, then pass that file:
crap --coverage coverage.xml src/Example.cs
For Rust, emit Cobertura from a coverage tool such as cargo llvm-cov --cobertura
or cargo tarpaulin --out Xml, then pass that file the same way.
The same command accepts TypeScript coverage from tools that emit Cobertura:
crap --coverage coverage/cobertura-coverage.xml src
For Angular v22, the CLI can run the specs and emit Cobertura without another coverage tool:
ng test --coverage --coverage-reporters=cobertura
crap --coverage coverage/cobertura-coverage.xml --threshold 20 --fail-on-threshold src
Angular writes reports below coverage/; multi-project workspaces may add the project name to the path. Pass the generated cobertura-coverage.xml path to crap. Older Angular projects using Karma may use --code-coverage and need cobertura enabled in their Karma coverage reporter configuration.
The .spec.ts and .test.ts files execute and generate coverage for application code, but crap excludes those test files from scoring unless --include-tests is set. Angular .html templates are not analyzed; CRAP scores cover the TypeScript component, service, directive, pipe, and other callable logic.
Accepted formats:
- Cobertura XML, matched to C#, Go, Rust, TypeScript, or TSX source by normalized file path
- Native Go coverprofiles produced by
go test -coverprofile
Cobertura coverage uses line-hit records. Go coverprofiles use their native statement counts. Callable names and ranges always come from the C#, Go, Rust, or TypeScript AST; Cobertura method names are ignored because instrumentation and source-map processing can rewrite them. A nested callable owns its own lines, so its coverage is excluded from its parent.
Coverage paths are matched by exact normalized path, then by a unique component suffix, then by a unique case-insensitive match. Cobertura <sources> entries and both slash styles are supported. Non-exact matches produce deterministic diagnostics. Unmatched or ambiguous files retain coveragePercent: null and are conservatively scored as 0% coverage; use --strict-coverage in CI to reject those reports instead. A matched file with uninstrumented callables produces coverage-callable-unknown; this warning is suppressed when the file already has an unmatched or ambiguous path diagnostic.
Changed Code
Use --diff-base to report only callables that intersect added or modified lines relative to a Git revision:
./crap --diff-base main --format json .
The command must run in a Git worktree when this option is used. Untracked .cs, .go, .ts, and .tsx files count as entirely changed. Deleted code has no callable in the current source tree, so it is not scored.
A typical changed-code CI check is:
./crap --coverage coverage.out --diff-base origin/main --threshold 20 --fail-on-threshold .
Actual Change Scope
Generate deterministic scope evidence for current source changed from a Git revision:
crap scope actual --diff-base origin/main --format json .
The change-scope v1 report contains selected current-source files with Git-reported changed line ranges, callables intersecting those ranges with their complexity, coverage, CRAP data, and threshold, file-to-callable contains edges, and explicit Git-derived seeds. It fingerprints the same source bytes, coverage artifact, Git commits, grammars, and semantic options used during analysis. Arrays and IDs are canonically ordered, so fixed inputs produce fixed JSON report content.
Git metadata, changed ranges, and source bytes are captured through separate reads. Concurrent repository changes can make one report internally inconsistent; avoid editing or checking out another revision during analysis.
Scope is structural evidence, not a behavioral-impact claim. Version 1 models only current-tree files, changed callable intersections, and file containment. It does not model deleted source, semantic calls, transitive dependencies, framework wiring, or prove that unlisted code is unaffected. A changed file remains in the report when no callable intersects its changed ranges. --format supports text and json; --coverage, discovery options, --output, thresholds, and safe output replacement work as they do for analysis.
Baseline Comparison
Compare the current worktree with source read directly from the Git merge base:
crap compare --base origin/main --format json --fail-on-regression .
crap compare reads baseline blobs with git ls-tree and git cat-file; it does not checkout another revision or modify the worktree, index, or branch. Baseline and current source use the same path, exclusion, test, generated-source, threshold, and strict-coverage options. Coverage artifacts are revision-specific: pass current coverage with --coverage and coverage generated from the exact merge-base source with --base-coverage. One report is never reused for both revisions.
Matching first uses the stable callable ID. Remaining callables match only when the same path-independent language, kind, name, and signature occurs exactly once on each side. Duplicate candidates are reported as ambiguous; the tool does not guess. Reports classify matched, moved, modified, added, removed, and ambiguous callables. Numeric deltas are current minus baseline.
--fail-on-regression exits 2 only when a current callable is newly above threshold: either it was added above threshold or a matched callable crossed from passing to failing with comparable coverage evidence. Existing above-threshold debt and removed callables do not fail the gate. Ambiguous groups make summary.complete false but are not silently classified as regressions. The complete report is written before exit 2. Comparison supports text and json; text omits unchanged rows while JSON retains all evidence.
Unified Review
Run one deterministic review that joins changed-code scope, baseline quality comparison, current graph quality, architecture rules, and optional Go affected-test evidence:
crap review --base origin/main --coverage coverage.out --base-coverage base-coverage.out --format json .
The review command requires --base. It accepts the same source-selection, threshold, coverage, exclusion, generated-source, test, and strict-coverage options as the other analysis commands. --rules evaluates architecture rules, and --call-graph adds compiler-backed affected-test evidence for a Go module. --fail-on-regression exits 2 when the review contains a new CRAP regression.
The review v1 report contains changed files and callables, current coverage and graph summaries, new regressions, architecture cycles and violations, optional affected tests, ranked findings, diagnostics, and limitations. Findings are ordered by new regressions, CRAP threshold findings, low or unknown coverage, affected-test candidates, and architecture evidence. Findings are evidence for review, not claims about runtime behavior or semantic impact. The report is incomplete when its underlying comparison, architecture, scope, or optional call-graph evidence is incomplete or truncated.
Code Graph
Build a deterministic module dependency graph and language-neutral declaration inventory:
crap graph --format json .
The code-graph v1 report contains logical module, file, type-declaration occurrence, and callable nodes. Exact Tree-sitter lexical relationships use contains and declares; files connect to modules through member-of; resolved internal static imports aggregate into module-to-module imports edges. Every recognized import or using occurrence remains available as a resolved, unresolved, or ambiguous reference. Callable and module nodes expose complexity, optional coverage, CRAP, and threshold status. Nodes, edges, references, and candidates are canonically sorted. Full graph construction fails instead of silently truncating above 100,000 nodes, 250,000 edges, 250,000 references, or 64 containment levels. A file root is level 1; the depth limit applies to text, JSON, and MCP output.
Module identity is language-specific: Go packages use the root go.mod module directive plus source directory and package clause; TypeScript and TSX use source-file modules; Rust uses crate::-rooted module paths derived from file layout (src as the crate source root, lib.rs, main.rs, and mod.rs naming their parent); C# uses declared namespaces. Resolution is deliberately bounded to selected repository source. Go workspaces, replacements, build tags, and vendor rules; TypeScript tsconfig, package exports, and path aliases; Rust Cargo.toml metadata, editions, external crates, inline mod blocks, #[path] attributes, and cfg gating; and C# assembly/project binding remain unresolved. Type nodes are source declaration occurrences, not compiler-resolved semantic types. Calls, runtime dispatch, and behavioral impact remain unmodeled. These limits are included in every report.
Graph output supports text and json. The JSON report is the AI-consumable contract for understanding application structure: nodes with module/type/callable identity and metrics, exact lexical contains/declares edges, module member-of and resolved imports edges, and every recognized import or using reference with resolution status, targets, candidates, and reasons. Text summarizes modules, references, and limitations.
Architecture
Evaluate architecture rules against the module dependency graph:
crap arch --format json .
Import cycles are forbidden by default. A rules file adds layering constraints:
{
"schemaVersion": "1",
"forbidCycles": true,
"forbid": [
{
"from": "**/cmd/**",
"to": "**/internal/mutation",
"system": "go-package",
"reason": "commands must go through the service layer"
}
]
}
crap arch --rules arch-rules.json .
Every nontrivial strongly connected component is reported as a cycle with a deterministic edge witness: an ordered list of module dependency edges covering the component, each with the import references that justify it. Forbid rules match module names with anchored globs where * stays within a path segment, ** crosses separators, ? matches one non-separator character, and an empty pattern matches every module; system optionally restricts a rule to go-package, ecmascript-file, rust-module, or csharp-namespace modules. Dependencies are allowed unless a rule forbids them.
The architecture v1 report is pure evidence: module and edge counts, cycle witnesses, and violations sorted by from, to, and kind. Exit code 2 is returned when any violation exists, so the command works as a CI gate. Architecture analysis inherits the code graph's bounded selected-source semantics: unresolved imports are not violations, and static lexical imports never prove runtime behavior. The report and rules JSON schemas are published under schemas/v1.
Call Graph
Build a compiler-backed call graph for the Go module containing the selected paths, and with --diff-base the tests affected by changed callables:
crap calls --format json .
crap calls --diff-base origin/main --format json .
The command walks up from each path to the nearest go.mod, loads the whole module with the Go compiler (GOWORK=off, module mode, test variants included), and resolves call edges from compiler type information. Direct and qualified calls produce static edges; interface and type-parameter method calls produce dispatch edges expanded to every implementation declared inside the module. Calls that cannot be connected stay as explicit unresolved entries with reasons such as builtin, conversion, function-value, outside-module, or outside-function-body. Each edge carries the number of occurrences and up to 25 call sites with file, line, and column.
Functions are joined with the deterministic callable inventory; any callable the compiler walk could not match is counted in unmatchedCallables. Tests are identified by _test.go file suffix and Test name prefix. When --diff-base REVISION is set, changed callables seed a reverse traversal over call edges, and each reachable test is reported with its minimum distance and the seeds that reach it. Affected tests are compiler-fact evidence for selecting a test run; they do not prove behavioral coverage.
The call-graph v1 report includes the compiler and module identity with fingerprints, the resolution, dispatch, and test-identification policies, per-kind edge counts, unresolved calls, affected tests, and explicit limitations. Runtime reflection, function values, cgo calls, and implementations outside the module are not modeled. The JSON schema is published under schemas/v1.
MCP Server
Start the stdio server with:
./crap mcp --root /absolute/path/to/project
Example MCP client configuration:
{
"mcpServers": {
"crap": {
"command": "/absolute/path/to/crap",
"args": ["mcp", "--root", "/absolute/path/to/project"]
}
}
}
Use an absolute executable path and an explicit --root because an MCP client may start the server from a different working directory. On Windows, use paths such as C:\\tools\\crap.exe and C:\\source\\my-project in JSON. Repeat --allow-root PATH to let callers select projects under additional roots. MCP requests cannot read source or coverage files outside the selected authorized root, including through existing symlinks.
The server exposes analysis, change-scope, comparison, and code-graph tools. analyze_code runs one analysis, stores an immutable serialized snapshot, and returns its first page. Its inputs are:
| Input | Type | Default | Purpose |
|---|---|---|---|
root |
string | server --root |
Select an existing project inside a configured --root or --allow-root. |
paths |
string array | root |
Analyze these files or directories, relative to root. |
coveragePath |
string | none | Read Cobertura XML or a Go coverprofile, relative to root. |
diffBase |
string | none | Return only callables changed from this Git revision. |
crapThreshold |
number | 30 |
Mark scores strictly greater than this value as above threshold. |
includeTests |
boolean | false |
Include Go _test.go, Rust tests/ and benches/, and TypeScript .spec/.test files. |
includeGenerated |
boolean | false |
Include recognized generated C# and TypeScript files. |
exclude |
string array | none | Add root-relative gitignore-style exclusions; negated entries are rejected. |
strictCoverage |
boolean | false |
Fail when supplied coverage paths are unmatched or ambiguous. |
resultMode |
string | violations |
Return summary, violations, highest, or all methods. |
limit |
integer | 20 |
Return at most this many methods; maximum 100. |
offset |
integer | 0 |
Deprecated initial-page offset retained for compatibility; prefer continuation cursors. |
Example tool input with a maximum allowed score of 10:
{
"root": "C:\\source\\my-project",
"paths": ["."],
"coveragePath": "coverage.out",
"diffBase": "origin/main",
"crapThreshold": 10,
"resultMode": "violations",
"limit": 20
}
Every response includes reportId, expiresAt, the full analysis summary, coverage diagnostics, and a page object. Methods are sorted by descending CRAP score before pagination. violations returns only methods above the requested threshold, highest and all return all methods, and summary returns no methods. When page.nextCursor is present, pass it by itself to get_analysis_results. A cursor is signed and bound to its report, result mode, offset, and limit. Alternatively, the retrieval tool accepts reportId with a result mode and limit to start that view from its first page. The legacy analyze_code.offset and page.nextOffset fields remain available, but cursors are preferred because they continue the same immutable snapshot. Snapshot pages never rerun analysis and remain unchanged if source or coverage files are modified or deleted. Snapshots expire at expiresAt and may be evicted earlier by bounded server storage.
Check summary.aboveThreshold for the violation count, summary.maximumCrap for the highest actual score, and each returned method's aboveThreshold field. Use the CLI JSON format when one complete unpaged report is required. The MCP server returns findings rather than a process exit code.
The analysis CLI emits analysis report schema v6. The current MCP envelope uses pageSchemaVersion: "4" and reportType: "analysis-page"; historical page schemas v1 through v3 remain published. Canceling an analysis request stops discovery, coverage and Git work, parsing, file dispatch, and initial-page construction as soon as practical. Cancellation and deadline errors observed before snapshot insertion are preserved without returning a partial report. Snapshot insertion is the final commit point; for the same inputs and project state, analyze_code retains idempotent analysis semantics even though each successful call receives a new report ID.
analyze_change_scope accepts the same root, path, coverage, threshold, and discovery inputs, but requires diffBase. It returns the complete change-scope v1 report inside a pageSchemaVersion: "1" envelope with reportId and expiresAt. Pass that ID to get_change_scope to retrieve the immutable report without rerunning Git or rereading source. Scope snapshots share the analysis server's bounded storage and expiration policy. These tools report structural change evidence only; callers must not present containment as semantic impact.
compare_change_scope accepts the same source-selection inputs, requires baseRevision, and accepts independent coveragePath and baseCoveragePath artifacts. It reads merge-base source directly from Git, returns the complete comparison v1 report, and stores an immutable typed snapshot. Pass its ID to get_change_scope_comparison; retrieval does not reread Git, source, or coverage. Comparison snapshot IDs cannot be used with analysis or scope retrieval tools.
review_change accepts baseRevision, current and optional baseline coverage paths, source-selection options, architecture rules, and optional includeCallGraph. It returns an immutable review v1 report joining changed-callable, regression, coverage, architecture, and optional Go affected-test evidence. Use get_review_change with the returned reportId to retrieve the same report without rerunning analysis. Review snapshots use pageSchemaVersion: "1" and the same bounded retention and expiry policy as other analysis snapshots. The report preserves structural and static limitations; callers must not present findings as semantic impact.
analyze_code_graph accepts source-selection, coverage, and threshold inputs, builds one complete structural and module-dependency graph, stores it as an immutable typed snapshot, and returns summary, nodes, edges, or references detail. get_code_graph pages the same snapshot with a signed cursor; it never rereads source. References preserve unresolved and ambiguous import evidence instead of guessing. Pages contain at most 100 records. MCP retention still has a separate 16 MiB serialized-report limit, 30-minute default expiry, eight-entry limit, and 64 MiB total bound.
get_code_graph_neighborhood accepts one through 20 retained node IDs, direction incoming, outgoing, or both, depth zero through five, optional contains/declares/member-of/imports edge filters, and bounded node/edge limits. It returns a coherent subgraph with each node's minimum seed distance and the exact import references supporting retained dependency edges. It never emits dangling edges and reports exact omitted node and edge counts when truncated. Neighborhoods are static structural/dependency evidence, not semantic impact.
Mutation Testing
crap-mutate owns engine selection, invocation, report parsing, score normalization, sorting, and threshold evaluation. An AI caller chooses the project, language, paths, and minimum score; it does not inspect test output and invent a score.
Mutation runs are not inherently deterministic. Flaky tests, timeouts, concurrency, and machine load can change engine results. For a fixed engine report, crap-mutate always emits the same score, counts, ordering, and JSON.
Install Engines
Install only the engines needed by your projects and pin them in each project where possible.
For C#, Stryker.NET currently requires the .NET 10 runtime. A local tool manifest keeps its version in source control:
dotnet new tool-manifest
dotnet tool install dotnet-stryker
dotnet tool restore
For TypeScript, initialize StrykerJS in the target project. This installs the core package and an appropriate test-runner plugin:
npm init stryker@latest
Commit the resulting package lock and Stryker configuration. crap-mutate invokes npx --no-install, so it will not download an unpinned package during a run.
For Go, install a selected Gremlins release and put the gremlins executable on PATH. Record the selected release in project setup or CI rather than silently following the latest release.
Mutation CLI
crap-mutate --language csharp|go|typescript [options] [path ...]
crap-mutate doctor --language csharp|go|typescript [options] [path ...]
crap-mutate mcp
Options:
| Option | Default | Purpose |
|---|---|---|
--language NAME |
required | Select csharp, go, or typescript. |
--format text|json|sarif |
text |
Select findings-oriented text, versioned JSON, or SARIF 2.1.0. |
--output PATH |
stdout | Write the report using safe same-directory replacement instead of stdout. |
--minimum-score SCORE |
80 |
Set the accepted score from 0 through 100. |
--fail-on-threshold |
false |
Exit with code 2 when the score is unavailable or below the minimum. |
--timeout DURATION |
30m |
Stop the engine after a Go duration such as 10m or 1h. |
--workers COUNT |
1 |
Limit parallel Gremlins mutants for Go. |
--test-cpu COUNT |
1 |
Limit CPUs used by each Gremlins Go test process. |
--incremental |
false |
Enable StrykerJS incremental mode. |
--report-path PATH |
StrykerJS default | Read a custom StrykerJS JSON reporter path. |
--dry-run |
false |
Validate inputs and print the exact native command plan without executing it. |
--version |
Print the wrapper version. | |
-h, --help |
Print help and exit successfully. |
Options must precede the first path. Parsing stops at that path, so later option-looking values are paths; use -- to end option parsing explicitly or before a path starting with -. Run from the directory where the native engine normally runs. Stryker.NET usually runs from the C# test project, Gremlins from the Go module root, and StrykerJS from the directory containing its configuration.
For completed mutation runs, --format sarif emits deterministic SARIF 2.1.0 containing actionable survived (MUT001) and uncovered (MUT002) mutants. GitHub locations are 1-based UTF-16 code units with explicit starts and ends. StrykerJS and Stryker.NET ranges are treated as native one-based UTF-16 coordinates and validated against source; zero-length native ranges are highlighted as one source character. Gremlins point columns are converted from Go byte columns and receive a one-character SARIF-only range; normalized mutation reports and IDs remain unchanged. Sources must be existing regular root-relative files and cannot traverse symlinks. partialFingerprints.primaryLocationLineHash and a tool-specific fingerprint contain the stable normalized mutant ID. Engine, status, and minimum-score properties are included. Rendering more than 25,000 actionable results fails instead of truncating. doctor and --dry-run do not accept SARIF because they do not produce mutation findings. Use --output mutation.sarif and GitHub's SARIF upload action for code scanning.
Run crap-mutate doctor --language NAME [path ...] before a first mutation run or after changing engine versions. Doctor checks the executable, a language-specific project marker, and the native version command without running mutation tests. It states the native report contract enforced later when a run is parsed: Gremlins v0.6 JSON, Stryker.NET schema 2, or StrykerJS schema 1.0. The version probe alone does not prove report compatibility. A missing project marker or unverified compatibility is a warning; a missing or failing engine is an error. Add --format json for automation.
--dry-run returns schema-versioned plan JSON or text. The argument list is the same one execution consumes. $REPORT_PATH marks a private temporary report created only during Gremlins and Stryker.NET execution; StrykerJS plans show the configured in-project report path.
# C#: paths become repeated Stryker.NET --mutate options
crap-mutate --language csharp --minimum-score 80 --fail-on-threshold "../Example/**/*.cs"
# Go: run one package directory at a time
crap-mutate --language go --minimum-score 80 --fail-on-threshold ./internal/analysis
# TypeScript: paths become StrykerJS --mutate values
crap-mutate --language typescript --minimum-score 80 --fail-on-threshold "src/**/*.ts" "!src/**/*.spec.ts"
For Go, pass at most one package directory per run. The path must stay within the project root. Run the command once per package when a module has no Go package at its root; Gremlins otherwise scans recursively without collecting useful package coverage. crap-mutate explicitly passes --workers 1 --test-cpu 1 by default so mutation cannot multiply unconstrained Go test workloads. Higher values are opt-in; each value and their product are capped at 16. Configure file exclusions in Gremlins itself. C# and TypeScript paths use each Stryker engine's glob syntax.
The text report prints survived and uncovered mutants. JSON includes every mutant and these fields:
scoreSource: "engine"means the engine supplied the score directly. Gremlins supplies test efficacy asKILLED / (KILLED + LIVED).scoreSource: "report-statuses"means the wrapper applied Stryker's mutation-score categories:(Killed + Timeout) / (Killed + Timeout + Survived + NoCoverage).- Compile errors, ignored mutants, and other non-scorable statuses remain in counts but not the Stryker score denominator.
passedis true when a score exists and is greater than or equal tominimumScore.
StrykerJS writes JSON to reports/mutation/mutation.json by default. If jsonReporter.fileName changes that location, pass the same path with --report-path. This must identify a .json file inside the project root. The wrapper verifies that StrykerJS updated the report during the current run, but it does not delete the previous report. Stryker.NET and Gremlins reports are written to temporary locations and removed after normalization.
Native engine threshold exits are accepted only when the engine produced a current, valid report; crap-mutate then applies minimumScore. A timeout or cancellation always fails the run and terminates the native process tree. If Gremlins finds no mutants, the report has score: null, scoreSource: "unavailable", zero mutants, and passed: false.
Exit codes match the CRAP CLI: 0 for a completed run, 1 for arguments or engine/report failure, and 2 for a requested threshold failure.
Angular
Use StrykerJS's Angular setup and keep Angular's mutation ignorer enabled. A minimal production-source selection is:
{
"$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"mutate": ["src/**/*.ts", "!src/**/*.spec.ts", "!src/test.ts", "!src/environments/*.ts"],
"ignorers": ["angular"],
"reporters": ["json"]
}
Keep the test-runner section generated for the Angular project's Karma, Jest, or other supported setup. Then run:
crap-mutate --language typescript --minimum-score 80 --fail-on-threshold
Mutation MCP Server
Start the separate mutation server with crap-mutate mcp --root /absolute/path/to/project. It exposes run_mutation_tests with these inputs:
| Input | Type | Default | Purpose |
|---|---|---|---|
root |
string | server --root |
Existing project inside a configured authorized root. |
language |
string | required | csharp, go, or typescript. |
paths |
string array | Go root package | C# or TypeScript source paths/globs; required for authorized MCP runs. |
minimumScore |
number | 80 |
Accepted score from 0 through 100. |
timeoutSeconds |
integer | 1800 |
Maximum native engine runtime. |
workers |
integer | 1 |
Parallel Gremlins workers for Go; combined resource product is capped at 16. |
testCpu |
integer | 1 |
CPUs per Gremlins test process for Go; combined resource product is capped at 16. |
incremental |
boolean | false |
Enable StrykerJS incremental mode. |
reportPath |
string | StrykerJS default | Custom StrykerJS JSON report path inside root. |
resultMode |
string | actionable |
Return summary, actionable, or all mutants. |
statuses |
string array | mode default | Override the mode with normalized status filters. |
limit |
integer | 20 |
Return at most this many mutants; maximum 100. |
Example MCP configuration:
{
"mcpServers": {
"mutation": {
"command": "/absolute/path/to/crap-mutate",
"args": ["mcp", "--root", "/absolute/path/to/project"]
}
}
}
run_mutation_tests executes the engine once, retains an immutable normalized report for 30 minutes, and returns a compact first page plus reportId. The paging envelope has pageSchemaVersion: "2" and reportType: "mutation-page"; the nested schemaVersion identifies mutation report v3. Actionable mode returns survived and uncovered mutants. Use the read-only get_mutation_results tool with page.nextCursor for continuation pages, or with reportId and a new mode/status filter to start another view. Snapshots are bounded and may be evicted; rerun the mutation tool when a report is expired or unavailable.
The read-only plan_mutation_run tool validates the same authorized inputs and returns the native command plan without execution. check_mutation_setup additionally executes the native version probe and returns project, executable, version, and report-contract checks. Because a project-local version command can execute project code, clients must not treat this tool as read-only or auto-approve it. These tools accept the setup fields through reportPath; paging fields apply only to run_mutation_tests and get_mutation_results.
MCP root checks prevent caller-supplied paths and report locations from escaping configured roots, including through existing symlinks. Authorized mutation globs reject brace expansion and wildcard scopes containing symlinks. These checks are not a process sandbox or a defense against concurrent filesystem replacement: mutation engines, build scripts, and project tests execute with the server process's filesystem and network privileges. Run crap-mutate mcp only for trusted projects and accounts.
Example tool input:
{
"root": "C:\\source\\angular-app",
"language": "typescript",
"paths": ["src/**/*.ts", "!src/**/*.spec.ts"],
"minimumScore": 80,
"incremental": true,
"timeoutSeconds": 3600
}
Both MCP servers publish initialization instructions that tell capable clients when to call their tool and forbid AI-estimated scores. These instructions are guidance, not enforcement; project-level agent rules remain the reliable way to require a tool call in a specific workflow.
Report Contracts
JSON outputs carry reportType, schemaVersion, one shared tool version, coordinate semantics, and deterministic fingerprints. Analysis is v6, actual change scope is v1, change-scope comparison is v1, unified review is v1, code graph and code-graph neighborhoods are v1, architecture reports and rules inputs are v1, Go call graphs are v1, mutation is v3, mutation plans are v2, and mutation doctor output has its independent v1 contract. Incompatible changes increment the contract that changed; MCP envelope versions are independent from their underlying report versions. Analysis v5 added Go function literals and C# lambdas, anonymous methods, and expression-bodied properties/indexers. Analysis v6 adds deterministic source-discovery policy and metadata.
Normalized analysis coordinates are 1-based UTF-8 byte columns with exclusive ends. Normalized mutation coordinates remain engine-native. SARIF is a separate presentation contract and converts or validates both forms as 1-based UTF-16 code units for GitHub. Analysis callable names, signatures, and ranges come from the language AST. Callable IDs hash language, normalized file path, kind, lexical signature, and same-signature occurrence, so inserting blank lines above a callable does not change its ID. Mutation wrapper IDs hash normalized file, range, mutator, and replacement; Stryker's nativeId and status do not affect them.
fingerprints.sources contains normalized displayed paths and exact source-byte SHA-256 digests. Coverage metadata identifies none, cobertura, or go-coverprofile and can include the original displayed path and exact-byte digest. Fingerprints also cover the native mutation report, resolved Git commits when changed analysis is used, and semantic options. Absolute checkout roots and temporary report paths are not serialized into fingerprints. No timestamp is added to deterministic CLI reports; MCP mutation snapshot expiry remains envelope metadata.
Published JSON Schema 2020-12 files are under schemas/v1, with matching golden examples under testdata/contracts. The schemas reject unknown properties and constrain versions, enums, bounds, nullability, and SHA-256 formats.
Change Intelligence Roadmap
Change intelligence will grow from deterministic evidence rather than inferred impact claims:
- Completed: baseline comparison and new-regression gates.
- Completed: lexical language-neutral file, type-declaration, and callable inventory.
- Completed: bounded selected-source module dependency graph with explicit unresolved references.
- Completed: AI-consumable
crap graphJSON and text structural contract. - Completed: architecture rules and cycle proofs.
- Completed: compiler-backed call relationships and affected-test traversal for Go modules.
- Completed: unified review joining scope, quality, tests, and architecture evidence.
Each phase must keep engine facts, unresolved edges, and AI or user judgment distinct.
Score Definition
The score uses the original CRAP formula:
CRAP = complexity^2 * (1 - coverage)^3 + complexity
coverage is a fraction from 0 to 1. Scores and percentages are rounded to two decimal places. CRAP is not a percentage: it combines cyclomatic complexity with test coverage. Higher complexity and lower coverage produce a higher score.
For example, a callable with complexity 4 and 0% coverage scores 20:
4^2 * (1 - 0)^3 + 4 = 20
Complexity starts at 1 for each callable. Every listed branch adds exactly 1, including each logical operator in a compound expression. Branches inside a nested callable belong only to that nested callable.
C# callables are methods, constructors, destructors, operators, local functions, accessors, lambdas, anonymous methods, and expression-bodied properties and indexers. C# adds one for each if, for, foreach, while, do, catch, switch expression arm, ternary, and/or pattern, and &&, ||, or ?? expression. Traditional switch statement labels do not add complexity. Nested callables are scored separately.
The bundled C# grammar supports C# 1 through C# 13. The analyzer rejects syntax errors rather than silently producing a partial score.
Go callables are functions, methods, and function literals. Go adds one for each if, for, non-default expression/type/communication case, and && or || expression. Nested function literals are scored separately.
Rust callables are functions and closures. Rust adds one for each if, while, for, non-catch-all match arm, ? operator, and && or || expression. Trait method signatures without a body are not callables, and loop is unconditional and adds nothing. Names are ::-qualified by enclosing mod, trait, and impl scopes, with trait impls written as <Type as Trait>::name. Macro bodies are not expanded, so complexity generated by a macro invocation is not counted. Nested closures are scored separately.
TypeScript and TSX callables are functions, generator functions, methods, function expressions, generator function expressions, and arrow functions. They add one for each if, for, for in/for of, while, do, catch, non-default case, ternary, and &&, ||, or ?? expression. Nested callables are scored separately.
Files and callables are sorted by normalized path and source line. JSON contains no timestamps or environment-dependent IDs. Invalid syntax fails analysis instead of returning a partial score.
Development
Run the automated checks from the repository root:
go test ./...
go test -race ./internal/mutation ./internal/mutationmcp ./cmd/crap-mutate
go vet ./...
go build ./...
go test -coverpkg=./... -coverprofile=coverage.out ./...
go run ./cmd/crap --coverage coverage.out --threshold 30 --fail-on-threshold .
go run ./cmd/crap compare --base HEAD^ --threshold 30 --fail-on-regression .
go run ./cmd/crap review --base HEAD^ --threshold 30 --fail-on-regression .
CI generates coverage for both the current source and a temporary archive of merge-base source, then fails only for new CRAP regressions above 30.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
crap
command
|
|
|
crap-install
command
|
|
|
crap-mutate
command
|
|
|
internal
|
|
|
scripts
|
|
|
package
command
|