Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9d708a4
docs(code-metrics): lock the audit-duplication fixes brief
claude Sep 11, 2026
33bbc0e
docs(code-metrics): link the audit-duplication brief to issue #4068
claude Sep 11, 2026
144d25d
docs(code-metrics): draft the audit-duplication implementation plan
claude Sep 11, 2026
cf2bf58
docs(code-metrics): revise the audit-duplication plan after review an…
claude Sep 11, 2026
b0234f2
feat(code-metrics): explicit duplication caps with a partial run row
claude Sep 11, 2026
37181fe
feat(code-metrics): merge duplication pairs into clone classes
claude Sep 11, 2026
668e242
feat(code-metrics): registry cluster lines for canonical-plus-copies …
claude Sep 11, 2026
2c2866e
feat(code-metrics): duplication rollups, largest-first sort, clearer …
claude Sep 11, 2026
b7328c0
docs(code-metrics): describe clone classes, caps, cluster lines; rele…
claude Sep 11, 2026
041fae6
merge: origin/main into claude/code-metrics-duplication-audit-0z4y1i
claude Sep 11, 2026
8455dc6
docs(adr): record the registry cluster-line grammar as ADR 0033
claude Sep 11, 2026
6fd57df
fix(code-metrics): pass jscpd a line bound no real file reaches
claude Sep 11, 2026
beb6169
fix(code-metrics): attribute a clone class the same way from any dire…
claude Sep 11, 2026
94b02fd
chore(planning): prune the code-metrics duplication audit contract slice
claude Sep 11, 2026
957e9a3
merge: origin/main into claude/code-metrics-duplication-audit-0z4y1i
claude Sep 11, 2026
d91a597
fix(code-metrics): hygiene for the duplication audit change
claude Sep 11, 2026
aa907c7
merge: origin/main into claude/code-metrics-duplication-audit-0z4y1i
claude Sep 11, 2026
d17096c
Merge remote-tracking branch 'origin/main' into claude/code-metrics-d…
claude Sep 12, 2026
692c366
Merge remote-tracking branch 'origin/main' into claude/code-metrics-d…
claude Sep 12, 2026
a94d6f0
fix(code-metrics): keep the no-detector headline when the other lane …
claude Sep 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Declare a canonical-plus-copies cluster as one arrow line

- Status: accepted
- Date: 2026-09-11

## Context

`scripts/cross-plugin-source-registry.txt` declares the replication this
repository does on purpose, one path-within-plugin per line, and three readers
key on that shape: the drift checker (`scripts/check-cross-plugin-source-drift.sh`),
the code-metrics replica collapser (`replica-collapse.py`), and the
duplication audit's exclusion filter (`registry-filter.py`). A path-within-plugin
cannot name a canonical copy that lives outside every plugin, so the eighteen
byte-identical copies of `hook-utils.sh` (root `lib/` plus seventeen plugins)
survived the duplication audit as one class with eighteen instances: the
seventeen plugin copies matched the line and the root copy did not.

## Decision

**A registry line containing ` -> ` is a cluster line:** the text before the
arrow is the root-relative canonical copy, the whitespace-separated tokens
after it are the members, each a literal root-relative path or a gitignore-style
glob (`lib/hook-utils.sh -> plugins/*/hooks/hook-utils.sh`). The duplication
filter excludes a clone class when every instance is the canonical or matches a
member and the instances sit in pairwise distinct directories; the drift
checker and the replica collapser skip the line, because they key clusters by
path-within-plugin and a root path is not one. A plain line keeps its meaning,
taken whole with any spaces. Lines are tried in file order and the first match
wins.

## Why

