Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

code-metrics

A Claude Code plugin that measures the shape of a change and cites where every number comes from. Lines per file, cyclomatic and cognitive complexity, Halstead difficulty, duplication, coverage per function with CRAP, and type debt, each as a read-only audit that reads source files and existing build artifacts, runs an external collector only when one already resolves, and prints a report whose references carry their provenance. It never runs tests, never edits code, never installs a tool, and never renders a keep-or-retire verdict: a reference here is a value to count against, not a bar.

The skills

Skill What it does
/code-metrics:audit-complexity Per-function cyclomatic and cognitive complexity and Halstead difficulty from whichever collector resolves (lizard, radon, ESLint rules, gocyclo, gocognit, shellmetrics, multimetric), beside the ISO/IEC 5055 §8.2.117 reference of 20 with 10 and 15 selectable; cognitive and Halstead carry no standard threshold.
/code-metrics:audit-size Lines per file (total, blank, comment, code through scc; total and non-blank from a bundled counter otherwise) beside a cited reference; size.mode: iso-8.2.115 adds the ISO function-percentage form.
/code-metrics:audit-duplication Clone classes (the detector's pairs merged; duplicated lines and tokens, every instance's range) from jscpd, dupl, or PMD CPD, rolled up per lane and per directory, minus the replication the repository declares in a sanctioned-replication registry (a path-within-plugin, or a canonical -> copies cluster line), which is an exclusion, not a suppression. A file over the size cap is reported as skipped, never silently dropped.
/code-metrics:audit-coverage Line coverage per file and per function read from the artifacts a build already produced (lcov 1.x and 2.2, Cobertura, coverage.py JSON, Go cover profile), plus CRAP per function from the complexity rows; it never runs a test, a missing artifact is a visible warning, and a function with no executable lines reports null, never zero.
/code-metrics:audit-type-debt The typed-code percentage per file and per lane: type-coverage for TypeScript, mypy's --any-exprs-report for Python; no standard or CWE anchors the measure, so the reference is null by design. C# is reported as not applicable.
/code-metrics:principles Metric literacy: what each measure can and cannot tell you, where every reference value came from, CRAP's corrected provenance, the cross-metric caveats (carried once, here), and gated pointers to the plugins that own mutation score, tautological tests, dead code, coupling, and lint.
/code-metrics:setup check probes the interpreter, every configuration layer, and every collector; apply writes the tracked team configuration per key, idempotently, and never installs a tool.

Works in any repo

