---
title: Pipelines and provenance
summary: Analyses, verification, documentation builds and deployment are stages of declarative pipelines. Stages are cached by content, run in isolation, and leave a provenance record — which is how every number in the documentation can be traced to a revision.
section: engineering
order: 6
level: 2
keywords: [pipeline, DAG, cache, provenance, CI, deployment]
---
<!-- gyre docs: expanded Markdown of engineering/pipelines-and-provenance.md (generated tables, verification numbers and chart data are written out). Base level of this page: L2 Practice; passages at another level are bracketed by 'begin level N' / 'end level N' comments and start with [LN Name]. Built from revision ceb1f4b0be. -->

<!-- begin level 1 (Essentials) -->
**[L1 Essentials] In brief**

* A pipeline is a YAML file of stages (`solve`, `sweep`, `order_study`, `verify`, `docs`,
  `shell`, `assert`, `report`); `gyre pipeline run FILE` executes it
  ([how to write one](../guide/pipelines.md)).
* A stage whose inputs and code have not changed is not run again: its recorded outputs are
  reused. Any edit of the working tree re-runs the stages that read it.
* Every result — a run, a verification study, a pipeline stage, a deployment, the
  documentation site — records the revision, device and time that produced it. The numbers on
  the verification pages are rendered from those records, never typed in.
* The same engine is the continuous integration: `make ci` runs lint, tests, verification,
  the documentation build and a smoke-tested deployment on port 8601.

<!-- end level 1 -->

## The engine

A pipeline is a YAML document: parameters, and a list of stages with a type (`uses`), inputs
(`with`) and dependencies (`needs`). References `${params.x}` and
`${stages.<id>.outputs.<path>}` wire stages together and create implicit dependencies.

| Stage type | Runs | Purpose |
|---|---|---|
| `solve` | subprocess | one case, optional gradients / error estimate / order selection (`adapt`) |
| `sweep` | subprocess | warm-started parameter sweep in one solver process |
| `order_study` | subprocess | the same case at several polynomial degrees |
| `verify` | subprocess | verification studies |
| `docs` | subprocess | build the documentation site |
| `shell` | local | any command (lint, tests, deployment) |
| `assert` | local | quality gates on values from earlier stages |
| `report` | local | Markdown/JSON summary |

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] Content-addressed caching**

A stage's key is the SHA-256 of its type, its *resolved* inputs, the content digest of a case
file named by `case`, the keys of the stages it depends on and the code state. If the stored
record of a stage has the same key and succeeded, the stage is skipped and its outputs reused.
Changing an input or an upstream stage therefore invalidates exactly the stages that depend on
it. `--force` ignores the cache; `cache: false` marks stages that must always run (deployment).
A stage whose execution fails replaces an earlier success record, so it is not served from the
cache afterwards.

The code state is the commit for a clean working tree and `<commit>+dirty.<digest>` otherwise,
where the digest covers `git diff HEAD` and the untracked, non-ignored files: every uncommitted
edit, and every new file that git does not ignore, invalidates every stage. Three paths of the
Gyre checkout are left out, because no stage reads them: uncommitted changes under `devlog/`
and `.github/` and to `CLAUDE.md` do not change the code state (a commit still does). Every
other file counts, `README.md` included. Where git cannot describe the tree Gyre runs from (an
installed package, the container image, no git) the code state is the revision followed by a
hash of the package sources, and `docs` and `verify` stages always run, because they read
files outside the package.

A `shell` stage reads the tree of the directory it runs in (`with.cwd`, else the engine's
working directory). If that directory is not inside the Gyre checkout, the state of the git
work tree around it enters the stage key as well; if there is no git work tree there, the
stage is never served from the cache.

The key is formed before a stage runs. If the code state has changed by the time a compute or
`shell` stage ends, nobody knows which code the stage saw: its record gets `code_changed: true`
and `revision_after`, its `stage.json` is stored with `key: null` so that it is never served
from the cache, the engine prints `the code changed while the stage ran (...); its result is
not cached`, and the stages that follow are keyed on the new state.

Runs that share `<workdir>/<name>` are serialised by an advisory file lock on that directory
(POSIX; without `fcntl` runs are not serialised): a second run waits for the first, then finds
its results in the cache where the keys match.

<!-- end level 3 -->

<!-- begin level 2 (Practice) -->
**[L2 Practice] Failure semantics**

A failed stage (non-zero exit, exception, or a gate whose check fails) marks the pipeline
failed; stages depending on it are recorded as `skipped` with the reason; independent stages
still run. The pipeline record (`pipeline.json`) lists every stage with status, key, wall time
and outputs.

<!-- end level 2 -->

## Provenance

<!-- begin level 2 (Practice) -->
**[L2 Practice] What is recorded where**

