The CLI engine
How the deterministic stages behave — what they record, what they refuse to do, and why re-runs are reproducible.
The CLI is the half of Research Scan that contains no judgement. It retrieves, dedups, expands, counts, orders, verifies and renders. Its job is to be boring and to leave a record.
A handful of rules govern how it behaves. They are worth stating explicitly, because each one is a decision that could have gone the other way.
Nothing is capped silently
Every drop is counted in manifest.json: retracted papers, must_not exclusions,
type filters, the pool cap, and every source that failed after retries.
This is the difference between a pool of 250 because that is what existed and a
pool of 250 because the cap truncated 400. Both look identical in
candidates.json. Only the manifest distinguishes them.
The same rule covers routing: a source that is routed but not built records
unavailable: true rather than being dropped from the map. Routed-but-missing
is recorded, never dropped quietly.
A bad status is data
When Semantic Scholar answers 429 or 503 because its queue is deep, the HTTP client retries and then returns the last response rather than raising.
The consequence is that a degraded run is a recorded degraded run, not a crash
and not a silent success. Graph expansion falls back to OpenAlex’s bare-id
reference list, which works but cannot be ranked — ordering it would cost a
metadata fetch per reference — so expansion recall drops, and
expansion.log.jsonl records that per call.
This matters more than it sounds. The project’s own measurement rules were tightened after a run in which arXiv failed every query with HTTP 429 and the result looked like a good number. The rule that came out of it:
A measured run is not quotable until every routed source reports zero failures.
Only one module talks to the network
All HTTP goes through a single client. Unit tests mock at that layer, and no test outside the live-marked set opens a socket.
The client holds its own rate limits — one request every three seconds to arXiv,
for instance — retries with backoff, and caches responses for seven days in
~/.cache/research-scan/http.sqlite. --no-cache bypasses it, which is what
doctor does so that a readiness check reflects the live services rather than
yesterday’s answers.
Clocks are injectable, so re-runs are byte-identical
The HTTP client takes its sleep, monotonic and now functions as parameters.
Cache TTL, backoff and rate-limit behaviour are therefore testable without
waiting, and — more importantly for anyone using the output — a re-run produces
byte-identical files rather than files that differ only by timestamp.
That is what makes two runs comparable. When the project measured whether a change helped, it could diff run directories and read the difference as a real effect.
Credentials are read in exactly one place
One module reads the environment for keys. Nothing else does. That single chokepoint is what lets the tool redact every secret before anything is logged or cached.
It is why doctor --verbose can show you which credentials are set, where each
came from, and the last four characters of each — without any risk that a full key
reaches a log file or the HTTP cache.
The CLI never edits what the agent wrote
If a query plan is poor, that is visible in queries.json. The CLI does not
rewrite it, improve it, or drop the query it thinks is redundant.
The same applies to exclusions. must_not terms are enforced in code at word
boundaries — never smuggled into query text as NOT-terms, where they would change
what the source returns in ways nobody recorded.
Bad input exits 2, with all of it
Schema validation is strict everywhere and reports every offending path, not the first.
- Unknown keys are errors. A typo does not silently default; it surfaces.
- Constraints are not loosened to make a test pass. If a packet requires at least one limitation, a packet without one does not validate.
Exit codes are stable: 0 success, 2 a validation or usage failure, 3 a
mandatory readiness check failed.
Ordering rules that carry meaning
Two small conventions worth knowing when reading output:
Rank is reading order. The emitted list is ordered to be read top to bottom, which is why foundational classics render after current work with ranks running straight through — a 2004 paper reads as context, not as the answer.
Coverage counts papers, not origins. A paper found by three queries and two citation hops still covers its criterion once. Counting origins would make a well-connected paper look like broad coverage.
Related
- Verification — the stage that checks the record.
- Artifacts and schemas — the file contracts.
- CLI reference — every command and flag.
Last updated Aug 21, 2026