Verifiable claims and citations
Can it check my draft? Yes — a claim is an HTML comment beside the prose it describes, checked against the run ledger for one of five verdicts, and a citation key is linted against your configured bibliography the same way.
Verifiable claims
A README says "MAE 0.353 eV vs experiment". That number was transcribed by hand and nothing checks it: re-run the benchmark and the document asserts 0.353 forever. A claim is an HTML comment beside the prose it describes, so the document renders exactly as before:
The cut leaves WER essentially unchanged (**0.053 vs 0.043** baseline).
<!-- claim: ablation/stack_4 metric=wer value=0.053 tol=0.001 -->
uv run attest claims ~/projects # verify every claim
uv run attest claims ~/projects --coverage # numbers no claim covers
Five verdicts, and the distinctions are the design. supported: a run agrees.
contradicted: a run disagrees — the document or the run is wrong.
unsupported: no run matches, so the claim may be true but nothing backs it.
ambiguous: a wildcard matched several runs, so which is meant is undecidable.
stale: the value matches but the artifact changed after as_of.
unsupported and contradicted never collapse together — one needs a run, the
other needs a correction. ambiguous exists because silently taking the first
of several matches is how a checker reports a confident wrong answer.
attest claims exits non-zero on a contradiction, so it can gate a commit.
A claim can also carry cite=<key>, and that key is linted too — uncited
when no configured .bib or Zotero source has it — from the CLI (attest
claims) as well as the cite.check / runs.claims_check MCP tools. It is a
lint ("no source has this key"), never "the paper does not support this
claim". See examples/citations/ for a worked run of both linters together.
--coverage is the inverse, and the more useful half for adoption: a document
with zero contradicted claims can still assert a dozen unverifiable numbers.
Only decimals count as measurements — on a real index, 212 numbers reduce to 30
decimals and the decimals are the results.
The library
One deduplicated store of references, filled by attest library sync (or the
cite.sync tool) from every configured source: the .bib files named in
ATTEST_BIB_PATHS (else any *.bib in the working directory), a Zotero
library at ATTEST_ZOTERO_PATH (else Zotero's default), and the feed's own
items that carry a DOI or arXiv id. The same paper from three sources is one
row with three source entries, each recording what that source offered and
when. Identity is a pure rule: DOI, else versionless arXiv id, else the
normalised title and year. Merging fills empty fields and keeps the first value
of a disagreement, recording the conflict on the source that offered it, so
cite.lookup can show a disagreement rather than lose it.
Two flags reach off the machine, both off by default and read only when the
readers are built: ATTEST_CITATION_WEB (arXiv and CrossRef: abstracts,
authors, venues) and ATTEST_CITATION_SCHOLAR (Semantic Scholar reference lists,
one request per second, cached forever). They fill fields on references the
library already holds and never add a paper on their own; cite.sources says
which are live.
uv run attest library sync # read every source; embed what it can
uv run attest library search "equivariant force fields"
uv run attest library search "" --author batzner --year 2022
uv run attest library tag --limit 50 # a model call per reference, ~2.3s each
uv run attest library status
Search is semantic when the library is embedded (the model server was
reachable during sync or library embed) and substring otherwise, and the
output says which — the cite.search tool carries the same semantic flag
and caveat. cite.check and attest claims resolve cite=<key> through
the store first, so a key synced from Zotero resolves even when no .bib sits
in the working directory. See docs/superpowers/specs/2026-09-05-library-store-design.md.