Regeneration

A wiki written by a model is not edited. It is regenerated — the whole corpus, or a large part of it, rewritten in one run. That changes what "is this wiki healthy" means.

Every link checker asks the absolute question: are there broken links right now? For a corpus somebody edits by hand, that is the right question, because the answer starts at zero and every regression is visible. For a corpus a generator rewrites wholesale, the answer is almost never zero, so the run that made things worse looks exactly like the twenty runs that did not.

Detangle asks the other question: is anything worse than it was?

Marking a baseline

Nothing tells Detangle that a generation run has finished. The file watcher sees dozens of changes while one is under way, and a baseline taken automatically would as often as not be taken halfway through. So marking is explicit.

In the application, the Link Doctor panel has a Mark this state button. From a terminal:

$ detangle-lint wiki/ --mark

Both write the same file: .detangle-baseline.json, at the vault root.

It sits at the root rather than in .detangle/ on purpose. .detangle/ is a cache — the search index and the diagram renders — and the FAQ says you may delete it at any time. A baseline is the opposite: it is the thing a repository commits so the next generation run can be measured against it, and it belongs where git status shows it.

$ git add .detangle-baseline.json
$ git commit -m "chore: mark the wiki's link baseline"

Asking what changed

$ detangle-lint wiki/ --since

The report grows a delta block:

{
  "delta": {
    "pages": { "added": 4, "removed": 0, "renamed": 1, "rewritten": 22 },
    "links": { "broke": 2, "healed": 5, "degraded": 7, "improved": 1, "retargeted": 0 },
    "regressed": true,
    "regressions": [
      { "source": "wiki/concepts/attention.md", "target": "Vaswani et al",
        "change": "broke", "was": "alias", "now": "unresolved", "resolvedTo": "" },
      { "source": "wiki/index.md", "target": "Getting Started",
        "change": "degraded", "was": "exactStem", "now": "fuzzyNearest",
        "resolvedTo": "wiki/getting-started.md" }
    ]
  }
}

Five words carry the whole idea, and the panel's change summary uses the same five:

WordMeans
brokeIt resolved before. It resolves to nothing now.
healedIt resolved to nothing before. It resolves now.
degradedIt still resolves, but a later rung of the chain had to rescue it.
improvedIt still resolves, and now by an earlier rung than before.
retargetedSame rung, different page.

degraded is the one no other tool can report. Every competitor's link is binary — it works or it does not — so none of them can represent a link that still works but works worse. Detangle's thirteen-rung chain is what makes the difference visible: "twelve links that resolved by an exact stem now resolve by a fuzzy nearest match" is a sentence about a naming convention that drifted, and it is actionable before those twelve links break.

Only the regressions are listed. The healed and improved ones are counted, because a report that listed every link that moved in either direction would bury the ones somebody has to act on.

Gating a pipeline

$ detangle-lint wiki/ --fail-on-regression

Exit 1 when anything broke or degraded, whatever the absolute counts are. It implies --since; there is nothing to fail on without a comparison.

- name: Regenerate the wiki
  run: ./generate-wiki.sh

- name: Fail if any link got worse
  run: ./detangle-lint wiki/ --fail-on-regression --output findings.json

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

An already-broken wiki that did not get worse passes. That is the point.

--fail-on-regression and --fail-on <severity> are independent and compose: the first asks whether this run made things worse, the second whether the wiki is above some absolute bar. A pipeline that wants both gets an exit 1 from either.

Accepting the new state

When a regression is deliberate — a page was renamed on purpose, a section was cut — mark again and commit the new baseline:

$ detangle-lint wiki/ --mark
$ git commit -am "chore: accept the link changes from the rename"

The commit is the record of somebody having looked.

When there is no baseline

--since against a vault that has never been marked reports a delta with everything at zero and regressed: false, rather than reporting the whole vault as new. Without the flag there is no delta block at all — "nothing regressed" and "nothing was compared" are different answers, and a gate reading the report has to be able to tell them apart.