fix(co-occurrence): exclude import declarations from clone detection at the tokenizer - #308
Conversation
…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>
📝 WalkthroughWalkthroughThe 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. ChangesClone filtering
Estimated code review effort: 3 (Moderate) | ~20 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
gate-engine/co-occurrence/__tests__/clone-detector.test.mtsgate-engine/co-occurrence/clone-detector.mts
| // Re-export barrels: export { a } from 'x'; / export * from 'x'; | ||
| String.raw`export\s*(\{[^}]*\}|\*)\s*from\s*['"][^'"]+['"];?`, |
There was a problem hiding this comment.
🎯 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")
PYRepository: 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)
PYRepository: 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)))
PYRepository: 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.
…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>
…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>
…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>
What
jscpd now runs with
--ignore-patternregexes 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 dynamicimport(...)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). Dynamicimport(...)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
LegacyAssetManifestdup 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)
review/evalvsreview/eval/conventionsbench+matcher family, pluscritique/eval): recommendguard-clone json | guard-dup-allowlist baseline-clonesrather 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 (theoverrides.mtsprecedent). Dedupe opportunistically when those harnesses next change substantively.run-review.mts↔reviewers/bench.mts(bench drives the real cascade by design; also implementation-hash-sensitive). Baseline it.cli/lib/ship/review/(repository/state.mtsis a 4-clone hub),sync-agents/sync-skillsresiduals (incl. the twice-declaredLegacyAssetManifestinterface),sync-manifest↔install-hooks,critique/lifecycle pair,decisions/check-alignment↔detect. 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
Tests