The registry is a contract every reader parses, so its grammar is hard to
change once lines exist. Splitting on whitespace was rejected: the drift
checker deliberately protects a registered path that contains a space, and a
second file or a YAML registry would have doubled the surface every reader
resolves. A marker that cannot occur in a path-within-plugin (` -> `) lets the
readers that do not understand a cluster ignore it with one test and lets the
one reader that does carry the whole class, canonical included, as a single
exclusion the report names by its line.
2 changes: 1 addition & 1 deletion plugins/claude-config/.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": "claude-config",
"version": "0.42.3",
"version": "0.42.4",
"description": "Nine configuration-health skills (plus setup) for a repo's Claude Code configuration: audit (settings.json / .mcp.json / hooks / plugins / permissions drift), audit-automation-gaps (evidence-gated verdicts on automation gaps), audit-permission-grants (allow-rule / allowed-tools grants for auto-mode durability and portability), audit-permission-state (the permission rules actually in effect: every settings scope merged with per-rule provenance, what auto mode drops on entry, config written where nothing reads it, and which managed intents are enforced versus loosenable), draft-auto-mode-rules (interview and draft a paste-ready autoMode classifier block; prints only, never writes), audit-instructions (locally-owned instruction surfaces vs current model capability, proposing removals/rewrites of instructions the model no longer needs, and detecting cross-surface instruction conflicts), audit-prompting-postures (the additive lane: posture guidance the prompting guide says a component's purpose needs but the component does not carry), audit-pass (one coordinated, ordered, resumable pass over a named target: three-scope inventory, run-time-derived exclusion set, stable finding identity, suppression memory, resume, one human gate, delegating every check to the plugin that owns it), and unhobble (the empirical bare-baseline experiment: reversibly strip a repo's standing instructions, log real stumbles against the current model, re-add only what evidence earns).",
"author": {
"name": "Melodic Software",
Expand Down
11 changes: 11 additions & 0 deletions plugins/claude-config/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@
All notable changes to the `claude-config` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.42.4]

### Changed

- **`audit-pass`: the exclusion-set reference describes both registry line shapes.** The
sanctioned-replication cluster class is derived from the marketplace's
`scripts/cross-plugin-source-registry.txt`, whose plain entries are paths within each plugin and
whose cluster lines, `<canonical> -> <member>...`, name a root-relative canonical copy and the
plugin paths or globs that carry it; the reference now says how to resolve each shape to its
copy set. Wording only; the pass's behavior is unchanged.

## [0.42.3]

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ dedicated script. Editing one copy breaks the sync path; a fix-capable pass that
corrupt the cluster.

**Derivation.** Ask the target whether it documents a shared-source registry. In this marketplace
that is `scripts/cross-plugin-source-registry.txt`, whose entries are paths *within* each plugin;
resolve each entry against every plugin root to get the live copy set. When the target documents no
that is `scripts/cross-plugin-source-registry.txt`, whose plain entries are paths *within* each
plugin (resolve each against every plugin root to get the live copy set) and whose cluster lines,
`<canonical> -> <member>...`, name a root-relative canonical copy and the plugin paths or globs that
carry it (the canonical and every match are the copy set). When the target documents no
such registry, **this class is empty**. Say so in `skipped` rather than inferring one from
similarity, which would exclude files nobody registered.

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.3.0",
"version": "0.3.1",
"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
50 changes: 50 additions & 0 deletions plugins/code-metrics/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,56 @@
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.1]

### Added

