ascribe permalinks

Keep a project’s list of permalinks, check a product’s names against it, and retire a permalink with a reason.

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

The commands that keep a project’s list of permalinks, check names against it, and retire permalinks. Permalinks is the task they’re used in. The options every command accepts are in the command reference, and the JSON each writes with --format json is in their JSON reports’ schemas.

Keeps the project’s list of permalinks, ascribe.permalinks beside ascribe.toml, checks names against it, and retires permalinks. The list names each permalink the content declares and where, and each name ascribe.addresses moves or retires, and is committed with the docs: with it, ascribe check reports a permalink that goes away (permalink-lost) and one the list doesn’t have yet (permalinks-list-behind), and a product’s CI can check the names it links to with verify. A project without the list isn’t told about either. Its format is in the permalinks list contract.

The examples below are one project’s: a page, guides/weave.md, with the permalinks weaving, weave-config, and rotate-keys, and an alias, config, for weave-config. Then weave-config’s section moves to reference/loom.md, and rotate-keys’s goes.

Brings ascribe.permalinks up to date with the project, and makes it when there’s none: it adds each permalink the list doesn’t have, records where each is declared now, so moving a heading to another page and running sync changes one line, and records each name ascribe.addresses moves or retires as the file has it. It never drops a name the project no longer has: that stays in the list, and ascribe check reports it until it’s back or retired, so running sync can’t hide a removal. With --check, it writes nothing, and says what it would change, so CI can fail on a list that wasn’t committed with the change; ascribe check reports the same thing as permalinks-list-behind.

  • --check: Write nothing, print how the list would change as a unified diff, and exit with 1 when it would: for CI.
  • --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.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.

The first time, it writes the list:

ascribe 0.4.4 on Linux
wrote ascribe.permalinks, with 3 permalinks; commit it with the docs

Once a section has moved and another has gone, --check says what it would change:

ascribe 0.4.4 on Linux
warning: ascribe.permalinks has rotate-keys, which the project no longer has; `ascribe check` reports it until it's back or retired
error: ascribe.permalinks is out of date; run `ascribe permalinks sync` to update it
--- a/ascribe.permalinks
+++ b/ascribe.permalinks
@@ -3,7 +3,7 @@

 [permalinks]
 rotate-keys = "guides/weave.md#rotate-keys"
-weave-config = "guides/weave.md#weave-config"
+weave-config = "reference/loom.md#weave-config"
 weaving = "guides/weave.md"

 [aliases]

And without it, it moves the one, keeps the other, and says why:

ascribe 0.4.4 on Linux
warning: kept rotate-keys, which the project no longer has; `ascribe check` reports it until it's back or retired
moved weave-config: guides/weave.md#weave-config to reference/loom.md#weave-config
wrote ascribe.permalinks

With --format json, it writes the document its schema describes:

The JSON for this example
ascribe 0.4.4 on Linux
{
  "schema_version": 1,
  "ascribe_version": "{version}",
  "changed": true,
  "written": false,
  "created": false,
  "added": [],
  "moved": [
    {
      "name": "weave-config",
      "from": "guides/weave.md#weave-config",
      "to": "reference/loom.md#weave-config"
    }
  ],
  "aliased": [],
  "forwarded": [],
  "retired": [],
  "removed": [
    "rotate-keys"
  ]
}
Code Meaning
0 The list is up to date, whether or not it changed; with --check, it was already
1 With --check, the list would change: it’s behind the content, or there’s none
2 It couldn’t run: the project doesn’t load, or the list can’t be read or written

Shows every permalink the content declares, with the file and line it’s declared on, and each name ascribe.addresses moves or retires, and says which ones ascribe.permalinks doesn’t have yet, has somewhere else, or has that the project no longer has. It changes nothing. Once the sections have changed, before the list is synced:

