> 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 diff` and `drift`

> Reference · Compare a project's output with another revision, and find sources that drifted.

The commands that compare a project with another revision of itself. The options every command accepts are in the [command reference](https://ascribed-dev.com/docs/command.md#options-every-command-accepts).

## `ascribe diff`

Shows what changed between a git revision and the working tree, as readers will see it: which pages of each [build](https://ascribed-dev.com/docs/glossary.md#build) changed, and which blocks on them were added, removed, changed, or moved. It compares **resolved pages**, not files, so a page whose own file didn't change but whose included [fragment](https://ascribed-dev.com/docs/glossary.md#fragment), [phrase](https://ascribed-dev.com/docs/glossary.md#phrase), or build settings did is listed, with what its change comes from. A change that doesn't reach the page (`ascribe fmt`, rewrapped lines) isn't.

- `[PAGE]`: Show only this page's changes, block by block, or, for a fragment, the pages that changed through it: a path from the current directory, or from the content root. With `--format prompt`, the prompt is about this page alone, in the first build it changed in, or about the pages that changed through a fragment.
- `--base <REV>`: The revision to compare with, anything git accepts (a branch, a tag, a commit). By default, the repository's default branch, the first of `origin/HEAD`, `origin/main`, `origin/master`, `main`, and `master` that exists. The comparison starts from the merge base of that revision and `HEAD`, as a pull request shows its changes, so commits made on the base branch since you branched aren't listed. A shallow clone (what `actions/checkout` makes by default) may not have the merge base: fetch more history (`fetch-depth: 0`) or use `--base-exact`.
- `--base-exact`: Compare with the revision itself instead of the merge base.
- `--build <NAME>`: Compare only this build. Repeat it for several. By default, every build in `ascribe.toml`.
- `--format <FORMAT>`: How to show the changes.
  - `text` (the default): The changed pages of each build, with counts, for people; with a PAGE, its changes too, each block as it was and as it is.
  - `json`: One JSON document, for tools.
  - `html`: One self-contained HTML file that shows every changed page rendered, with its changes marked, for reviewers.
  - `prompt`: A prompt for an agent that reviews the changes as readers will see them: about every changed page, or about the PAGE named. Nothing when nothing changed.
- `--exit-code`: Exit with 1 when anything changed, as `git diff --exit-code` does.
- `--pages-only`: List the changed pages without their block-level changes. With `--format json`, each page's `changes` is empty and its `counts` still count them, so the report stays short on a large change. With a PAGE, text leaves out the page's blocks; without one, it lists only pages anyway. HTML needs the blocks.

The argument is a page, not a revision as in `git diff main`: the revision goes in `--base`. When the argument isn't a file of the project but is a revision git knows, the error says which `--base` compares with it: `ascribe diff main` names `--base main`. `ascribe drift`, which takes no argument, says the same of a revision given as one.

The other side is the working tree: the files on disk, committed or not, as `ascribe build` would read them now. Unsaved editor changes aren't included. The base is read from git, `ascribe.toml` included, so a change to the [content model](https://ascribed-dev.com/docs/glossary.md#content-model) is compared too; if the project didn't exist at the base, every page is added. `ascribe diff` needs `git` on the path, and nothing else: no network and no GitHub account. It runs `git` so that the repository can't make it run a command of its own: with no file system monitor, and taking a bare repository only when one is named, never a folder in the project shaped like one, where `git` says it can't use the repository.

The text output lists each build's changed pages with counts, and where a change comes from when it isn't only the page's own file. A page that changed the same way in every build that publishes it is said once, first, "in every build" (or "in every build that publishes it", when some don't), and each build lists its other pages:

```text
compared with main (375192d), from its merge base with HEAD (e49444a)
getting-started.md: 1 changed, 1 added (through _fragments/prereqs.md), in every build
site: 3 more pages changed
  guides/rollouts.md: 4 changed, 5 added, 2 removed, 1 moved
  guides/schedules.md: added
  reference/limits.md: title changed
