See cyclomatic paths and cognitive load in Go code in a TUI or hand it to an agent
Install cyclo globally with Go:
go install github.com/shanejonas/cyclo@latestScan the current directory:
cyclo .Pass one or more files or directories to scan them instead:
cyclo ./domain ./adaptersThe treemap combines both complexity scores: tile area shows cyclomatic complexity, and color shows cognitive complexity from green to red. Files group their function tiles. Click a tile to inspect its source and both scores. The map appears in terminals at least 100 columns wide and 30 rows tall.
Inside Git, Cyclo automatically shows added and deleted source lines against main or master.
Cyclo starts a localhost JSON-RPC control API on port 8197. Pick another port with --control-port:
cyclo --control-port 9000 .Ask the running app for its OpenRPC document:
curl -s http://127.0.0.1:9000 \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"rpc.discover"}'The control API reads and changes the live TUI model. cyclo.getReport returns
complexity scores and a typed quality report with deterministic diagnostics and
effect counts. Each analyzed function includes quality metrics, diagnostics,
effects, and mutation events. cyclo.getState includes this evidence alongside
the selected function's source. Agents can focus panes, select files and functions,
scroll source, refresh analysis, and wait for a new revision without polling.
See application/openrpc.json for the full contract.
Print the operating skill for coding agents:
cyclo --skillgo build -o cyclo .
./cycloRun typed mutation and side-effect analysis without starting the TUI:
cyclo check .
cyclo check --format json ./domain ./adapters
cyclo check --config examples/quality/cyclo.toml .Directories include Go packages recursively. File arguments load their enclosing
packages for type information, then report only the selected files. Run from the
repository root; inputs must be inside that root. Analysis respects Go's active
build configuration. Use --tests to include tests and --tags for build tags.
Generated files are excluded. Package/type errors fail the check.
The TUI also runs quality analysis. Its header shows Q with the report's finding
count; the selected source title shows its function's count. Press e to switch
the details pane between source and quality evidence, then j/k to scroll.
Switching back preserves the source cursor. Use a policy in the TUI with:
cyclo --config examples/quality/cyclo.toml .The TUI includes tests to match its existing complexity scan. Generated functions,
inactive build files, and nested literals that lack separate typed records show
quality as unavailable. Type-check failures appear as quality failed, with the
error in the quality pane; the syntax-based complexity view remains usable.
RPC exposes the same data through cyclo.getReport.quality and
cyclo.getState.selection.function.quality. Report quality has status (ready
or error), an optional error, and the successful report. Nested quality fields
use the same snake_case names as cyclo check --format json. Switch the shared
details pane with cyclo.setDetailsView {"view":"quality"} or {"view":"source"}.
cyclo.getState.report.quality contains status, aggregate statistics, and a
finding count; the full report comes from cyclo.getReport.
State includes detailsView and qualityOffset. cyclo.revealLines and
cyclo.scrollSource return to source view. Refresh reruns both analyses.
| Rule | Default |
|---|---|
fn_length |
More than 50 code lines, excluding comments and blank lines |
fn_params |
More than 4 parameters, excluding the receiver |
mutation_per_target |
More than 3 writes to a target |
mutated_targets |
More than 3 distinct targets |
side_effect_density |
More than 500 milli, with at least 3 statements |
invalid_suppression |
Malformed suppression, unknown rule, or missing reason |
Mutation includes assignments to existing bindings, increments, field/index/
pointer writes, and append, copy, delete, and clear. Initial bindings do
not count. Shadowed variables have separate identities. Global reads/writes are
global effects instead of budgeted mutations. Nested closure bodies contribute
to their enclosing function even if not invoked; package-level function literals
get their own records.
Mutation provenance is local, external, or unknown. Owned local writes
count toward mutation budgets but do not add side effects. Writes through shared
parameters and their aliases add mutation effects. Conflicting alias assignments,
reference fields with unresolved ownership, and unresolved calls remain unknown.
Pointer-receiver calls are not automatically treated as writes.
Effects use resolved import/type/method paths and the longest matching config
prefix. os/exec.Cmd.Output is IO; os/exec.Command is a builder. Unclassified
calls add unknown effects unless a same-package helper body establishes their
effects. Local body summaries propagate effects; there is no cross-package effect
inference. Goroutines and channel operations also report unknown effects. Zero
density means no effects were detected under the policy, not proof of purity.
complete means no unknown effects remain under the policy; it is not a
whole-program correctness claim.
Density is 1000 * sum(effect weights) / max(statements, 1) with integer division.
Weights default to mutation 1, IO/network 3, global 2, unsafe 4, time/random 1,
panic 0, unknown 1. Go statements count recursively; blocks, labels, and case
clauses do not add extra statements. Pure arithmetic scores zero.
TOML config retains unspecified defaults and rejects unknown keys. Project
prefixes extend defaults; later entries win equal-length ties. Kind none
explicitly excludes a call from scoring. See the example policy.
Suppress selected rules with a reason:
// cyclo-allow(fn_params): stable public interface
func Export(a, b, c, d, e int) {}Doc comment lines may appear between the suppression and function. Invalid suppressions never suppress findings.
Save facts and change policy without reloading or type-checking the code:
cyclo check --format facts . > /tmp/cyclo-facts.json
cyclo check --facts-in /tmp/cyclo-facts.json --config examples/quality/cyclo.toml --format jsonReports contain relative paths and no timestamps. Exit codes are 0 for no findings or facts export, 1 for findings, and 2 for configuration, analysis, or IO failure. Start with advisory reporting and review the source evidence before using density as a CI gate.
Static calls to named helpers in the same package now use their body summaries. Effect-free helpers contribute no effects; effectful helpers contribute evidence at the caller line. Recursive cycles, dynamic dispatch, unavailable bodies, and summary limits remain unknown. Explicit prefix rules still take precedence. Parameter writes are propagated conservatively without argument ownership remapping. Summaries stop at 32 active helpers or 4,096 summarized effects and retain unknown coverage when a limit is reached.
Facts exports use schema_version: 2 and retain a flat graph of helper bodies for
policy reevaluation, including helpers outside a selected file. Version 1 inputs
remain readable; missing helper evidence stays unknown. Report JSON retains
schema version 1. Older Cyclo builds do not accept the new facts format.
Run the generated quality harness with
go run ./examples/quality-hunt to check ownership behavior and equivalent
syntax across a deterministic corpus. Mismatches can be automatically reduced
with Tree-sitter. The generated checks also run in normal CI.
Try the dashboard with a bundled input and checker from the repository root:
sh examples/bug-reducer/demo.shEach demo run saves to a fresh temporary directory, so the command is repeatable. See the demo guide for details.
bug-reducer works with inputs for any codebase. Supply a command that confirms
one specific bug; the checker decides what counts as a bug. Default reduction
removes whole lines. Go mode uses Tree-sitter to remove complete declarations
and statements (including adjacent groups), reparsing each candidate before invoking the checker.
The filenames below are placeholders: supply an existing input and checker.
cyclo bug-reducer failing-input -- ./check-bug.sh
# Run directly from this repository:
go run . bug-reducer failing-input -- ./check-bug.shFor Go source, install the Tree-sitter CLI and configure a Go grammar, or supply an existing Go parser dynamic library explicitly:
cyclo bug-reducer --language go --go-parser /path/to/go.so --tui=false failing.go -- ./check-bug.shOmit --go-parser when Tree-sitter resolves the configured source.go grammar.
The Go reducer tries larger syntax units first and reparses after each accepted
deletion. Syntax errors skip the checker; build errors and the specific bug still
need to be checked by your command. This mode has no line-deletion fallback.
Tree-sitter is a runtime dependency for Go reduction; Cyclo builds still support
CGO_ENABLED=0. See the Tree-sitter CLI documentation.
The checker receives an absolute candidate file path as its last argument, following any arguments you supply. It runs in the directory you launched Cyclo from, inherits your environment, and receives no stdin. Its output appears in the live dashboard; plain mode suppresses it. Exit 0 means the same bug remains; an ordinary nonzero exit rejects the candidate. The checker must reject unrelated syntax errors or other failures. For project-dependent tests, the checker arranges its own build/test workspace or overlay using that candidate path. Candidates are temporary single files, not copies of the entire project.
The command saves to failing-input.reduced, preserves the original, and refuses
to overwrite an existing output. Flags go before the input:
cyclo bug-reducer --timeout 30s --output smaller.json failing.json -- ./check-bug.sh --strict
cyclo bug-reducer --helpIn a terminal, the reducer opens a live dashboard inspired by Shrink Ray:
- Statistics: best size, bytes removed, elapsed time, checks, and current chunk size.
- Size over time: the size of the best accepted input throughout the run.
- Recent reductions: accepted deletions with the removed lines.
- Checker output: live stdout and stderr from the current check (last 16 KiB).
Use tab / shift+tab to select a pane, j/k or arrow keys to scroll, and
enter to expand the selected pane. Small terminals show one pane at a time.
Press q or Ctrl-C to stop, clean up the checker, and save the best accepted
input. Completed runs stay open for inspection; q closes the dashboard.
Redirected output stays plain. Use --tui to force the dashboard or
--tui=false for the original summary-only behavior:
cyclo bug-reducer --tui=false failing-input -- ./check-bug.shInput must be a regular file; FIFOs, devices, and directories are rejected before reading. Symlinks to regular files are supported.
The timeout applies to each checker invocation. Timeout, Ctrl-C, Unix SIGTERM, checker signal, or launch failure stops reduction; once the seed is accepted, the best accepted candidate is saved before the command reports the error. If the seed is rejected, no output file remains. On Unix, cancellation and normal checker completion clean up its process group, including ordinary child processes; descendants that detach into another group are outside that scope. Other platforms currently cancel only the direct checker process. Checkers should wait for their own subprocesses and avoid leaving background jobs running.
Default reduction deletes chunks of whole lines until no single remaining line can be removed while preserving the check. Go reduction tries declarations, statements, and groups of adjacent units; it also handles statements sharing a line. Neither mode promises a global minimum. Use deterministic checks. A permissive line-mode check can accept an empty file.
For a quick non-Go smoke test (the marker stands in for a bug):
printf 'noise\nspecific failure\nmore noise\n' > /tmp/reducer-demo.txt
cyclo bug-reducer /tmp/reducer-demo.txt -- grep -Fq -- 'specific failure'
cat /tmp/reducer-demo.txt.reduced
# specific failureFinding bugs still needs tests, fuzzing, or inspection. For Cyclo, a checker can compare the analyzer's reports for equivalent Go forms, or assert a known wrong score, source location, or panic. Keep each check focused on the specific failure; use the reduced input as the regression test after fixing it.
To check a specific Go input for initializer inconsistencies and reduce failures:
go run ./examples/cyclo-hunt path/to/input.goSee the Cyclo checker guide for its supported inputs and checks. The exploratory fixture generators have been removed.