"Measure duplicated code as clone classes over a change, a path, or the tree, with every instance's file and line range, subtracting replication the repo declares sanctioned. Reports, never gates. Use when: 'is this duplicated', 'find copy-paste code', 'clone detection', 'duplication report', 'how much of this change is copied', 'DRY check', 'redundant code', 'duplicated lines in the diff'. Lines per file: /code-metrics:audit-size."
Resources
2Install
npx skillscat add melodic-software/claude-code-plugins/plugins-code-metrics-skills-audit-duplication Install via the SkillsCat registry.
Pre-computed context
Current branch: !git branch --show-current 2>/dev/null || echo "unknown"
Purpose
Duplication is the measure most likely to report noise, because some replication is deliberate:
a repository that vendors one helper into seventeen plugins on purpose has seventeen copies and
zero debt. This skill reports the clone groups a detector found, then subtracts the groups the
target repository has already declared about itself, and stops there: no pass or fail, no
severity, no finding. What to do about a surviving clone is the reader's call.
Collectors, tried in ladder order per lane:
| Collector | Lanes | Values per clone group | When |
|---|---|---|---|
jscpd |
every lane | lines, tokens |
jscpd resolves on PATH or in ./node_modules/.bin |
dupl |
Go only | lines, tokens null |
jscpd did not resolve and dupl did |
cpd (PMD) |
TypeScript/JavaScript, Python, Go, C# | lines, tokens |
jscpd did not resolve and pmd did (never for Bash: CPD has no shell language) |
No collector resolves means the lane's row says so with the tool's install hint and the run
continues. This plugin never installs, downloads, or npx-fetches a detector.
Run it
"${CLAUDE_SKILL_DIR}/scripts/audit-duplication.sh" # the change: diff from the merge-base plus uncommitted files
"${CLAUDE_SKILL_DIR}/scripts/audit-duplication.sh" src/ lib/ # explicit paths (a missing one is a usage error)
"${CLAUDE_SKILL_DIR}/scripts/audit-duplication.sh" --all # every tracked or untracked-but-not-ignored file
"${CLAUDE_SKILL_DIR}/scripts/audit-duplication.sh" --registry scripts/cross-plugin-source-registry.txt --all
"${CLAUDE_SKILL_DIR}/scripts/audit-duplication.sh" --json --all src/ # the code-metrics/v2 document instead of markdownPresent the markdown report as printed. It opens with the scope and a "Coverage of this run"
table (lane, collector, status, reason), then one row per clone group, largest first, listing
every instance as file:start-end, then a rollup per lane and per directory, then the summary
lines: files with clones, the duplicated-line total, how many groups a registry excluded, and
which lanes were partial. Files with clones leaves out any file holding nothing but excluded
groups; the scope's count keeps it. When the report opens with No clone detector ran in any lane, offer the user the install command that headline carries (npm install -g jscpd, or a
devDependency) and run it only when they confirm; never install silently and never npx-fetch it.
Keep the --json document when the numbers feed a comparison:/verification:measure metrics consumes it when the verification plugin is installed (treat a
report whose status is empty on either side as INCONCLUSIVE); otherwise keep the JSON beside
your notes and compare by hand.
Reading the numbers
- A clone group is reported beside no reference. There is no configured bar for duplication in
this plugin and no standard sets one, so nothing is ever counted asover_reference. - A group is a clone class, not a detector pair.
jscpdand PMD CPD report clones as pairs, so
seventeen identical copies arrive as sixteen two-instance rows; this skill merges rows that
share an instance with an identical file and line range into one row per class before it
counts anything. Copies that share only part of a fragment are named with different ranges and
stay separate groups: the merge joins on identity, never on overlap, so a class is never wider
than what the detector called identical. summary.duplicated_linescounts each class once, using the length of the class, not the sum
over its instances: three copies of a 41-line block are 41 duplicated lines, not 82 or 123. The
count is what survived the registries.summary.by_laneandsummary.by_directoryroll the surviving classes up: each maps to{groups, duplicated_lines}, the directory map for.and every ancestor of each class's first
instance. A class counts once under every ancestor, so a parent includes its children and the
directory rows cannot be summed;by_directory["."]and the per-lane sum both restate the
totals. The markdown lists directories toduplication.rollup_depth; the JSON carries all.- A file larger than
duplication.max_size(or longer thanduplication.max_lines, when set) is
left out of the scan and never silently dropped: the lane's run row ispartialwith how many
files were skipped and the largest one, the document ispartial, and the summary carries aPartial:line naming the lane. - The registry is an exclusion, not a suppression: it is derived from the target repository's
own declaration that those copies are deliberate, so no suppression record is involved and the
excluded groups stay in the document underexcluded[]with the registry path, the 1-based line
number, and the line's text. A group is excluded only when one registry line accounts for every
instance and each instance sits under a different carrying directory; two copies inside one
directory are ordinary duplication and stay. - A value the detector did not produce is
null, never0:duplreports no token count, so
its rows carrytokens: null. statusiscompletewhen every lane in scope was measured,partialwhen one was not or
when a cap left files out of one, andemptywhen nothing was; a run that measured nothing
prints "Measured nothing" and states no duplication figure at all, which is not the same as zero
duplication. A lane that skipped every file is stillpartial: its row says what was skipped.- Exit 0 whenever a report was produced, including an
emptyone; exit 2 for a usage error such
as a named registry or scope path that does not exist; exit 3 when a detector resolved but
produced nothing parseable, with its stderr in the run table. A detector's own non-zero exit is
not a failure when it produced a report, which is howjscpdand PMD CPD signal "clones found".
Configuration
Everything tunable resolves through .claude/code-metrics.yaml (user-global, team, local
overlay; per-key override; keys in ${CLAUDE_PLUGIN_ROOT}/reference/config.md):duplication.min_tokens (default 50), duplication.min_lines (default 5),duplication.ignore (globs handed to the detector's own ignore option), duplication.max_size
(default 1mb, binary units; a larger file is left out of the scan and reported), duplication.max_lines
(default null, no line cap), duplication.rollup_depth (default 2, how deep the markdown
per-directory rollup lists), and scope.registries (sanctioned-replication registries, each path
relative to the repository root, each also nameable on the command line with --registry, and
read by every audit in this plugin; duplication.registries is the older name and still resolves
when the scope-level list is empty). A cap of null or 0 means no cap. /code-metrics:setup
writes the team file and probes the collectors.
A registry line has one of two shapes. A plain line is one path-within-plugin, taken whole with
any spaces: a class is excluded when every instance ends with that path and the copies sit in
distinct carrying directories. A cluster line, <canonical> -> <member>..., names a root-relative
canonical copy and the plugin paths or gitignore-style globs that carry it
(lib/hook-utils.sh -> plugins/*/hooks/hook-utils.sh): a class is excluded when every instance is
the canonical or matches a member and the instances' directories are pairwise distinct. Instance
paths are compared root-relative, so a run from a subdirectory matches the same lines, and the
first matching line in file order wins.
This script exports the five tunables to the collector adapters asCODE_METRICS_DUP_MIN_TOKENS, CODE_METRICS_DUP_MIN_LINES, CODE_METRICS_DUP_IGNORE,CODE_METRICS_DUP_MAX_LINES, and CODE_METRICS_DUP_MAX_SIZE, which is the only channel an
adapter reads them through. jscpd passes the first three to the tool and applies the two caps
itself before the tool runs; the dupl and cpd adapters apply the minimum after parsing, report
the ignore globs as unused, and scan every file in scope. Which options each tool honors, what its
own defaults are, and why the caps are applied here rather than passed through are tool facts that
move with the tools, so they are not restated here: the duplication rows of${CLAUDE_PLUGIN_ROOT}/reference/collectors.md carry each claim with its basis, the date it was
verified, and the upstream event that obliges a recheck.
cpd (PMD) sits after jscpd on ${CLAUDE_PLUGIN_ROOT}/scripts/collector-ladder.tsv for every
lane but Bash, so it runs only when jscpd does not resolve and pmd does. A repository that
already runs PMD puts it first with lanes.<lane>.collectors.duplication: [cpd, jscpd]; collector
overrides are validated against the ladder file and an unknown name is dropped with a warning.
What this skill does not do
- It does not run tests, edit files, remove a clone, or install a detector; when none resolves the
run table says so and prints the install hint. - It does not judge. No duplication figure here is a bar, and no
checkgate exists in this
version. - It does not find cross-language clones. Every detector here is token-based within one language
(jscpdgroups JavaScript with TypeScript and nothing wider), so a C# method reimplemented in
Python is invisible to it. - It does not measure size, complexity, coverage, or type debt; those are the sibling
audit-*
skills in this plugin, and/code-metrics:principlesexplains what each number can and cannot
tell you.
Next
- The numbers feed a before-and-after comparison:
/verification:measure metrics. - A surviving clone is about to be called debt:
/code-metrics:principles.
Gotchas
- Change scope needs a merge-base with the default branch; outside a git repository, or on a
branch with no default-branch ancestor, pass paths or--all(the usage error says which). - A registry named on the command line or in
scope.registriesthat does not exist is a
usage error, not a silent no-op: a stale registry path would otherwise turn every exclusion off
without saying so. - Clone detection compares the files in scope with each other. A default-scope run sees only the
changed files, so a block copied from a file the change did not touch is not found; use--all
or name both paths when that is the question. - The pair merge is exact only for byte-identical copies. A class whose copies drifted by a line
is reported as the detector saw it: the identical span as one class, and the drifted copy's
shorter overlap as a second group naming the same file with a different range. - Two versions of one detector can tokenize the same tree differently and so report different
class counts (the jscpd rows of${CLAUDE_PLUGIN_ROOT}/reference/collectors.mdrecord the
verified case); compare runs made with one detector version, never across a version boundary. - Lowering
duplication.min_tokensfinds more and smaller clones, most of them boilerplate the
language forces; the defaults are the detector's own conservative pair.