Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Amiss and link checkers

Link checkers and Amiss solve different halves of one problem, and the halves compose. A link checker asks whether destinations are alive, most valuably the external ones: it fetches URLs, follows redirects, and reports the dead. Amiss asks whether the repository agrees with its own prose: it resolves every in-repository reference against an exact Git snapshot, compares two snapshots to see what moved under what, and gates the change that broke the agreement.

lychee is the strongest of the checkers and the one worth comparing against honestly. It is fast, async, and reads Markdown, HTML, and reStructuredText; it checks external URLs, which Amiss never fetches by design; it checks local file links, and with --include-fragments it verifies heading anchors, which Amiss also does, against thirteen pinned renderer rules. If your failure mode is dead links on a published site, lychee alone is the right tool, and nothing here argues otherwise.

The composition is literal rather than aspirational. Every external destination is recorded in the report after the format’s own decoding, the address a fetcher would request, and the external plan turns a written report into the delta a checker actually wants: the destinations this change introduced, not the whole corpus every run.

amiss check --repo . --object-format sha1 --base "$BASE" --candidate "$(git rev-parse HEAD)" \
  --profile observe --format json > report.json
amiss external-plan --report report.json --format json |
  jq -r '.payload.introduced[].destination' | lychee -

Checking only what the change added is what keeps a checker tolerable in a gate: the author can fix a link they just wrote, and the pre-existing corpus rots on its own schedule instead of failing every pull request.

What a link checker cannot see is change. It examines one state of the world, so it can say a target is missing but not who removed it, whether it was missing before your pull request, or that a target still resolves while its content moved out from under the paragraph citing it. Those questions need two snapshots and exact comparison, and they are where Amiss lives:

Amisslychee
Checks external URLsnever fetches, lists them for youyes
Checks heading anchorsagainst thirteen pinned renderer ruleswith --include-fragments
Compares two snapshotsalwaysno
Attributes a finding to the changeintroduced, pre-existing, resolvedno
Reports changed content under unchanged proseyes, as advisoryno
Checks the staged index before commit--indexno
Policy can loosen the gatenever, loosening is itself a findingconfiguration is open
Byte-identical reports with digestsyesno

The smaller checkers sit on the same side of the line as lychee with less reach: markdown-link-check and linkinator examine files one state at a time from JavaScript, and mdbook-linkcheck is scoped to mdBook books and was last released in 2022. None of them compares snapshots. markdownlint’s MD052 reports a full or collapsed reference link whose label nothing defines, which renders as bracketed text rather than a link.

Documentation generators check their own links while they build: Docusaurus through onBrokenLinks and onBrokenMarkdownLinks, MkDocs through its validation settings, Sphinx under -n -W. Each sees only its own pages, reports built routes rather than source lines, and runs only when the site is built, so none of them says which change broke a link. They catch what this engine declines, slash-rooted routes and generated pages above all, which is why a site wants both.

For a repository with a published site the honest answer is both tools: lychee for the web, Amiss for the tree. For a repository whose documentation points mostly at itself, which is most repositories, Amiss covers the surface that actually breaks and notices the thing no checker looks for: the code moved and the prose did not. Every row of that report explains itself.

Last change: , commit: 6d3f8a85