Find duplication
Agent skills for cleaning up codebase drift — stale comments, duplicate code, and inconsistent imports. Two-phase inventory-then-execute workflow for Claude Code, Cursor, and other AI coding agents.
npx -y skills add niuma996/code-hygiene-skills --skill find-duplicationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when the user asks to find duplicate or near-duplicate code in a codebase. Triggers on phrases like "find duplication", "deduplicate code", "merge similar code", "DRY violations", "repeated code", "clone code". Produces a cluster inventory of copy-paste families with merge strategies. Skip for: third-party/vendor code, generated files, or refactoring a single function.
SKILL.md
11.9 KB, ~2.7k tokens by cl100k_base, as published. Nobody here has run it
Find Duplication
Overview
Workflow for surfacing duplicate and near-duplicate code ("clones") and producing a merge plan. Default mode is LLM-driven (anchor-grep + structured reading); an optional tool-driven mode uses canonical open-source detectors for token-level precision. Like cleanup-stale-code, it enforces a two-phase workflow: produce a prioritized inventory of clone families first, then decide a merge strategy per family. The key insight is that detection and refactoring are separate steps with different failure modes — merging prematurely turns a typo into a behavior change.
When to Use
- "find duplication / dedupe code / DRY violations / repeated code" — see Step F1
- A codebase has accumulated copy-pasted helpers, copy-pasted error-handling wrappers, or structurally similar functions across modules
- Code review surfaces "this is the same as
<other-file>" comments
When NOT to Use
- Refactoring a single function
- Third-party / vendor / generated code (node_modules, dist, build artifacts)
- Type-only duplication (two identical type definitions that exist because of structural or duck typing — usually a feature, not a bug)
- Change is ≤3 lines (do it inline)
General Workflow
Phase 1: Detect → cluster inventory of clone families (do not edit files)
Phase 2: Decide → merge strategy per family (extract / parameterize / delete / keep)
Phase 3: Execute → apply strategy, verify with tests/lint/typecheck
Do not skip Phase 1 even if "I can already see the duplicates." The inventory forces a per-family merge-strategy decision, which is where the work actually happens.
Detection Modes
Step F1 — Pick the detection mode
Default: LLM mode. Use anchor-grep + targeted reads. Cheap, no setup, works on any language.
Opt-in: Tool mode. Use a canonical clone detector for token-level precision on large codebases. The tool gives you a list of candidate clones; the LLM still has to classify and decide the merge.
| Language family | Tool | Install / invocation |
|---|---|---|
| JavaScript / TypeScript | jscpd | npx jscpd <src-dir> --reporters json |
| Python | copydetect | pip install copydetect && copydetect <src-dir> --json |
| Java / Kotlin / C# | pmd-cpd | pmd cpd --language java --dir <src-dir> |
| Go | dupl | go install github.com/mibk/dupl@latest && dupl <src-dir> |
| Cross-language (token-based) | lizard | lizard <src-dir> |
Use tool mode when:
- The codebase has > 50k lines (LLM context budget)
- Token-level diff matters (e.g. block-level, not just file-level)
- The CI pipeline already runs the tool and the report is the entry point
Use LLM mode when:
- The codebase is small or medium
- The team hasn't installed any detector
- The suspected clones are structural (same shape, different names) — tools miss these
Step F2 — LLM-mode anchor search
Search with anchors, not full reads. Patterns that signal a clone (adapt to the project's language):
- Same doc comment / docstring across files: identical multi-line doc blocks in two or more files
- Same constant block: identical sequences of constant definitions across files
- Same error-handling wrapper: identical error-guard or exception-handling shapes
- Same boilerplate accessor: identical getter/setter chains
- Same utility function copy: common helpers (date formatting, string truncation, slug generation, etc.) defined in multiple files
- Same imports block: identical import groups across siblings — often a sign that files were forked
- Same test fixture: identical mock objects or test data definitions in multiple test files
Use whatever text-search tool your environment provides (ripgrep, grep, IDE search). Read ±5 lines around each match. Do not full-read every file — it burns context with diminishing returns and finds off-scope issues.
Step F3 — Build the clone-family inventory
Group findings into clone families — the original + all of its copies. For each family, produce a row:
| Field | Meaning |
|---|---|
family_id | A short hash or label (e.g. clone-7a3b or format-date-v1) |
anchor | The signature / first line / docstring that ties the family together |
members | List of file:line ranges |
size | Lines per member (smallest → largest) |
drift | None (identical), minor (1–2 token diffs), structural (same shape, different names) |
merge_strategy | TBD (decide in Step F5, simultaneously with priority) |
priority | P0–P3 (decide in Step F4, simultaneously with strategy) |
Important: a clone family is not the same as a single duplicated file. A family with 5 copies of the same 30-line block is one family, not five findings.
Priority Tiers
Step F4 — Output a prioritized table (fill priority and merge_strategy together)
Assign priority and pick a merge strategy at the same time — they are interdependent decisions, not sequential ones.
| Priority | Criterion | Examples |
|---|---|---|
| P0 | Identical ≥ 10 lines, zero drift, ≥ 2 members | Same 30-line config-loader block in 3 files |
| P1 | Identical < 10 lines OR 1–2 token differences | Same 5-line string-slugging function in 2 files; same 20-line block with one variable renamed |
| P2 | Structural clones (same shape, different names) — extraction candidate | Two auth-wrapper functions with the same lifecycle and different storage backends |
| P3 | Similar intent, different implementation — usually keep | Two different mergeSort implementations in different modules |
A finding is P0 only if removing it is unambiguously correct. If you need to think, it's P1 or lower.
Heuristic for sizing: clones that are 5+ lines and have ≥ 2 members are almost always worth acting on. Smaller clones (1–3 lines) and 2-member families are usually noise unless the line is a hot path.
Merge Strategies
Step F5 — Decide the merge strategy per family
For each P0/P1/P2 family, pick one strategy. Do not invent intermediate strategies; if none of these fit, the family is a P3 keep.
| Strategy | When | Steps |
|---|---|---|
| Extract helper | All copies do the same thing with the same inputs/outputs | Create a new module (or extend an existing one) with the shared logic. Replace all call sites. Delete the duplicates. |
| Parameterize | Copies do the same thing but with different constants/types | Identify the varying axes. Replace copies with calls to one parameterized function. |
| Delete dead copy | One copy is the canonical one, the others have zero callers OR callers can switch trivially | Keep the canonical. Migrate callers. Delete the duplicates. |
| Keep all | Copies exist for a real reason (different teams, different lifecycles, intentional API duplication) | Mark as P3. Add a one-line comment in each copy explaining the deliberate duplication. |
Important: if the user said the goal is to delete duplicated code, treat P0/P1 as Extract helper / Delete dead copy candidates by default, not as Keep all. Confirm before keeping a family that the user wanted removed.
Step F6 — Execute per family
Within a priority batch, parallelize edits across files. For extractions, do this in order:
- Create the new shared module (with the unified implementation).
- Replace one call site at a time. Verify the project still builds after each.
- Delete the duplicate after the last call site is migrated.
For parameterizations, the same shape — but you may need to thread a parameter through call sites. Verify the parameter defaults to the original behavior to keep tests passing.
For delete-dead-copy, a bulk deletion is usually safe if the analyzer confirmed zero callers.
Step F7 — Verify (per priority batch)
After each priority batch completes, run the project's standard checks (formatter / linter / type checker / tests). Do not wait until all batches are done — verifying per batch makes it easier to isolate which family caused a failure. All checks must pass before moving to the next batch. After the final batch, re-run the detection pass (tool or LLM) and confirm the priority table is now empty for the strategies you applied.
Step F8 — Look for second-order cleanup
After merging, the new shared module may itself have dead code revealed (e.g. a parameter that turned out to be unused after all copies collapsed to one). Run cleanup-stale-code Section A against the new module's comments and Section B against its tests as a follow-up — but only as a follow-up, not as part of this task's scope.
Common Mistakes
| Mistake | Fix |
|---|---|
| Inventing extra strategies the user didn't ask for | Stick to the four strategies above; ask if unclear |
| Full-reading every file to "be thorough" | Anchor greps + ±5 line context |
| Merging detection and refactoring | Always: inventory first, then decide strategy, then execute |
| Calling two implementations with different shapes "duplication" | If the behavior differs, it's not duplication; it's a polymorphism candidate |
| Extracting a helper that's only called once | The "extract" just moved code; you need a real second caller |
Adding a P4 tier for "subjective" items | If user said 3 tiers, give 3 tiers |
| Confirming "duplicates are gone" without re-running detection | Always re-run after Phase 3 |
| Treating tool output as ground truth | Tools report candidate clones; the LLM still classifies and decides |
| "I'll also rewrite the call sites for clarity" | Out of scope; the user asked to dedupe, not to refactor call sites |
Anti-Patterns — Reject These
- "Let me also unify the doc comment style across all these files" → out of scope unless user asked
- "I noticed
<file>:<line>has a real performance bug" → surface as a separate finding; don't expand scope - "I'll do a comprehensive DRY sweep to be safe" → user asked for X, do X
- "Adding a 'possibly-duplicate' tier for ambiguous cases" → if you can't decide, mark P3 keep and explain why
- "I'll extract a base class to unify these" → if no inheritance relationship exists, the base class is just a container for shared code; extract a shared helper or utility instead
- "Let me also consolidate the test fixtures while I'm at it" → second-order task; do it as a follow-up
Common Workflow Endings
After Phase 3, report:
- Clone families: total count, count per priority tier, count per strategy
- Lines removed: total line count delta per file (always non-positive after dedup)
- Verification: lint + typecheck + test status
- Second-order findings: surface any dead code or comment/test drift revealed by the merge (propose a follow-up
cleanup-stale-codepass)
Then ask if the user wants to commit. Do not auto-commit — refactoring is review-heavy and the user often wants to adjust the diff before it lands.
See also
cleanup-stale-code— for cleaning up the comments and tests around the code you've just deduplicatedconsolidate-imports— for auditing import statements in the files you've just merged; deduplication often reveals import-style drift- The four canonical detectors (jscpd, copydetect, pmd-cpd, lizard) are the de-facto standard for token-level detection; the LLM mode here is the fallback when the project hasn't installed any