Skip to content

fix(co-occurrence): exclude import declarations from clone detection at the tokenizer - #308

Merged
norvalbv merged 1 commit into
mainfrom
fix/clone-gate-import-preamble
Aug 2, 2026
Merged

fix(co-occurrence): exclude import declarations from clone detection at the tokenizer#308
norvalbv merged 1 commit into
mainfrom
fix/clone-gate-import-preamble

Conversation

@norvalbv

@norvalbv norvalbv commented Aug 2, 2026

Copy link
Copy Markdown
Owner

What

jscpd now runs with --ignore-pattern regexes that remove import/re-export declarations from the token stream before clone detection. The gate's semantic becomes: clones are measured over non-import code. Two modules using the same shared libs are forced into identical, linter-sorted import preambles — the module system working, not copy-paste — and post-#305 the repo scan showed two such blocks big enough to clear the 50-token floor on their own, which would false-block any commit touching those sibling files once a release wires the gate into hooks.

Why tokenizer-level, not fragment classification

The first design classified reported fragments post-hoc (isImportPreamble). The commit-gate correctness reviewer killed six drafts across seven rounds, each with a verified exploit: a ratio threshold hid a duplicated statement behind imports; /^import\b/ matched dynamic import(...) whose unterminated state absorbed callbacks; an incomplete-statement excusal let a truncated call ride; a floorless excusal swallowed zero-import JSX molecules; the interior state accepted any quote-terminated line (SECRET_KEY = "x"); import-attributes stuck the state open. The lesson is structural: classifying jscpd's token-boundary fragments after the fact is unfixable — every heuristic boundary is exploitable. Removing imports before tokenization eliminates the attack surface: nothing can ride a preamble over the token floor, and nothing real can hide behind one. The adversarial cases live on as end-to-end gate-contract tests (31 total, all green; full suite passed pre-push).

The four patterns are bounded (single-line forms forbid \n; the multi-line form crosses lines only inside {...} and requires the } from '...' closer) and comma-free (the CLI comma-splits the option). Dynamic import(...) deliberately matches none. Unmatched exotic shapes fall through to normal tokenization — the safe direction: worst case a clone reports and the allowlist is the remedy.

Live scan: 62 → 60. The dropped sync-agents fragment's residual LegacyAssetManifest dup is sub-floor on its own merits — it only crossed 50 tokens by riding the imports (it's still on the inventory below as dedup-worthy).

The remaining 60 clones — inventory + recommendation (your call, not in this PR)

  • 43 eval-harness clones (review/eval vs review/eval/conventions bench+matcher family, plus critique/eval): recommend guard-clone json | guard-dup-allowlist baseline-clones rather than dedup — each suite hashes its runner/scorer implementation, so deduping the copied harness drifts published checkpoint hashes for 2–3 suites with zero behavior change (the overrides.mts precedent). Dedupe opportunistically when those harnesses next change substantively.
  • 1 deliberate: run-review.mtsreviewers/bench.mts (bench drives the real cascade by design; also implementation-hash-sensitive). Baseline it.
  • 16 real product-code clones worth actual dedup, biggest clusters: cli/lib/ship/review/ (repository/state.mts is a 4-clone hub), sync-agents/sync-skills residuals (incl. the twice-declared LegacyAssetManifest interface), sync-manifestinstall-hooks, critique/ lifecycle pair, decisions/check-alignmentdetect. Happy to take these as a follow-up branch when you're out of the repo.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved duplicate-code detection by excluding import and re-export statements from clone analysis.
    • Import-only repetition no longer incorrectly blocks validation.
    • Duplicate executable code, including code following imports and dynamic-import logic, continues to be detected and reported.
  • Tests

    • Added coverage for import-only duplication, duplicated code after imports, and duplicated dynamic-import runtime code.

…at the tokenizer

Two modules using the same shared libs are forced into identical, linter-sorted
import blocks — the module system working, not copy-paste debt — and a big
enough preamble clears jscpd's 50-token floor on its own, false-blocking any
commit touching sibling files once the gate is wired into hooks (post-#305 the
repo scan showed two such blocks).

Imports are now removed from the token stream itself via jscpd
--ignore-pattern (four bounded, comma-free regexes: named/multi-line,
default/namespace single-line, side-effect, re-export barrels; dynamic
import(...) deliberately matches none). The semantic becomes "clones are
measured over non-import code": nothing can ride a preamble over the token
floor, and nothing real can hide behind one.

