dry-refactoring
Guided workflow to eliminate copy-paste duplication in packages/* TypeScript source. Use after jscpd has produced a clone list.
Prerequisites
Run jscpd first — see jscpd for the exact command this repo uses:
bunx jscpd@5 packages --reporters ai --format typescript
Workflow
- Run jscpd, scoped to
packages/only (neverskills/, see thejscpdskill for why). - Parse each clone line to identify the two duplicated locations (file + line range).
- Read both code fragments and understand what they do — don't refactor blind.
- Decide the refactor target using the placement rules below.
- Extract the shared logic; update all call sites, not just the two jscpd reported.
- Run
bun run typecheckandbun run test(repo-wide or--filterto the affected package — seeturbo). - Re-run jscpd to confirm the clone is gone.
- Repeat, highest-impact cluster first (most repeated pattern, not just the first line in the report).
Where extracted code goes in this repo
This repo separates code by surface and domain under
packages/hr-skills/src/cli/, packages/hr-skills-build/src/client/,
packages/hr-skills-build/src/server/, packages/hr-skills-ref/src/client/,
and packages/hr-skills-ref/src/server/. Follow that structure — don't dump
extracted helpers into whichever file happens to be open:
- Duplicated logic used by 2+ files in the same domain folder (e.g. two files under
src/server/validation/) → new file in that same folder, e.g.src/server/validation/security-helpers.ts, imported by both. - Duplicated logic used across domain folders (e.g.
src/server/build/*andsrc/server/evaluation/*) →src/server/shared/, alongside the existingshared/constants.ts,shared/helpers.ts,shared/schema.ts,shared/types.ts. - Duplicated logic across packages (
hr-skills-buildandhr-skills-refboth define it) → do not create a third copy in either package. Prefer keeping a single canonical definition in the package it conceptually belongs to (hr-skills-reffor skill-file parsing primitives,hr-skills-buildfor build/registry/CLI concerns) and importing it from the other, or promote it to a small shared internal package if both genuinely need to own it independently. Never resolve this by copy-pasting into a third location. - Intentional client/server duplication → each public surface must remain self-contained. If sharing a module would create a client/server import edge, keep separate surface-local implementations and test both surfaces instead.
- Constants → their own
constants.tsin the relevant folder; don't fold them into a file that already holds functions or types. - Types/interfaces → their own
types.ts(or*.types.tsiftypes.tsalready exists and would grow unrelated concerns) in the relevant folder — never appended to a functions file "for now."
Naming collisions
Before extracting, grep -rn "export (const|function|type|interface) <name>" packages for the name you're about to reuse. If a same-named export already exists elsewhere in the monorepo:
- Prefer a more specific name over a generic one (e.g.
parseSkillFrontmatteroverparse) rather than renaming the existing export and risking unrelated churn. - If both are genuinely the same concept split across two files, that's itself a duplication signal — consolidate into one export instead of two similarly-named ones.
Refactoring strategies
Extract function — duplicate is a block of logic → shared function, called from both places.
Extract module — duplicate spans multiple files in the same or related domains → shared file per the placement rules above, imported by all call sites.
Extract constant — duplicate is repeated literal data or config → named constant in the domain's constants.ts.
Extract type/interface — duplicate or near-duplicate shape appears in two files → single definition in the domain's types.ts, both files import it.
Avoid the template/base class strategy in this codebase unless a real inheritance hierarchy already exists — this repo favors small composable functions and Valibot schemas (see valibot) over class hierarchies.
Tips
- All call sites updated, not just the two jscpd reported —
grepfor other near-identical blocks jscpd's thresholds may have missed. - Tests still pass after refactoring (
bun run test), andbun run typecheckis clean. - The extracted abstraction has a clear, descriptive name — see naming collisions above.
- If the duplication is between
packages/hr-skills-buildandpackages/hr-skills-ref, check whether a changeset is needed — seechangeset. - Format with Biome after refactoring (
bun run formator letlefthookcatch it on commit) — seebiome.