fix(papers): the arXiv topic was never actually searched for
deploy / test (push) Successful in 4m13s
deploy / build (push) Successful in 5m19s

The operator's topic went RAW into `search_query=`, unfielded. arXiv matched
essentially nothing, and `sortBy=submittedDate` then returned the newest
submissions across the entire archive — so the library shelved whatever had been
posted in the last few minutes and called it research.

A real run for "agentic topology", "retrieval augmented generation" and "vector
index pruning" shelved, among 13 papers: Galois extensions of geometric fixed
point spectra, a bulk path integral for a quantum black hole microstate, blazar
boosted dark matter in IceCube, and colloidal packing. Nothing was broken —
every layer reported success, the notes were written, the seen-set was updated,
the branch auto-merged. The papers were simply unrelated to anything asked for.

Measured against the live API:

    speculative decoding         -> pixel-space diffusion, simplicial actions
    all:"speculative decoding"   -> S2-MoE self-speculative decoding, DARTree

So a bare topic is quoted into `all:` and bound to `cat:cs.*`. The quotes make
it a phrase (unquoted, "vector index pruning" matches any paper containing all
three words anywhere, which is most of cs), and the category bound is needed
because the archive's physics and maths volume dominates any recency sort.

A topic that already starts with a field prefix passes through untouched, so an
operator who knows arXiv syntax keeps control.

The passthrough originally also accepted anything containing " AND "/" OR ", and
the injection test caught it on the first run: `agent" OR cat:hep-th` escaped the
phrase and rewrote the category bound. Only a LEADING field prefix counts now.

348 tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-17 20:17:02 -07:00
co-authored by Claude Opus 5
parent d524107b37
commit 2cd0872e50
+72 -1
View File
@@ -143,13 +143,51 @@ fn unescape(s: &str) -> String {
.replace("&#39;", "'")
}
/// Turn an operator topic into an arXiv `search_query`.
///
/// A bare topic is NOT a search. Passed through unfielded, arXiv matched
/// essentially nothing and `sortBy=submittedDate` then returned the newest
/// submissions across the whole archive — so a run for "speculative decoding"
/// shelved Galois extensions, a quantum black hole microstate, and blazar dark
/// matter in IceCube. Measured against the live API:
///
/// ```text
/// speculative decoding -> pixel-space diffusion, simplicial actions
/// all:"speculative decoding" -> S2-MoE self-speculative decoding, DARTree
/// ```
///
/// So the phrase is quoted into `all:` (title, abstract, authors, comments) and
/// constrained to `cat:cs.*` — this library exists to serve software projects,
/// and without the category bound the archive's physics and maths volume
/// dominates every recency-sorted result.
///
/// A topic that already looks fielded (`cat:`, `ti:`, `abs:`, `all:`) is passed
/// through untouched, so an operator who knows arXiv's syntax keeps full control.
pub fn arxiv_query(topic: &str) -> String {
let t = topic.trim();
const FIELDED: &[&str] = &["all:", "ti:", "abs:", "au:", "cat:", "co:", "jr:"];
// Only a topic that STARTS with a field prefix is treated as hand-written
// arXiv syntax. Also accepting anything containing " AND "/" OR " was the
// first version, and a test caught it immediately: `agent" OR cat:hep-th`
// passed straight through, so a topic string could escape the phrase and
// rewrite the category bound. A natural-language topic may legitimately
// contain the word "and" too.
if FIELDED.iter().any(|p| t.starts_with(p)) {
return t.to_string();
}
// Quotes make it a phrase; without them "vector index pruning" matches any
// paper containing all three words anywhere, which is most of cs.
let escaped = t.replace('"', "");
format!("all:\"{escaped}\" AND cat:cs.*")
}
/// Search arXiv. `max_results` is capped to keep one run bounded.
pub async fn search(query: &str, max_results: usize) -> Result<Vec<Paper>, String> {
let max = max_results.clamp(1, 50);
let url = format!(
"https://export.arxiv.org/api/query?search_query={}&start=0&max_results={max}\
&sortBy=submittedDate&sortOrder=descending",
urlencoding(query)
urlencoding(&arxiv_query(query))
);
let body = reqwest::Client::new()
.get(&url)
@@ -251,6 +289,39 @@ fn urlencoding(s: &str) -> String {
#[cfg(test)]
mod tests {
/// A bare topic must become a PHRASE search bound to cs — unfielded, arXiv
/// matched nothing and recency-sort returned the whole archive, so a run
/// for "speculative decoding" shelved blazar dark matter in IceCube.
#[test]
fn a_bare_topic_becomes_a_fielded_phrase_query() {
let q = arxiv_query("speculative decoding");
assert_eq!(q, "all:\"speculative decoding\" AND cat:cs.*");
assert!(q.contains('"'), "unquoted, the words match separately");
assert!(q.contains("cat:cs.*"), "without a category bound physics wins");
}
/// An operator who writes arXiv syntax keeps control — wrapping their query
/// in another `all:"..."` would search for the literal text of their query.
#[test]
fn an_already_fielded_topic_is_left_alone() {
for q in [
"cat:cs.IR AND all:\"dense retrieval\"",
"ti:\"world model\"",
"abs:hnsw OR abs:\"vector index\"",
] {
assert_eq!(arxiv_query(q), q, "{q} must pass through untouched");
}
}
/// Quotes in a topic would terminate the phrase early and corrupt the query.
#[test]
fn quotes_in_a_topic_cannot_break_out_of_the_phrase() {
let q = arxiv_query("agent\" OR cat:hep-th");
assert_eq!(q.matches('"').count(), 2, "exactly one balanced phrase: {q}");
assert!(q.ends_with("cat:cs.*"), "{q}");
}
use super::*;
/// A revision must not read as a new paper.