A post-hoc fragment classifier was tried first and abandoned after six
opus-confirmed reviewer holes across seven gate rounds (ratio leaks, dynamic
import absorption, quote-terminated statements, import attributes, ...) — each
patch surfaced the next hole because classifying jscpd's token-boundary
fragments after the fact is unfixable by construction. The adversarial cases
live on as end-to-end gate-contract tests: preamble-only pair passes, a real
body behind identical imports still blocks, a duplicated dynamic-import block
still blocks. Live scan 62 -> 60; the dropped sync-agents fragment's residual
interface dup is sub-floor on its own merits (it only crossed 50 tokens by
riding the imports).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The clone detector now passes bounded import and re-export ignore patterns to jscpd. Integration tests verify that import-only duplication is ignored, while duplicated executable and dynamic-import code still blocks.

Changes

Clone filtering

Layer / File(s) Summary
Import pattern filtering
gate-engine/co-occurrence/clone-detector.mts
IMPORT_IGNORE_PATTERNS covers named, namespace, default, side-effect, and re-export declarations. detectClones passes these patterns to jscpd.
Clone filtering validation
gate-engine/co-occurrence/__tests__/clone-detector.test.mts
Tests verify that identical import preambles do not block, while duplicated executable code and dynamic-import blocks produce exit status 1.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes excluding import declarations from tokenizer-level clone detection.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/clone-gate-import-preamble

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@gate-engine/co-occurrence/clone-detector.mts`:
- Around line 239-240: Extend the re-export pattern in IMPORT_IGNORE_PATTERNS to
match `export type { ... } from ...` alongside existing named and wildcard
re-exports, while preserving current matching behavior; add contract cases
covering type-only re-exports so duplicated type-only preambles are ignored.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 186c329f-93a9-42e3-88ef-63de126ef444

📥 Commits

Reviewing files that changed from the base of the PR and between 6e9949c and 8ce4663.

📒 Files selected for processing (2)
  • gate-engine/co-occurrence/__tests__/clone-detector.test.mts
  • gate-engine/co-occurrence/clone-detector.mts

Comment on lines +239 to +240
// Re-export barrels: export { a } from 'x'; / export * from 'x';
String.raw`export\s*(\{[^}]*\}|\*)\s*from\s*['"][^'"]+['"];?`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C2 --glob '*.{ts,tsx,mts}' \
  '^\s*export\s+(type\s+)?\*\s+as\s+|^\s*export\s+type\s+\{' .

Repository: norvalbv/devkit

Length of output: 2263


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== targeted source =="
sed -n '220,250p' gate-engine/co-occurrence/clone-detector.mts

echo
echo "== relevant static re-export declarations =="
rg -n --glob '*.{ts,tsx,mts}' '^\s*export\s+(\*|\{[^}]*\})\s+as\s+|\bexport\s+type\s+\{' . || true

echo
echo "== read-only regex behavior probe =="
python3 - <<'PY'
import re
pattern = re.compile(r"export\s*(\{[^}]*\}|\*)\s*from\s*['"][^'"]+['"];?", re.I)
cases = [
    "export { Widget } from './widgets';",
    "export * from './widgets';",
    "export {} from './widgets';",
    "export type { Widget } from './widgets';",
    "export * as widgets from './widgets';",
    "export * as $widgets from './widgets';",
]
for case in cases:
    print(case, "-->", "matches" if pattern.search(case) else "no match")
PY

Repository: norvalbv/devkit

Length of output: 2945


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== pattern definitions =="
rg -n --context 8 "IMPORT_IGNORE_PATTERNS" gate-engine/co-occurrence/clone-detector.mts || true

echo
echo "== broad source form =="
rg -n "Re-export barrels|export\\s\\(\\{\\[^\\}\\]\\*\\}\\)|import_ignore_patterns|IMPORT_IGNORE_PATTERNS" gate-engine -S || true

echo
echo "== existing export declarations =="
rg -n --glob '*.{ts,tsx,mts}' '^\s*export\s+(type\s+)?\*(\s+as\s+[\w$]+)?\s+from\s+|^\s*export\s+type\s+\{' . || true

echo
echo "== read-only regex behavior probe =="
python3 - <<'PY'
import re

literal_source = r"""String.raw`export\s*(\{[^}]*\}|\*)\s*from\s*['"][^'"]+['"];?`,"""
current = re.compile(r"export\s*(\{[^}]*\}|\*)\s*from\s*['"][^'"]+['"];?")
proposed = re.compile(r"export\s*(type\s+)?(\{[^}]*\}|\*(\s+as\s+[\w$]+)?)\s*from\s*['"][^'"]+['"];?")

cases = [
    "export { Widget } from './widgets';",
    "export * from './widgets';",
    "export {} from './widgets';",
    "export type { Widget } from './widgets';",
    "export * as widgets from './widgets';",
    "export * as $widgets from './widgets';",
]

for case in cases:
    print(case)
    print(" current matches", current.search(case) is not None)
    print(" proposed matches", proposed.search(case) is not None)
PY

Repository: norvalbv/devkit

Length of output: 3483


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 <<'PY'
import re

current = re.compile(r"export\s*(\{[^}]*\}|\*)\s*from\s*['\"].*?['\"];\??")
namespace = re.compile(r"export\s*(type\s+)?(\{[^}]*\}|\*(\s+as\s+[\w$]+)?)\s*from\s*['\"].*?['\"];\??")

cases = [
    "export { Widget } from './widgets';",
    "export * from './widgets';",
    "export type { Widget } from './widgets';",
    "export * as widgets from './widgets';",
    "export * as $widgets from './widgets';",
]

for case in cases:
    print(case)
    print(" current matches", bool(current.search(case)))
    print(" proposed matches", bool(namespace.search(case)))
PY

Repository: norvalbv/devkit

Length of output: 562


Match type-only re-exports.

Type-only exports are valid module declarations, and export type { ... } from 'module'; is currently present in the repo. It falls outside the current IMPORT_IGNORE_PATTERNS re-export regex, so duplicated type-only re-export preambles can still block the gate. Extend the pattern for export type { ... } from ... and add matching contract cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gate-engine/co-occurrence/clone-detector.mts` around lines 239 - 240, Extend
the re-export pattern in IMPORT_IGNORE_PATTERNS to match `export type { ... }
from ...` alongside existing named and wildcard re-exports, while preserving
current matching behavior; add contract cases covering type-only re-exports so
duplicated type-only preambles are ignored.

