Skip to content

Commit 1d6e852

Browse files
authored
Merge PR #154: Add MDS037 duplicated-content rule for cross-file paragraph detection
2 parents afe06e6 + 150c2a4 commit 1d6e852

16 files changed

Lines changed: 1555 additions & 12 deletions

File tree

PLAN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ footer: |
1616

1717
| ID | Status | Title |
1818
|-----|--------|-----------------------------------------------------------------------------------------------------------------|
19-
| 50 | 🔲 | [Redundancy / Duplication Detection](plan/50_redundancy-duplication-detection.md) |
19+
| 50 | | [Redundancy / Duplication Detection](plan/50_redundancy-duplication-detection.md) |
2020
| 51 || [Section-Level Size Limits](plan/51_section-level-size-limits.md) |
2121
| 52 | 🔲 | [Archetype / Template Library for Agentic Patterns](plan/52_archetype-template-library.md) |
2222
| 53 || [Conciseness Scoring](plan/53_conciseness-scoring.md) |
@@ -47,4 +47,6 @@ footer: |
4747
| 87 || [Flavor validation for GitHub Alerts](plan/87_markdown-flavor-github-alerts.md) |
4848
| 88 || [TOC directive migration aid](plan/88_toc-directive-migration.md) |
4949
| 89 | 🔲 | [TOC generator directive and MDS035 auto-fix](plan/89_toc-generator-directive.md) |
50+
| 90 | 🔲 | [Isolate corpus test git config from host signing](plan/90_corpus-test-git-config-isolation.md) |
51+
| 91 | 🔲 | [MDS037 skips paragraphs inside generated sections](plan/91_mds037-skip-generated-sections.md) |
5052
<?/catalog?>

cmd/mdsmith/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
_ "github.com/jeduden/mdsmith/internal/rules/concisenessscoring"
3131
_ "github.com/jeduden/mdsmith/internal/rules/crossfilereferenceintegrity"
3232
_ "github.com/jeduden/mdsmith/internal/rules/directorystructure"
33+
_ "github.com/jeduden/mdsmith/internal/rules/duplicatedcontent"
3334
_ "github.com/jeduden/mdsmith/internal/rules/emptysectionbody"
3435
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodelanguage"
3536
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodestyle"

internal/config/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
_ "github.com/jeduden/mdsmith/internal/rules/concisenessscoring"
1919
_ "github.com/jeduden/mdsmith/internal/rules/crossfilereferenceintegrity"
2020
_ "github.com/jeduden/mdsmith/internal/rules/directorystructure"
21+
_ "github.com/jeduden/mdsmith/internal/rules/duplicatedcontent"
2122
_ "github.com/jeduden/mdsmith/internal/rules/emptysectionbody"
2223
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodelanguage"
2324
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodestyle"

internal/engine/categories_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
_ "github.com/jeduden/mdsmith/internal/rules/catalog"
1919
_ "github.com/jeduden/mdsmith/internal/rules/crossfilereferenceintegrity"
2020
_ "github.com/jeduden/mdsmith/internal/rules/directorystructure"
21+
_ "github.com/jeduden/mdsmith/internal/rules/duplicatedcontent"
2122
_ "github.com/jeduden/mdsmith/internal/rules/emptysectionbody"
2223
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodelanguage"
2324
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodestyle"

internal/integration/rules_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
_ "github.com/jeduden/mdsmith/internal/rules/concisenessscoring"
2424
_ "github.com/jeduden/mdsmith/internal/rules/crossfilereferenceintegrity"
2525
"github.com/jeduden/mdsmith/internal/rules/directorystructure"
26+
_ "github.com/jeduden/mdsmith/internal/rules/duplicatedcontent"
2627
_ "github.com/jeduden/mdsmith/internal/rules/emptysectionbody"
2728
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodelanguage"
2829
_ "github.com/jeduden/mdsmith/internal/rules/fencedcodestyle"

