rumdl 0.2.79

A fast Markdown linter and formatter written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# Contributing to rumdl

Thank you for your interest in contributing to rumdl! This document provides guidelines and instructions for contributing.

## Table of Contents

- [Development Setup]#development-setup
- [Commit Message Convention]#commit-message-convention
- [Adding a New Rule]#adding-a-new-rule
- [Changelog Workflow]#changelog-workflow
- [Testing]#testing
- [Code Style]#code-style
- [Pull Request Process]#pull-request-process

## Development Setup

### Prerequisites

- [Rust]https://rustup.rs/ 1.94.0 or later
- [mise]https://mise.jdx.dev/ for development tool management (recommended)

### Quick Start

1. **Clone the repository**:

   ```bash
   git clone https://github.com/rvben/rumdl.git
   cd rumdl
   ```

2. **Install development tools** (using mise - recommended):

   ```bash
   make dev-setup
   ```

   Or manually:

   ```bash
   cargo install cargo-nextest cargo-watch maturin
   ```

3. **Install prek hooks**:

   ```bash
   prek install                        # Code quality hooks
   prek install --hook-type commit-msg # Conventional commits validation
   prek install --hook-type pre-push   # Comprehensive validation
   ```

4. **Verify installation**:

   ```bash
   make dev-verify
   ```

5. **Run tests**:

   ```bash
   make test-dev    # Recommended: ~20s, skips slowest tests
   make test-quick  # Faster: ~15s, skips slow/stress tests
   make test        # Full suite with dev profile
   ```

## Commit Message Convention

