Skip to content

code-metrics

code-metrics tells you what your change did to the numbers — structure, debt, readability, decay, load risk — at the one moment you can still act on it cheaply: before you open the PR.

What it solves

Quality metrics regress quietly, and the author is the last to notice. A function creeps past a complexity ceiling, coverage slips a couple of points, a fresh import cycle appears — review rarely catches any of it, and a dashboard only surfaces the trend long after the change shipped. The feedback arrives too late and too far from the person who caused it.

This skill closes that gap by being two things at once: immediate (a local read on your branch, no CI round-trip) and near-free (the numbers are pure tooling output, so running it costs essentially no tokens). It only spends a model when something is actually wrong.

How it works

The skill is hybrid — deterministic measurement, model-assisted only on breach.

The numbers are produced by a per-stack adapter that runs your language’s real tooling and normalizes the output to a common contract. The adapter is selected automatically from the repo’s detected stack, so the same command and the same metric set work across languages. Each metric is declared in a single registry (direction + where its threshold lives), so the catalog grows by one line plus one adapter function — never a new branch in the orchestrator.

What it measures

The catalog is layered by the question it answers and the cost of answering it:

  • Structurecoverage, complexity, module_size, dependency_cycles.
  • Debt & readability — counts of TODO/FIXME, @deprecated/[Obsolete], marked dead code, and linter/compiler suppressions; plus nesting_depth, average param_count, magic_numbers, lint_density, and doc_coverage. These are the cheap, objective signals a team can’t argue with — the point of measuring readability with counters instead of a subjective grade.
  • Decayhotspots: the files that change a lot and are complex (git churn × cyclomatic), where risk actually concentrates over time.
  • Load riskperf_risk: a static proxy for queries/allocations inside loops and nested loops. It is heuristic and high-false-positive by nature, so it is reported as info — surfaced, never gated.

Every metric is a deterministic shell heuristic (grep / awk / lizard / git) and ratchet-only by default, so a legacy repo is never born red: only a new regression on your branch is flagged.

Full metric reference

higher is better · lower is better · info reported, never gated. “Default” is the behaviour with no .octopus.yml config; the parenthesised key is the optional absolute threshold you can set under code_metrics:.

MetricLayerDirWhat it measuresDefault
coveragestructureLine coverage % (TS: vitest→LCOV · C#: dotnet-coverage/coverlet→Cobertura)ratchet (coverage.min)
complexitystructureAverage cyclomatic complexity per function (lizard)ratchet (complexity.max)
module_sizestructureAverage NLOC per function/file (lizard)ratchet (module_size.max)
dependency_cyclesstructureImport cycles (TS: madge) / project-reference cycles (C#: dotnet list reference + Tarjan)ratchet (dependencies.cycles_allowed)
todo_markersdebtCount of TODO/FIXME/HACK/XXXratchet
deprecationsdebt@deprecated (TS) / [Obsolete] (C#)ratchet
dead_codedebtMarked dead code (#if false, // dead, unused-disable) — not reachabilityratchet
suppressionsdebteslint-disable/@ts-ignore/@ts-nocheck (TS) · #pragma warning disable/[SuppressMessage] (C#)ratchet (suppressions.max)
nesting_depthreadabilityDeepest brace nesting levelratchet (nesting_depth.max)
param_countreadabilityAverage parameters per function (lizard)ratchet (param_count.max)
magic_numbersreadabilityNumeric literals, excluding 0/1/-1, named constants and stringsratchet
lint_densityreadabilityLinter findings per 1000 NLOC (TS: eslint · C#: build warnings); 0 if no linterratchet
doc_coveragedocsDocumented public/exported symbols ÷ totalratchet (doc_coverage.min)
hotspotsdecayCount of files that are both high-churn and high-complexity — git log churn × lizard CCN. Tune via hotspots.{window_days,churn_min,ccn_min}ratchet (hotspots.max)
perf_riskload riskinfoQueries/allocations inside loops + nested loops (O(n²) candidates)info-only (never gated)

Heuristic caveats (all metrics are shell-grade, approximate by design):

  • perf_risk keys on a loop opener with its { on the same line; Allman-brace codebases (idiomatic C#, where { sits on its own line) under-detect and often read 0. It is info-only precisely because of this.
  • magic_numbers, nesting_depth and doc_coverage scan the source tree and do not exclude generated code (e.g. EF migrations), so they can read high — they are ratchet anchors, not absolute quality scores.

It reports a dual delta per metric:

  • vs. baseline — your branch against the last-merged-to-main baseline (the trend anchor).
  • vs. main — your branch against your local main HEAD (this PR’s isolated impact).

The baseline is read (git fetch) from a dedicated, non-protected ref (octopus/code-metrics) that a push-to-main Action keeps fresh; the read path never writes it.

Thresholds are ratchet by default — a metric may not regress against the baseline — with optional absolute targets layered in .octopus.yml (coverage.min, complexity.max, suppressions.max, nesting_depth.max, hotspots.max, and so on; perf_risk takes none). A field with no configured target stays on the ratchet. Only when a gated metric crosses its threshold does the skill invoke a low-cost model to read the offending change, explain why the number moved, and suggest a fix. No breach means no model call — the cost contract that keeps it worth running on every PR.

Usage & parameters

octopus code-metrics [--stack <id>] [--metric <name>] [--verbose] [--emit-baseline]
FlagWhat it doesDefault
--stack <id>Force a stack adapter (csharp, typescript) instead of auto-detecting.auto-detected
--metric <name>Report a single metric instead of the full catalog.all
--verboseShow raw tooling output alongside the summary.off
--emit-baselinePrint the flat baseline.json from this run (used by the writer-Action).off

The numeric run is always model-free; the curation step fires only on a threshold breach, on the harness’s low-cost model.