> 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 sources` and `observe`

> Reference · Fetch and update the code a project copies from other repositories, and record what tested a folder's examples.

The commands that look after the code a project's pages show: the copies it keeps of its sources in other repositories, and the record of what tested its examples. [Docs in another repository](https://ascribed-dev.com/docs/examples/docs-repository.md) and [Tested examples](https://ascribed-dev.com/docs/examples/tested.md) are the tasks 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/sources.md).

## `ascribe sources`

Copies code from [sources in other repositories](https://ascribed-dev.com/docs/content-model/sources.md#a-source-in-another-repository) into the project, and moves their pins. `check` and `build` read only the copies, so `ascribe sources fetch` and `ascribe sources update` are the only commands that reach another repository. They run `git`, with `git`'s own credentials: whatever lets `git fetch <url>` work in your shell lets them fetch. They never prompt, so a missing credential fails at once.

Fetched repositories are kept in a cache outside the project, one per URL, so a second run fetches only what's new: `ascribe` in your cache folder (`$XDG_CACHE_HOME`, `~/.cache`, `~/Library/Caches`, or `%LOCALAPPDATA%`), or `ASCRIBE_CACHE_DIR`. Nothing needs the cache but these commands; deleting it costs only a slower next run.

What's fetched is someone else's repository, so it's only ever read as text: `git` runs with no hooks, no submodules, and only the `https`, `http`, `ssh`, `git`, and `file` transports, and a file over 1 MB, or one that isn't text, isn't copied.

### `ascribe sources fetch`

Makes the copies match `ascribe.lock`: at each source's pin, it copies the files snippets name that have no copy or a wrong one, and removes the copies no snippet names. It's what you run after writing a new `@snippet` from a source in another repository, or after cloning if the copies were left out. It never moves a pin; a source with no pin yet is pinned to the head of its branch.

Beside the files snippets name, it copies the repository's [tested examples](https://ascribed-dev.com/docs/examples/tested.md#when-the-code-is-in-another-repository) config, `ascribe.examples.toml`, and every file in its `examples.observations/`, whatever the source's `include` says, and the lock lists them as it does every copy. They're read as text, and nothing in them is run. A source whose copies are all in order is left alone, without reaching its repository, so a pin made by an `ascribe` that didn't copy them gets them with the next `update`, or the next time a copy is fetched.

- `[NAME]...`: The sources to fetch. By default, every source in another repository.
- `--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 copied.
  - `text` (the default): Short text, for people and agents to read.
  - `json`: One JSON document, for tools.

