# Dmetrics

> How to run dmetrics and read its reports. Use when running dmetrics, reading a dmetrics report, deciding what to do about a cyclomatic, cognitive or coupling warning, or refactoring a scope to lower a warning.

- Skill: `kascote/dmetrics` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kascote/dmetrics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kascote/dmetrics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: kascote (https://skillmd.com/u/kascote)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/kascote/dmetrics

---


<!-- Generated by `dmetrics init`; a re-run overwrites this file. -->

dmetrics — how to use and read it

dmetrics measures Dart code and judges each value against thresholds the
project configured. It measures; it does not fix. Warnings are evidence to
weigh, not orders to obey.

When to run
  Once per task, after your edits, on the package's source roots:
    dmetrics analyze lib bin      (or the project's `make check`)
  Do not run it after every edit. Run it on a single file only when you
  want to compare before and after a refactor of that file.
  Exit 0 clean • 1 violations • 2 analysis incomplete (fix those first:
  parse errors, unreadable files, bad config) • 3 usage error.

Reading a report line
  lib/a.dart:42:3 • warn • method Foo.bar • cognitive 18 [warn ≥ 15, fail ≥ 25] • if ×4, loop ×2, else ×3
  where • verdict • scope kind and name • metric and value • thresholds that
  applied • contributors: which constructs produced the score, in source
  order. Only warn, fail and suppressed lines print; the last line is the
  summary. `--all` prints every scope. `--json` gives the same data
  structured. No thresholds configured means every scope is ok: nothing
  is judged.
  Counts are occurrences, not points. For cyclomatic they sum to value − 1.
  For cognitive nested constructs cost more, so counts do not sum to the
  value; the gap is the nesting.
  `if ×9 @2` means most of those ifs sit two levels down; no suffix means
  the top of the body. Same count, different shape: a run of guards or a
  nest. `--json` has the depth of every contributor.
  Closures are scopes of their own: a warning on `build.<closure#2>` points
  at the second closure inside `build`, and `build` itself may read ok.
  Less obvious labels: `if-case` is `if (x case p)`, `when` a case guard,
  `pattern-or` a `||` inside a pattern, `loop` any for/while/do.

The metrics
  cyclomatic  Number of independent paths through a scope: every branch,
              loop, case arm, &&, ||, ??, ?: and catch adds one. Measures
              how much there is to test.
  cognitive   How hard the scope is to read: nesting makes each construct
              cost more, sequences of the same construct cost less. A
              switch counts once here but once per arm in cyclomatic, so
              the same code reads `switch ×1` and `case ×15`. Measures how
              much there is to understand.
  coupling    Per library: how many other libraries of this package it
              imports. `dart:` and other packages do not count; exports do
              not count. Measures how much can break it when the package
              changes. `cycle of N` on the line means an import cycle.

What to do about a warning
  1. Read the contributors before the value. `case ×15` and `if ×8, loop ×5`
     at the same value are different problems.
  2. `table-shaped: <kind>` means one kind supplies most of the score: a
     dispatch switch, a field-wise ==, a copyWith, a parser step. Its size
     is the table's, not a tangle's. `if@1` means the ifs sit one level
     down, typically a switch whose arms each hold a run of ifs. Usually
     leave it; suppress if it must be silent.
  3. A scope your task did not touch is not your problem. Mention it,
     do not fix it, unless the user asked for a cleanup. `--changed main`
     (or `HEAD` for uncommitted work) adds `touched` to the tag of every
     scope your change hit, so you need not guess.
  4. A scope your task touched and pushed over a threshold: the shape of
     the contributors says which move fits, and `dmetrics agent refactor`
     maps the shapes to the moves. If the honest shape is one long
     function, say so and suppress with the reason next to it.
  5. Never lower a threshold or add an override to make a run pass.
     Thresholds are the project's decision; propose the change instead.

Suppressing
  // ignore: dmetrics_cognitive        on the line before the declaration
  // ignore: dmetrics                  every metric
  // ignore_for_file: dmetrics_coupling
  A suppression on a method does not cover its closures. Suppressed scopes
  still print, so nothing is hidden.

Baseline
  When the project has a baseline file (dmetrics_baseline.json next to
  analysis_options.yaml), every line carries its status against it:
    fail (new)            a scope the baseline does not know: yours to fix
    fail (worse, was 11)  a known violation that got worse: yours to fix
    fail (baselined)      accepted debt; prints so nothing is hidden, does
                          not fail the run, not your problem unless asked
    warn (was 6)          moved but still under the fail line: drift
  Exit 1 means new or worse. The `Changed since baseline` section lists
  scopes that moved while staying ok, largest delta first: that is the
  drift; report it, do not chase it. Never run `dmetrics baseline` to
  make a run pass: refreshing the baseline accepts debt and is the
  project's decision, like a threshold. Run it only when asked, or after
  fixing violations so the file shrinks.
  `--baseline-ref main` compares against main's tree instead of a file:
  exit 1 means this branch made something new or worse. Nothing to
  refresh; the clause reads `baseline (main): …`.

Other commands
  dmetrics stats [paths]   Distribution, share above thresholds, a sweep
                           over candidate thresholds, contributor mix.
                           For calibrating thresholds, not for finding
                           violations. Read p90/p95 against the warn line.
  dmetrics deps [paths]    The package as a graph: cycles, fan-out and
                           fan-in hubs with instability I = out/(in+out),
                           and the graph folded onto directories with the
                           edges that break the layering marked. Cycles
                           and hubs are information, not violations; a
                           barrel file is a normal fan-in hub. Complete
                           only when the whole package is in the run.

dmetrics — refactoring a flagged scope

Read this once a scope your task touched crossed a threshold and you have
decided to change it. `dmetrics agent` comes first: it explains the report
line. This layer maps the shapes a report prints to the move each shape
usually supports. Hypotheses, not orders: the code decides.

The point of the change
  A refactor is not a fix to get the run green. It is the one chance the
  task gives you to leave this code in better shape than you found it:
  simpler to read, one responsibility per unit, in the terms the package
  already uses and the shape its architecture already has. Keep it simple,
  build nothing the task does not need, add no abstraction for its own
  sake. A helper that exists only to lower a number is worse than the
  number.

Before choosing
  Run `dmetrics analyze <file> --all`. Only the warning line prints by
  default, and several shapes below read cyclomatic against cognitive for
  the same scope. Then read the contributors, not the value.

Shapes
  table-shaped: case  (or &&, ??)      cyclomatic high, cognitive low
    A table: a dispatch switch, a field-wise ==, a copyWith, a parser step.
    Leave it, or a lookup map when the arms are data. Never split it into
    helpers: its size is the domain's, and a helper per row hides that.
  if ×N @2, @3                         cognitive high, cyclomatic modest
    Nesting, not branching: a ladder of conditions each waiting on the
    last. Guard clauses and early returns; invert the condition that wraps
    the rest of the body. Extract the deepest block only when it has a
    name of its own.
  switch ×1, if ×N @1                  table-shaped: if@1, cognitive line
    Arms that each hold a run of ifs. The switch is fine; the arms are the
    units. One helper per busy arm, or a `when` guard that lifts the if
    into the case.
  && ×N, || ×N
    Predicates spelled inline. Name them: a local, a getter, a small
    predicate method. Extract booleans, not blocks; the branch count stays
    and the reading cost drops.
  loop ×N, if ×M @1 or deeper
    A loop body that filters and acts. When the ifs are filters, an
    iterator pipeline (`where`, `map`, `firstWhere`); when the body does
    one thing per element, extract the body with the element as its
    argument.
  ternary ×N in a build method
    Presentation branching. Extract the widget the ternaries decide, or a
    `switch` expression over the state when they all test the same thing.
  catch ×N
    One try per unit of failure: what fails together is caught together.
    Catches that log or rethrow alike collapse into one handler.
  <closure#N> on its own line, or a parent that fails only with its
  closures folded in
    The callback is the unit. Give it a name, a method or a top-level
    function, and the parent reads as a sequence of named steps.
  library … coupling N, cycle of M
    Not a function problem, a module boundary problem. Read `dmetrics deps`
    first. A cycle is two libraries that are one, or a type that belongs
    to a third; a hub with high fan-out splits by what its importers
    actually use.

Closing the loop
  measure → change → re-measure → tests → baseline. Re-measure the file
  with the same `--all` run as before: the flagged scope's value should
  drop and the file's sum should not grow much. Complexity moved into
  three helpers with the same total is not a refactor, it is the same
  code with more names. The full run against the baseline then shows the
  delta as a fact anyone can check, not as your opinion. If the honest
  shape is one long function, say so and suppress with the reason next to
  it. Never lower a threshold or refresh the baseline to make the run
  pass.

