Skip to content

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.

Three flags reach off the machine. ATTEST_CITATION_WEB (arXiv and CrossRef: abstracts, authors, venues) and ATTEST_CITATION_SCHOLAR (Semantic Scholar reference lists, one request every three seconds, cached forever) are both off by default and read only when the readers are built; they fill fields on references the library already holds and never add a paper on their own. ATTEST_RESEARCH_WEB is the opposite default -- on unless set to 0 -- and governs feed.research, research: topic feeds, and attest library fulltext: the one path that introduces a reference from the wire rather than filling one that is already there. 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.

attest library fulltext pulls bodies (arXiv PDF, or PMC open-access XML by PMCID) for references that have none, ten per run by default and also at the end of attest ingest; cite.lookup serves them in 2000-character windows, never whole, and never embeds them. attest library export --bib refs.bib [--author A] [--year Y] [--tag T] [--source S] writes a filtered set as BibTeX rendered from the rows -- @article for a journal, @misc with eprint/archivePrefix for a preprint, the .bib key when one supplied it, else <family><year><word> -- refusing to overwrite without --force. cite.lookup returns the same bibtex for one record.

References join the concept graph through their tags — from attest library tag, or from a .bib keywords field — and attest library related KEY (the cite.related tool) walks a paper's citation edges both ways, from Semantic Scholar reference lists or a .bib cites field (identity|title; identity|title); a cited paper not in the library is listed, never fetched. examples/molecular-ai/ shows both from a committed .bib that real software generated from real papers. See docs/superpowers/specs/2026-09-05-library-graph-and-molecular-ai-design.md.