Skip to content
2 changes: 1 addition & 1 deletion docs/SKILL-CHEAT-SHEET.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ owned by [docs/CATALOG-TAXONOMY.md](CATALOG-TAXONOMY.md).
| [`/code-metrics:audit-coverage`](../plugins/code-metrics/skills/audit-coverage/SKILL.md) | `code-metrics` | Coverage and CRAP read from build artifacts, no verdict |
| [`/code-metrics:audit-duplication`](../plugins/code-metrics/skills/audit-duplication/SKILL.md) | `code-metrics` | Clone groups minus the replication the repo declares, no verdict |
| [`/code-metrics:audit-size`](../plugins/code-metrics/skills/audit-size/SKILL.md) | `code-metrics` | Lines per file beside a cited reference, no verdict |
| [`/code-metrics:audit-type-debt`](../plugins/code-metrics/skills/audit-type-debt/SKILL.md) | `code-metrics` | Typed-code percentage per lane, with no standard behind it |
| [`/code-metrics:audit-type-debt`](../plugins/code-metrics/skills/audit-type-debt/SKILL.md) | `code-metrics` | Typed-code percentage per file and per lane, with no standard behind it |
| [`/code-metrics:principles`](../plugins/code-metrics/skills/principles/SKILL.md) | `code-metrics` | What each code measure can and cannot tell you |
| [`/code-tidying:audit-dead-code`](../plugins/code-tidying/skills/audit-dead-code/SKILL.md) | `code-tidying` | Whole-repo dead-code hunt across four labelled lanes with adjudicated candidates |
| [`/code-tidying:tidy`](../plugins/code-tidying/skills/tidy/SKILL.md) | `code-tidying` | Proactively hunt one lane for safe structural tidyings and ship a structure-only PR |
Expand Down
2 changes: 1 addition & 1 deletion plugins/code-metrics/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "code-metrics",
"version": "0.2.1",
"version": "0.3.0",
"description": "Read-only code measures for a change, with cited references and no verdict: lines per file (audit-size), cyclomatic, cognitive, and Halstead complexity (audit-complexity), duplication with sanctioned-replication exclusions (audit-duplication), coverage per function with CRAP from existing lcov, Cobertura, coverage.py, or Go artifacts (audit-coverage), type debt for TypeScript and Python (audit-type-debt), the literacy router for what each number can and cannot say (principles), and a setup skill for the consumer's .claude/code-metrics.yaml. Runs external collectors only when they already resolve, never installs, never runs tests, never emits a finding.",
"author": {
"name": "Melodic Software",
Expand Down
54 changes: 54 additions & 0 deletions plugins/code-metrics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,60 @@
All notable changes to the `code-metrics` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.3.0]

### Added

- **`audit-type-debt` reports per file.** One row per scope file the tool listed (`function`
null) plus one row per lane labelled `lane-total`; the summary's `Files:` count is the file
rows, where it read 0 before. The Python lane row sums the file rows, so a change-scoped run
reports the scope's own coverage rather than everything mypy followed. mypy names modules, not
files, so the collector re-derives its `--explicit-package-bases` naming from each scope path
(checked against a real 186-file run of this repository, every listed name matched), matches
the shorter names a config base such as `mypy_path = src` gives by suffix, and, when nothing
matches, keeps mypy's own Total as the lane row and says so in the run row's reason. A
TypeScript file row carries `any_count` alone, the occurrences
`type-coverage --detail --show-relative-path` lists for the file, because the CLI exposes no
per-file denominator; the tsconfig program's file set is read through the project's own
`typescript`, so a scope file the program leaves out gets no row and is counted in the run
row's reason rather than reported as 0. In the raw rows the lane row comes first, and the
rendered table leads with it and never drops it under the row cap.
- **mypy's error count reaches the run table.** When mypy exits 1 the Python run row's reason
reads `mypy reported N errors (M missing stubs)`, the missing ones being the `import-untyped`
and `import-not-found` codes; `--show-error-codes` and `--no-pretty` are passed so a consumer
config that hides codes or wraps messages does not hide the count.

### Changed

- **An `ok` run row carries what its collector said on stderr.** The dispatcher dropped an
adapter's stderr on exit 0; it is now the row's reason (500 characters, newlines folded), and
null when the adapter said nothing. Every skill's run table gains this.
- **The renderer sorts a `file: null` row among file rows and joins rows per lane.** The
`lane-total` row and a file row can tie on every earlier sort key, which compared `None` with a
path; and two lanes' rows with the same values used to join into one line, because the join
key left the lane out.

