citations
Bibliographic records, read from disk by default.
claims.py can verify that a number in prose matches a run in the ledger. It
could not verify that a citation in prose points at a real paper, and had no
way to express "supported by someone else's published result" as distinct from
"supported by my run". A reference had no representation here at all.
The offline guarantee and its exception. CLAUDE.md states "Local models
via Ollama; nothing leaves the machine." The web reader breaks that, so:
- It is absent unless
ATTEST_CITATION_WEBis set, checked when the resolver is BUILT rather than when it is called -- a disabled reader cannot be coaxed into one request by an unusual code path. - Every record carries
sourceandfetched_at, so any answer can be asked where it came from. cite.sourcesreports which readers can reach the network, from the same surface that would have done the reaching.
A guarantee with a documented exception is honest. One that quietly stopped holding is not.
No Zotero library existed on the machine where this was written. The reader is built from Zotero's documented schema and tested against a fixture built to the same document, so it is plausible rather than verified. If you have a real library, point this at it -- the shape-tolerance tests say what should happen when the layout differs, but only a real one proves what does.
BibtexReader
.bib files on disk.
A hand-rolled reader rather than a dependency: the grammar used here is
@type{key, field = {value},}, which is what every tool emits, and this
codebase has a standing preference against pulling a parser in for a
machine-generated format (see ledger_adapters/generic._config_shape).
A truncated entry yields nothing rather than raising: a .bib killed
mid-write is the commonest way one ends, and losing the whole file for one
bad entry is silent data loss.
all()
Every entry each .bib file yields, skipping ones with no title.
A missing file is silently skipped rather than an error, matching
ZoteroReader: a .bib named in config but not (yet) present is an
absent source, not a broken one.
lookup(key)
The entry whose citation key or DOI matches key, case-insensitive.
Scans all() rather than an index: .bib files are small and this
reader has no persistent state to keep one in sync with a file that
may have changed on disk between calls.
Reference
dataclass
One bibliographic record, and where it came from.
source and fetched_at are the provenance pair. A record from disk has
fetched_at=None; one from the network carries a date. A cache keeps the
original date rather than refreshing it -- the cache must not launder a
network record into something that looks local.
matches(needle)
Whether a free-text query plausibly names this record.
to_row()
This reference's cite.* wire projection.
Truncates authors to 6 and adds n_authors (the true count) so a
long author list does not blow out a response -- the same 3-of-n
budget pattern RankedItem.to_row applies to tags.
arxiv_id is deliberately OMITTED: it is redundant with doi/url
for most records, and dropping it here is a stated decision rather
than a silent gap a future editor might "fix" back in inconsistently.
Resolver
The configured readers, asked in order, recording which one answered.
from_env(*, zotero_path=None, bib_paths=None, cache_dir=None, store=None)
classmethod
Build from the environment.
ATTEST_CITATION_WEB is read HERE (via web_enabled) and by the
library sync's reader list, both at construction. Reading it at call
time would mean a resolver built while disabled could still make a
request if the variable changed under it.
lookup(key)
The library store's answer, else the first reader's, tried in order.
Order is the constructor's reader list, which from_env fixes as
zotero, then bibtex, then web -- so a network lookup is only ever
tried after every local, offline reader has already said no.
search(needle)
Free-text search across the readers that can be enumerated.
Network readers are skipped rather than queried: all() raises on
them, and a search that silently fanned out to CrossRef would be the
offline guarantee failing quietly.
sources()
Which readers are configured, and which of them can reach the
network -- so cite.sources reports the offline exception from the
same surface that would have done the reaching, rather than a
separate claim about it.
WebReader
Metadata by DOI or arXiv id, from CrossRef and arXiv.
This is the only thing in the project that leaves the machine, and it
exists only when ATTEST_CITATION_WEB was set at construction. Records are
cached content-addressed and never expire: a published paper's metadata
does not change, and an expiring cache turns one network call into a
recurring one, which is the opposite of the guarantee.
all()
Not supported: a network source has no fixed set to enumerate.
Resolver.search relies on this raising -- it skips every reader
with network=True before calling all(), so a search that fanned
out to CrossRef here would be the offline guarantee failing quietly.
This exists so that bypassing that guard is loud rather than a silent
empty result.
lookup(key)
DOI or arXiv id metadata from CrossRef, cached content-addressed.
A cache hit returns the ORIGINAL fetched_at, never today's date --
the cache must not launder a network record into one that looks
local. Any failure (unreachable network, a 404, a changed payload
shape) returns None rather than raising: an unreachable network is
an absent source, exactly like a missing Zotero library, and should
not break a lookup whose other readers can still answer.
ZoteroReader
A local Zotero library, opened read-only.
Zotero holds an exclusive lock while running, so this opens with
mode=ro&immutable=1: it reads a live library without being able to write
to it. If that fails -- no library, a corrupt file, a schema this does not
recognise -- the reader returns no records rather than raising. A missing
Zotero is an absent source, not an error, and a resolver whose other
readers work must keep working.
all()
Every item in the library with a title, read-only and tolerant.
No library, a corrupt file, or a schema this does not recognise all yield nothing rather than raising -- see the class docstring. This reader ships tested only against a fixture built to Zotero's documented schema; there was no real library on the machine where it was written, so the fixture is plausible, not verified. If you have a real one, point this at it.
lookup(key)
The item whose Zotero key or DOI matches key, case-insensitive.
Scans all() -- the library is opened fresh each call (see
_connect), so there is no cached index to keep in sync with a
library that changed since the last lookup.
raw_items()
(zotero key, {field: value}, [authors]) for every titled, undeleted item.
The library sync reads this rather than all() because it wants
fields Reference does not carry (abstractNote, publicationTitle,
extra). Same tolerance as all(): nothing on any sqlite error.
bib_paths_from_env()
ATTEST_BIB_PATHS (os.pathsep-separated), else every *.bib in the cwd.
s2_enabled()
ATTEST_CITATION_SCHOLAR: Semantic Scholar reference lists.
A second flag because reference lists are a larger, rate-limited surface than a metadata lookup, and a reader who accepts one need not accept both.
web_enabled()
ATTEST_CITATION_WEB: CrossRef and the arXiv API.
Read by the caller that BUILDS readers, never by a reader at call time -- a disabled reader cannot be coaxed into one request by an unusual path.
zotero_path_from_env()
ATTEST_ZOTERO_PATH, else Zotero's default location.