Documentation

Renaming Aliases

Renaming a document used to be an operation with delayed damage. The links kept working, then broke later and one document at a time — whenever the referring content happened to be re-parsed, months after the rename, attributed to whoever edited the text rather than to whoever renamed. The server now keeps the old address alive as a redirect, and the CLI’s job is to make sure a repository never fights that.

The vocabulary

An alias has three possible relationships to a document:

  • Current — the name in the document’s frontmatter. One per document.
  • Former — a name the document used to answer to. Any number of them, and the server still resolves each one. Exposed on the CLI document tree as formerAliases[].
  • Free — nobody has it, currently or formerly.

A stale link is one that resolved — it found a document — under a name that document has moved away from. That is a different thing from a broken link, which resolved to nothing. The two sets are disjoint, and conflating them is the mistake to avoid: a stale link works today.

The three decisions that shape CLI behaviour

push follows the redirect; the file’s alias: is never rewritten. A push that named a former alias used to create a second document. It now resolves through the redirect and updates the one that already exists, reporting resolvedViaFormerAlias and the currentAlias it landed on. What it does not do is edit the file to match: content in a repository belongs to whoever wrote it, and a sync tool silently rewriting frontmatter is how a “helpful” tool becomes one nobody trusts. Moving the file is a separate, explicit command — see below.

pull finds the file a renamed document already lives in, and it prefers IDENTITY over any name. The strategies, strongest first:

  1. a local file whose frontmatter declares the document’s current alias;
  2. a cache entry carrying that alias;
  3. the cache entry carrying the document’s ID — immune to any number of renames, and available whenever anything has been synced before;
  4. a former alias, for the case the cache cannot answer: a fresh clone.

Step 4 is guarded, and the guard is fail-closed. A former alias is not exclusive — document A renamed guide → guide-1 while document B was created as guide is an ordinary state — so a former alias is followed only when no document in this pull currently holds it, and when the caller cannot say (a single-document pull has no tree to ask), the step is skipped rather than guessed at. The set of taken names is read from the whole tree, not from the documents this pull happens to be writing: a scope filter narrows what is written, not what is named.

Without steps 3 and 4 a rename produced a second file, and the damage landed on the next push. Every strategy used to be keyed on the current alias, so nothing local matched and pull generated a fresh path; the original file stayed behind under its old name, because pull only deletes on an explicit op: "delete" and a rename arrives as an upsert. Both files then resolved to one document on push — the old one through the retired-alias stage — and the server’s bucket is keyed by document id, so one silently overwrote the other. Which one survived was randomised by Go’s map iteration (measured: 24/6 over 30 runs), and when the stale file won, the push wrote old content over the document and reported success.

Landing in the old file does not move the alias. pull preserves whatever alias: the local file declares, so a file matched through step 3 or 4 keeps its old name and the repository stays internally consistent — no file on a new name while other files still link to the old one. Moving the name, and the [[…]] targets with it, stays the job of meldoc migrate.

validate treats a link to a former alias as a warning, never an error. It resolves — the CI publish must not be blocked by a link that works. The warning is what tells you the debt exists. Note that content issues do not set the exit code at all in this CLI: validate returns 1 only for structural problems (unparseable frontmatter, a missing required field, a duplicate alias).

Offline, validate says nothing about former aliases. It asks the server for the mapping; without a token or a server it cannot know, and it does not guess. That distinction is deliberate — “could not ask” and “asked, and there is nothing” must not produce the same output, or a repository would look clean because it was offline.

meldoc migrate, phase 2

Migrating off a retired name is the second phase of meldoc migrate, not a command of its own. The first phase gives an alias to a file that has none; the second moves a file off a name the server has retired. The two act on disjoint sets — one on alias == "", the other on an alias the server reports as retired — so they are two branches of the same question, “what is wrong with this file’s alias”, and a second command called migrate would only have made a reader ask which one they wanted.

Phase 2 edits both halves at once: the alias: in frontmatter and every [[former-name]] in the body, across the whole repository, in one atomic plan.

Doing only the first half is worse than doing nothing. validate builds its alias set from local files, so a file that has adopted the new name while other files still write [[old-name]] makes those links read as broken, and CI blocks the publish. That is also why teaching pull to adopt the server’s alias was rejected: pull only touches the files it fetches, and the links to a renamed document live in the ones it does not.

The rewrite operates on the link parser’s own match positions, so it only ever edits text inside [[...]]. A plain find-and-replace would also rewrite the same word in prose, in a code fence, and inside a URL. Code fences are skipped for the same reason the parser skips them: a [[alias]] shown as an example is documentation about links, not a link.

All four link forms are handled, and a cross-project link has both halves resolved in one pass rather than by running the two mappings in sequence — sequential application could rewrite one target twice and produce a name that never existed.

[[old]]           ->  [[new]]
[[proj::old]]     ->  [[proj::new]]
[[oldproj::doc]]  ->  [[newproj::doc]]
[[oldproj::old]]  ->  [[newproj::new]]

The display half of [[target|Display text]] is left alone: it is prose the author wrote, and rewriting it would edit their words rather than their link.

Nothing is written until the whole plan is computed, and the plan is printed whether or not --dry-run is set. A migration interrupted halfway is exactly the partial state the command exists to avoid, so a network failure leaves the working tree untouched.

What prune does, and why it is not a problem here

prune deletes server documents that are absent from the repository, and it is driven by the local cache rather than by the tree — so it is skipped entirely when the cache is empty. A renamed document whose file still carries the old alias is therefore not a pruning candidate: the push resolved it through the redirect and recorded it as present.

What the CLI deliberately does not do

  • Rewrite alias: on push. See above. Use meldoc migrate.
  • Fail CI on a stale link. It resolves; blocking a publish over a working link trades a real cost for a cosmetic one.
  • Retire an alias by hand. Retirement happens on the server, as part of the rename. There is no CLI verb for it and there should not be — an alias with a history row nothing renamed into is a redirect pointing at a decision nobody made.