Metadata-Version: 2.4
Name: docmv
Version: 0.1.0
Summary: Move or rename Markdown files and update the relative links that point to and from them, plus an audit command for broken internal links.
Project-URL: Homepage, https://github.com/LLeon360/docmv
Project-URL: Repository, https://github.com/LLeon360/docmv
Project-URL: Issues, https://github.com/LLeon360/docmv/issues
Author: Leon
License-Expression: MIT
License-File: LICENSE
Keywords: cli,docs,documentation,links,markdown,refactor
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Documentation
Classifier: Topic :: Software Development :: Documentation
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: markdown-it-py>=3.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# docmv

[![CI](https://github.com/LLeon360/docmv/actions/workflows/ci.yml/badge.svg)](https://github.com/LLeon360/docmv/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/docmv.svg)](https://pypi.org/project/docmv/)
[![Python versions](https://img.shields.io/pypi/pyversions/docmv.svg)](https://pypi.org/project/docmv/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/LLeon360/docmv/blob/main/LICENSE)

**Move or rename Markdown files and update the relative links that point to and from them.**

`docmv` is a command-line tool for moving and renaming Markdown files without
breaking relative links. When you move a file or a folder, it updates links in
two places: links elsewhere that point at the moved file, and links inside the
moved file that point back out. It also has an `audit` command that reports
broken internal links.

Every command can emit JSON (`--json`) for use in scripts. `apply` refuses to run
when a move is ambiguous or would break a link, rather than guessing, and the
`explain` command shows how a given link resolves without changing anything.

## Why

Editors like VS Code, JetBrains, and Obsidian update Markdown links on move, but
only when they perform the move. The moment a file is moved by `git mv`, a
script, or an automated agent, those updates never fire and links rot. The usual
fallback is a manual `git grep` for the old path, which misses the links inside
the moved file entirely. `docmv` closes that gap for command-line and automated
workflows.

## Install

```bash
# from PyPI (recommended)
uv tool install docmv      # or: pipx install docmv

# from a local clone
uv tool install .          # or: pipx install .

# run without installing
uv run docmv --help
```

Pure standard library, no runtime dependencies. Requires Python 3.10+.

## Usage

```bash
docmv audit [paths...]          # read-only: every internal link must resolve
docmv plan  SRC DST             # dry-run: show the move + link edits, write nothing
docmv apply SRC DST             # perform the move + edits (refuses on ambiguity)
docmv explain FILE[:LINE]       # show how each link in FILE resolves
```

`SRC`/`DST` may be files or directories. Add `--json` to any command for stable,
machine-readable output.

`apply` moves files on disk and rewrites links, then leaves staging and
committing to you (git detects the rename in the resulting diff). Pass `--stage`
to also stage the move with `git mv`, or `--no-stage` to force a plain move.

To make staging the default without typing the flag each time, set it in git
config (a repo setting overrides the global one), or via an env var for one-offs:

```bash
git config --global docmv.stage true   # stage in every repo
git config docmv.stage true            # stage in this repo only
DOCMV_STAGE=1 docmv apply SRC DST      # stage for a single invocation
```

Precedence is `--stage`/`--no-stage` > `DOCMV_STAGE` > `git config docmv.stage` >
off. `docmv apply --help` documents this too.

### Examples

```bash
# What would break / change if I moved this doc into a new folder?
docmv plan docs/architecture/loot-tables.md docs/architecture/loot/loot-tables.md

# Do it (only if the working tree is clean and nothing would break)
docmv apply docs/architecture/loot-tables.md docs/architecture/loot/loot-tables.md

# Move an entire folder
docmv apply docs/meta docs/meta/docs-and-rules

# CI link check
docmv audit            # exits non-zero if any internal link is broken
```

## How it stays correct

- **Matches by resolved absolute target, not by raw string.** `../x.md` means
  different files from different folders, so resolving first is what makes
  cross-folder and folder moves correct.
- **One recompute pass** handles inbound, outbound, intra-folder, and cross-depth
  links the same way: for every link, recompute the relative path from its
  post-move container to its post-move target.
- **Code-aware, CommonMark-faithful parsing.** Links inside fenced blocks, inline
  code spans, HTML comments, raw HTML tags, and autolinks are treated as opaque,
  so the example links that fill docs-about-docs are never rewritten or falsely
  flagged. The destination, title, and label scanners are ported from
  `markdown-it-py` and tested against the full CommonMark 0.30 spec corpus with
  zero false positives (see Limitations).
- **Display-path detection.** When the visible link text mirrors the path (e.g.
  `[../meta/x.md](../meta/x.md)`), the text is rewritten too, but only on an exact
  match. Fuzzy matches are surfaced for review rather than auto-applied.
- **Fragments and queries preserved.** `./b.md#section` keeps its `#section`.
- **Post-move verification.** After planning, a simulation confirms no link is
  left dangling, and separates breakage the move would introduce (a bug, shown
  loudly) from pre-existing rot.

## Agent safety

- `apply` refuses and changes nothing if there are blockers (destination exists,
  dirty working tree), if the move would introduce any broken link, or if
  low-confidence review items exist (override with `--allow-low-confidence`).
- All output is available as `--json` with a flat, stable schema.
- Exit codes: `0` ok, `1` audit found broken links, `2` apply was blocked,
  `3` usage error.
- `explain` is read-only and shows the base dir, normalized path, and existence
  for each link, so an agent or human can verify a flagged link instead of
  trusting a bare verdict.

## Scope and safety

**docmv only writes Markdown files. It never edits source code.**

- The only files whose contents change are `.md` and `.markdown`. Code files
  (`.py`, `.ts`, and so on) are touched only when you name one as the `SRC` or
  `DST` of a move, and then they're relocated, not content-edited.
- One consequence: doc references embedded inside code, like a path in a Python
  docstring or comment, are not auto-updated. That's the safe side of the trade;
  docmv would rather miss a code-embedded reference than risk corrupting code.
- Scanning code isn't reliable here anyway: a doc-path string in code is just
  data, and there's no way to know whether it points at this repo's own docs or
  somewhere else. Any future code scanning would be opt-in and review-only.

## Limitations

docmv's scanner is a small, line-oriented stdlib tokenizer, not a full
block-level CommonMark parser. It's validated against the official CommonMark
0.30 spec corpus using `markdown-it-py` as a reference oracle. The trade-off is
deliberately asymmetric:

- **Zero false positives (enforced).** Every destination docmv extracts is one
  the reference parser also recognises. It won't mistake code, an escaped
  bracket, an autolink, or raw HTML for a rewritable link, so it can't corrupt a
  file by rewriting a non-link.
- **A few safe false negatives (tolerated).** A handful of valid-but-rare
  constructs aren't rewritten. A missed link is surfaced by `audit` rather than
  corrupted. These are:
  - multi-line reference definitions, and reference definitions nested inside
    blockquotes or lists;
  - inline links whose destination or title wraps across lines;
  - nested links and images: a clickable badge like `[![logo](logo.png)](/url)`
    updates the outer `/url` but not the inner `logo.png`;
  - email autolinks (`<me@example.com>`), which are external and never rewritten.
- Heading-anchor (`#fragment`) checks use an approximate GitHub slug algorithm
  and are reported as best-effort warnings, not hard failures.
- Setext headings (`===`/`---`) are not collected for anchor checks.

## Development

```bash
uv run --extra dev pytest
```

If you change the parser or link-resolution logic, please add adversarial test
cases, not just the happy path.

## License

MIT — see [LICENSE](https://github.com/LLeon360/docmv/blob/main/LICENSE).