It lists each copy it wrote or removed. It changes a copy or `ascribe.lock` only when git could put it back, as every [command that writes](https://ascribed-dev.com/docs/command.md#commands-that-write) does: one with changes not committed is refused, and named, unless you pass `--force`. `update` does the same. With `--dry-run`, each says what it would write and remove, and writes nothing; `update` then doesn't compare the pages. The first time a source's files are copied, it says so, on standard error: the copies are committed with the docs, so everyone who can read the docs repository can read them. In a new project with one snippet from `api`:

```text
note: these are the first files copied from api, into sources/api/. Commit them with the docs: everyone who can read this repository can read them, which matters when the code's repository is private.
api: pinned to e06ca34
api: copied src/auth.rs
wrote ascribe.lock
```

Tested with ascribe 0.4.4 on Linux.

Available: Ascribe (in main, not yet released, 0.4.5)

With `--format json`, it writes what it did for each source, the files it couldn't copy among them, in the document its [schema](https://ascribed-dev.com/docs/contracts/json-reports/sources.md#ascribe-sources-fetch) describes. The note about a first copy still goes to standard error.

| Code | Meaning |
| --- | --- |
| `0` | The copies match the lock |
| `1` | A file a snippet names couldn't be copied: it isn't in the repository at the pin, it's over 1 MB, or it isn't text. The reason goes to standard error, and `ascribe check` reports the snippet. |
| `2` | It couldn't run: no `ascribe.toml`, a [content model](https://ascribed-dev.com/docs/glossary.md#content-model) with errors, an `ascribe.lock` it can't read, a name that isn't a source in another repository, without `--force`, a file git couldn't put back, or `git` failing (no network, no access), with the source named and `git`'s own message |

### `ascribe sources update`

Moves each source's pin to the head of its `branch` (or to `--to`), copies the files snippets use again at the new commit, rewrites `ascribe.lock`, and says what changed: the commits between the old pin and the new (how many, and the first lines of the newest 20, or that the pin moved back), the copies that changed, the [tested examples](https://ascribed-dev.com/docs/examples/tested.md#when-the-code-is-in-another-repository) whose status changed, and the pages whose examples changed, grouped as [`ascribe drift`](https://ascribed-dev.com/docs/review/diff.md#ascribe-drift) groups them. With nothing to move, it changes no file and says so.

A tested example snippets use is listed when the new pin changed its status: it went stale (the file changed, and its observation didn't follow), no observation lists it any more, or it's tested as it is now. Each is read from the copies, before and after.

- `[NAME]...`: The sources to update. By default, every source in another repository.
- `--to <REV>`: Move the pin to this revision instead of the head of the source's branch: a commit's full hash, a branch, or a tag. Only for one source: name it.
- `--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): Each source's commits and copies, then the pages, for people.
  - `json`: One JSON document, for tools.
  - `markdown`: Markdown for a pull request's description.

```text
api: e06ca34 → 3f7a23a, the head of main, 3 commits
  3f7a23a Take a user when logging in
  77319c2 Document the client
  13d55fc Rename the examples folder
  changed src/auth.rs

Examples that changed. The page shows the new code; check the words around it:
  guides/auth.md
    api:src/auth.rs#login (+1 −1)
```

Tested with ascribe 0.4.4 on Linux.

The pages are found as `ascribe drift` finds them, comparing the working tree, with the new copies, against `HEAD`, so run it in a clean checkout. A snippet whose region is gone at the new commit is listed among the examples that no longer resolve, and the update still completes: `ascribe check` then fails on it, which is the signal to fix the page. When the project isn't in a git repository, or has no commit yet, the pages aren't listed, and it says why: commit the docs once before the first update.

`--format markdown` writes Markdown for a pull request's description: each source that moved, with its commits, its copies, and its tested examples whose status changed, then the pages, each linked to its route on `[consumer] site`. For a repository on github.com, the old and new pins link to GitHub's comparison of them. Commit subjects are code spans, so a subject like `Fixes #12 @someone` in the code's repository can't close an issue or notify anyone from the docs' pull request. It writes nothing when nothing moved. `--format json` writes one document, as its [schema](https://ascribed-dev.com/docs/contracts/json-reports/sources.md#ascribe-sources-update) has it; its `pages` are as the [drift JSON](https://ascribed-dev.com/docs/review/diff.md#drift-json) has them.

**The JSON for this example**

```json
{
  "schema_version": 1,
  "ascribe_version": "{version}",
  "changed": true,
  "written": true,
  "sources": [
    {
      "name": "api",
      "git": "https://github.com/lantern/api",
      "followed": "main",
      "from": "e06ca349578f9184cd36724d9ed237e0ef2f0eff",
      "to": "3f7a23ad44ad1f6c9c2814bc82c96a0382f02f19",
      "moved": true,
      "back": false,
      "commits": {
        "count": 3,
        "newest": [
          {
            "commit": "3f7a23ad44ad1f6c9c2814bc82c96a0382f02f19",
            "subject": "Take a user when logging in"
          },
          {
            "commit": "77319c2418961a96ceabb9711fa39b393fc8987f",
            "subject": "Document the client"
          },
          {
            "commit": "13d55fc94ea0515f87527da901c257c617439efe",
            "subject": "Rename the examples folder"
          }
        ]
      },
      "files": [
        {
          "path": "src/auth.rs",
          "change": "changed"
        }
      ],
      "examples": [],
      "failed": [],
      "first_copy": false
    }
  ],
  "pages": [
    {
      "path": "guides/auth.md",
      "route": "/guides/auth/",
      "builds": [
        "site"
      ],
      "page_changed": false,
      "examples": [
        {
          "address": "api:src/auth.rs#login",
          "source": "api",
          "file": "sources/api/src/auth.rs",
          "was_file": null,
          "added": 1,
          "removed": 1
        }
      ],
      "broken": []
    }
  ],
  "pages_unavailable": null
}
```

Tested with ascribe 0.4.4 on Linux.

| Code | Meaning |
| --- | --- |
| `0` | It ran, whether or not a pin moved. A file a snippet names that couldn't be copied is reported on standard error. |
| `2` | It couldn't: no network, no access, an unknown revision, `--to` with more than one source, or the reasons `fetch` gives. The source is named, with `git`'s own message. |

### `ascribe sources status`

Shows each source in another repository: its repository and branch, its pin, and each copy's state (current, changed here, missing, not in the lock, unused, not copied yet, or not in the repository at the pin). A tested examples' config or observation is current when it's the file the lock pins, though no snippet uses it. It reads only the project's files: no `git`, no network. After the first `fetch`:

```text
api (https://github.com/lantern/api, main)
  pinned to e06ca349578f9184cd36724d9ed237e0ef2f0eff
  src/auth.rs: current
```

Tested with ascribe 0.4.4 on Linux.

- `--format <FORMAT>`: How to show it.
  - `text` (the default): Each source and its copies, for people.
  - `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/sources.md#ascribe-sources-status) describes:

**The JSON for this example**

```json
{
  "schema_version": 1,
  "ascribe_version": "{version}",
  "sources": [
    {
      "name": "api",
      "git": "https://github.com/lantern/api",
      "branch": "main",
      "commit": "e06ca349578f9184cd36724d9ed237e0ef2f0eff",
      "files": [
        {
          "path": "src/auth.rs",
          "state": "current"
        }
      ]
    }
  ]
}
```

Tested with ascribe 0.4.4 on Linux.

| Code | Meaning |
| --- | --- |
| `0` | It showed the sources, whatever state their copies are in |
| `2` | It couldn't run: a usage error, no `ascribe.toml`, or one with errors |

## `ascribe observe`

Runs the test of a folder of [tested examples](https://ascribed-dev.com/docs/examples/tested.md), and when it passes, writes down what it passed with: `examples.observations/default.toml`, beside `ascribe.examples.toml`, or `examples.observations/<name>.toml` with `--name`. It's run in the code's folder, the one that holds `ascribe.examples.toml`, not the docs project's: it doesn't look in parent folders for a config, and doesn't read `ascribe.toml` or `--config`. For code in another repository, that's the code's repository: it refuses to run in a source's copies, which it knows from the `ascribe.lock` above them. So `ascribe` has to be installed where the code is: with npm, or on the path where there's no Node ([Install](https://ascribed-dev.com/docs/install.md)).

- `--name <NAME>`: The observation to write: `examples.observations/<NAME>.toml`. Lowercase words of letters and digits joined by `-`, such as `node-22`, one for each environment the examples are tested in. Each observation is whole and separate, and running one doesn't touch another. By default, `default`.
- `--check`: Run nothing, and exit 1 when the observation is stale against the folder as it stands: for a pre-commit hook. It reads the config and the examples, and starts no program, the config's commands included. A stale observation's hashes are printed as a unified diff of the file.
- `--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.
- `--yes`: Run the commands without asking first. It asks only when standard input is a terminal. Without one, as in CI, a hook, or an agent's shell, it lists the commands and runs nothing unless given `--yes`.
- `--format <FORMAT>`: How to show what it did. With `json`, what the commands print goes to standard error, so standard output holds the JSON alone.
  - `text` (the default): What it runs and what it wrote, for people.
  - `json`: One JSON document, for tools.

It prints the config's path and every command it will run. When its standard input is a terminal, it then asks **Run them? \[y/N\]**, on standard error, and runs nothing unless the answer is `y` or `yes`; with `--format json`, the list goes to standard error too, so standard output holds the JSON alone. `--yes` skips the question. Where standard input isn't a terminal, as in CI, a hook, or an agent's shell, nobody is there to answer, so it lists the commands, runs nothing, and exits 2 unless given `--yes`.

Then it runs each `[environment]` command, then `test`, through the platform's shell (`sh`, or `cmd` on Windows), in the folder, with your environment and standard input closed. The test's output goes to the terminal as it comes, and there's no time limit: you started it, and you can stop it. Last, it names the observation it wrote, with how many files and what it recorded of the environment.

When `test` exits 0, it hashes every example and writes the observation whole, to a file beside it that it then renames, so a run that's stopped part way leaves the old one. When it doesn't, it writes nothing. A file that changes while the test runs is hashed as it is at the end, so run it on a clean tree. When the observation replaces one, it says what changed since: the files added, removed, and changed, and the environment values that differ. An observation of another name is never touched.

With `--check`, it runs nothing: not `test`, not the `[environment]` commands. It hashes the examples as they are, and compares them with the observation `--name` names, `default` without it, so a pre-commit hook can run it. In [`examples/tested/code`](https://github.com/ascribed-dev/ascribe/tree/main/examples/tested/code), after an example changes:

```text
--- a/examples.observations/default.toml
+++ b/examples.observations/default.toml
@@ -10,4 +10,4 @@

 [files]
 "examples/greet.out.txt" = "sha256-lf:231462024105beaa2d3fcc21f4f29a2a2768328bb8bdb514e37ce65914552cec"
-"examples/greet.ts" = "sha256-lf:224cb5bb3ee428652127268d6b08c6e9dde5e3ab3ac3731633ef34898cf55f0d"
+"examples/greet.ts" = "sha256-lf:398ebfc857666088b8cb814036af12e39781c33d21ce53d5b2c3dc590a3beed2"
examples.observations/default.toml is stale: changed examples/greet.ts. Run `ascribe observe` here to test the examples again.
```

Tested with ascribe 0.4.4 on Linux.

| Code | Meaning |
| --- | --- |
| `0` | The test passed, and the observation is written |
| `1` | The test failed. Nothing was written, and the old observation is as it was. |
| `2` | It couldn't run: a `--name` that isn't one; the current folder is a source's copies (`sources/<name>/` of a project whose `ascribe.lock` pins it), whose config is never run; no `ascribe.examples.toml` in the current folder, one with errors or with no `test`, the question answered with anything but yes, no terminal to ask at and no `--yes`, an `[environment]` command that failed or printed nothing, no shell, or an observation it couldn't write |

With `--check`, `0` means the observation is current, `1` that it's stale or missing, and `2` that the config, the examples, or the observation can't be read.

With `--format json`, what the commands print goes to standard error, and standard output has one document, which its [schema](https://ascribed-dev.com/docs/contracts/json-reports/sources.md#ascribe-observe) describes. When the test fails, `observation`, `recorded`, `files`, `tested`, and `changes` are empty. With `--check`, `check` is `true` and `test` is null:

**The JSON for this example**

```json
{
  "schema_version": 1,
  "ascribe_version": "{version}",
  "config": "ascribe.examples.toml",
  "check": true,
  "environment": [],
  "test": null,
  "observation": "examples.observations/default.toml",
  "written": false,
  "current": false,
  "changes": {
    "added": [],
    "removed": [],
    "changed": [
      "examples/greet.ts"
    ],
    "environment": []
  },
  "recorded": {},
  "files": {
    "examples/greet.out.txt": "sha256-lf:231462024105beaa2d3fcc21f4f29a2a2768328bb8bdb514e37ce65914552cec",
    "examples/greet.ts": "sha256-lf:398ebfc857666088b8cb814036af12e39781c33d21ce53d5b2c3dc590a3beed2"
  },
  "tested": null
}
```

Tested with ascribe 0.4.4 on Linux.