- **`audit-duplication` merges detector pairs into clone classes.** `jscpd` and PMD CPD report a
clone as a pair, so N copies of one fragment arrived as N-1 rows and the summary counted the
fragment's lines N-1 times. A post-pass (`cluster-clones.py`) now joins rows that share an
instance with an identical file and line range into one row per class, the instances sorted by
path and the row labelled `clustered`; the lines count once. The merge joins on identity, not
overlap: a copy that shares only part of a fragment stays its own group.
- **Explicit size and line caps, reported instead of hidden.** `duplication.max_size` (default
`1mb`, binary units) and `duplication.max_lines` (default `null`) are applied by the jscpd
adapter before the tool runs, because jscpd 4 and 5 disagree on their own `--max-size` and
`--max-lines` defaults and on what `0` means, and neither names a skipped file. A skipped file
makes the lane's run row `partial` with the count and the largest file, the document `partial`,
and the markdown summary carries a `Partial:` line. `0` or `null` means no cap.
- **Registry cluster lines.** A sanctioned-replication registry line `<canonical> -> <member>...`
names a root-relative canonical copy and the plugin paths or gitignore-style globs that carry
it, so a canonical file outside any plugin (this repository's `lib/hook-utils.sh`) can declare
its copies; instance paths are compared root-relative and the first matching line wins. A plain
line is still one path-within-plugin taken whole.
- **Per-lane and per-directory rollups.** `summary.by_lane` and `summary.by_directory` (every
ancestor of each class's first instance, cumulative) are additive `code-metrics/v1` fields,
computed after registry exclusion; `duplication.rollup_depth` (default 2) decides how deep the
markdown `## Rollup` section lists. A class is attributed by its first instance after a
root-relative sort, so the rollup reads the same from the repository root and from a
subdirectory. The schema reference states that readers ignore unknown keys.
- **Run rows carry the install hint as a field.** `run[].hint` holds the first install hint a
failed probe produced, apart from the prose reason, so a renderer can print it once.

### Changed

- **The duplication markdown reads as a duplication report.** Clone rows are listed largest
first; the summary line is `Files with clones: N.` instead of the size-shaped `Files. Functions.
Over reference.`; an empty exclusion list is stated with its reason; and a run in which no clone
detector resolved for any lane opens with one headline carrying the install hint and
`/code-metrics:setup`. The skill offers that install to the user and never performs it
unprompted. Every other skill's document renders as before.
- **`reference/collectors.md` pins jscpd 5.2.0** and records the 4.x maintenance line (4.3.0),
which the adapter also translates, the binary size grammar, and the token-count difference
between the majors.

### Fixed

- **A lane that skipped every file is `partial`, not `empty`**, and the zero floor counts a
`partial` duplication row as measured, so an all-excluded or clone-free lane that skipped a file
still states `duplicated_lines: 0`.
- **A run from a subdirectory matches the same registry lines as a run from the root**, because
instance paths are normalized against the repository root before matching.

## [0.3.0]

### Added
Expand Down
5 changes: 3 additions & 2 deletions plugins/code-metrics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ value to count against, not a bar.
|---|---|
| `/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 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-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. |
Expand Down Expand Up @@ -129,7 +129,8 @@ figure for a live session.
`scripts/config-defaults.json`. The setup template and the `reference/config.md` key table both
are, by a test and by `scripts/check-code-metrics-config-reference.py`; what remains unbound is
the number written into a sentence or a small illustrative table, currently `coverage.reference`
in `audit-coverage`, `duplication.min_tokens` and `duplication.min_lines` in `audit-duplication`,
in `audit-coverage`, `duplication.min_tokens`, `duplication.min_lines`, `duplication.max_size`,
`duplication.max_lines`, and `duplication.rollup_depth` in `audit-duplication`,
`type_debt.reference` in `audit-type-debt`, and the cyclomatic reference in `setup`. Those drift
silently until someone reads them.

Expand Down
4 changes: 3 additions & 1 deletion plugins/code-metrics/reference/collectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ built them could not run the tool; the first live run is that row's recheck trig

| Tool or format | Lane(s) | Measure | Claim the adapter relies on | Basis | Verified | Recheck trigger |
|---|---|---|---|---|---|---|
| `jscpd` 5.1.2 | every lane | `duplication` | v5 is a Rust binary that only writes `<output>/jscpd-report.json`, never stdout, so the adapter runs `--reporters json --output <tmpdir>` and prints the file itself; `duplicates[]` carries `firstFile`/`secondFile` with `start`, `end`, plus `lines` and `tokens`; `--absolute` is required because names are otherwise relative to the common ancestor of the inputs, which collapses two vendored copies sharing a basename; jscpd 4 wrote a different document under the same name | github.com/kucherenko/jscpd, run in this repository (the capture is `scripts/fixtures/tool-output/jscpd.json`) | 2026-09-05 | a jscpd major release, or a change to the report filename or the `duplicates[]` shape |
| `jscpd` 5.2.0 | every lane | `duplication` | v5 is a Rust binary that only writes `<output>/jscpd-report.json`, never stdout, so the adapter runs `--reporters json --output <tmpdir>` and prints the file itself; `duplicates[]` carries `firstFile`/`secondFile` with `start`, `end`, plus `lines` and `tokens`, and 5.2.0 adds a per-duplicate `kind` (`exact`) the adapter does not read; `--absolute` is required because names are otherwise relative to the common ancestor of the inputs, which collapses two vendored copies sharing a basename; `statistics.total.sources` counts token sources, not files, so it is not a skipped-file signal | github.com/kucherenko/jscpd, run in this repository (the capture is `scripts/fixtures/tool-output/jscpd.json`, from 5.2.0) | 2026-09-11 | a jscpd major release, or a change to the report filename or the `duplicates[]` shape |
| `jscpd` 4.3.0 (the 4.x maintenance line) | every lane | `duplication` | v4 is a Node program that writes the same `<output>/jscpd-report.json` with the same `firstFile`/`secondFile`, `start`, `end`, `lines`, and `tokens` keys, so one adapter translates both majors; it tokenizes differently from v5, so a clone count can differ between the majors on the same input, and the plugin never compares counts across a major boundary | github.com/kucherenko/jscpd (`npm install jscpd@4`), run in this repository over the same cluster fixture | 2026-09-11 | a 4.x release that changes the report shape, or the 4.x line being retired upstream |
| `jscpd` size and line caps (both majors) | every lane | `duplication` | the adapter applies `duplication.max_size` and `duplication.max_lines` itself, before jscpd runs, and passes jscpd one above its own bound (or `2147483647` lines and `1099511627776` bytes when there is no cap, bounds no real file reaches), because the majors disagree on the flags: 4.x defaults to 100kb and 1000 lines and reads `--max-lines 0` as that default, 5.x defaults to 1mb with no line cap, both read `--max-size 0` as skip every file, and neither names a skipped file in the report; jscpd's size grammar is binary (`1kb` is 1,024 bytes, `1mb` is 1,048,576) and the adapter uses the same multipliers | github.com/kucherenko/jscpd, both majors run in this repository with `--max-size` and `--max-lines` set to `0`, the default, and one byte or line below a fixture file's size | 2026-09-11 | either major changes a `--max-size`/`--max-lines` default or the meaning of `0`, or a report gains a skipped-file list |
| PMD CPD 7.27.0 | typescript, python, go, dotnet | `duplication` | `pmd cpd --minimum-tokens N --format xml --language <lang> --file-list <file>` (one path per line) prints a namespaced `pmd-cpd` document whose `duplication` elements carry `lines` and `tokens` with one `file` child per instance (`path`, `line`, `endline`); CPD has no JSON reporter, no minimum-lines option, no ignore-glob option, and no Bash or shell language; exit 4 means duplications were found, not that the run failed | docs.pmd-code.org CPD user documentation, CLI reference, and report formats; the adapter and its fixture are unverified against a live run | 2026-09-05 | a PMD 8 release, a JSON reporter, a shell CPD language, or the first live run of this adapter |
| `dupl` v1.1.0 | go | `duplication` | the default text printer emits `found <n> clones:` per group, then an indented `<file>:<start>,<end>` line per instance, then a total footer; `-plumbing` is pairwise and loses groups of three or more, so the text printer is parsed; `-t` is a token threshold with no line equivalent; dupl reports no token count and ships no version flag | github.com/mibk/dupl `printer/text.go` and `main.go`; the adapter and its fixture are unverified against a live run | 2026-09-05 | a dupl release that changes the printer, adds a version flag, or adds a token count |

Expand Down
Loading
Loading