ascribe 0.4.4 on Linux
docs/guides/weave.md:3: weaving
docs/reference/loom.md:6: weave-config  (ascribe.permalinks has it in guides/weave.md#weave-config)
ascribe.addresses: config moved to the permalink weave-config
ascribe.permalinks: rotate-keys was in guides/weave.md#rotate-keys, and nothing declares it now
  • --format <FORMAT>: How to show the permalinks.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.

With --format json, it writes the same in the document its schema describes. It exits with 0 whatever it finds, and 2 when the project doesn’t load or the list can’t be read.

The JSON for this example
ascribe 0.4.4 on Linux
{
  "schema_version": 2,
  "ascribe_version": "{version}",
  "has_list": true,
  "permalinks": [
    {
      "name": "weaving",
      "place": "guides/weave.md",
      "file": "docs/guides/weave.md",
      "line": 3,
      "state": "listed",
      "listed_at": null
    },
    {
      "name": "weave-config",
      "place": "reference/loom.md#weave-config",
      "file": "docs/reference/loom.md",
      "line": 6,
      "state": "moved",
      "listed_at": "guides/weave.md#weave-config"
    }
  ],
  "aliases": [
    {
      "name": "config",
      "target": "weave-config",
      "state": "listed"
    }
  ],
  "moved": [],
  "retired": [],
  "removed": [
    {
      "name": "rotate-keys",
      "was": "permalink",
      "place": "guides/weave.md#rotate-keys"
    }
  ]
}

Checks that each name is a permalink or an alias in a list, and names each one that isn’t, and each one that was retired, with the reason the docs give. It reads only the list, not the docs, so a product’s repository can run it against a copy of the docs’ list: Check the links in the product’s CI shows the step.

ascribe permalinks verify cli-check ASC036
ascribe permalinks verify --against docs/ascribe.permalinks --from permalinks.txt
  • [NAME]...: The names to check.
  • --from <FILE>: Also check the names in this file, one on each line. Blank lines and lines that start with # are skipped.
  • --against <PATH>: The list to check against. By default, the ascribe.permalinks beside the project’s ascribe.toml.
  • --format <FORMAT>: How to show the answer.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.

What it says goes to standard error. Once rotate-keys is retired, ascribe permalinks verify weave-config config rotate-keys weave-colour --against ascribe.permalinks passes the permalink and its alias, and names the other two:

ascribe 0.4.4 on Linux
error: rotate-keys was retired from the docs: Merged into the security guide
error: weave-colour isn't a permalink in ascribe.permalinks

With --format json, it writes the document its schema describes:

The JSON for this example
ascribe 0.4.4 on Linux
{
  "schema_version": 1,
  "ascribe_version": "{version}",
  "list": "ascribe.permalinks",
  "found": [
    "weave-config",
    "config"
  ],
  "retired": [
    {
      "name": "rotate-keys",
      "reason": "Merged into the security guide"
    }
  ],
  "missing": [
    "weave-colour"
  ]
}
Code Meaning
0 The list has every name
1 It doesn’t have one, or has it as retired; each is named on standard error, a retired one with its reason
2 It couldn’t check: no list where it looked, or a list or a --from file it can’t read

Retires permalinks on purpose, with a reason, or moves them to --to: it writes an entry for each in ascribe.addresses, keyed go: and the name, [retired]’s with the reason and [moved]’s with where it leads, making the file when there’s none, and, when the project has ascribe.permalinks, records there each one the content no longer declares. A retired permalink’s address shows the reason, and verify tells a product it was retired, with the reason; a moved one’s leads to its new place, and verify accepts it. ascribe check no longer reports either as lost.

ascribe permalinks retire rotate-keys --reason "Merged into the security guide"
ascribe permalinks retire old-keys --to guides/security.md#keys
ascribe permalinks retire --page guides/legacy.md --reason "The sync agent was removed in 4.0"
ascribe permalinks retire --under guides/legacy/ --reason "The sync agent was removed in 4.0"

It retires a name given only when the content no longer declares it and ascribe.permalinks has it, and with --page or --under, each permalink declared in that page or in a file under that folder, or that the list has there. --page, --under, and --to take a path from the current folder, as ascribe check prints it, or from the content root; what it writes in ascribe.addresses is from the content root. --to takes a page, and # and a heading’s id, or go: and another permalink’s name, which makes the name an alias of it. It takes --reason or --to, not both. It refuses a name still declared anywhere else, or one ascribe.addresses already moves or retires, and changes nothing then. Retiring what a page holds before the page is deleted leaves each entry doing nothing (addresses-entry-live) until it is. The first command above, in the project above:

ascribe 0.4.4 on Linux
retired rotate-keys
wrote ascribe.addresses
wrote ascribe.permalinks
  • [NAME]...: Names to retire: permalinks the content no longer declares, which ascribe.permalinks has.
  • --reason <TEXT>: Why they were retired, for a reader whose link leads there.
  • --to <PAGE>: Where their addresses lead now, in place of a reason: a page, by its path from the current folder or from the content root, and # and a heading’s id; or another permalink, go:<name>.
  • --page <PAGE>: Retire every permalink this page declares, or that ascribe.permalinks has on it: a path from the current folder, or from the content root.
  • --under <FOLDER>: Retire every permalink declared in a file under this folder, or that ascribe.permalinks has there: a path from the current folder, 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 was retired.
    • text (the default): Short text, for people and agents to read.
    • json: One JSON document, for tools.

With --format json, it writes the document its schema describes. It exits with 0 when the names are retired or moved, and 2 when the project doesn’t load, the reason is empty, a name can’t be retired, or a file can’t be written.

The JSON for this example
ascribe 0.4.4 on Linux
{
  "schema_version": 2,
  "ascribe_version": "{version}",
  "retired": [
    "rotate-keys"
  ],
  "reason": "Merged into the security guide",
  "to": null,
  "created": false,
  "list_changed": true,
  "written": true
}