# Docs markdown linting: rumdl adoption rationale (https://jackin.tailrocks.com/reference/research/documentation/docs-markdown-linting/)



**Status**: Reference — captures why rumdl was selected as the mechanical markdown linter for the docs corpus, and the alternatives rejected. See [Docs markdown linting (rumdl)](/roadmap/docs-markdown-linting/) for the current implementation state.

## Problem [#problem]

The docs site under `docs/content/docs/` has automated gates for structural breakage (the Fumadocs build), broken links (lychee), code-span references to real repo files (`cargo xtask docs repo-links`), and roadmap sidebar completeness (`cargo xtask roadmap audit`). The Rust implementation lives in <RepoFile path="crates/jackin-xtask/src/docs.rs">crates/jackin-xtask/src/docs.rs</RepoFile>. Nothing gates mechanical markdown style — heading hierarchy, code-fence languages, list-marker consistency, line-length policy, emphasis-marker mixing. Each PR's MDX looks fine in isolation; the corpus drifts across contributors and sessions without a deterministic check.

## Why it matters [#why-it-matters]

A markdown linter does not catch deep bugs; it is worth adding for the same reason `cargo clippy -D warnings` is on the Rust side — one enforced source of truth for style frees review attention for substance. Concretely: a code fence without a language tag loses syntax highlighting, a skipped heading level (`##` to `####`) breaks Fumadocs' right-rail table of contents, and mixed `-`/`*` bullets make diffs noisier than they need to be. None of these block a single PR; together they erode trust in the docs surface over the corpus's lifetime. A prose rule in <RepoFile path="docs/AGENTS.md">docs/AGENTS.md</RepoFile> is a polite request; a CI rule is a constraint — the project already learned this with `cargo xtask docs repo-links`, written because "use `<RepoFile>`" was a soft policy contributors and agents inconsistently followed.

The case is sharper because most docs edits are agent-produced. An agent session sees `CLAUDE.md`/<RepoFile path="AGENTS.md">AGENTS.md</RepoFile> and a handful of nearby files; it cannot internalize every existing style decision and instead produces a plausible local extrapolation, which diverges across agent vendors (Codex favors compact lists, Claude favors spacious ones) and across sessions of the same agent. Agents also respond to deterministic, rule-coded feedback ("MD040: code fence missing language at line 23") far more reliably than to fuzzy review comments ("this looks slightly off"). A linter converts the latter into the former.

## Decision: rumdl [#decision-rumdl]

[rumdl](https://github.com/rvben/rumdl) — a Rust port of markdownlint with native MDX support and an [official GitHub Action](https://github.com/rvben/rumdl) — was selected over the alternatives below:

* Implements all 53 `MD001`-`MD059` markdownlint rules plus 18 rumdl-specific rules (relative-link existence, forbidden-term policy, ToC validation, footnote rules); functionally on par with `markdownlint-cli2`.
* Native MDX support: treats capitalized JSX tags as components rather than malformed HTML, recognizes top-of-file `import` statements, and auto-relaxes `MD013`/`MD033`/emphasis-marker rules inside JSX expressions. General-purpose markdown linters tend to reject MDX outright or false-positive on the self-closing component tags this codebase relies on (`<RepoFile />`, `<Aside />`).
* Single static Rust binary — no Node runtime needed for the lint step, unlike `markdownlint-cli2`, and aligns with the project's Rust-first tooling posture.
* Pre-built `rvben/rumdl@v0` GitHub Action with `version`/`path`/`config`/`report-type` inputs; `annotations` mode surfaces violations directly in the PR "Files changed" tab.
* Configuration surface is small: a single `.rumdl.toml` for rule enable/disable, per-file-ignore globs, inline `<!-- rumdl-disable -->` directives, and `extends` for inheritance.

## Alternatives considered and rejected [#alternatives-considered-and-rejected]

* **`markdownlint-cli2` (Node).** Mature and well-known, but adds a Node dependency for the lint step, weaker out-of-the-box MDX support, slower. No functional advantage at this corpus's scale.
* **`remark-lint`.** AST-based and elegant, but pulls in `unified` + `remark-parse` + `remark-mdx` + a separate package per rule. More dependencies for similar coverage.
* **`mado`.** Earlier-stage Rust port of markdownlint; smaller rule set than rumdl, less active. Worth reassessing if rumdl stalls.
* **No linter, rely on review.** The status quo; the cost is style drift accumulating silently across an indefinitely-growing corpus, described above.

## Trade-offs and costs [#trade-offs-and-costs]

* **One-time cleanup tax.** A dry run against the existing corpus (several dozen MDX files at proposal time) will surface a nontrivial violation count, some auto-fixable via `rumdl check --fix`, some not. The adoption PR is expected to carry a larger-than-usual diff.
* **Rule churn.** rumdl ships frequent releases; pin the Action to a SHA and treat upgrades as routine dependency-bump PRs, same trade-off as pinning a Rust toolchain version for `clippy`.
* **No custom-rule API.** rumdl has no plugin/Lua/wasm hook system. Project-specific rules — like "code spans referencing real repo files must be `<RepoFile>` links" — still need bespoke tooling; rumdl complements `cargo xtask docs repo-links`, it does not replace it.
* **Disabled-rule creep.** Each rule disabled in `.rumdl.toml` is a hole in the lint surface; the list needs to stay small, justified inline, and periodically re-audited.
* **MDX parser limits.** rumdl's MDX support is good but not perfect on deeply nested JSX expressions; cases that hit this are usually a sign the content should be a React component, not inline MDX.

## Open design questions [#open-design-questions]

1. **Initial strictness.** Start from the default rule set (strict, then disable as violations surface) or a minimal set (loose, then add rules as drift is observed)? Default-strict is more honest about what mechanical rules buy; minimal-then-grow is less upfront work but tends to stay minimal in practice.
2. **Auto-fix in CI.** Should CI ever run `rumdl check --fix` and commit back, or should `--fix` stay local-only with CI strictly verifying? Local-only is the safer default — auto-fix in CI risks committing unreviewed changes, especially from agent-authored PRs.
3. **Scope.** Lint only `docs/content/docs/`, or also repo-root markdown (<RepoFile path="README.md">README.md</RepoFile>, `CHANGELOG.md`, <RepoFile path="AGENTS.md">AGENTS.md</RepoFile>, `RULES.md`, <RepoFile path="TESTING.md">TESTING.md</RepoFile>, <RepoFile path="PROJECT_STRUCTURE.md">PROJECT\_STRUCTURE.md</RepoFile>, `DEPRECATED.md`, `CONTRIBUTING.md`)? Those root files are read by humans and agents from session start, so their style consistency arguably matters at least as much as the docs site's.
4. **Failure mode.** Hard CI failure (consistent with `cargo fmt --check`, repo-link checks, roadmap-sidebar checks, lychee) or PR-annotations-only? Whether the CI job is branch-protected is a repository setting, not something decidable from repo files.
