Docs markdown linting: rumdl adoption rationale
Compares Markdown linters and explains the selection, configuration boundaries, and adoption risks of rumdl.
Research state: Reference
Summary
rumdl best matches the repository's Markdown needs, provided configuration and rollout preserve existing MDX semantics.
This study preserves why rumdl was selected and which alternatives were rejected; Docs markdown linting owns implementation.
Research question
The docs site under docs/content/ 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 crates/jackin-xtask/src/docs.rs. 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
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 docs/AGENTS.md 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/AGENTS.md 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
rumdl — a Rust port of markdownlint with native MDX support and an official GitHub Action — was selected over the alternatives below:
- Implements all 53
MD001-MD059markdownlint rules plus 18 rumdl-specific rules (relative-link existence, forbidden-term policy, ToC validation, footnote rules); functionally on par withmarkdownlint-cli2. - Native MDX support: treats capitalized JSX tags as components rather than malformed HTML, recognizes top-of-file
importstatements, and auto-relaxesMD013/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@v0GitHub Action withversion/path/config/report-typeinputs;annotationsmode surfaces violations directly in the PR "Files changed" tab. - Configuration surface is small: a single
.rumdl.tomlfor rule enable/disable, per-file-ignore globs, inline<!-- rumdl-disable -->directives, andextendsfor inheritance.
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 inunified+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
- 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 complementscargo xtask docs repo-links, it does not replace it. - Disabled-rule creep. Each rule disabled in
.rumdl.tomlis 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.
Limitations and open questions
- 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.
- Auto-fix in CI. Should CI ever run
rumdl check --fixand commit back, or should--fixstay 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. - Scope. Lint only
docs/content/, or also repo-root markdown (README.md,CHANGELOG.md,AGENTS.md,RULES.md,TESTING.md,PROJECT_STRUCTURE.md,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. - 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.