internal/lint/file.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ type File struct {
2222
FrontMatter []byte
2323
LineOffset int
2424

25+
// StripFrontMatter records whether this file was parsed in
26+
// front-matter-stripping mode. Rules that read other files
27+
// from the corpus should mirror the same mode so that line
28+
// numbers in cross-file diagnostics are computed against the
29+
// same coordinate system as the current file.
30+
StripFrontMatter bool
31+
2532
// MaxInputBytes is the maximum file size in bytes that rules
2633
// should enforce when reading secondary files (includes, schemas,
2734
// cross-references). Zero or negative means unlimited.
@@ -110,6 +117,7 @@ func NewFileFromSource(path string, source []byte, stripFrontMatter bool) (*File
110117
}
111118
f.FrontMatter = fm
112119
f.LineOffset = offset
120+
f.StripFrontMatter = stripFrontMatter
113121
return f, nil
114122
}
115123

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
id: MDS037
3+
name: duplicated-content
4+
status: ready
5+
description: Paragraphs should not repeat verbatim across Markdown files.
6+
---
7+
# MDS037: duplicated-content
8+
9+
Paragraphs should not repeat verbatim across Markdown files.
10+
11+
- **ID**: MDS037
12+
- **Name**: `duplicated-content`
13+
- **Status**: ready
14+
- **Default**: disabled (opt-in via `.mdsmith.yml`);
15+
include: [], exclude: [], min-chars: 200
16+
- **Fixable**: no
17+
- **Implementation**:
18+
[source](./)
19+
- **Category**: meta
20+
21+
## Settings
22+
23+
| Setting | Type | Default | Description |
24+
|-------------|------|---------|------------------------------------------------|
25+
| `include` | list | `[]` | glob patterns limiting which siblings to scan |
26+
| `exclude` | list | `[]` | glob patterns of siblings to skip |
27+
| `min-chars` | int | `200` | minimum normalized paragraph length to compare |
28+
29+
Before comparing, the rule normalizes each paragraph. Whitespace
30+
collapses to single spaces. Letters become lowercase. Leading and
31+
trailing space is trimmed. A paragraph shorter than `min-chars` runes
32+
is skipped; short stubs would otherwise produce noise.
33+
34+
The rule walks `RootFS` when the project root is known. Otherwise it
35+
falls back to the file's own directory. An `include` list narrows the
36+
scan to matching paths. An `exclude` entry takes precedence.
37+
38+
## Performance
39+
40+
Each checked file reads every other Markdown file in scope
41+
(`.md` and `.markdown`). A project
42+
with *N* Markdown files performs *O(N²)* reads. Small and medium
43+
corpora stay fast. For large corpora add an `exclude` entry for
44+
generated or vendored directories.
45+
46+
## Config
47+
48+
```yaml
49+
rules:
50+
duplicated-content:
51+
include:
52+
- "docs/**"
53+
exclude:
54+
- "docs/generated/**"
55+
min-chars: 200
56+
```
57+
58+
Disable:
59+
60+
```yaml
61+
rules:
62+
duplicated-content: false
63+
```
64+
65+
## Examples
66+
67+
### Good
68+
69+
<?include
70+
file: good/simple.md
71+
wrap: markdown
72+
?>
73+
74+
```markdown
75+
# Simple Fixture
76+
77+
One short fixture sits alone in its folder and exists to exercise
78+
the duplicate detector. Every other rule stays quiet because the
79+
text is simple and brief. The paragraph holds enough characters to
80+
pass two hundred runes after normalization. Each sentence is plain
81+
and ends early. No other file here repeats this wording.
82+
```
83+
84+
<?/include?>
85+
86+
### Bad -- duplicated paragraph
87+
88+
<?include
89+
file: bad/duplicate.md
90+
wrap: markdown
91+
?>
92+
93+
```markdown
94+
# Duplicate Fixture
95+
96+
A distinctive paragraph appears in this file and in a sibling
97+
fixture, so MDS037 must flag the match and point at the other
98+
location. The wording stays above the default two-hundred character
99+
threshold after normalization. It stays unique relative to the
100+
other rule fixtures so nothing matches by accident across the test
101+
suite.
102+
```
103+
104+
<?/include?>
105+
106+
### Bad -- duplicated source
107+
108+
<?include
109+
file: bad/ref/source.md
110+
wrap: markdown
111+
?>
112+
113+
```markdown
114+
# Source Fixture
115+
116+
A distinctive paragraph appears in this file and in a sibling
117+
fixture, so MDS037 must flag the match and point at the other
118+
location. The wording stays above the default two-hundred character
119+
threshold after normalization. It stays unique relative to the
120+
other rule fixtures so nothing matches by accident across the test
121+
suite.
122+
```
123+
124+
<?/include?>
125+
126+
## Diagnostics
127+
128+
| Condition | Message |
129+
|-------------------|--------------------------------------------------------------------------|
130+
| paragraph repeats | paragraph duplicated in {other}:{line} |
131+
| invalid glob | duplicated-content: {include,exclude}: invalid glob pattern "{pat}": ... |
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
diagnostics:
3+
- line: 3
4+
column: 1
5+
message: 'paragraph duplicated in ref/source.md:3'
6+
---
7+
# Duplicate Fixture
8+
9+
A distinctive paragraph appears in this file and in a sibling
10+
fixture, so MDS037 must flag the match and point at the other
11+
location. The wording stays above the default two-hundred character
12+
threshold after normalization. It stays unique relative to the
13+
other rule fixtures so nothing matches by accident across the test
14+
suite.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Source Fixture
2+
3+
A distinctive paragraph appears in this file and in a sibling
4+
fixture, so MDS037 must flag the match and point at the other
5+
location. The wording stays above the default two-hundred character
6+
threshold after normalization. It stays unique relative to the
7+
other rule fixtures so nothing matches by accident across the test
8+
suite.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Simple Fixture
2+
3+
One short fixture sits alone in its folder and exists to exercise
4+
the duplicate detector. Every other rule stays quiet because the
5+
text is simple and brief. The paragraph holds enough characters to
6+
pass two hundred runes after normalization. Each sentence is plain
7+
and ends early. No other file here repeats this wording.

0 commit comments

Comments
 (0)