From a4ada11762f0a3825bf31168dcfa4f801164a0e0 Mon Sep 17 00:00:00 2001 From: norvalbv Date: Sun, 2 Aug 2026 08:52:38 +0100 Subject: [PATCH] =?UTF-8?q?fix(co-occurrence):=20clone=20gate=20vacuous=20?= =?UTF-8?q?on=20.mts/.mjs=20=E2=80=94=20CODE=5FEXT=20dropped=20module-suff?= =?UTF-8?q?ixed=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The post-filter kept only .ts/.tsx/.js/.jsx, but devkit's own scanRoots (cli, gate-engine) are pure .mts — jscpd tokenizes mts/cts as typescript and mjs/cjs as javascript and reported the clones, then CODE_EXT discarded every one. `guard-clone scan` returned 0 repo-wide while raw jscpd found real clones (verified during #303's gates): a silently dead gate. With the fix the same scan reports 62 cross-file clones (min-tokens 50), dominated by the review/eval vs review/eval/conventions bench+matcher family. Regression tests: .mts and .mjs clone fixtures must block (exit 1). Co-Authored-By: Claude Fable 5 --- .../__tests__/clone-detector.test.mts | 18 ++++++++++++++++++ gate-engine/co-occurrence/clone-detector.mts | 6 ++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/gate-engine/co-occurrence/__tests__/clone-detector.test.mts b/gate-engine/co-occurrence/__tests__/clone-detector.test.mts index 4e01e68d..05d1c5e0 100644 --- a/gate-engine/co-occurrence/__tests__/clone-detector.test.mts +++ b/gate-engine/co-occurrence/__tests__/clone-detector.test.mts @@ -153,6 +153,24 @@ describe.skipIf(!HAS_JSCPD)('clone-detector --gate exit-code contract', () => { expect(run(['scan', '--gate', '--paths', tmp], {}).status).toBe(1); }); + it('exit 1 — a clone in .mts files blocks (regression: CODE_EXT dropped module-suffixed exts)', () => { + // devkit's own scanRoots are pure .mts; the old /\.(tsx?|jsx?)$/ filter discarded every + // jscpd hit in .mts/.mjs, so the gate reported 0 repo-wide — a silently vacuous gate. + const mts = mkdtempSync(join(tmpdir(), 'clone-mts-')); + writeFileSync(join(mts, 'a.mts'), `${SHARED}\nexport const A_ONLY = 1;\n`); + writeFileSync(join(mts, 'b.mts'), `${SHARED}\nexport const B_ONLY = 2;\n`); + expect(run(['scan', '--gate', '--paths', mts], {}).status).toBe(1); + rmSync(mts, { recursive: true, force: true }); + }); + + it('exit 1 — a clone in .mjs files blocks (jscpd tokenizes mjs/cjs as javascript)', () => { + const mjs = mkdtempSync(join(tmpdir(), 'clone-mjs-')); + writeFileSync(join(mjs, 'a.mjs'), `${SHARED}\nexport const A_ONLY = 1;\n`); + writeFileSync(join(mjs, 'b.mjs'), `${SHARED}\nexport const B_ONLY = 2;\n`); + expect(run(['scan', '--gate', '--paths', mjs], {}).status).toBe(1); + rmSync(mjs, { recursive: true, force: true }); + }); + it('exit 0 — no cross-file clone (clean)', () => { const clean = mkdtempSync(join(tmpdir(), 'clone-clean-')); writeFileSync(join(clean, 'solo.ts'), `${SHARED}\nexport const SOLO = 1;\n`); diff --git a/gate-engine/co-occurrence/clone-detector.mts b/gate-engine/co-occurrence/clone-detector.mts index ce493641..3399a525 100755 --- a/gate-engine/co-occurrence/clone-detector.mts +++ b/gate-engine/co-occurrence/clone-detector.mts @@ -63,8 +63,10 @@ const DEFAULTS = { ignore: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**', '**/__mocks__/**'], }; -// jscpd auto-detects formats by extension; we keep only source code clones. -const CODE_EXT = /\.(tsx?|jsx?)$/; +// jscpd auto-detects formats by extension; we keep only source code clones. Must cover the +// module-suffixed extensions (.mts/.cts/.mjs/.cjs — jscpd tokenizes all of them): devkit's own +// scanRoots are pure .mts, so excluding them made this gate report 0 repo-wide (vacuous). +const CODE_EXT = /\.([cm]?[tj]s|[tj]sx)$/; // jscpd bin resolution lives in the side-effect-free ./jscpd-bin.mts (shared with the prefix-cache // config-fingerprint, which folds the SAME resolution into the cache key). Read JSCPD_BIN here (not as