```

Tested with ascribe 0.4.4 on Linux.

A page that moved is listed once, where it is now, when git sees its file renamed (as `git mv` makes it, or a move `git add` has staged) or when [`ascribe.addresses`](https://ascribed-dev.com/docs/publishing/redirects.md) records the move. It says what it moved from, and what changed on it besides the move, or that nothing did. A page whose link changed only because the page it leads to moved says so; when its own file changed too, the move isn't named again, since the moved page is listed itself:

```text
site: 2 pages changed
  guides/install.md: 1 changed (a link changed because guides/configure.md moved)
  guides/setup.md: moved from guides/configure.md, no change to the page
```

The [lines](https://ascribed-dev.com/docs/versions/lines.md) of a versioned build are listed together, under the build's name, and a page they share once, with the lines it changed in:

```text
site: 2 pages changed
  install.md: 1 changed, in every line
  upgrade.md: 1 changed, only in 3.4
```

Name a page to see its changes: every changed block, as it was (`-`) and as it is (`+`), with where it's written. In changed prose, the words removed are marked `[-like this-]` and the words added `{+like this+}`, as `git diff --word-diff` marks them; a block that isn't prose, such as a code block, shows its lines on both sides, and a moved block shows its lines once. A build the page didn't change in says so. Name a fragment, or another file pages take text from, to see the pages that changed through it. `--format json` and `--format html` are narrowed the same way, with the same shape and fewer pages, and `--exit-code` exits with `1` only when what's shown changed. A PAGE that isn't a file of the project, at the base or now, is a usage error.

```text
compared with main (375192d), from its merge base with HEAD (e49444a)
site: guides/rollouts.md: 4 changed, 5 added, 2 removed, 1 moved
  changed guides/rollouts.md:6
    - Roll the agent out to a few servers first, then to the [-rest-].
    + Roll the agent out to a few servers first, then to {+all+} the {+others+}.
  changed guides/rollouts.md:10
    - Pick servers that see little traffic.
    + Pick servers that see little traffic {+and run the newest release+}.
  changed guides/rollouts.md:14
    - Install
    + Install {+the agent+}
  changed guides/rollouts.md:16
    - Install the agent on the tagged servers.
    + Install the agent on the tagged servers{+, one at a time+}.
  removed guides/rollouts.md:18 in the base
    - Watch their logs for an hour.
  removed guides/rollouts.md:22 in the base
    - Remove the agent from the tagged servers.
  added guides/rollouts.md:20
    + Watch their logs for a day.
  added guides/rollouts.md:22
    + Check the error rate in the dashboard.
  moved guides/rollouts.md:24, from guides/rollouts.md:20 in the base
      ## Roll back
  added guides/rollouts.md:26
    + Remove the agent from the tagged servers, and its configuration.
  added guides/rollouts.md:28
    + Send us the logs.
  added guides/rollouts.md:34
    + Untag the servers.
