mdrepo
Portable links. Reachable docs. Exceptions that do not quietly live forever.
Most Markdown linters stop at the file boundary. mdrepo starts at the repository boundary.
mdrepo is a focused Python 3.11+ CLI for policies that require cross-platform filesystem
semantics or a view of the complete documentation set. It complements rumdl rather than
replacing it.
It catches repository-level problems such as:
- backslash links and machine-local absolute paths;
- links that escape the repository root;
- path spelling that differs from exact on-disk case;
- Markdown pages unreachable from configured entry documents;
- expired exceptions and exceptions that no longer suppress anything.
mdrepo is deterministic and offline. It performs no HTTP crawl, documentation-site build,
Node.js or npm orchestration, subprocess wrapping, or public plugin discovery.
Quick start
From a checkout, install mdrepo as a persistent command and check a repository:
uv tool install .
mdrepo check /path/to/repository
For development inside this repository:
uv sync --extra dev
uv run python -m mdrepo check .
Preview the deliberately narrow safe-fix set before changing files:
mdrepo fix . --dry-run
mdrepo fix .
Diagnostics are stable and source-located:
docs/setup.md:24:15: error MDR001 local destination uses backslashes instead of POSIX '/' separators
fix: normalize the local destination to a POSIX relative path
notes.md:1:1: error MDR101 Markdown document is unreachable from every configured graph root
hint: Link the document into the graph or add a narrow structured exception.
Where mdrepo fits
rumdlvalidates Markdown as documents.mdrepovalidates Markdown references as relationships inside a repository.
| Question | Authority |
|---|---|
| Is the Markdown valid, consistently formatted, and flavor-aware? | rumdl |
| Does a relative target or heading fragment exist? | rumdl MD057 and MD051 |
| Are local paths portable, root-bounded, and cased exactly like the filesystem? | mdrepo |
| Is each page reachable from a configured documentation root? | mdrepo |
| Are policy exceptions justified, current, and still necessary? | mdrepo |
| Does a generated documentation site build and route correctly? | The site build |
| Does an external URL currently respond? | A dedicated network checker |
The tools are independent. mdrepo does not invoke rumdl, consume its diagnostics, interpret its
inline suppressions, or forward its configuration. Each tool owns its own rules, configuration,
output, and exit status.
See the complete responsibility boundary for overlap policy, MkDocs guidance, guarantees, and non-goals.
What the focused tool adds
| Capability | What it protects against |
|---|---|
| Portable destinations | Backslashes, local absolute paths, and disallowed root-relative paths |
| Repository boundaries | Local destinations that resolve outside the configured root |
| Exact path case | Links that pass on case-insensitive filesystems and fail on Linux |
| Durable local targets | Existing links to Git-ignored or mdrepo-excluded files |
| Rooted document graph | Pages disconnected from configured roots |
| Structured exceptions | Anonymous ignores, expired waivers, and stale policy debt |
| Conservative fixing | Ambiguous edits, overlapping replacements, and changed CRLF line endings |
Safe fixes are applied only when the source span is verified and the replacement is unambiguous. Writes are atomic, and existing CRLF line endings are preserved.
Orphan analysis is generic Markdown graph reachability. It is not the same as membership in an
MkDocs nav tree. An optional mkdocs.yml file configures an MkDocs documentation site, and its
nav section is the publication membership/order authority; rumdl MD074 owns that
site-generator-specific check. Use MDR101 for repository-link reachability, or disable it when
MkDocs navigation is the only discoverability policy.
Configuration
mdrepo searches upward for the nearest project root containing one of these markers:
pyproject.tomlwith a[tool.mdrepo]table;mdrepo.toml;.mdrepo.toml;- a
.gitentry, used only as a root fallback.
A small paired configuration is often enough:
# mdrepo-doc-example
[tool.rumdl]
flavor = "gfm"
extend-enable = ["MD029", "MD060"]
[tool.rumdl.MD029]
style = "ordered"
[tool.rumdl.MD060]
style = "aligned"
[tool.mdrepo.orphans]
enabled = true
roots = ["README.md", "docs/index.md"]
The link-policy defaults are strict and portable. In the normal paired workflow,
links.check-missing-targets remains false so rumdl MD057 is the sole authority for ordinary
missing targets. links.check-durable-targets remains enabled so existing links cannot silently
point at transient or excluded files.
Structured exceptions
Exceptions are explicit policy records rather than casual ignore globs:
[tool.mdrepo.exception-policy]
report-expired = true
report-unused = true
expired-severity = "warning"
unused-severity = "warning"
[[tool.mdrepo.exceptions]]
id = "standalone-changelog"
rule = "MDR101"
path = "CHANGELOG.md"
reason = "Package metadata exposes this file independently of the documentation graph."
expires = 2027-01-01
MDR201 reports expired exceptions. MDR202 reports exceptions that no longer suppress a finding.
Unknown configuration keys are errors. Repeated --config overlays and typed
--set dotted.key=value overrides are applied last.
See the configuration reference for discovery patterns, link policy, graph resolution, rule selection, severity overrides, and configuration layering.
This repository is also a complete integration example: see its pyproject.toml
for independent rumdl and mdrepo configuration, and the CI integration guide
for dependency, hook, and CI setup patterns.
By default, discovery excludes Markdown files matched by the repository's .gitignore, along with
common transient locations such as virtual environments, tool caches, build/, and dist/. Use
top-level exclude patterns for project-specific generated or legacy trees. Set
respect-gitignore = false only when ignored Markdown should be inspected explicitly; those files
remain outside the orphan graph, and links to them can still receive MDR006.
Recommended workflow with rumdl
Run rumdl's configured document formatting first, then repository-policy fixes, and finish with read-only checks:
uvx rumdl check --fix .
mdrepo fix .
uvx rumdl check .
python -m mdrepo check .
In CI, keep the two authorities visible and independent:
rumdl check .
python -m mdrepo check . --format github
A green run means the Markdown layer and repository-policy layer are clean. It does not claim that remote websites respond or that a documentation generator can build the published site.
CI integration
See the CI integration guide for mdrepo's standalone installation,
root and configuration contract, mutation ordering, exclusions, platform commands, exit codes,
and a minimal GitHub Actions job.
MDR004 is available as a standalone missing-target fallback, but it remains disabled in the normal
rumdl workflow to avoid duplicate ownership and duplicate diagnostics.
Commands
| Command | Purpose |
|---|---|
mdrepo check [PATH ...] |
Check repository policy without modifying files. |
mdrepo fix [PATH ...] |
Apply only source-verified safe fixes. |
mdrepo fix --dry-run |
Print proposed diffs and return 1 when edits are available. |
mdrepo fix --diff |
Apply fixes and also print unified diffs. |
mdrepo graph --graph-format FORMAT |
Render the graph as text, json, or dot. |
mdrepo rules |
List built-in rule metadata. |
mdrepo config |
Print the fully resolved configuration as JSON. |
Useful invocation overrides:
mdrepo check . --select MDR001,MDR005
mdrepo check . --ignore MDR101
mdrepo check . --format github
mdrepo check . --fail-on warning
mdrepo check . --set links.check-missing-targets=true
Exit statuses are stable:
0: no visible finding meetsfail-on;1: a visible finding meetsfail-on, orfix --dry-runfound applicable edits;2: configuration, invocation, discovery, parsing, or safe-fix failure.
Built-in rules
| Rule | Purpose | Safe fix |
|---|---|---|
MDR001 |
Backslash in a local destination | Yes |
MDR002 |
Machine-, protocol-, or repository-root-absolute destination | Root-relative only |
MDR003 |
Local destination escapes the repository root | No |
MDR004 |
Standalone missing-target fallback; disabled with rumdl | No |
MDR005 |
Path spelling differs from exact on-disk case | Yes |
MDR006 |
Existing local target is Git-ignored or mdrepo-excluded | No |
MDR100 |
No configured orphan-graph root exists | No |
MDR101 |
Markdown document is unreachable from all roots | No |
MDR201 |
Structured exception is expired | No |
MDR202 |
Structured exception is unused | No |
Rule IDs use the MDR namespace so they remain distinct from rumdl's MD rules.
Integration
See the CI integration guide for installation, repository-root requirements, configuration discovery, exclusions, mutation ordering, platform commands, exit codes, CI, and pre-commit setup. That guide is the canonical integration reference; this README keeps only the product overview and command summary.
Deliberate boundaries
mdrepo does not format Markdown, validate heading anchors, crawl external URLs, parse
arbitrary raw HTML links, build documentation sites, or invoke other tools. The internal
Rule protocol remains a small extension seam without committing the project to a public
plugin-loading system.
Development
Run the same read-only gate used by hosted CI:
uv sync --extra dev
uv run python -m scripts.ci
During development, apply safe fixes first and then rerun the read-only gate:
uv run python -m scripts.ci --fix
uv run python -m scripts.ci
Hosted CI runs the supported operating-system and Python-version matrix defined in
.github/workflows/ci.yml. The gate includes compilation, linting,
tests, isolated package validation, type checking, documentation checks, pre-commit validation,
and an mdrepo self-check.
Documentation
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mdrepo-0.0.1.post0.tar.gz.
File metadata
- Download URL: mdrepo-0.0.1.post0.tar.gz
- Upload date:
- Size: 77.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ea8eb2e25ef4da7517cc7c835e08760eb85e7dfc27a7dfb36928589bff08305
|
|
| MD5 |
81771eb4b37b11859b183b398cdcbe3d
|
|
| BLAKE2b-256 |
176889d09b45e84af8bb852835c4691f36ddf4fe25add4b3a2f53b1d3b115974
|
File details
Details for the file mdrepo-0.0.1.post0-py3-none-any.whl.
File metadata
- Download URL: mdrepo-0.0.1.post0-py3-none-any.whl
- Upload date:
- Size: 40.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67a5a020854c6e9d57e8e364798efb489a3dfb25fd8595c49283cef619edce7a
|
|
| MD5 |
5869f2f61a64b32f7c6ebd9511ab096f
|
|
| BLAKE2b-256 |
3c580763e316244097b4cc28f894abbfeff3fff355a5965e0be8f58ead59bde5
|