@norvalbv
norvalbv merged commit 98adfcd into main Aug 2, 2026
1 of 2 checks passed
norvalbv added a commit that referenced this pull request Aug 2, 2026
…clone-gate ruling (#310)

benchmarks-grow-from-telemetry gains its convergence record before the release
that ships it: capture loop closed end-to-end (#295/#302/#303/#309, first 8
pure-telemetry rows, corpus 128), label-trust precondition met (#304: κ 0.735
post-triage, 4.2% noise floor; cleanlab floor still pending bench pred_probs),
and the Target's c-CRAB/CR-Bench known-answer path recorded as falsified
(#307) with the replacement candidates awaiting ratification.

New axis clone-gate-non-import-code ([VALIDATED]): clones are measured over
non-import code, excluded at the jscpd tokenizer — with the six-hole failure
of post-hoc fragment classification recorded as the rejected road so a future
simplifier can't silently re-vacuous the gate (#305/#308).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
norvalbv added a commit that referenced this pull request Aug 2, 2026
…rerequisite (sc-1414) (#311)

The repaired clone gate (#305/#308) goes live in consumer hooks at the next
release; without a baseline every commit touching a cloned file blocks on
debt that predates the gate working. `guard-clone json | guard-dup-allowlist
baseline-clones` freezes all 60 as decaying baseline entries (the matcher
baseline burn-down pattern).

Disposition choice: baseline ALL 60, not just the 43 eval-harness clones —
leaving the 16 product clones unbaselined would block unrelated commits
touching those files before the dedup work (sc-1414 part 2) lands. Baseline
entries decay, so the product clones resurface on schedule; the eval-harness
family stays frozen deliberately (deduping it would drift published
runner/scorer checkpoint hashes for 2-3 suites — the overrides.mts precedent),
as does the deliberate run-review<->bench mirror. Gate verified clean after:
scan --gate exit 0.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
norvalbv added a commit that referenced this pull request Aug 2, 2026
…art 2, cluster 1 (#312)

The clone gate's inventory (#308) flagged repository/state.mts as a 4-clone
hub: fail/errorMessage/gitEnvironment/objectValue copy-pasted across
repository/, cache/, and the setup-manifest family, plus a duplicated
lstat-probe. New cli/lib/ship/review/shared/common.mts owns them; the
lstat-probe case imports the already-exported reviewSetupStat instead of a
local twin.

Behavior-preserving by construction: objectValue now takes the caller's full
message (each call site's string reconstructed byte-identically from its old
template), and gitEnvironment takes optional extra pins (cache/root passes
GIT_NO_LAZY_FETCH + GIT_TERMINAL_PROMPT; the stripped-GIT_* + OPTIONAL_LOCKS=0
base is shared). gitnexus flags the fan-in as HIGH (these feed the
review-setup verification paths) — mitigated by string-identical messages,
identical env composition, and the full cli suite green (1182 tests).

Repo-wide clone scan 60 -> 54; the six ship/review cluster clones are gone
rather than baselined. Remaining fail() copies in files the gate did not flag
are left for opportunistic cleanup.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant