The layer runners

elife-extract is one subcommand per runnable layer. Each reads the paths its layer declares and writes the path its layer declares, and does nothing else — no dependency resolution, no ledger. The runner does both.

Every subcommand takes --paper <slug> and finds its inputs from there. The DOI comes from claims/<paper>/index.md; a paper not yet in the corpus has no index, so prepare takes an explicit --doi the first time.

Subcommand Layer Produces
prepare prepare runs/<paper>/prepared.json
results-reader results-reader runs/<paper>/results-reader.output.json
caption-reader caption-reader runs/<paper>/caption-reader.output.json
structure-reader structure-reader runs/<paper>/structure-reader.output.json
reconcile reconcile runs/<paper>/reconciler.output.json
external-review external-review runs/<paper>/external-review.output.json
edge-inference edge-inference runs/<paper>/edge-inference.output.json
write claim-tree claims/<paper>/*.md
verify-refs reference-check runs/<paper>/reference-check.output.json
coverage coverage coverage/<paper>.json
mark marks marked/<paper>.marked.md

evaluate is the twelfth subcommand and is not a layer; it has a page of its own.

prepare — the paper the readers read

elife-extract prepare --paper gadeke-2026-guilt-insula
elife-extract prepare --paper <slug> --doi 10.7554/eLife.<id>    # a paper new to the corpus
elife-extract prepare --paper <slug> --pdf-path ./paper.pdf      # not on the eLife CDN

Fetches the paper — JATS XML for eLife DOIs, which gives clean section boundaries and caption text where a PDF gives a column-order guess — and slices it into the abstract, results, captions and methods that the three readers each get. The fetch is cached under ~/.cache/elife-extract/, so re-running any layer on the same paper is free and offline.

runs/<paper>/prepared.json is the exact text the readers were given — check its slice sizes if a paper extracts badly, since a short results usually means section detection failed and everything downstream read the wrong text.

The three readers

elife-extract results-reader   --paper <slug>
elife-extract caption-reader   --paper <slug>
elife-extract structure-reader --paper <slug>

Each reads one slice under one prompt and proposes candidate claims, each with a panel, a role and the verbatim sentence it rests on.

  • results-reader reads the abstract and results prose — framing, hypotheses, predictions, synthesis and interpretation as the prose presents them.
  • caption-reader reads the figure and table captions panel by panel — the quantitative claims, anchored to the panel that shows them.
  • structure-reader reads the methods, supplements and section structure — methodological capabilities, scope conditions, and what the paper frames as its own conclusions.

None sees another's output, so where two readers surface the same claim that agreement is evidence — which is what reconcile records as high confidence. A single agent reading the whole paper loses that, and fails in characteristic ways: inventing numbers where the prose summarises, and attaching claims to whichever panel it read last.

Each output names the model that produced it, which is what the ledger records as the author.

reconcile — which candidates survive

elife-extract reconcile --paper <slug>
elife-extract reconcile --paper <slug> --reconcile-strategy union

Reads the three readers' outputs from disk and aligns them into one draft claim table, tagged high, contested or single-source, carrying which readers surfaced each claim and the evidence each quoted.

Single-source is not a synonym for weak. A claim only one reading strategy surfaced may be real but buried, or it may be an artefact of that strategy — the tag records which question is open rather than answering it.

--reconcile-strategy selects how the three lists combine: confidence-tagged (default), union, intersection-only, majority-vote.

external-review — what the readers systematically miss

elife-extract external-review --paper <slug>

One Opus pass over the reconciled draft, recovering the structure prose-level extraction under-covers: the prediction and hypothesis roles, and multi-panel claims collapsed to a single panel. Roughly $2 and three minutes per paper, and it lifts role agreement from ~65% to ~96% on the Headley round-trip.

extract/prompts/external-reviewer.md is a declared input, so editing it marks every claim tree built on it stale.

Running it is optional. claim-tree builds on this layer's output where it exists and on the reconciled draft where it does not, so a paper that has not been through it is an ordinary case rather than a failure — pipeline.py state shows which papers have.

This was --review-mode external.

edge-inference — which claims depend on which

elife-extract edge-inference --paper <slug>
elife-extract edge-inference --paper <slug> --dump-prompt /tmp/edges.txt
elife-extract edge-inference --paper <slug> --edges-json /tmp/answer.json

Infers typed relations between the draft's claims — the deductive spine, entails from hypothesis to prediction and tests back from the empirical result.

--dump-prompt writes the exact request and exits, so that whatever answers it — an analyst, a reasoning agent, a different provider — answers the same question the layer would have asked rather than a paraphrase of it written from memory. --answer records that reply, and it goes through exactly the same validation an inferred one gets: edges naming an unknown slug, or pointing at themselves, are dropped either way. An invented target is worse than a missing edge, and that has to hold no matter who produced the answer.

Reach for --dump-prompt when the backend is unavailable or you want a specific model to answer. Edges are the part of a tree most easily lost to a failed call — every other stage can succeed and leave you with claims and no structure — and this is the way to supply them without the answer being produced against a different question.

Answering a layer from outside the backend

--dump-prompt and --answer are on every layer a model answers — the three readers, reconcile, external-review and edge-inference — not just this one. Any of them can stop when a provider does, and the escape is the same:

elife-extract reconcile --paper <slug> --dump-prompt /tmp/q.txt
# answer it anywhere — another model, another provider, a person
elife-extract reconcile --paper <slug> --answer /tmp/a.json

A supplied answer is validated exactly as a backend reply is, and its output records model: supplied:<path> rather than naming a model that never ran — so the ledger's by field distinguishes a backend call from something else answering the same prompt.

write — the claim tree

elife-extract write --paper <slug>
elife-extract write --paper <slug> --format oxa    # one OXA JSON Document instead

Assigns slugs and UUIDs, attaches the edges edge-inference produced, and writes one file per claim into <corpus-dir>/<paper>/ plus the paper's index.md.

It reads the edges from edge-inference's output rather than calling a model again, so it costs nothing and needs no credentials — but it does need that layer to have run. Without it the claims are written with no edges, and the command says so.

Refuses to overwrite a non-empty paper directory. Move or delete it to re-run.

verify-refs — the reference check

elife-extract verify-refs --paper <slug>
elife-extract verify-refs --paper <slug> --dry-run   # resolve, write nothing
elife-extract verify-refs                            # sweep the corpus, write no report

Confirms the paper's own DOI from its index.md, then for each role: literature-context claim takes one of two paths:

  • confirm — the claim already has a top-level doi:; CrossRef confirms it resolves to a real paper. This is the anti-hallucination check, and it is the one that matters.
  • found — no DOI yet; hints are extracted from the claim text and slug, CrossRef is queried, and the highest-scoring match above a confidence threshold is written back.

Put the cited DOI at top-level doi:. That is where the schema expects it, and where this layer looks first; assertions[0].doi is read only as a fallback. All 5 of the 8 literature-context claims in the published corpus use the top-level field.

The distinction matters because the two paths do different things. With a DOI present the layer confirms it against CrossRef — the check that catches a hallucinated citation. Without one it searches on author and year, and a search can return a confident match to the wrong paper.

Omitting --paper sweeps the whole corpus but writes no report, since a report belongs to one paper.

coverage and mark

elife-extract coverage --paper <slug> --json ../coverage/<slug>.json
elife-extract coverage --paper <slug> --mapping ../mappings/<slug>.json
elife-extract coverage --paper <slug> --fail-on-orphans
elife-extract mark --paper <slug> --mapping ../mappings/<slug>.json -o ../marked/<slug>.marked.md

Neither calls a model, so both are free and fast enough to run over the whole corpus.

coverage takes its denominator from the paper rather than from the claim set. It builds the paper's own inventories — every panel, every table, every reported statistic — then cuts the whole text into one-sentence spans and asks of each whether any claim accounts for it. That is what lets it see what a comparison between two claim sets cannot: you can score 100% agreement on a third of a paper.

--mapping folds in the adjudicated verdicts from mappings/<paper>.json, so the report shows real gaps rather than everything a string comparison could not match. Without it, Gädeke reports 36 unaccounted spans; with it, 22 were the matcher being wrong and 14 were real.

mark writes those verdicts into the document itself, as tika marks carrying each claim's UUID. A span that states no result says so; a span carrying a result that no claim states is marked as a gap, so it is visible by looking rather than only countable in a report. The marks are read straight back after writing — a document that cannot reproduce the assignments it was written from is not a record of anything.

Flags every runner takes

Flag Purpose
--paper The paper slug. Every runner needs it
--root The corpus repository, which pipeline/layers.yaml resolves paths against. Defaults to where the package ships
--corpus-dir Where claim files are read and written (default: <root>/claims)
--prompts-dir, --prompt-variant Override the prompt directory, or select a named variant
--backend, --api-key vertex (default), anthropic, or anything litellm routes
--model-{results,caption,structure,reconcile} Per-layer model selection
-v, --verbose Per-chunk DEBUG logging