The runner
scripts/pipeline.py is the pipeline. Five subcommands and one job: run a layer after
running whatever it still needs, and record that it happened.
It needs only PyYAML, holds no credentials and calls no model — so reading the graph, the state matrix and the whole run history costs nothing and works offline.
graph — the declaration, as a shape
python3 scripts/pipeline.py graph
Prints every layer in dependency order with its kind, scope, what it needs and what it
produces. It reads pipeline/layers.yaml and nothing else, so it is the fastest way to check
that an edit to the declaration is well formed: an unknown dependency or a cycle is a hard
error here rather than a confusing state later.
state — the paper × layer matrix
python3 scripts/pipeline.py state
python3 scripts/pipeline.py state --json # what the site reads
python3 scripts/pipeline.py state --fail-on-stale # as a CI gate
A cell is one layer asked of one paper, and it is in exactly one state:
| State | Meaning |
|---|---|
current |
It ran, and every input still hashes to what that run recorded |
stale |
An input changed since the run, or an output is gone |
blocked |
Something it needs is stale or blocked, so its own result cannot be trusted |
absent |
It has never run, and produced nothing |
unrecorded |
Its outputs exist but no run records them — it ran before the ledger did |
n/a |
Declared impossible for this paper, with a reason |
open |
An undecided question; everything downstream of it is provisional |
stale and unrecorded are easy to confuse and mean different things. Stale: an input
changed since the run, so the output is probably wrong — rebuild it. Unrecorded: the
output exists but no run accounts for it, which is the normal state for anything produced
before the ledger existed. An unrecorded artifact may be perfectly good; backfill gives it
a record.
A jagged edge in the matrix is the normal condition. A blank means the layer has not been run for that paper — not that it was run and found nothing.
run — run a layer, and what it needs
python3 scripts/pipeline.py run <paper> <layer>
python3 scripts/pipeline.py run <paper> <layer> --dry-run # print the commands, run nothing
python3 scripts/pipeline.py run <paper> <layer> --no-deps # only the layer named
python3 scripts/pipeline.py run <paper> <layer> --note "…" # the changelog line for the ledger
The command it runs comes from the layer's command: field, so there is one definition of how
a layer is produced and the copy-and-run text on the site cannot drift from what actually runs.
It prunes at satisfied ancestors. Asking for coverage on a paper whose claim tree is
already current runs coverage and nothing else — it does not walk through the tree into the
induction layers beneath it. Rebuilding a subtree nothing is waiting for would mean three
reader calls, a reconciliation and an Opus review to regenerate inputs to a file that is
already correct. Satisfied means current or unrecorded; stale and blocked are rebuilt.
Start --dry-run. It prints the chain in dependency order without running any of it, and on a
paper that has been through the pipeline it usually prints one line.
After each layer succeeds, run appends a record naming every input by path and content hash:
{
"layer": "prepare",
"v": 1,
"ran": "2026-09-11T11:00:39Z",
"kind": "step",
"note": "the paper the readers read, recorded",
"by": "scripts/pipeline.py run",
"cmd": "cd extract && python3 -m elife_extract.cli prepare --paper … --doi …",
"in": [{"path": "extract/elife_extract/prepare.py", "sha": "8ee35979be8f"}],
"out": [{"path": "runs/…/prepared.json", "sha": "5512285faceb"}]
}
by names who answered. For a layer a model answers it is the model, read out of the layer's
own output at the key its declaration names in by_from; otherwise it is the runner.
approve — record that a person read a version
python3 scripts/pipeline.py approve <paper> <layer> --by "your name" \
--note "roles checked against figures 2-4"
python3 scripts/pipeline.py approve <paper> <layer> --by "…" --v 2 # a specific version
Approval is not a layer and not a gate. It is an operation on a version: a person reads what a
layer produced and approves that output, and the record names the version it was granted to.
When the layer runs again the approval does not follow — it was given to text that no longer
exists, and state reports it as no longer applying rather than carrying it forward.
It defaults to the version currently on the ledger, because approving a version that is not the
one on disk is almost always a mistake. --v names one explicitly, since reading v2 and
recording it after v3 has run is a coherent thing to have done.
Approvals live in runs/<paper>/approvals.jsonl and apply to any layer. They are empty for
every cell in this corpus — nothing here has been checked by a person, and the site reports
that from this file rather than from a disclaimer.
Looking for --review-mode interactive? It is gone, and this is its replacement. The
difference is when it happens: that flag opened the draft in an editor before the claim
files were written, so what you approved was not the version that reached the corpus. You now
approve a version that exists and can be read.
To correct a claim, edit the file. That makes the layer stale against its own ledger
entry, which is accurate — the files are no longer what the layer produced. Re-run the layer
to rebuild from source, or leave the edit and let the state say so.
backfill — ledgers from what already exists
python3 scripts/pipeline.py backfill
python3 scripts/pipeline.py backfill --force # rewrite ledgers that already exist
Writes run records for artifacts that predate the ledger, marked backfilled: true so they are
never mistaken for observed runs. Nine of these papers were extracted before any of this
existed; backfill is what makes their cells current rather than permanently unrecorded,
without claiming to know more about them than the files support.
Using it as a library
load() returns the indexed declaration; state() returns the matrix. The site's
src/lib/pipeline.ts reads the JSON that state --json emits, which is why the pipeline pages
and this tool can never disagree about what has been run.