From 5d99764e141804b3e98e0c1c74b82b0c4cac791e Mon Sep 17 00:00:00 2001 From: Lukas Date: Wed, 5 Aug 2026 14:27:51 +0200 Subject: [PATCH] Make jscpd actually scan, and tighten its threshold to 3% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .jscpd.json set pattern to an array, but jscpd's --pattern takes a single glob string. An array matches nothing, so the gate had reported success without reading a file since 2793a66 introduced it — its 0.074ms "detection time" and absent stats table were the tell. Bisecting the config confirmed that key alone reduces the run to zero files. The ignore entries were also bare names rather than globs, so once the pattern worked it swept apps/web/dist and .pnpm-store — 948 files. Excludes __tests__, matching what .jsinspectrc already does. Duplication between test cases is usually deliberate: parallel arrange/act/assert blocks read better than shared setup, and the factories under apps/web/src/__tests__/factories cover the deduplication worth having. Real duplication is 1.72% across 209 source files, so the threshold drops from 5% to 3%. jscpd measures a ratio rather than blocking each new clone, and a 5% budget left roughly 3x headroom before it would ever fire. Gives jscpd explicit paths behind `pnpm jscpd`, so lefthook and the check script share one invocation, as jsinspect already does. Also guards the pattern key in check-gates.mjs, since an array there fails silently rather than erroring. Co-Authored-By: Claude Opus 5 (1M context) --- .jscpd.json | 12 +++++++++--- lefthook.yml | 2 +- package.json | 4 ++-- scripts/check-gates.mjs | 6 ++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.jscpd.json b/.jscpd.json index 1945b32..88773b1 100644 --- a/.jscpd.json +++ b/.jscpd.json @@ -1,8 +1,14 @@ { - "threshold": 5, + "threshold": 3, "minLines": 5, "minTokens": 50, - "pattern": ["**/*.ts", "**/*.tsx"], - "ignore": ["node_modules", "dist", "build", "coverage", ".specify", "specs"], + "pattern": "**/*.{ts,tsx}", + "ignore": [ + "**/node_modules/**", + "**/dist/**", + "**/build/**", + "**/coverage/**", + "**/__tests__/**" + ], "reporters": ["console"] } diff --git a/lefthook.yml b/lefthook.yml index 8a344c4..dfd08f4 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -14,7 +14,7 @@ pre-commit: - name: check-props run: node scripts/check-component-props.mjs - name: jscpd - run: pnpm exec jscpd + run: pnpm jscpd - name: jsinspect run: pnpm jsinspect - name: typecheck-oxlint-test diff --git a/package.json b/package.json index 6cddaf8..2eabb44 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,12 @@ "test": "vitest run", "test:watch": "vitest", "knip": "knip", - "jscpd": "jscpd", + "jscpd": "jscpd apps/web/src packages/domain/src packages/application/src", "jsinspect": "jsinspect -c .jsinspectrc apps/web/src packages/domain/src packages/application/src", "oxlint": "oxlint --tsconfig tsconfig.json --type-aware --deny-warnings", "check:ignores": "node scripts/check-lint-ignores.mjs", "check:classnames": "node scripts/check-cn-classnames.mjs", "check:props": "node scripts/check-component-props.mjs", - "check": "pnpm audit --audit-level=high && knip && biome check . && node scripts/check-lint-ignores.mjs && node scripts/check-cn-classnames.mjs && node scripts/check-component-props.mjs && jscpd && pnpm jsinspect && tsc --build && pnpm oxlint && vitest run" + "check": "pnpm audit --audit-level=high && knip && biome check . && node scripts/check-lint-ignores.mjs && node scripts/check-cn-classnames.mjs && node scripts/check-component-props.mjs && pnpm jscpd && pnpm jsinspect && tsc --build && pnpm oxlint && vitest run" } } diff --git a/scripts/check-gates.mjs b/scripts/check-gates.mjs index cc941f8..b69cad8 100644 --- a/scripts/check-gates.mjs +++ b/scripts/check-gates.mjs @@ -63,6 +63,12 @@ export function checkGates() { violations.push("vitest.config.ts sets passWithNoTests"); } + // jscpd's pattern is a single glob string. An array matches no files, and + // the run then reports success having read nothing. + if (Array.isArray(JSON.parse(read(".jscpd.json")).pattern)) { + violations.push(".jscpd.json sets pattern to an array, not a glob string"); + } + // The canary rule below is not type-aware, so the probe alone cannot // detect type-aware rules being switched off. if (!scripts.oxlint.includes("--type-aware")) {