cloud: guides/rollouts.md: no changes
```

Tested with ascribe 0.4.4 on Linux.

Pages with errors are compared as they are. When `ascribe check` would find errors for the builds compared, `ascribe diff` says so on standard error, as review does in every place ([When the project has errors](https://ascribed-dev.com/docs/review/how-it-works.md#when-the-project-has-errors) says why):

```text
warning: the working tree has 3 errors; `ascribe check` lists them
```

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Compared, whether or not anything changed, and whether or not the working tree has errors |
| `1` | With `--exit-code`: something changed |
| `2` | It couldn't run: a usage error, an unknown build, not a git repository, an unknown revision, no merge base (a shallow clone, or unrelated histories), `git` not found, or a project that doesn't load at the base or in the working tree. The reason goes to standard error. |

### The HTML preview

`--format html` writes one HTML file to standard output, with everything in it: its styles and script, the element library, and every image a changed page uses, up to 1 MB each (a larger one shows as a placeholder naming its file). It makes no network requests, so it opens from a CI artifact, an email, or a disk with nothing else:

```sh
ascribe diff --format html > review.html
```

It lists the changed pages of each build, and marks a page that changed only through something it uses ("via `_fragments/prereqs.md`"), and a page that moved with where it moved from, its "As it was" rendered from there; with more than one build changed, a picker switches between them. Each page is rendered as it is now, the way the editor's [page preview](https://ascribed-dev.com/docs/glossary.md#page-preview) renders it, without the site's layout, with review's [marks](https://ascribed-dev.com/docs/review/how-it-works.md#the-marks):

![The HTML preview in a browser, with the list of changed pages above a page whose changed, added, removed, and moved paragraphs are marked.](https://ascribed-dev.com/docs/_ascribe/files/images/html-report-light.png)
![The HTML preview in a browser, with the list of changed pages above a page whose changed, added, removed, and moved paragraphs are marked.](https://ascribed-dev.com/docs/_ascribe/files/images/html-report-dark.png)

Above each page are its file and route, its count of changes, **Show**, which switches between **Changes**, **As it will be**, and **As it was**, and arrows to the previous and next change.

Hovering over a block shows the source file and line it came from (`guides/install.md:12`).

The lines of a versioned build are one build in the list: a changed page is listed once, with "in every line" or the lines it changed in, and a **Line** switch shows it as each line publishes it.

**Copy prompt** copies the page's [agent prompt](https://ascribed-dev.com/docs/review/diff.md#the-agent-prompt), the one `ascribe diff --format prompt <page>` writes, to paste into your agent. The preview has no link that opens an agent: a file opened from a disk or a CI artifact can't tell which editor you use, so it only copies.

The preview renders at most 300 changed pages; the rest are listed by name, and the preview says so at the top. Each rendered page and image is stored once, however many builds or pages share it. [Attach the HTML preview to every pull request](https://ascribed-dev.com/docs/review/ci-report.md) has a GitHub Actions job that uploads it.

### The agent prompt

`--format prompt` writes a prompt that asks your agent to review the change as readers will see it, in the [agent prompt format](https://ascribed-dev.com/docs/agents/commands.md#what-a-prompt-says), and writes nothing when nothing changed. Without a PAGE, it's about every changed page, grouped by build when more than one changed. With a PAGE, it's about that page in the first build it changed in, listing each change with its source lines; for a fragment, or any other file pages changed through, it's about the pages that show it, and asks the agent to check that the new text fits each. A PAGE that isn't a file of the project, at the base or now, is a usage error.

```text
Review what this change does to `docs/getting-started.md`, as a reader of build `site` sees it.

Where: docs/getting-started.md
Paths and commands here are from `docs` in the repository.
Build: `site`

Base: `main`, from where this branch left it (e49444a)
What changed: 1 changed
Changed through: `docs/_fragments/prereqs.md`
To read it: `ascribe resolve docs/getting-started.md --build site`
Changes:
- changed: docs/_fragments/prereqs.md:3, shown through docs/getting-started.md:11

