Add MDS037 duplicated-content rule for cross-file paragraph detection - #154
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #154 +/- ##
==========================================
+ Coverage 87.63% 87.68% +0.04%
==========================================
Files 108 108
Lines 13597 13597
==========================================
+ Hits 11916 11922 +6
+ Misses 1222 1218 -4
+ Partials 459 457 -2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a new opt-in mdsmith rule (MDS037) to detect verbatim duplicated paragraphs across Markdown files, along with docs, fixtures, and registration so it can be enabled via config when desired.
Changes:
- Implement MDS037
duplicated-contentrule using normalized paragraph SHA-256 fingerprints and cross-file corpus scanning. - Add unit tests plus folder-based integration fixtures and rule documentation.
- Register the rule in CLI/test imports and update the rule index + plan status.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
internal/rules/duplicatedcontent/rule.go |
New rule implementation: paragraph extraction, normalization, corpus scanning, include/exclude filtering, and diagnostics. |
internal/rules/duplicatedcontent/rule_test.go |
Unit tests for detection, normalization, settings, and error handling. |
internal/rules/MDS037-duplicated-content/README.md |
New rule documentation, configuration options, and examples. |
internal/rules/MDS037-duplicated-content/good/simple.md |
“Good” fixture content for the rule docs/fixtures. |
internal/rules/MDS037-duplicated-content/bad/duplicate.md |
“Bad” fixture declaring expected diagnostic for a duplicate paragraph. |
internal/rules/MDS037-duplicated-content/bad/ref/source.md |
Reference file used as the duplication source during fixture runs. |
internal/rules/index.md |
Add MDS037 to the rules index. |
cmd/mdsmith/main.go |
Register rule via blank import. |
internal/integration/rules_test.go |
Register rule for fixture-based integration test runs. |
internal/engine/categories_test.go |
Register rule for category-related tests. |
internal/config/config_test.go |
Register rule for config-related tests. |
plan/50_redundancy-duplication-detection.md |
Mark plan complete and summarize implementation decisions. |
PLAN.md |
Update plan index status for item 50. |
- resolveCorpus: handle relative f.Path correctly. In CLI runs ResolveFiles returns './docs/a.md' while RootDir is absolute, so filepath.Rel would fail and the rule quietly fell back to FS-only scope. Split rootRelative() out: absolute paths go through Rel, relative ones are treated as root-relative, and '..' traversal rejects either. - matchesFilters: match include/exclude globs against both the full slash path and basename, matching MDS027's semantics so patterns like 'draft.md' work regardless of directory depth. - README: fix 'Default: enabled' to 'disabled (opt-in via .mdsmith.yml)'. - Fixtures: drop the self-referential 'appears in ref/source.md' wording; neutral phrasing works in both duplicate.md and source.md. - Tests: add coverage for relative f.Path under absolute RootDir, basename-only exclude patterns across directories, and '..' traversal falling through to FS scope.
- resolveCorpus: handle relative f.Path correctly. In CLI runs ResolveFiles returns './docs/a.md' while RootDir is absolute, so filepath.Rel would fail and the rule quietly fell back to FS-only scope. Split rootRelative() out: absolute paths go through Rel, relative ones are treated as root-relative, and '..' traversal rejects either. - matchesFilters: match include/exclude globs against both the full slash path and basename, matching MDS027's semantics so patterns like 'draft.md' work regardless of directory depth. - README: fix 'Default: enabled' to 'disabled (opt-in via .mdsmith.yml)'. - Fixtures: drop the self-referential 'appears in ref/source.md' wording; neutral phrasing works in both duplicate.md and source.md. - Tests: add coverage for relative f.Path under absolute RootDir, basename-only exclude patterns across directories, and '..' traversal falling through to FS scope.
547ab9b to
7419f6a
Compare
- resolveCorpus: handle relative f.Path correctly. In CLI runs ResolveFiles returns './docs/a.md' while RootDir is absolute, so filepath.Rel would fail and the rule quietly fell back to FS-only scope. Split rootRelative() out: absolute paths go through Rel, relative ones are treated as root-relative, and '..' traversal rejects either. - matchesFilters: match include/exclude globs against both the full slash path and basename, matching MDS027's semantics so patterns like 'draft.md' work regardless of directory depth. - README: fix 'Default: enabled' to 'disabled (opt-in via .mdsmith.yml)'. - Fixtures: drop the self-referential 'appears in ref/source.md' wording; neutral phrasing works in both duplicate.md and source.md. - Tests: add coverage for relative f.Path under absolute RootDir, basename-only exclude patterns across directories, and '..' traversal falling through to FS scope.
93dc8c0 to
cc5f0a1
Compare
- resolveCorpus: handle relative f.Path correctly. In CLI runs ResolveFiles returns './docs/a.md' while RootDir is absolute, so filepath.Rel would fail and the rule quietly fell back to FS-only scope. Split rootRelative() out: absolute paths go through Rel, relative ones are treated as root-relative, and '..' traversal rejects either. - matchesFilters: match include/exclude globs against both the full slash path and basename, matching MDS027's semantics so patterns like 'draft.md' work regardless of directory depth. - README: fix 'Default: enabled' to 'disabled (opt-in via .mdsmith.yml)'. - Fixtures: drop the self-referential 'appears in ref/source.md' wording; neutral phrasing works in both duplicate.md and source.md. - Tests: add coverage for relative f.Path under absolute RootDir, basename-only exclude patterns across directories, and '..' traversal falling through to FS scope.
72698d2 to
94b5572
Compare
|
🟢 Merge Queue — picked up This PR is in the queue and will be batched with other Next: No action needed — you'll get another comment when CI starts on the batch. View merge queue run. |
|
🔵 Merge Queue — CI running Merged into batch branch Next: No action needed — you'll be notified when CI completes. |
Two defensive branches were unreachable from Linux-normal tests. Pin their behavior: - TestRootRelative_RelErrorOnRelativeRoot calls filepath.Rel with a relative rootDir and absolute path, the same shape that fires on Windows cross-volume inputs, so the Rel error return runs. - TestRootRelative_AbsErrorWhenCWDIsRemoved chdirs into a fresh temp dir, removes it, then calls rootRelative with a relative path; os.Getwd now fails and filepath.Abs returns the error. Package coverage is 100% of statements.
|
✅ Merge Queue — merged This PR landed on Next: Done — nothing more to do here. |
|
🟢 Merge Queue — picked up This PR is in the queue and will be batched with other Next: No action needed — you'll get another comment when CI starts on the batch. View merge queue run. |
|
🔵 Merge Queue — CI running Merged into batch branch Next: No action needed — you'll be notified when CI completes. |
|
✅ Merge Queue — merged This PR landed on Next: Done — nothing more to do here. |
Summary
Implements MDS037, a new opt-in linting rule that detects paragraphs duplicated verbatim across Markdown files in a project. The rule uses SHA-256 fingerprinting over normalized text (lowercase, whitespace-collapsed) to identify substantial duplicate content.
Key Changes
New Rule Implementation (
internal/rules/duplicatedcontent/rule.go):min-charsthreshold (default 200 runes) to avoid noise from short boilerplateRootFSwhen project root is known, falls back to file's directoryincludeandexcludeglob patterns (matched against slash path and basename) to scope the cross-file comparisonEnabledByDefault() == false) since projects intentionally share prose across filessettings.ToInt/settings.ToStringSlicehelpers (plan 85)Comprehensive Test Suite (
internal/rules/duplicatedcontent/rule_test.go) — 97%+ coverage, including CLI-style relativef.Pathunder an absoluteRootDir, basename-only exclude across directories, and..traversal rejection.Documentation (
internal/rules/MDS037-duplicated-content/README.md):excludeentry for generated directories.mdsmith.yml)Test Fixtures: single-file good fixture; a bad/duplicate.md + bad/ref/source.md pair that shares neutral wording (no self-references) to avoid confusing future readers.
Integration: rule registered in
cmd/mdsmith/main.goand test imports; rules index and plan 50 status updated.Also in this PR
plan/90_corpus-test-git-config-isolation.md): a small follow-up plan filed in the same session to track isolatinginternal/corpus/clone_test.go:makeBareRepofrom host-level git commit signing, which surfaced as a pre-existing test failure while verifying this PR. The plan itself is documentation only; no code change.Implementation Details
https://claude.ai/code/session_018U79bfk1Gde7UHt9M7Ycm3