Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Paperclip-style multi-source literature search across arXiv, Crossref, local JSON corpora, and (opt-in) the Paperclip API. Provides Paper/SearchQuery/SearchResult data models, a LiteratureClient aggregator with per-backend failure isolation, DOI/arXiv-aware deduplication via merge_papers, deterministic JSON caching via SearchCache, an HttpClient protocol for test injection, and a CLI (search/to-bibtex). Use when finding papers by topic, building reading lists, populating references.bib from a qu
.claude/skills/docxology-infrastructure-search-literature/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 13% | 0% |
| case-01 | ✗→✓ | ▲ Improved | -25% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 25% | 0% |
| case-03 | ✗→✓ | ▲ Improved | -31% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 24% | 0% |
Multi-source literature search modelled after Paperclip's agent-native abstractions.
pythonfrom infrastructure.search.literature import ( LiteratureClient, SearchQuery, ArxivBackend, CrossrefBackend ) client = LiteratureClient([ArxivBackend(), CrossrefBackend(mailto="you@example.org")]) result = client.search(SearchQuery(text="protein language model fitness", max_results=20)) print(f"{len(result)} unique papers from {len(result.per_source_counts)} sources") print(f"Errors: {result.errors}") # {} when all backends succeeded for paper in result.papers[:5]: print(f" [{paper.score:.2f}] {paper.title} ({paper.year}) — {paper.doi or paper.url}")
pythonfrom infrastructure.search.literature import ( LocalBackend, ArxivBackend, CrossrefBackend, PaperclipBackend ) # Offline / reproducible — searches a JSON corpus on disk. local = LocalBackend("data/curated_corpus.json") # Public APIs, no auth. arxiv = ArxivBackend() crossref = CrossrefBackend(mailto="you@example.org") # Paperclip — requires API key. import os paperclip = PaperclipBackend(api_key=os.environ["PAPERCLIP_API_KEY"])
pythonSearchQuery( text="adam optimizer", max_results=50, year_min=2014, year_max=2025, sources=["arxiv"], # subset of configured backends )
pythonfrom infrastructure.search.literature import SearchCache cache = SearchCache("output/search_cache", ttl_seconds=86400) client = LiteratureClient([ArxivBackend()], cache=cache) # First call hits arXiv; second is a deterministic file read. client.search(SearchQuery(text="x")) client.search(SearchQuery(text="x")) # Force refresh: client.search(SearchQuery(text="x"), use_cache=False)
pythonfrom infrastructure.search.literature import merge_papers unique = merge_papers([*result_a.papers, *result_b.papers])
Priority: DOI → arXiv id → normalised (title, year). Higher-scored copy wins; missing fields on the winner are filled from the loser ("union of evidence").
pythonresult = client.search(query) if result.errors: for source, message in result.errors.items(): log.warning("Backend %s failed: %s", source, message) # A network outage in one backend never breaks the search; surviving # backends still contribute results.
bash# JSON to stdout uv run python -m infrastructure.search.literature.cli search \ "scaling laws" --source arxiv,crossref --max-results 10 # Direct BibTeX to a file uv run python -m infrastructure.search.literature.cli to-bibtex \ "GRPO hyperparameters" --source arxiv \ --output output/grpo_refs.bib # Cached, offline-only over a local corpus uv run python -m infrastructure.search.literature.cli search \ "convex" --source local --corpus data/corpus.json \ --cache-dir output/cache
pythonfrom infrastructure.search.literature import CrossrefBackend, HttpResponse class MyClient: def get(self, url, *, params=None, headers=None, timeout=10.0): # ... return HttpResponse(status_code=200, text=..., url=url) ... backend = CrossrefBackend(http_client=MyClient())
The project's no-mocks policy is satisfied with pytest-httpserver:
pythondef test_crossref_parses_real_payload(httpserver): httpserver.expect_request("/works").respond_with_json(payload) backend = CrossrefBackend(base_url=httpserver.url_for("/works")) results = backend.search(SearchQuery(text="x")) assert len(results) > 0
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→pass | 8,573 | 2,404 | -72% | 1 | 1 | 0% | 1,262 | 1,420 | +13% | 0 | 0 | — |
case-01 | fail→pass | 15,701 | 7,532 | -52% | 1 | 1 | 0% | 3,656 | 2,759 | -25% | 0 | 0 | — |
case-02 | fail→pass | 7,927 | 3,122 | -61% | 1 | 1 | 0% | 1,204 | 1,505 | +25% | 0 | 0 | — |
case-03 | fail→pass | 11,747 | 2,992 | -75% | 1 | 1 | 0% | 2,254 | 1,556 | -31% | 0 | 0 | — |
case-04 | fail→pass | 6,028 | 3,158 | -48% | 1 | 1 | 0% | 1,188 | 1,475 | +24% | 0 | 0 | — |
case-05 | pass→pass | 11,550 | 3,376 | -71% | 1 | 1 | 0% | 1,805 | 1,685 | -7% | 0 | 0 | — |
case-06 | fail→pass | 17,047 | 1,545 | -91% | 1 | 1 | 0% | 3,300 | 1,260 | -62% | 0 | 0 | — |
case-07 | fail→pass | 10,305 | 1,903 | -82% | 1 | 1 | 0% | 1,870 | 1,397 | -25% | 0 | 0 | — |
case-08 | fail→pass | 11,022 | 4,510 | -59% | 1 | 1 | 0% | 1,604 | 1,451 | -10% | 0 | 0 | — |
case-09 | pass→pass | 11,284 | 10,969 | -3% | 1 | 1 | 0% | 2,087 | 1,330 | -36% | 0 | 0 | — |
case-10 | pass→pass | 6,561 | 9,293 | +42% | 1 | 1 | 0% | 1,187 | 1,218 | +3% | 0 | 0 | — |
case-11 | pass→pass | 14,554 | 1,517 | -90% | 1 | 1 | 0% | 2,156 | 1,233 | -43% | 0 | 0 | — |
case-12 | pass→pass | 11,846 | 2,069 | -83% | 1 | 1 | 0% | 2,167 | 1,392 | -36% | 0 | 0 | — |
case-13 | fail→pass | 5,889 | 2,685 | -54% | 1 | 1 | 0% | 1,104 | 1,351 | +22% | 0 | 0 | — |
case-15 | pass→pass | 12,736 | 3,719 | -71% | 1 | 1 | 0% | 2,219 | 1,812 | -18% | 0 | 0 | — |
case-16 | pass→pass | 9,039 | 6,076 | -33% | 1 | 1 | 0% | 1,935 | 2,341 | +21% | 0 | 0 | — |
case-17 | pass→pass | 13,133 | 1,983 | -85% | 1 | 1 | 0% | 2,658 | 1,371 | -48% | 0 | 0 | — |
case-18 | fail→fail | 11,631 | 2,154 | -81% | 1 | 1 | 0% | 2,049 | 1,313 | -36% | 0 | 0 | — |
case-19 | fail→pass | 14,109 | 1,446 | -90% | 1 | 1 | 0% | 2,267 | 1,255 | -45% | 0 | 0 | — |
case-20 | pass→pass | 13,575 | 10,327 | -24% | 1 | 1 | 0% | 2,915 | 3,577 | +23% | 0 | 0 | — |
case-21 | pass→pass | 14,116 | 17,089 | +21% | 1 | 1 | 0% | 2,976 | 4,855 | +63% | 0 | 0 | — |
case-22 | pass→pass | 11,404 | 8,877 | -22% | 1 | 1 | 0% | 2,128 | 2,898 | +36% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +45 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.