### Fixed

- **`audit-type-debt`: an aborted mypy run no longer reads as 100% typed.** mypy exits 2 on a
blocking error (a duplicate module name, a usage or config error) before analysing anything and
still writes a report whose only row is `Total 0 0 100.00%`; the collector accepted that as a
measurement labelled `mypy-reported-errors`, so a repository carrying sanctioned replication read
as fully typed over zero expressions. Exit 2 is now the adapter contract's exit 4: the Python row
reads `unavailable` with mypy's own message and the run continues. A Total row with zero
expressions reports `type_coverage_pct: null`, never 100.
- **`audit-type-debt`: sanctioned replication measures instead of aborting.** The collector passes
`--explicit-package-bases`, so mypy names each module by its path (`plugins.a.lib.x`) and two
same-named files under identifier-named directories no longer collide. Same-named files under
two hyphenated directories still collide, because mypy's module walk stops at a directory whose
name is not a Python identifier; that case reaches the `unavailable` row above. mypy accepts the
flag only with namespace packages on, so when the consumer's config turns them off the run
repeats without it, in mypy's own `__init__.py` naming, and the run row's reason says so.
- **`audit-type-debt`: no `.mypy_cache/` in the consumer's tree.** The collector passes
`--cache-dir` with the platform's null device, mypy's documented value for disabling the cache;
a one-shot report gained nothing from it (6.8s without a cache against 8.2s with a warm one over
this repository's 179 Python files).

## [0.2.1]

### Changed
Expand Down
2 changes: 1 addition & 1 deletion plugins/code-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ value to count against, not a bar.
| `/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 groups (duplicated lines and tokens, every instance's range) from `jscpd`, `dupl`, or PMD CPD, minus the replication the repository declares in a sanctioned-replication registry, which is an exclusion, not a suppression. |
| `/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 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: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. |

Expand Down
8 changes: 5 additions & 3 deletions plugins/code-metrics/reference/report-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ different files whatever the registry says, and all of them stay. Clone-group ro
## `run[]` rows

`lane`, `measure`, `collector` (the tool and version that produced the rows, or `null`), `status`
(`ok`, `partial`, `unavailable`, `not-applicable`, `deferred`), `reason` (`null` only when `ok`). A
(`ok`, `partial`, `unavailable`, `not-applicable`, `deferred`), `reason` (`null` only when `ok` and
the collector said nothing; an `ok` row whose collector wrote to stderr while succeeding carries
that text, such as mypy's `mypy reported 386 errors (349 missing stubs)`). A
run whose scope holds no measurable file carries one row `*/*` with status `not-applicable` and a
reason that opens with `no measurable files in scope` and, under `change`, says why: the branch is
at its merge-base with a clean working tree (naming the ref and the `--all` alternative), or the
Expand All @@ -62,7 +64,7 @@ read as complete while one of its own rows says `N of M`.

Common fields: `file`, `function` (`null` for a per-file row), `lane`, `values` (measure name to
number or `null`), `collector`, `labels` (strings such as `comment-agnostic`, `start-line-only`,
`file-level`, `replicated`), `over_reference` (the measures whose reference the row is at or
`file-level`, `replicated`, `lane-total`), `over_reference` (the measures whose reference the row is at or
beyond), and `replicas` on a collapsed row only (see "Sanctioned replication"). Granularity by
skill:

Expand All @@ -72,7 +74,7 @@ skill:
| `audit-complexity` | function (`start_line`, `end_line` when the collector reports them) | none |
| `audit-coverage` | function | `cov_source` (`artifact-region`, `line-range`, `statement-ratio`, or `ambiguous`), `hit` (the artifact's function-hit flag or `null`), `reason` (why the join was refused; present only on an `ambiguous` row) |
| `audit-duplication` | clone group | `instances[]` (`file`, `start_line`, `end_line`) replaces `file` and `function` |
| `audit-type-debt` | lane | `file` and `function` are `null` |
| `audit-type-debt` | file | one row per scope file the tool listed (`function` is `null`) plus one lane row per lane with `file` `null` and the label `lane-total`. The Python lane row sums its file rows, so a change-scoped run reports the scope's own coverage; when no listed module matched a scope file it is mypy's own Total and no file row is emitted. A TypeScript file row carries `any_count` alone (the occurrences `type-coverage --detail` listed for that file; the CLI gives no per-file denominator) with the other three values `null`, and the lane row carries all four |

A value the collector did not produce is `null`, never `0`.

Expand Down
Loading
Loading