ascribe link, refs, and rename

Check a link before writing it, list where a page, a heading, or a phrase is used, and rename one with everything that uses it.

For AI agents: the documentation index is at llms.txt, and this page is available as Markdown.

The commands for what a page links to: ascribe link and ascribe refs answer questions without changing anything, and ascribe rename changes a name and every reference to it. The other commands that answer questions, such as ascribe outline and ascribe search, are in Questions about a project. The options every command accepts are in the command reference.

Says whether a link works, written on a given page: whether its target exists, the page’s title or the heading’s text, and the destination to write there. It resolves the link as ascribe check and the editor do, so the three agree.

  • TARGET: The link’s destination, as the page would write it: keys.md, keys.md#rotate-keys, #install.
  • --from <PAGE>: The page the link is on: a path from the current directory, or from the content root.
  • --format <FORMAT>: How to show the answer.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.
keys.md#rotate-keys exists: a heading on keys.md.
Write: [Rotate keys](keys.md#rotate-keys), or [](keys.md#rotate-keys), which shows the same title

When the target doesn’t work, it says why and lists the closest targets that do: the page’s headings for a heading that isn’t there, first those whose ids start with the words of the one written, as ask-instead does ask-instead-of-guessing, then those whose words start the written one’s, then the rest, closest first; for a page that isn’t, the pages with the heading the link names, wherever they are, then pages with a similar path; the pages that include a fragment; or the file that a published route belongs to. ascribe check offers the same targets as the broken link’s fixes, marked unsafe, and when one clearly wins, such as the only heading whose id starts with the written id’s words, its message ends with “did you mean loop.md#ask-instead-of-guessing?”, so the concise output and the edit hook carry it too.

keys.md#rotate doesn't work from examples/quill/docs/install-agent.md: `keys.md` has no heading with the id `rotate`.
Closest:
  [Rotate keys](keys.md#rotate-keys)
  [Create a key](keys.md#create-key)
  [Sign webhook events](keys.md#sign-webhooks)

For a page or a heading, Write: gives the link with its title and with empty text, [](../keys.md#rotate-keys), which shows the same title: either does, and the empty one is the smaller edit to a link that has no text.

A URL with a scheme isn’t checked: it exists as far as link knows. A file that isn’t a page, such as an image, exists when the project has it. A link to an API’s operation exists when the spec has it, and the answer says which: see Link to operations in the editor.

Code Meaning
0 The target exists
1 It doesn’t
2 It couldn’t run: no ascribe.toml, one with errors, or a --from that isn’t a page or fragment of the project

With --format json, the document is:

{
  "schema_version": 1,
  "ascribe_version": "0.4.5",
  "from": "examples/quill/docs/install-agent.md",
  "target": "keys.md#rotate-keys",
  "exists": true,
  "kind": "heading",
  "path": "keys.md",
  "id": "rotate-keys",
  "title": "Rotate keys",
  "href": "keys.md#rotate-keys",
  "problem": null,
  "closest": [],
  "lines": [
    {
      "build": "site",
      "line": "3.5",
      "page": "keys.md",
      "leads_to": null
    },
    {
      "build": "site@3.4",
      "line": "3.4",
      "page": "keys.md",
      "leads_to": null
    }
  ]
}

from is the page as --from gave it, from the folder the command was run in, the way ascribe check names a file, whether it was given that way or from the content root, and target is the target as given. exists says whether the link works. path is the file the target names, from the content root, id the heading’s id, and title the page’s title or the heading’s text. kind is page, heading, fragment, file, operation, external, or missing. href is null when the target doesn’t work, and problem says why; closest lists other targets, best first, each with href, path, id, and title. With a versioned build, lines says where a link to a page goes in each line. For an operation, operation has its api, operation_id, method, path, summary, tag, deprecated, and address. Every field is in the schema.

ascribe refs

Lists the places that use a page, a fragment, a heading, an image, or an entry of the content model, each as file:line:column, with what kind of use it is.

It’s the editor’s Find All References, on the command line: it finds a phrase only where it’s a phrase, and a heading in a fragment through every page that includes it, which a text search can’t.

  • TARGET: What to find the uses of: a page or fragment (keys.md), a heading (keys.md#rotate-keys), an image (images/keys.png), phrase:<key>, feature:<key>, term:<id>, dimension:<name>, note:<type>, widget:<name>, or a line of a release target, release:<target>@<version> (release:self-managed@3.4). A path is from the current directory, or from the content root.
  • --limit <N>: List at most this many places. By default, 50 in text and 500 in JSON.
  • --format <FORMAT>: How to show the answer.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.
docs/install-agent.md:8:181: phrase
docs/install-agent.md:62:12: phrase
docs/install-agent.md:84:1: phrase

5 places use phrase:cloud.
Showing 3 of them; for all: `ascribe refs phrase:cloud --limit 5 --config examples/quill`

A target that’s a path, with or without #id, finds its project from the path. An entry of the model, or a glossary term, is written with a prefix: phrase:product (or phrase:{product}), feature:sso, term:api-key, dimension:deployment, note:warning, or widget:release-note, and the project is found from the current directory, or from --config. The places are in path order, and in order within a file. A use is link, include, phrase, availability, term, variant, note, or widget.

An availability place shows its spec as written, after the use: docs/keys.md:25:13: availability `self-managed 3.5` .

A line of a release target, one with [releases.<target>], is written release:<target>@<version>. Its places are the availability specs that name a version in that line, as each is written, with planned read as the target’s planned line. A line holds every version that starts with its parts, so release:ascribe@0.4 finds ascribe 0.4.1 and ascribe (preview 0.4.0, ga 0.5.0), and release:ascribe@0.4.1 only the first. A spec that names the target’s dimension, such as release, names no version of it. A target with no [releases.<target>] doesn’t exist.

docs/configure-legacy.md:5:1: availability `self-managed (ga 3.4, removed 3.5)`
docs/configure.md:5:1: availability `self-managed 3.5`
docs/keys.md:25:13: availability `self-managed 3.5`
docs/webhooks.md:5:1: availability `self-managed 3.5`

4 places use release:self-managed@3.5.

An image under the content root is a target too, named as a page is: ascribe refs images/keys.png. Its places are each file that shows it (image), links to it (link), names it in raw HTML such as <img src> (html), or names it in a frontmatter field such as cover (frontmatter): the uses that keep image-unused from reporting it. An image nothing uses has no places, and exits with 0, as any target that exists does.

A memo file about the target is a place too, with the use memo, at its about line, after the uses. One whose about names a target that doesn’t exist is listed under that target, so ascribe refs finds the memos a rename left behind.

Code Meaning
0 The target exists, used or not
1 It doesn’t exist
2 It couldn’t run: no ascribe.toml, one with errors, a target that’s neither a path nor an entry, or a file that’s neither a page, a fragment, nor an image

With --format json, the document is:

{
  "schema_version": 1,
  "ascribe_version": "0.4.5",
  "target": "phrase:cloud",
  "kind": "phrase",
  "exists": true,
  "places": [
    {
      "file": "docs/install-agent.md",
      "line": 8,
      "column": 181,
      "use": "phrase",
      "personal": false,
      "spec": null
    },
    {
      "file": "docs/install-agent.md",
      "line": 62,
      "column": 12,
      "use": "phrase",
      "personal": false,
      "spec": null
    },
    {
      "file": "docs/install-agent.md",
      "line": 84,
      "column": 1,
      "use": "phrase",
      "personal": false,
      "spec": null
    }
  ],
  "total": 5,
  "shown": 3,
  "truncated": true,
  "next_command": "ascribe refs phrase:cloud --limit 5 --config examples/quill --format json"
}

kind is what the target is: page, fragment, heading, image, phrase, feature, term, dimension, note, widget, or release. Each place’s spec is its availability spec as written, for an availability place, and null for any other. total is how many places use the target, and shown how many are listed. When the list was cut, truncated is true and next_command is the command that lists them all; otherwise it’s null.

ascribe rename

Renames a phrase, a dimension’s value, a heading’s id, or a page, and changes everything that uses it, as the editor’s rename does: it makes the same edits.

A text search and replace finds a phrase in the pages, but misses what a rename knows about: frontmatter fields with phrases = true, code blocks with phrases=true, the code a @snippet {phrases=true} shows, and a link whose path changes when the page it’s on moves.

  • TARGET: What to rename: phrase:<key>, value:<value> (a dimension’s value), a heading’s id (keys.md#rotate-keys), or a page or fragment (keys.md), which moves. A path is from the current directory, or from the content root.
  • NEW: The new name: a phrase’s key, a value, or an id. For a page or fragment, where it moves to, read as TARGET is: from the current directory, or from the content root.
  • --dry-run: Say what it would change, and change nothing.
  • --force: Change files even when git couldn’t put them back as they are: ones with changes not committed, ones git doesn’t track, or ones in no repository.
  • --format <FORMAT>: How to show what changed. json gives each changed file’s whole text, the file that moves, and the entries recorded in ascribe.addresses.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.
changed ascribe.toml (1 place)
changed docs/install-agent.md (4 places)
changed docs/keys.md (1 place)

What it renames, written as ascribe refs writes a target:

Target What changes
phrase:cloud (or phrase:{cloud}) Its key under [phrases], and each {cloud} that uses it.
value:self-managed A dimension’s value: in ascribe.toml (its values, labels, and versionless, features’ available, and builds’ variants and filter), and in pages (@variant attributes, variant and available frontmatter, @available lines, and table rows’ available).
keys.md#rotate-keys A heading’s id, and each link and include that names it, each [moved] value in ascribe.addresses that names it, and each memo file’s about that names it, in memos/ and .ascribe/memos/. A heading with an @id has it changed. One whose id is made from its text gets an @id line under it, so its text stays, and links to the headings after it that were numbered after it follow.
keys.md The page or fragment moves to the new path, and each link and include to it, or in it, whose path changes is changed. So is what ascribe.toml names it by: its path in [glossary] pages, and a phrase that’s a link to it; and each [moved] value in ascribe.addresses that leads to it, keeping its #id. So is each memo file’s about that names it or a heading in it, keeping its #id, and its entry in its section’s pages in ascribe.nav, which is taken out when it moves to another folder. A section written inline, as sections = [{ … }], isn’t followed: the file is left as it is, and ascribe check reports the name that’s gone. The new path is read as the target was: from the current folder, or from the content root. A new path that changes only the file’s case, such as Keys.md, renames it on a file system that matches names without case too, as macOS’s and Windows’ do.

A path is from the current folder, or from the content root, and the project is found from it; for a phrase or a value, the project is found from the current folder. --config names the project for any target, so ascribe rename keys.md guides/keys.md --config docs works from the repository’s root. A page that isn’t found is named with both places it was looked for. It writes the files it changes, then moves the page, and prints each file it changed, from the current folder. It changes only files git could put back, as every command that writes does: a file with changes not committed is refused, and named, unless you pass --force. With --dry-run, it says what it would change and move, and changes nothing.

Some things aren’t renamed or moved:

  • A permalink’s id isn’t renamed, since links from outside the docs use it: change its @id by hand, then move the old name to the new one, or retire it, with ascribe permalinks retire.
  • A page isn’t moved through a folder that’s a symbolic link.
  • A phrase used in the code of a source in another repository isn’t renamed either, since that code is a copy.
Code Meaning
0 It renamed
1 What it names doesn’t exist: a phrase, a value, a page, or a heading
2 It couldn’t rename: no ascribe.toml, one with errors, a new name that can’t be its name or is taken, a target it doesn’t rename, a file it can’t write, or, without --force, a file git couldn’t put back

With --format json, it writes what it changed as one document, instead of a line for each file: files, each file it changed, from the current folder, with its whole text after and how many places changed (edits), in path order, the page that moved under where it was; moved, the page’s from and to, when one moved; and recorded, each entry of ascribe.addresses’s [moved] it recorded, as a key and a value, as ascribe addresses move writes one. written is true, and false with --dry-run: the MCP server’s ascribe_rename gives the same document with written false, and writes nothing. Its schema has every field.