ascribe sources and observe

Fetch and update the code a project copies from other repositories, and record what tested a folder’s examples.

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

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 and Tested examples are the tasks 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.

ascribe sources

Copies code from sources in other repositories 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 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 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:

ascribe 0.4.4 on Linux
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
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 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 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 whose status changed, and the pages whose examples changed, grouped as 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.
ascribe 0.4.4 on Linux
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)

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 has it; its pages are as the drift JSON has them.

The JSON for this example
ascribe 0.4.4 on Linux
{
  "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
}
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:

ascribe 0.4.4 on Linux
api (https://github.com/lantern/api, main)
  pinned to e06ca349578f9184cd36724d9ed237e0ef2f0eff
  src/auth.rs: current
  • --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 describes:

The JSON for this example
ascribe 0.4.4 on Linux
{
  "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"
        }
      ]
    }
  ]
}
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, 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).

  • --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, after an example changes:

ascribe 0.4.4 on Linux
--- 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.
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 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
ascribe 0.4.4 on Linux
{
  "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
}