Lanes are detected from file extensions (TypeScript/JavaScript, Python, Bash, Go, and C#, whose complexity lane is deferred and reported as such). Every other text file, markdown, JSON, YAML, PowerShell, a Makefile, lands in the catch-all other lane, which carries a line count and nothing else: audit-size measures it, and every other measure reports it as not applicable. When the consuming repository tracks .claude/ecosystems/<lane>.yaml files, their globs override the bundled map for that lane. Two scopes are first-class: the default is the change, files that differ from the merge-base with the default branch plus uncommitted and untracked files, and --all is the whole tree (every tracked or untracked-but-not-ignored file); explicit paths narrow either. Nothing depends on a framework, a build system, or the publisher.

Requirements

  • Bash 4 or later and Python 3.9 or later (python3, python, or py -3), plus git for change scope. macOS ships bash 3.2, so install a current bash (brew install bash); Windows needs Git Bash. Every entry point checks the bash version and stops with that remediation. Python is required for correctness: every entry point stops with a remediation message when it is absent.
  • Collectors, all optional. A lane whose collector is absent reports unavailable with the install hint and the run continues; nothing is installed on your behalf.
Collector Used for Install
scc comment-aware line counts, every lane go install github.com/boyter/scc/v3@latest, brew install scc, or a release binary
lizard cyclomatic complexity and function ranges for TypeScript/JavaScript, Python, Go pip install lizard or pipx install lizard
radon Python cyclomatic complexity, Halstead, function ranges pip install radon
eslint with the core complexity rule TypeScript/JavaScript cyclomatic complexity when ESLint is wired npm install --save-dev eslint
eslint-plugin-sonarjs TypeScript/JavaScript cognitive complexity npm install --save-dev eslint eslint-plugin-sonarjs
gocyclo, gocognit Go cyclomatic and cognitive complexity go install github.com/fzipp/gocyclo/cmd/gocyclo@latest, go install github.com/uudashr/gocognit/cmd/gocognit@latest
shellmetrics Bash cyclomatic complexity one POSIX shell script from github.com/shellspec/shellmetrics, placed on PATH
multimetric Halstead difficulty in every lane; Bash cyclomatic as a labeled approximation pip install multimetric
jscpd duplication in every lane npm install -g jscpd, or a devDependency
dupl Go duplication go install github.com/mibk/dupl@latest
PMD CPD duplication for the non-shell lanes when jscpd is absent the PMD 7 distribution or brew install pmd (needs a JVM)
type-coverage TypeScript type coverage; needs a resolvable typescript in the project npm install --save-dev type-coverage typescript
mypy Python Any-expression report pip install mypy, pipx install mypy, or uv tool install mypy

Python has no maintained cognitive-complexity collector and Bash has none for cognitive complexity or function ranges; those rows report unavailable with the validation date, and the run continues.

Install

/plugin marketplace add melodic-software/claude-code-plugins
/plugin install code-metrics@melodic-software

Configuration

This plugin has no userConfig. Everything tunable lives in the consumer's .claude/code-metrics.yaml, layered as user-global (~/.claude/code-metrics.yaml), team (tracked), and local overlay (.claude/code-metrics.local.yaml, gitignored; recommended line .claude/**/*.local.*) with per-key override, and every key has a bundled default (scripts/config-defaults.json), so the plugin works with no configuration at all; the one opinionated default is scope.exclude, which drops node_modules, vendor, dist, and build directories at any depth and reports what it dropped, and a team file that sets the key replaces the list whole. The consumer's .claude/ecosystems/<lane>.yaml files, when tracked, override lane detection with their globs and enabled. References ship with their provenance: cyclomatic 20 cites ISO/IEC 5055:2021 §8.2.117; the 1000-line file default is the plugin's own number and says so. Files are written in a documented YAML subset (block style, flow sequences of scalars, no flow mappings). Every key: reference/config.md; /code-metrics:setup writes the team layer and probes the collectors.

The report

Every audit prints one code-metrics/v2 JSON document (--json) or its markdown rendering. The document opens with a "Coverage of this run" table naming, per lane and measure, the collector used or the reason none did, and a status of complete, partial, or empty, so a run that measured nothing can never read as green. The markdown table shows each function once with every collector's values on that line, rows over a reference first, and stops at 200 rows; every markdown run also writes the whole document under CLAUDE_PLUGIN_DATA (else ~/.claude/plugins/data/code-metrics/reports) and names the path, so the rows past the cap need no second run. A repository that declares its deliberate replication in a registry (scope.registries) sees each replicated function once, with the copy count beside the path. Measured paths are relative to the document's root. Field reference: reference/report-schema.md. Tool provenance stamps: reference/collectors.md.

Getting a first artifact

audit-coverage reads what a test run already wrote and never runs one. When it finds no artifact, every lane is unavailable, the report lists the paths searched, and its last line points at this table. One command per lane produces an artifact the next run can read. Each row restates a producer's documented default, verified against the page in its Basis column on 2026-09-12; the producer's page wins over the row. Recheck trigger: a release note or changelog entry of that producer naming the flag or the output path in its row, or a read-time fetch of the Basis page that no longer states what the row does; either re-derives the row from the page and refreshes the date.

Lane Producer Command shape Writes Basis
TypeScript/JavaScript vitest npx vitest run --coverage --coverage.reporter=lcov lcov .info at coverage/lcov.info, auto-discovered Vitest coverage guide, coverage.reporter
TypeScript/JavaScript jest npx jest --coverage lcov .info at coverage/lcov.info, auto-discovered; the default coverageReporters list carries lcov Jest CLI, --coverage; Jest configuration, coverageReporters default ["clover", "json", "lcov", "text"]
TypeScript/JavaScript c8 or nyc, over any test runner npx c8 --reporter=lcov <test command> lcov .info at coverage/lcov.info, auto-discovered c8 README, --reporter, which takes any Istanbul reporter, lcov among them
Python coverage.py python -m coverage run -m pytest && python -m coverage json coverage.py JSON at coverage.json, auto-discovered; coverage xml writes coverage.xml, also auto-discovered; coverage lcov writes coverage.lcov, which needs --artifacts coverage.py command pages run, json, xml, lcov
Python pytest-cov pytest --cov=<package> --cov-report=json coverage.py JSON at coverage.json, coverage.py's default name for the json report, auto-discovered; --cov-report=json:<path> moves it pytest-cov reporting, --cov-report; the default file name is coverage.py's, per its json page
Bash kcov kcov <outdir> bash <test script> Cobertura-compatible XML under <outdir>; pass the cobertura.xml it writes with --artifacts kcov README, "Kcov will also write cobertura-compatible XML output"
Go go test go test ./... -coverprofile=coverage.out Go cover profile at the path -coverprofile names; coverage.out and cover.out are auto-discovered; file rows carry the statement ratio, function rows need a line artifact too go command, testing flags, -coverprofile
C# coverlet, as the dotnet test collector dotnet test --collect:"XPlat Code Coverage" Cobertura XML at TestResults/<run id>/coverage.cobertura.xml; pass it with --artifacts. The C# complexity lane is deferred, so the lane reports file rows and no CRAP coverlet VSTest integration, --collect:"XPlat Code Coverage"

Auto-discovered means the output lands on one of the well-known names the run looks for with no --artifacts (coverage/lcov.info, lcov.info, coverage.xml, cobertura.xml, coverage.json, coverage.out, cover.out, at most two directory levels below the repository root); anything else is named explicitly or listed under coverage.artifacts in the configuration.

Testing the plugin

The Python suites are the test_*.py files beside the scripts they cover, and python3 -m pytest -q from this directory runs all of them. To measure them, run the same command under coverage.py from this directory:

python3 -m coverage run -m pytest -q && python3 -m coverage json

The .coveragerc here sets source = ., so every module under the plugin is reported whether or not a test imported it, and patch = subprocess (coverage.py 7.10 or later), so the scripts the suites drive at their command line, join.py and the parsers among them, are measured in their child interpreters instead of reading 0 percent. The patch leaves one data file per process; coverage json combines them on its own from coverage.py 7.14, and an older release needs python3 -m coverage combine between the two commands. The run writes .coverage and coverage.json into this directory, both ignored by git, and coverage.json sits two levels below the repository root, where /code-metrics:audit-coverage plugins/code-metrics run from the root auto-discovers it.

Listing budget

Every skill description in a session shares one listing budget, and Claude Code drops the least-invoked descriptions first when it overflows. Measured with plugins/skill-quality/scripts/check-listing-budget.sh over every marketplace plugin's skills on 2026-09-05, this plugin adds six listing-eligible descriptions (setup is model-hidden and costs nothing) at these estimated sizes:

Measurement Characters
Marketplace aggregate before this plugin 135,541
Marketplace aggregate with this plugin 141,411
This plugin alone 5,870

The aggregate is an upper-bound estimate against the documented 8,000-character fallback; a consumer who installs only this plugin sits well inside it, and /doctor reports the resolved figure for a live session.

Known gaps

  • Bash has no collector for cognitive complexity or function ranges, and Python none for cognitive complexity; those rows report unavailable with the validation date rather than a number.
  • C# is counted and its duplication measured, but its complexity lane is deferred to a native collector and its type debt is reported as not applicable.
  • The sentences that restate the coverage.* defaults, the duplication defaults (in audit-duplication and the principles measures reference), type_debt.reference, the cyclomatic reference, and the file-length reference are pinned to scripts/config-defaults.json by scripts/check-code-metrics-skill-prose.py.

License

MIT (SPDX-License-Identifier: MIT).