| Artefact | Record |
|---|---|
| run result (`result.json`) | spec and its digest, outputs, history, timings, version, commit (+dirty), device, timestamp |
| verification study (`<id>.json`) | checks, charts, tables, version, commit, device, timestamp, wall time |
| pipeline stage (`stage.json`) | key, status, stage type, `revision` (the code state of the stage key), finish time; for a succeeded stage the resolved inputs, outputs and wall time, for a failed stage the error |
| pipeline run (`pipeline.json`) | parameters, `revision` (the same code state), start and finish time, and every stage's status, key, wall time and outputs |
| deployment (`deployment.json`, `/api/v1/info`) | commit, start time, URLs, smoke-test result |
| documentation site (`site-manifest.json`) | build stamp shown in the footer of every page |

The documentation build reads verification files and renders their provenance next to each
chart and table. A deployment builds its site from the working tree it runs, so the served
documentation, the served API and the published evidence refer to one revision.

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] What makes a revision dirty**

`+dirty` on a stamp means that code a run can depend on differed from the commit. Three
kinds of path do not count: the effort ledger and roadmap under `devlog/`, the hosted
workflow definition and the agent instructions, which no code reads; and the verification
records under `var/verification/`, which runs write and no computation reads. Without the
last exception a verification run would be dirty by its own output: the first study to
finish rewrites its tracked record, and every later study of the same run would carry
`+dirty` on a clean checkout. The records are committed after the run, so a record names
the commit of the code that produced it, which is the parent of the commit that stores it.

<!-- end level 3 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] When a record counts as evidence for the build**

Next to every chart and table the site compares the revision of the record with the revision
it was built from. A record is marked with a tick when it was produced by the build's own
commit, by its parent, or by an earlier commit with the same code: the build walks back from
its commit through the commits that change only documentation text (`docs/content`, the
README), evidence records or bookkeeping, up to and including the last commit that changed
anything else (`gyre.identity.code_base`), and a record from any of them was produced by the
code of the build. A record from any other commit, or from a tree with uncommitted changes,
is marked as a mismatch. The theme and the builder of the documentation count as code.

<!-- end level 3 -->

## Continuous integration and deployment

```
lint ─┬─▶ verification-short
      └─▶ unit-tests ─┬─▶ service-tests ───────────────┐
                      └─▶ verification ─┬─▶ docs ──────┴─▶ deploy (+ smoke test over the tailnet)
                                        └─▶ image (build + container smoke test)
```

<!-- begin level 2 (Practice) -->
**[L2 Practice] The last complete run**

The whole pipeline was last run with `make ci` on the development host (8 CPU threads, RTX
3060 Ti) at revision `76a489be21`, from a clean working tree, started 2026-11-11 10:38:

| Stage | Result | Wall time [s] |
|---|---|---:|
| `lint` | succeeded | 0 |
| `verification-short` | succeeded | 41 |
| `unit-tests` | succeeded | 1256 |
| `service-tests` | succeeded | 118 |
| `verification` | succeeded | 1971 |
| `docs` | succeeded | 7 |
| `image` | succeeded | 67 |
| `deploy` | succeeded | 26 |

58 minutes in all. `verification-short` (six cheap studies) and `lint` form the fast tier
(`make ci-fast`); `verification` is the whole suite, whose records this site renders. On that
host the image stage needs `GYRE_BUILD_NETWORK=host`
([why](../guide/deployment.md#container)).

<!-- end level 2 -->

<!-- begin level 3 (Mechanics) -->
**[L3 Mechanics] `pipelines/ci.yaml`**

The CI pipeline is an ordinary pipeline, runnable locally with `make ci`; a hosted workflow
(`.github/workflows/ci.yml`) mirrors it without the deployment. Because of its `shell` stages,
`ci` is not startable through the service unless the operator sets
`GYRE_PIPELINES_ALLOW_SHELL=1`. The final stage calls `tools/deploy.py up`, which refuses a port
on which something already listens, builds the site, starts the service on all interfaces, and
runs `deploy/smoke.py` against the tailnet address: health, deployment info, documentation
pages, presets, structured errors, pipeline definitions, the MCP tool list, a real solver job
with an adjoint gradient (never answered from the result cache), and metrics. `deploy.py`
smoke-tests and records only the process it started: an answer from another service on the
port is an error.

<!-- end level 3 -->

<!-- begin level 2 (Practice) -->
**[L2 Practice] Deployment forms**

* **Native** (`tools/deploy.py up|down|status`): named deployments under `var/deploy/<name>/`,
  each with its own site, data directory, log and record.
* **Container** (`deploy/Dockerfile`, `deploy/compose.yaml`): multi-stage image that installs the
  package, stamps the revision, builds the site, ships the analysis pipeline definitions and
  the examples, and serves on port 8600 bound to all interfaces; GPU and AVX-only hosts are
  handled by build arguments.
* **systemd** (`tools/deploy.py systemd`, from the template `deploy/gyre.service.in`): a user
  unit for this checkout that builds the documentation before every start and restarts the
  service on failure.

<!-- end level 2 -->

## Effort ledger

`tools/worklog.py` keeps an append-only ledger of category switches (core capability,
verification, logistics) and of delegated agent time in `devlog/time.jsonl`; `make report`
integrates it. The measured split, how it is measured and how far it can be trusted are on
the [effort ledger](effort-ledger.md) page.
