detangle-lint

The Link Doctor, pointed at a terminal. Same scan, same resolution chain, same findings — printed as JSON so the thing that wrote the wiki can read the report on the wiki it wrote.

Where it comes from

It ships inside the portable archive (detangle-<version>-<rid>.zip or .tar.gz) rather than in the installers. The installers are for somebody who wants to read a wiki; this is for a pipeline. Unpack the archive and detangle-lint is beside detangle.

$ detangle-lint samples/
$ detangle-lint . --fail-on warning --compact
$ detangle-lint ./wiki --output findings.json

Options

OptionMeans
--fail-on <severity>Exit 1 when any finding is this severe or worse: error (the default), warning, info, or never
--output, -o <path>Write the report to a file instead of standard output
--compactOne line of JSON rather than an indented document
--markRecord the vault as it stands as the baseline, in .detangle-baseline.json at the vault root
--sinceReport what changed since the baseline, as a delta block
--fail-on-regressionExit 1 when a link broke or now needs a later rule than it did. Implies --since
--choices <path>Read settled ambiguities from this file rather than from .detangle-choices at the vault root
--emit-patch <path>Write the repair as a unified diff, or - for standard output
--patch-policy <what>safeonly (the default) or all
--help, -hThe usage text

The last three are the regeneration gate, and for a wiki a generator rewrites wholesale they are the ones worth reaching for.

Exit codes are 0 for a clean run, 1 for findings at or past the threshold, and 2 when the vault could not be read at all — a missing folder, a permission error, or a report that could not be written. A 2 is never a finding about your wiki; it means the tool did not run.

Why --fail-on exists

Only a broken link is an Error. Everything else — ambiguity, a fragment that matches no heading, a link that resolved by a late rule — is a Warning or Info, because none of them stop a reader getting to the page.

That makes the default gate almost useless on its own: --fail-on error means "fail on broken links", which is what every link checker written in the last twenty years already does. The interesting findings are the advisory ones, and --fail-on warning is what a pipeline that cares about them should use.

The part worth reading

{
  "counts": {
    "error": 4, "warning": 6, "info": 17,
    "byKind": {
      "brokenLink": 4, "ambiguousLink": 2, "duplicateSlug": 4,
      "nonCanonicalLink": 17, "brokenAnchor": 1, "anchorDialectDrift": 1 } },
  "rules": {
    "wiki": {
      "noteRelativePath": 1, "pathSuffix": 1, "caseInsensitiveStem": 1,
      "normalizedName": 3, "alias": 2 } }
}

The findings array is the ordinary part: kind, severity, path, line, message, and the rewrite or heading the tool would suggest. Every finding is suggested here, unlike in the application, where the edit-distance search is deferred until a reader opens one — a report has no reader who will open one later, and one that says a link is broken without saying what it probably meant leaves its consumer to do the search again.

rules is the part no other tool can print. It is a histogram of which step of the resolution chain each folder's links needed. In the block above, eight links written inside wiki/ resolved, and exactly one of them did so by a rule that is not a rescue. A folder whose links all needed step 6 or later is a folder whose naming does not match the vault it was written into — and that is a fact a generator can act on, unlike "seven links are broken", which only says the generator already failed.

Using it in CI

- name: Check the wiki's links
  run: ./detangle-lint wiki/ --fail-on warning --output findings.json

- name: Keep the report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: link-findings
    path: findings.json

The report identifies itself: schema is its version, vault the folder it read, and flavor the wiki convention that was detected. A consumer should check schema before trusting the shape of anything else.

What it does not do

It never rewrites your markdown. There is no --fix, and there will not be one: applying a rewrite is a decision that wants the before-and-after line in front of a person, which is what the Link Doctor panel is for. The CLI reports; the application acts.

The one file it will write inside the vault is the baseline, and only when you ask for it with --mark. That file is a record of a decision, not a change to the wiki.

Emitting a repair

--emit-patch is how a pipeline gets a fix without this command applying one:

$ detangle-lint wiki/ --emit-patch - | git apply
$ detangle-lint wiki/ --emit-patch repair.diff --patch-policy all

The output is an ordinary unified diff, with one addition — each hunk names the rung that resolved the link:

--- a/wiki/index.md
+++ b/wiki/index.md
@@ -12,1 +12,1 @@ resolved by NormalizedName (Attention Is All You Need)
-See [[Attention Is All You Need]] for the original.
+See [[attention-is-all-you-need]] for the original.

safeonly, the default, plans only the links that resolved through rungs 4 to 8 and have exactly one canonical form — the same set the panel's Fix all safe applies, planned by the same code, so the two cannot come to mean different things. all adds the edit-distance guesses at broken links, which are a plan for a person to read rather than something to pipe into git apply unread.

Anchors are not repaired yet, whatever the policy. A broken fragment's suggestion is a heading, and the rewriter replaces a link's target; repairing one is its own change.