rumdl uses [Conventional Commits](https://www.conventionalcommits.org/) for automated changelog generation and semantic versioning.

### Format

```text
<type>(<scope>): <description>

[optional body]

[optional footer(s)]
```

### Types

| Type       | Section         | Description              | Example                                            |
| ---------- | --------------- | ------------------------ | -------------------------------------------------- |
| `feat`     | **Added**       | New features             | `feat(cache): add file-level caching`              |
| `fix`      | **Fixed**       | Bug fixes                | `fix(MD013): enforce line length in sentence mode` |
| `perf`     | **Performance** | Performance improvements | `perf(fix): enable parallel file processing`       |
| `refactor` | **Changed**     | Code refactoring         | `refactor(cache): simplify cache key generation`   |
| `docs`     | **Changed**     | Documentation only       | `docs(readme): update installation steps`          |
| `chore`    | *(skipped)*     | Maintenance tasks        | `chore(deps): update dependencies`                 |
| `test`     | *(skipped)*     | Adding/updating tests    | `test(cache): add cache invalidation tests`        |
| `ci`       | *(skipped)*     | CI configuration         | `ci: update GitHub Actions workflow`               |
| `style`    | *(skipped)*     | Code formatting          | `style: run cargo fmt`                             |

### Examples

**Good commit messages**:

```bash
feat(cache): implement Ruff-style parallel caching with Arc<Mutex<>>
fix(pre-push): use dev profile for test-push to avoid hanging
perf(fix): enable parallel file processing for fix mode (4.8x speedup)
docs(changelog): update v0.0.163 with HTML comments fix
```

**Breaking changes**:

```bash
feat(api)!: change linting API to return Result

BREAKING CHANGE: The linting API now returns Result<Vec<Warning>, Error>
instead of Vec<Warning>. Update your code to handle the new error type.
```

### Scope

The scope should be a noun describing the section of the codebase:

- `cache` - Caching infrastructure
- `fix` - Fix mode/auto-fix functionality
- `cli` - Command-line interface
- `lsp` - Language Server Protocol
- `rules` - Linting rules (or specific rule like `MD013`)
- `ci` - Continuous integration
- `docs` - Documentation

## Adding a New Rule

New rules are wired through the Rumdl registry and must have matching metadata,
documentation, and tests.

### Implementation

1. Choose the next available `MD` number. Keep numbering compatible with the
   existing rule set; do not reuse a number that is already implemented.
2. Create `src/rules/mdNNN_rule_name.rs` and implement the `Rule` trait from
   `src/rule.rs`. The required methods are `name`, `description`, `check`, and
   `fix`. Add configuration methods when the rule accepts options, using the
   existing `RuleConfig` and `impl_rule_config_methods!` patterns where
   appropriate.
3. Register the module and re-export its public rule type in
   `src/rules/mod.rs`.
4. Add a `RuleEntry` to the `RULES` registry in that file. Set the canonical
   kebab-case alias and mark the rule `opt_in: true` when it should be disabled
   by default.
5. If the rule is opt-in, add its rule number to
   `test_opt_in_rule_set_is_frozen` in
   `tests/integration/rules_mod_test.rs`.
6. Add the rule's user-facing and compatibility aliases to
   `src/config/registry.rs`, including `RULE_ALIAS_MAP` or the related alias
   table when appropriate.
7. Add the rule's metadata to `rules.json`, including its code, name, summary,
   category, fix availability, and documentation URL.

### Tests and Documentation

1. Add focused unit tests in the rule implementation file's `#[cfg(test)]`
   module. Cover valid input, violations, fixes when supported, and important
   edge cases. Configurable rules should also test their options. Add broader
   integration tests under `tests/rules/` when the rule needs them.
2. Add a simple representative test for the rule to
   `get_test_content_for_rule()` in `tests/cli/cli_lsp_fix_consistency.rs`.
   This lets the shared test verify that CLI batch fixes and LSP warning fixes
   produce the same result.
3. Create `docs/mdNNN.md` using an existing rule page as a template. Include
   the rule's purpose, rationale, correct and incorrect examples,
   configuration, and flavor-specific behavior when relevant.
4. Add exactly one row for the rule to the appropriate category table in
   `docs/rules.md`. If the rule is opt-in, also add it to the opt-in overview.
5. Update generated rule-count markers with:

   ```bash
   make sync-rule-docs
   ```

### Validation

Run the documentation and registry checks before submitting the change.
Those are the most likely to fail during CI:

```bash
make check-rule-docs
cargo nextest run --profile ci -E 'test(config::tests) | test(config::registry) | test(cli::cli_lsp_fix_consistency) | test(integration::rules_mod_test)'
make lint
```

The documentation checker verifies rule counts and category-table coverage.
The alias test catches rules that were added to the implementation without a
registered user-facing alias. See `src/rules/md077_list_continuation_indent.rs`
for a simple rule example and `src/rules/md010_no_hard_tabs.rs` for a
configurable rule example.

## Changelog Workflow

`CHANGELOG.md` is generated from [conventional commits](#commit-message-convention)
by [`vership`](https://github.com/rvben/vership) (configured in `vership.toml`).
You do not edit `CHANGELOG.md` by hand for routine changes: when a release is
cut (see [Release Process](#release-process)), every conventional commit since
the previous tag becomes an entry in the new version's section, grouped by type
and prefixed with its scope.

The single most important thing you can do for the changelog is write a good
conventional commit message, because that text becomes the changelog entry.

### Best Practices

- ✅ **Do write detailed commit messages** - they become changelog entries
- ✅ **Do use scopes** - they organize changelog sections
- ❌ **Don't hand-edit generated entries** - they are regenerated at release time
- ❌ **Don't skip conventional format** - unconventional commits won't appear

### Curated notes

Some things the tooling cannot infer from a commit subject: a migration note,
a breaking change that deserves a paragraph, credit to a contributor. Write
those under the `## [Unreleased]` heading at the top of `CHANGELOG.md`, in
the section they belong to (`### Added`, `### Fixed`, and so on). At release
time `vership` merges the generated entries into those notes: a curated section
keeps its text and receives the generated entries of the same name after it,
and sections with no curated counterpart are appended.

A generated entry is dropped only when a curated note cites its commit, so a
note that replaces a commit's own subject names the short hash:

```markdown
## [Unreleased]

### Fixed

- **MD013**: reflow no longer splits a sentence before a code span (576e2c1).
  Documents formatted with sentence-per-line reflow may change on the next run.
```

Every other commit still lands as its generated entry. Preview the exact
section a release would produce with `vership changelog patch`.

## Testing

### Test Profiles

rumdl uses [cargo-nextest](https://nexte.st/) with optimized test profiles:

| Command                | Duration | Use Case                            |
| ---------------------- | -------- | ----------------------------------- |
| `make test-prek`       | ~6s      | prek hook (lib tests only)          |
| `make test-quick`      | ~15s     | Quick feedback (skips slow tests)   |
| `make test-dev`        | ~20s     | Development default (skips slowest) |
| `make test`            | ~30s     | Full suite with dev profile         |
| `make test-ci`         | varies   | CI environment                      |

**⚠️ Never use `cargo test` directly** - it's 30-100x slower!

### Writing Tests

```rust
#[test]
fn test_cache_invalidation() {
    // Test implementation
}

// For slow tests, use ignore + filter
#[test]
#[ignore = "slow"]
fn test_large_file_processing() {
    // Slow test implementation
}
```

### Running Specific Tests

```bash
# Run specific test
cargo nextest run test_cache_invalidation

# Run all cache tests
cargo nextest run cache

# Run with specific profile
cargo nextest run --profile quick
```

### Reflow property tests and fuzzing

Sentence-layout tests generate Markdown and expected boundaries together,
independently of the production sentence splitter. They check emphasis labels,
lowercase and link openers, abbreviations, quotes, containers, and sentence
packing. The rendering oracle also compares three Markdown renderers and checks
that a second formatting pass makes no changes.

```bash
PROPTEST_CASES=2000 cargo nextest run --profile ci --test lib -E 'test(md013_sentence_layout_test)'
make fuzz-sentence-layout FUZZ_TIME=900
make fuzz-reflow FUZZ_TIME=900
```

Fuzzing requires nightly Rust and `cargo-fuzz`. Synthetic fixtures in
`fuzz/seeds/` initialize the ignored `fuzz/corpus/` directories. Scheduled runs
reuse a cached corpus and save crash inputs as workflow artifacts. Minimize new
findings into focused regression tests and synthetic seed fixtures; keep raw
working corpora and crash reports out of Git.

## Code Style

### Formatting

```bash
# Format code and run clippy fixes
make fmt

# Check without modifying
make lint
```

### Guidelines

- **No dead code** - Remove unused code instead of `#[allow(dead_code)]`
- **Tests test excellence** - Write tests for correct behavior, not current broken behavior
- **Prefer explicit over implicit** - Clear code over clever code
- **Use inline format args** - `format!("{foo}")` instead of `format!("{}", foo)`

## Pull Request Process

### Before Submitting

1. **Run tests**:

   ```bash
   make test-dev
   ```

2. **Format code**:

   ```bash
   make fmt
   ```

3. **Lint code**:

   ```bash
   make lint
   ```

4. **Add changelog notes** (only if the commit messages cannot say it):
   - Write them under `## [Unreleased]` in `CHANGELOG.md`, see
     [Curated notes]#curated-notes
   - Routine changes need nothing here: their commit messages become the
     entries at release time

### PR Guidelines

- ✅ Use conventional commit format for all commits
- ✅ Include tests for new features
- ✅ Update documentation if needed
- ✅ Keep PRs focused - one feature/fix per PR
- ✅ Reference issues: `Closes #123` or `Fixes #456`
- ❌ Don't include unrelated changes
- ❌ Don't commit `CLAUDE.md` or temporary files

### PR Template

```markdown
## Description

Brief description of changes

## Type of Change

- [ ] Bug fix (non-breaking change fixing an issue)
- [ ] New feature (non-breaking change adding functionality)
- [ ] Breaking change (fix or feature causing existing functionality to change)
- [ ] Documentation update

## Testing

- [ ] Tests added/updated
- [ ] All tests passing (`make test-dev`)
- [ ] Manual testing performed

## Checklist

- [ ] Code follows project style (`make fmt` && `make lint`)
- [ ] Conventional commit messages used (they generate the changelog)
- [ ] Documentation updated (if needed)
```

## Release Process

Releases are cut by maintainers with [`vership`](https://github.com/rvben/vership),
which performs the whole flow in one step: bump the version across every
manifest, regenerate `CHANGELOG.md` from conventional commits, create the commit
and annotated tag, and push. Pushing the `v*` tag triggers the release workflow,
which builds and publishes to crates.io, PyPI, npm, the container registry, and
GitHub Releases.

```bash
# Patch release (the default; use for fixes and most features)
make release-patch      # == vership bump patch

# Minor / major (maintainers only, by explicit decision)
make release-minor      # == vership bump minor
make release-major      # == vership bump major

# Preview without publishing
vership bump patch --dry-run
```

You do not need to edit the version or `CHANGELOG.md` by hand; `vership` does
both, merging any [curated notes](#curated-notes) under `## [Unreleased]` into
the new section. Contributors never cut releases directly - open a PR and a
maintainer handles the release.

## Questions?

- 📖 [Documentation]https://github.com/rvben/rumdl
- 🐛 [Issue Tracker]https://github.com/rvben/rumdl/issues
- 💬 [Discussions]https://github.com/rvben/rumdl/discussions

## License

By contributing to rumdl, you agree that your contributions will be licensed under the [MIT License](LICENSE).