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
Metadata
Release files for mdrepo 0.0.1.post0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| mdrepo-0.0.1.post0.tar.gz | 77.3 kB | Without provenance | Details for mdrepo-0.0.1.post0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| mdrepo-0.0.1.post0-py3-none-any.whl | Python 3 | none | any | Details for mdrepo-0.0.1.post0-py3-none-any.whl |
Total release size: 118.2 kB
Release files / mdrepo-0.0.1.post0.tar.gz
| Download URL | mdrepo-0.0.1.post0.tar.gz |
|---|---|
| Size | 77.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8ea8eb2e25ef4da7517cc7c835e08760eb85e7dfc27a7dfb36928589bff08305
|
|
BLAKE2b-256 checksum How to use checksums |
176889d09b45e84af8bb852835c4691f36ddf4fe25add4b3a2f53b1d3b115974
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Release files / mdrepo-0.0.1.post0-py3-none-any.whl
| Download URL | mdrepo-0.0.1.post0-py3-none-any.whl |
|---|---|
| Size | 40.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
67a5a020854c6e9d57e8e364798efb489a3dfb25fd8595c49283cef619edce7a
|
|
BLAKE2b-256 checksum How to use checksums |
3c580763e316244097b4cc28f894abbfeff3fff355a5965e0be8f58ead59bde5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.9
|
Security for mdrepo 0.0.1.post0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.