> For the complete documentation index, see [llms.txt](https://ascribed-dev.com/docs/llms.txt). This documentation describes Ascribe 0.4.4. Anything marked in main, not yet released isn't in that release yet.

# `ascribe permalinks`

> Reference · Keep a project's list of permalinks, check a product's names against it, and retire a permalink with a reason.

The commands that keep a project's list of [permalinks](https://ascribed-dev.com/docs/glossary.md#permalink), check names against it, and retire permalinks. [Permalinks](https://ascribed-dev.com/docs/permalinks.md) is the task they're used in. The options every command accepts are in the [command reference](https://ascribed-dev.com/docs/command.md#options-every-command-accepts), and the JSON each writes with `--format json` is in [their JSON reports' schemas](https://ascribed-dev.com/docs/contracts/json-reports/addresses.md#ascribe-permalinks-sync).

## `ascribe permalinks`

Keeps the project's list of [permalinks](https://ascribed-dev.com/docs/writing/directives.md#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`](https://ascribed-dev.com/docs/publishing/redirects.md#ascribeaddresses) 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](https://ascribed-dev.com/docs/contracts/permalinks-list.md).

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.

### `ascribe permalinks sync`

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:

```text
wrote ascribe.permalinks, with 3 permalinks; commit it with the docs
```

Tested with ascribe 0.4.4 on Linux.

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

```text
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]
```

Tested with ascribe 0.4.4 on Linux.

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

```text
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
```

Tested with ascribe 0.4.4 on Linux.

With `--format json`, it writes the document its [schema](https://ascribed-dev.com/docs/contracts/json-reports/addresses.md#ascribe-permalinks-sync) describes:

**The JSON for this example**

```json
{
  "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"
  ]
}
```

Tested with ascribe 0.4.4 on Linux.

| 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 |

### `ascribe permalinks list`

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:

```text
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
```

Tested with ascribe 0.4.4 on Linux.

- `--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](https://ascribed-dev.com/docs/contracts/json-reports/addresses.md#ascribe-permalinks-list) 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**

```json
{
  "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"
    }
  ]
}
```

Tested with ascribe 0.4.4 on Linux.

### `ascribe permalinks verify`

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](https://ascribed-dev.com/docs/permalinks.md#check-the-links-in-the-products-ci) shows the step.

```sh
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:

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

Tested with ascribe 0.4.4 on Linux.

With `--format json`, it writes the document its [schema](https://ascribed-dev.com/docs/contracts/json-reports/addresses.md#ascribe-permalinks-verify) describes:

**The JSON for this example**

```json
{
  "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"
  ]
}
```

Tested with ascribe 0.4.4 on Linux.

| 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 |

### `ascribe permalinks retire`

Retires permalinks on purpose, with a reason, or moves them to `--to`: it writes an entry for each in [`ascribe.addresses`](https://ascribed-dev.com/docs/publishing/redirects.md#ascribeaddresses), 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.

```sh
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:

```text
retired rotate-keys
wrote ascribe.addresses
wrote ascribe.permalinks
```

Tested with ascribe 0.4.4 on Linux.

- `[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](https://ascribed-dev.com/docs/contracts/json-reports/addresses.md#ascribe-permalinks-retire) 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**

```json
{
  "schema_version": 2,
  "ascribe_version": "{version}",
  "retired": [
    "rotate-keys"
  ],
  "reason": "Merged into the security guide",
  "to": null,
  "created": false,
  "list_changed": true,
  "written": true
}
```

Tested with ascribe 0.4.4 on Linux.