Report what reads wrongly; don't edit.
```

Tested with ascribe 0.4.4 on Linux.

The prompt asks for a report, not edits: a review reads the change, and its author decides what to do. Its paths and commands (`ascribe resolve`, `git diff`, `ascribe diff`) are written from the folder `ascribe diff` was run in, which it names once, with the base, the build, and `--config` as this run had them. The [HTML preview](https://ascribed-dev.com/docs/glossary.md#html-preview)'s **Copy prompt** gives the same prompt, and the editor's **Prompt agent** gives it from the repository's root.

### Diff JSON

`--format json` writes one JSON document to standard output. It follows the [JSON output](https://ascribed-dev.com/docs/checks/check.md#json-output)'s rules: `schema_version` changes only when a field is removed or changes meaning, so **ignore fields you don't know**.

| Field | Type | Meaning |
| --- | --- | --- |
| `schema_version` | number | `1` |
| `ascribe_version` | string | The version of `ascribe` that wrote the report |
| `base` | object | `requested`: the revision asked for, or the default branch used. `commit`: the commit it names. `merge_base`: the merge base with `HEAD` that was compared with, or `null` with `--base-exact`. |
| `repository` | object | `root`: the repository's top-level directory. `project_prefix`: the project's folder in it, with a trailing `/`, or `""` at the root. |
| `working_tree_errors` | number | How many errors `ascribe check` finds in the working tree for the builds compared (with the same `--build` options). The comparison runs either way. |
| `blocks_omitted` | boolean | Only with `--pages-only`, and then `true`: each page's `changes` is empty, and its `counts` still count them. |
| `builds` | array | One entry per build compared: `build`, its name, and `pages`, the pages that changed, in path order. |

Each line of a versioned build is an entry of its own in `builds`, such as `site@3.4`, its pages' routes under the line's folder.

Each page:

| Field | Type | Meaning |
| --- | --- | --- |
| `path` | string | The page's path, relative to the content root |
| `moved_from` | string | For a page that moved, its path at the base. The page is listed once, at `path`, with `status` `"changed"`, compared with the page as it was at `moved_from`: its `counts` and `changes` are what differs besides the move, none when it only moved. Absent for a page that didn't move. |
| `route` | string | Its route; for a removed page, the route it had |
| `status` | string | `"added"` (the build publishes it now and didn't), `"removed"`, or `"changed"` |
| `own_file_changed` | boolean | Whether the page's own file changed, or exists on one side only |
| `because` | array of strings | The other changed files its change comes from, relative to the content root: fragments it includes, and pages its links take their text or a heading id from (a link to a page that changed otherwise isn't a cause). Then the [snippets](https://ascribed-dev.com/docs/writing/directives.md#snippet) whose code changed, by address (`code:service/client.py#connect`). Then the operations of a [declared API](https://ascribed-dev.com/docs/openapi.md) whose spec changed what the page writes for them, an [`@operation`](https://ascribed-dev.com/docs/openapi/operation.md)'s method, path, summary, or deprecation, or a link's address, each by its spec's path from the project root, `#`, and the fragment the page names it by (`api/openapi.yaml#createWidget`); a spec change the page doesn't show, such as a schema or a description, isn't a cause. `ascribe.toml` comes last when a change to the content model (a phrase's value, a label, a build's settings) is a cause. A page that moved is named once, by its path now. |
| `page_changed` | array of strings | What changed about the page as a whole, besides its blocks: `"title"`, `"frontmatter"` (fields other than `title` and `available`), `"availability"` (the page-level [availability](https://ascribed-dev.com/docs/glossary.md#availability), as shown), and `"route"`, in that order. Empty for an added or removed page. A moved page's route moves with it, so `"route"` isn't listed for one. |
| `moved_links` | array | The pages in `because` whose only change is that they moved, each `{from, to}`, its path at the base and now: a link on this page leads to its new address. Absent when there's none. |
| `counts` | object | `changed`, `added`, `removed`, and `moved`: how many changes of each kind |
| `changes` | array | The block changes, in the page's order, each removed block where it was. Empty for an added or removed page, and for a page whose only changes are in `page_changed`. |

Each change:

| Field | Type | Meaning |
| --- | --- | --- |
| `kind` | string | `"changed"`, `"added"`, `"removed"`, or `"moved"` |
| `now` | object | Where the block is written now (absent for a removed block): `source` and `via`, below |
| `was` | object | Where it was written at the base (absent for an added block) |
| `words` | object | For changed prose: `now` and `was`, the ranges of words that differ, as `[start, end]` character offsets (Unicode characters, end exclusive) into `now_text` and `was_text`, the block's text as a reader reads it, with inline HTML's tags and comments removed and whitespace collapsed. Absent when that text is the same (only a link's target or an HTML comment changed, say) or the block is too long to compare word by word. |
| `after` | object | For a removed block, and for a moved block's old place: the block it came after, as it is now. Absent when it was first in its container. |
| `parent` | object | For a removed block, and for a moved block's old place: the block it was inside, as it is now. Absent at the top of the page. |
| `text` | string | For a removed block: its text, whitespace collapsed |

A block is a heading, paragraph, code block, table, list, list item, block quote, directive, container, group, or a group's arm; changes inside a list, a container, or an arm are listed on the blocks inside it, so a one-word change in a step marks that step's paragraph. A line-form directive that renders as an element around the block after it (`@note`, `@steps`, `@details`, a widget) is one block with that block inside it, from the directive's line through the block's last, like the element.

A block's `source` is `<path>:<first>-<last>`: the file its text is written in, relative to the content root with each path segment percent-encoded (except ASCII letters, digits, `-`, `.`, `_`, and `~`), and its first and last lines, from 1. `via` lists the includes it came through, outermost first, each `<path>:<line>`; it's empty for a block written in the page itself. These are the same strings the site output's source anchors carry, so a tool can find a block in a rendered page.

```json
{
  "schema_version": 1,
  "ascribe_version": "0.4.4",
  "base": {
    "requested": "main",
    "commit": "375192d7ee62fbe72258e0adab3747503f45d106",
    "merge_base": "e49444a7968397b56129c1eb801ca02b0e96ed61"
  },
  "repository": {
    "root": "/home/me/lantern",
    "project_prefix": "docs/"
  },
  "working_tree_errors": 0,
  "builds": [
    {
      "build": "site",
      "pages": [
        {
          "path": "getting-started.md",
          "route": "/getting-started/",
          "status": "changed",
          "own_file_changed": false,
          "because": [
            "_fragments/prereqs.md"
          ],
          "page_changed": [],
          "counts": {
            "changed": 1,
            "added": 0,
            "removed": 0,
            "moved": 0
          },
          "changes": [
            {
              "kind": "changed",
              "now": {
                "source": "_fragments/prereqs.md:3-3",
                "via": [
                  "getting-started.md:11"
                ]
              },
              "was": {
                "source": "_fragments/prereqs.md:3-3",
                "via": [
                  "getting-started.md:11"
                ]
              },
              "words": {
                "now": [
                  [
                    14,
                    17
                  ]
                ],
                "was": [
                  [
                    14,
                    17
                  ]
                ],
                "now_text": "Lantern agent 2.4 or later",
                "was_text": "Lantern agent 2.2 or later"
              }
            }
          ]
        }
      ]
    }
  ]
}
```

Tested with ascribe 0.4.4 on Linux.

How blocks are matched: blocks are compared by their content without positions, so moving a block down the file or rewrapping it is no change. Blocks that didn't stay are paired, in order, with a block of the same kind whose words overlap by at least half, or that keeps its words whole with words added or taken away at one end, as **changed**; a block that matches nothing is **added** or **removed**, and a removed block identical to an added one elsewhere on the page is **moved**. A block over about 2,500 words gets no word ranges, and a list or container with over 1,000 blocks in it that changed is marked changed as a whole.

## `ascribe drift`

Lists the pages whose code examples changed between a git revision and the working tree, and whether the words around them changed too, and the pages whose examples no longer resolve. A page covers the code it shows, and nothing else: every region it takes a [snippet](https://ascribed-dev.com/docs/writing/directives.md#snippet) from, and every whole file it takes as one, including its fragments' snippets. It's for CI, where it says which pages a reviewer should reread; see [Report the pages whose examples changed](https://ascribed-dev.com/docs/examples/drift.md).

- `--base <REV>`: The revision to compare with, anything git accepts (a branch, a tag, a commit). By default, the repository's default branch, found as `ascribe diff` finds it. The comparison starts from the merge base of that revision and `HEAD`, as a pull request shows its changes. A shallow clone (what `actions/checkout` makes by default) may not have the merge base: fetch more history (`fetch-depth: 0`).
- `--build <NAME>`: Look only at this build. Repeat it for several. By default, every build in `ascribe.toml`.
- `--format <FORMAT>`: How to show the report.
  - `text` (the default): The groups of pages, with each example that broke or changed, for people.
  - `json`: One JSON document, for tools.
  - `markdown`: Markdown for a CI job's summary, each page linked to the site; nothing when no example changed.
- `--exit-code`: Exit with 1 when a page's example no longer resolves, or changed while the page didn't.

The base is found as `ascribe diff` finds it, and the other side is the working tree. A snippet's region is taken from its file on both sides and compared as text, dedented, with trailing spaces trimmed, so an edit elsewhere in the file, a region that only moved, or a change of indent isn't a change. A renamed file is followed. A region the file didn't have at the base is a new example, not a changed one. Then, for each page with a changed example, it compares the page itself as `ascribe diff` does, apart from its snippets: the page changed when its own file did, or when anything else it uses did, such as a fragment it includes or a phrase in `ascribe.toml`.

The pages come in up to three groups:

```text
compared with main (94c2212), from its merge base with HEAD (8a43d4b)

Examples that no longer resolve. `ascribe check` reports them too:
  guides/install.md
    code:service/client.py#connect: the file has no region `connect`

Examples that changed. The page shows the new code; check the words around it:
  reference/content-model.md
    code:examples/quill/ascribe.toml#dimensions (+3 −1)

Examples that changed along with the page:
  guides/review.md
    code:examples/quill/ascribe.toml#builds (+1 −0)
```

Tested with ascribe 0.4.4 on Linux.

An example **no longer resolves** when it did at the base and doesn't now: its region was renamed, its file moved, or its source is gone. `ascribe check` fails on it too, but the report names the page so the pull request that broke it says so. A `@snippet` the change adds broken isn't listed; that's `check`'s alone. Of the changed examples, the first group is the one to read: the example updated itself, and nobody changed the sentence that explains it. The second is listed so a reviewer can see the change reached the page. Each example says how many lines were added and removed. With nothing to report, it says `No examples changed.`

A changed example isn't always a changed behavior. The report says what changed and how much, and leaves the judgment to you.

It reads little: one `git diff` listing of the change, and at the base only the files that listing names that a snippet uses. The base's pages are read only when an example changed. `ascribe drift` needs `git` on the path, and nothing else: no network and no GitHub account.

### The summary

`--format markdown` writes Markdown for a CI job's summary: the same groups, each page linked to its route on `[consumer] site` (plain text when there's no `site`), and nothing at all when no example changed, so it can be appended to `$GITHUB_STEP_SUMMARY` on every run.

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | Compared, whatever it found |
| `1` | With `--exit-code`: an example no longer resolves, or a page's example changed and the page didn't |
| `2` | It couldn't run: a usage error, an unknown build, not a git repository, an unknown revision, no merge base (a shallow clone, or unrelated histories), `git` not found, or a project that doesn't load at the base or in the working tree. The reason goes to standard error. |

### Drift JSON

`--format json` writes one JSON document to standard output, with the [JSON output](https://ascribed-dev.com/docs/checks/check.md#json-output)'s rules: `schema_version` changes only when a field is removed or changes meaning, so **ignore fields you don't know**.

| Field | Type | Meaning |
| --- | --- | --- |
| `schema_version` | number | `1` |
| `ascribe_version` | string | The version of `ascribe` that wrote the report |
| `base` | object | As in the [diff JSON](https://ascribed-dev.com/docs/review/diff.md#diff-json): `requested`, `commit`, and `merge_base` |
| `repository` | object | As in the diff JSON: `root` and `project_prefix` |
| `pages` | array | Every page with an example that changed or no longer resolves, in path order |

Each page:

| Field | Type | Meaning |
| --- | --- | --- |
| `path` | string | The page's path, relative to the content root |
| `route` | string | Its route, in the first build that shows it |
| `builds` | array of strings | The builds that show it with a changed example |
| `page_changed` | boolean | Whether the page changed apart from its examples, in any of those builds. With `false`, its changed examples are in the group to read. |
| `examples` | array | The examples that changed, by address |
| `broken` | array | The examples that resolved at the base and don't now, by address: `address`, `source`, `problem` (the slug `ascribe check` reports, such as `snippet-region-missing`), and `reason`, in a few words |

Each example:

| Field | Type | Meaning |
| --- | --- | --- |
| `address` | string | The snippet's address, as the page writes it (`code:service/client.py#connect`) |
| `source` | string | The source it names |
| `file` | string | The code file, relative to the repository's root |
| `was_file` | string or null | The code file at the base, when it was renamed since |
| `added` | number | Lines only in the code now |
| `removed` | number | Lines only in the code at the base |
