paperstack-cli 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ _site/
2
+ .ruff_cache/
3
+ .claude/
4
+ __pycache__/
5
+ .env
6
+ # uv tool resolves fresh; uv run uses the PEP-723 metadata.
7
+ uv.lock
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.4
2
+ Name: paperstack-cli
3
+ Version: 0.1.0
4
+ Summary: Review, inspect, and retrieve research papers from one CLI
5
+ Project-URL: Repository, https://github.com/MilkClouds/my-paperstack
6
+ Requires-Python: >=3.11.4
7
+ Requires-Dist: polars>=1.43
8
+ Requires-Dist: python-dotenv>=1.2.2
9
+ Requires-Dist: pyyaml>=6
10
+ Provides-Extra: pdf
11
+ Requires-Dist: pymupdf4llm>=0.0.17; extra == 'pdf'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # Critical Reads
15
+
16
+ Some papers mistake their own story for evidence. Some are optimized for acceptance rather than truth. Others are
17
+ careful, competent work on directions that do not matter. Critical Reads exists to tell them apart, preserve the
18
+ insights that survive scrutiny, and choose research directions worth pursuing.
19
+
20
+ Body-read reviews of papers, posts, and talks. Each entry lives in `entries/<citation-key>.md` and carries its identity in frontmatter.
21
+
22
+ - [Collections](#collections)
23
+ - [CLI](#cli)
24
+ - [DBLP index](#dblp-index)
25
+ - [Configuration](#configuration)
26
+ - [Adding a review](#adding-a-review)
27
+ - [Getting the source in front of you](#getting-the-source-in-front-of-you)
28
+ - [Review guide](#review-guide)
29
+ - [For robotics papers](#for-robotics-papers)
30
+
31
+ ## Collections
32
+
33
+ Curated, ordered lists live in [`collections.json`](collections.json), the single source used by the viewer and validation.
34
+ Every collection has a `published` or `draft` status. Published collections are stable reference catalogs; draft
35
+ collections are provisional reading paths shown in a separate collapsed section. Select one to filter entries in its
36
+ declared order.
37
+
38
+ CI publishes the generated viewer after GitHub Pages is enabled with GitHub Actions as its source.
39
+
40
+ ## CLI
41
+
42
+ Run `make serve` for the local viewer. Entry links open rendered HTML; `.md` URLs expose UTF-8 Markdown source. Install the CLI from PyPI, then authenticate with GitHub for review syncing and DBLP index downloads:
43
+
44
+ ```bash
45
+ uv tool install paperstack-cli
46
+ gh auth login
47
+ ```
48
+
49
+ The command groups have separate data and authority boundaries:
50
+
51
+ | Group | Data | Behavior |
52
+ |---|---|---|
53
+ | `review` | `entries/` review database | Read, initialize, validate, or audit authored judgments |
54
+ | `paper` | External metadata and arXiv content | Return source records or content without choosing a citation |
55
+ | `index` | Optional local indexes | Install and manage lookup data |
56
+
57
+ Review commands:
58
+
59
+ ```bash
60
+ paperstack review show black2024pi0 --brief
61
+ paperstack review show arxiv:2410.24164 --json
62
+ paperstack review list --quality poor --tag vla
63
+ paperstack review search "flow matching"
64
+ paperstack review sync --force
65
+ paperstack review init <key> --id arxiv:NNNN.NNNNN --title "Verbatim title" --editor <name>
66
+ paperstack review check --style
67
+ paperstack review audit
68
+ paperstack review citations --fetch
69
+ ```
70
+
71
+ Paper commands:
72
+
73
+ ```bash
74
+ paperstack paper search "Attention Is All You Need" --source dblp
75
+ paperstack paper metadata arxiv:2106.09685
76
+ paperstack paper metadata arxiv:2410.15549 --source semantic_scholar --json
77
+ paperstack paper metadata doi:10.1109/CVPR.2016.90 --source crossref
78
+ paperstack paper read arxiv:2604.23073
79
+ paperstack paper read arxiv:2604.23073 --outline
80
+ paperstack paper read arxiv:2604.23073 --section 6
81
+ paperstack paper pdf arxiv:2602.09017
82
+ ```
83
+
84
+ `metadata` accepts `arxiv:`, `doi:`, `dblp:`, and `openreview:` references. Semantic Scholar records include total,
85
+ influential, and reference counts. `read` and `pdf` require an `arxiv:` reference. Metadata output keeps
86
+ source records separate and includes provenance; it never selects or generates a citation.
87
+
88
+ Options are scoped to commands that use them. `--json` is available for structured records and `--offline` for
89
+ commands with a local or cached path. PDF conversion requires the optional dependency:
90
+
91
+ ```bash
92
+ uv tool install 'paperstack-cli[pdf]'
93
+ ```
94
+
95
+ From a clone, replace `paperstack` with `uv run paperstack`.
96
+ For PDF conversion in a clone, use `uv run --extra pdf paperstack paper pdf <arxiv-ref>`.
97
+ `review init` requires a writable review tree; `review check` requires a clone containing `scripts/build/check.sh`.
98
+ `review citations --fetch` updates `citations.json` for every arXiv-backed entry through Semantic Scholar's batch API;
99
+ without `--fetch`, it only removes cached records for entries that no longer exist. The static viewer exposes a minimum-citation
100
+ filter and shows counts alongside entries. `SEMANTIC_SCHOLAR_API_KEY` is optional but raises the API rate limit.
101
+
102
+ ### DBLP index
103
+
104
+ The optional index accelerates DBLP search and is required by `review audit`. It covers selected CS venues, not all
105
+ of DBLP. The `2026.08` Parquet snapshot contains 285,521 structured records and is about 25 MiB. Installation is explicit:
106
+
107
+ ```bash
108
+ paperstack index dblp status
109
+ paperstack index dblp install
110
+ paperstack index dblp update
111
+ paperstack index dblp remove --yes
112
+ ```
113
+
114
+ `install` downloads the pinned Parquet snapshot through authenticated `gh`. `update` discovers the newest
115
+ `dblp-index-YYYY.MM` paperstack Release and replaces an older index only after its SHA-256, schema, and embedded metadata checks pass. `status` reports the version,
116
+ coverage, size, record count, and location. `review audit` reports title and venue matches without editing reviews.
117
+ Snapshots with mismatched metadata or fewer than 250,000 records are rejected. Installed files are immutable and
118
+ content-addressed; an atomic pointer switch preserves the previous index if an update is interrupted.
119
+ See [DBLP snapshot releases](docs/DBLP_RELEASES.md) for the publishing procedure.
120
+
121
+ ### Configuration
122
+
123
+ | Variable | Purpose |
124
+ |---|---|
125
+ | `PAPERSTACK_DIR` | Review database to use instead of the surrounding clone or GitHub cache |
126
+ | `PAPERSTACK_REPO` | GitHub review repository; default `MilkClouds/my-paperstack` |
127
+ | `PAPERSTACK_TTL` | Review-cache refresh interval in seconds; default `3600` |
128
+ | `PAPERSTACK_PAPERS_DIR` | arXiv source/PDF cache; default `${XDG_CACHE_HOME:-~/.cache}/paperstack/papers` |
129
+ | `SEMANTIC_SCHOLAR_API_KEY` | Optional key for Semantic Scholar discovery |
130
+ | `OPENREVIEW_ACCESS_TOKEN` | Optional OpenReview `openreview.accessToken` cookie value |
131
+ | `XDG_CACHE_HOME` | Review and paper cache root |
132
+ | `XDG_DATA_HOME` | DBLP index root |
133
+
134
+ Configuration comes from the process environment. The CLI also loads the nearest `.env` without overriding exported
135
+ variables, so a clone can keep local configuration in a gitignored `.env` file.
136
+
137
+ Review lookup reads `$PAPERSTACK_DIR`, a surrounding clone, or a GitHub-backed cache, in that order. Scoped
138
+ `--offline` flags serve cached review or paper data without network access. Exit codes are `0` for
139
+ hits, `1` for no match, `2` for ambiguity, and `3` for unavailable data.
140
+
141
+ ## Adding a review
142
+
143
+ ```bash
144
+ paperstack review init <key> --id arxiv:NNNN.NNNNN --title "Verbatim title" --editor <name>
145
+ ```
146
+
147
+ This initializes an ungraded scaffold only. The review itself remains a reading and judgment task. Review files are
148
+ stored internally as `entries/<key>.md`. See the [review guide](#review-guide) for editorial guidance.
149
+
150
+ - Name files `<first-author surname><arXiv v1 year><first significant title word>`, lowercase; suffix collisions with `a`, `b`, and so on.
151
+ - Use the established method name or full title as the `#` heading.
152
+ - Use a registered CURIE (`arxiv:`, `doi:`, `hdl:`, `isbn:`) for `id`, or a URL when none exists.
153
+ - `tags` are lowercase and singular. Reuse before inventing.
154
+ - Include only verified affiliations in `lab`. Use a person's name in `editor`, or `model effort (harness)` for an agent.
155
+ - Use CommonMark with GFM, including tables for tabular results.
156
+
157
+ Run `make check`; `make style` adds prose-length warnings. The scripts require `jq` and mikefarah's `yq` (`go-yq` on conda-forge).
158
+
159
+ ### Getting the source in front of you
160
+
161
+ The source fetcher uses `latexpand` from PATH, falling back to a vendored copy via Perl.
162
+
163
+ ```bash
164
+ paperstack paper read arxiv:2604.23073 # the complete LaTeX body
165
+ paperstack paper read arxiv:2604.23073 --outline # the section outline
166
+ paperstack paper read arxiv:2604.23073 --section 6
167
+ paperstack paper pdf arxiv:cs/9301101 # when there is no LaTeX source
168
+ yt-dlp --skip-download --write-auto-subs --sub-lang en -o talk <url>
169
+ ```
170
+
171
+ - Prefer the LaTeX source; use the PDF fallback only when no source exists.
172
+ - Cross-check malformed tables with `pdftotext -layout <pdf> -`.
173
+ - Treat commented-out results as evidence only when their surviving values match the published version; a mismatched baseline may be an earlier run.
174
+ - Strip timestamps and duplicate cues from video captions.
175
+
176
+ ## Review guide
177
+
178
+ Read the body, then write the critical read, paper summary, reason to read, and one-liner, in that order.
179
+
180
+ - Keep the review body within 2,500 visible non-whitespace characters. Use prose for argument, bullets for independent points, and tables for repeated comparisons.
181
+ - Make the summary self-contained: a reader who has not read the paper should understand its core problem, approach, evidence, and findings. Choose the form that reads best; five bullets is one option, not a target.
182
+ - In the critical read, consult prior and subsequent work as needed, then focus on what matters for interpreting, trusting, or using the paper.
183
+ - Keep the one-liner to one sentence and `Why read it` to two. `Why read it` captures significance, originality, or practical value; use `none` when there is no reason.
184
+ - `Quality` measures how much of the title and abstract's main claim survives the evidence:
185
+ - `excellent`: the claim stands and has lasting importance
186
+ - `good`: the claim stands
187
+ - `fair`: only a narrower claim stands
188
+ - `poor`: the claim is not established
189
+ - Grade the advertised claim, not the narrower verdict. Use `fair` only when narrowing scope preserves the core claim; materially replacing it is `poor`.
190
+ - For SOTA or efficiency claims, check the strongest comparable result and name the denominator.
191
+ - If an official protocol fits the claim, an unjustified custom replacement caps `Quality` at `fair` unless matched, interpretable anchors restore comparability.
192
+ - Disclosed weaknesses still count when the paper claims past them. Side contributions do not raise the grade.
193
+ - On `fair` or `poor`, add `Read it anyway.` only with a checked citation count from Hugging Face or Semantic Scholar.
194
+
195
+ ### For robotics papers
196
+
197
+ See [What Are We Actually Benchmarking in Robot Manipulation?](entries/jiang2026benchmarking.md).
198
+
199
+ - A benchmark counts only when success requires the claimed capability. LIBERO-only evidence caps Quality at `poor`; so does an uncounted real-world result added to it.
200
+ - Judge the hardest benchmark, note omissions, and account for benchmark age and test-set proximity.
201
+ - Treat margins within evaluation noise as ties; check SOTA claims against the [VLA Evaluation Harness](https://allenai.github.io/vla-evaluation-harness/leaderboard/).
202
+ - Recover exact values and trial counts where possible; otherwise state that they are unavailable.
203
+ - Compare baselines only under the same training and evaluation protocol.
@@ -0,0 +1,190 @@
1
+ # Critical Reads
2
+
3
+ Some papers mistake their own story for evidence. Some are optimized for acceptance rather than truth. Others are
4
+ careful, competent work on directions that do not matter. Critical Reads exists to tell them apart, preserve the
5
+ insights that survive scrutiny, and choose research directions worth pursuing.
6
+
7
+ Body-read reviews of papers, posts, and talks. Each entry lives in `entries/<citation-key>.md` and carries its identity in frontmatter.
8
+
9
+ - [Collections](#collections)
10
+ - [CLI](#cli)
11
+ - [DBLP index](#dblp-index)
12
+ - [Configuration](#configuration)
13
+ - [Adding a review](#adding-a-review)
14
+ - [Getting the source in front of you](#getting-the-source-in-front-of-you)
15
+ - [Review guide](#review-guide)
16
+ - [For robotics papers](#for-robotics-papers)
17
+
18
+ ## Collections
19
+
20
+ Curated, ordered lists live in [`collections.json`](collections.json), the single source used by the viewer and validation.
21
+ Every collection has a `published` or `draft` status. Published collections are stable reference catalogs; draft
22
+ collections are provisional reading paths shown in a separate collapsed section. Select one to filter entries in its
23
+ declared order.
24
+
25
+ CI publishes the generated viewer after GitHub Pages is enabled with GitHub Actions as its source.
26
+
27
+ ## CLI
28
+
29
+ Run `make serve` for the local viewer. Entry links open rendered HTML; `.md` URLs expose UTF-8 Markdown source. Install the CLI from PyPI, then authenticate with GitHub for review syncing and DBLP index downloads:
30
+
31
+ ```bash
32
+ uv tool install paperstack-cli
33
+ gh auth login
34
+ ```
35
+
36
+ The command groups have separate data and authority boundaries:
37
+
38
+ | Group | Data | Behavior |
39
+ |---|---|---|
40
+ | `review` | `entries/` review database | Read, initialize, validate, or audit authored judgments |
41
+ | `paper` | External metadata and arXiv content | Return source records or content without choosing a citation |
42
+ | `index` | Optional local indexes | Install and manage lookup data |
43
+
44
+ Review commands:
45
+
46
+ ```bash
47
+ paperstack review show black2024pi0 --brief
48
+ paperstack review show arxiv:2410.24164 --json
49
+ paperstack review list --quality poor --tag vla
50
+ paperstack review search "flow matching"
51
+ paperstack review sync --force
52
+ paperstack review init <key> --id arxiv:NNNN.NNNNN --title "Verbatim title" --editor <name>
53
+ paperstack review check --style
54
+ paperstack review audit
55
+ paperstack review citations --fetch
56
+ ```
57
+
58
+ Paper commands:
59
+
60
+ ```bash
61
+ paperstack paper search "Attention Is All You Need" --source dblp
62
+ paperstack paper metadata arxiv:2106.09685
63
+ paperstack paper metadata arxiv:2410.15549 --source semantic_scholar --json
64
+ paperstack paper metadata doi:10.1109/CVPR.2016.90 --source crossref
65
+ paperstack paper read arxiv:2604.23073
66
+ paperstack paper read arxiv:2604.23073 --outline
67
+ paperstack paper read arxiv:2604.23073 --section 6
68
+ paperstack paper pdf arxiv:2602.09017
69
+ ```
70
+
71
+ `metadata` accepts `arxiv:`, `doi:`, `dblp:`, and `openreview:` references. Semantic Scholar records include total,
72
+ influential, and reference counts. `read` and `pdf` require an `arxiv:` reference. Metadata output keeps
73
+ source records separate and includes provenance; it never selects or generates a citation.
74
+
75
+ Options are scoped to commands that use them. `--json` is available for structured records and `--offline` for
76
+ commands with a local or cached path. PDF conversion requires the optional dependency:
77
+
78
+ ```bash
79
+ uv tool install 'paperstack-cli[pdf]'
80
+ ```
81
+
82
+ From a clone, replace `paperstack` with `uv run paperstack`.
83
+ For PDF conversion in a clone, use `uv run --extra pdf paperstack paper pdf <arxiv-ref>`.
84
+ `review init` requires a writable review tree; `review check` requires a clone containing `scripts/build/check.sh`.
85
+ `review citations --fetch` updates `citations.json` for every arXiv-backed entry through Semantic Scholar's batch API;
86
+ without `--fetch`, it only removes cached records for entries that no longer exist. The static viewer exposes a minimum-citation
87
+ filter and shows counts alongside entries. `SEMANTIC_SCHOLAR_API_KEY` is optional but raises the API rate limit.
88
+
89
+ ### DBLP index
90
+
91
+ The optional index accelerates DBLP search and is required by `review audit`. It covers selected CS venues, not all
92
+ of DBLP. The `2026.08` Parquet snapshot contains 285,521 structured records and is about 25 MiB. Installation is explicit:
93
+
94
+ ```bash
95
+ paperstack index dblp status
96
+ paperstack index dblp install
97
+ paperstack index dblp update
98
+ paperstack index dblp remove --yes
99
+ ```
100
+
101
+ `install` downloads the pinned Parquet snapshot through authenticated `gh`. `update` discovers the newest
102
+ `dblp-index-YYYY.MM` paperstack Release and replaces an older index only after its SHA-256, schema, and embedded metadata checks pass. `status` reports the version,
103
+ coverage, size, record count, and location. `review audit` reports title and venue matches without editing reviews.
104
+ Snapshots with mismatched metadata or fewer than 250,000 records are rejected. Installed files are immutable and
105
+ content-addressed; an atomic pointer switch preserves the previous index if an update is interrupted.
106
+ See [DBLP snapshot releases](docs/DBLP_RELEASES.md) for the publishing procedure.
107
+
108
+ ### Configuration
109
+
110
+ | Variable | Purpose |
111
+ |---|---|
112
+ | `PAPERSTACK_DIR` | Review database to use instead of the surrounding clone or GitHub cache |
113
+ | `PAPERSTACK_REPO` | GitHub review repository; default `MilkClouds/my-paperstack` |
114
+ | `PAPERSTACK_TTL` | Review-cache refresh interval in seconds; default `3600` |
115
+ | `PAPERSTACK_PAPERS_DIR` | arXiv source/PDF cache; default `${XDG_CACHE_HOME:-~/.cache}/paperstack/papers` |
116
+ | `SEMANTIC_SCHOLAR_API_KEY` | Optional key for Semantic Scholar discovery |
117
+ | `OPENREVIEW_ACCESS_TOKEN` | Optional OpenReview `openreview.accessToken` cookie value |
118
+ | `XDG_CACHE_HOME` | Review and paper cache root |
119
+ | `XDG_DATA_HOME` | DBLP index root |
120
+
121
+ Configuration comes from the process environment. The CLI also loads the nearest `.env` without overriding exported
122
+ variables, so a clone can keep local configuration in a gitignored `.env` file.
123
+
124
+ Review lookup reads `$PAPERSTACK_DIR`, a surrounding clone, or a GitHub-backed cache, in that order. Scoped
125
+ `--offline` flags serve cached review or paper data without network access. Exit codes are `0` for
126
+ hits, `1` for no match, `2` for ambiguity, and `3` for unavailable data.
127
+
128
+ ## Adding a review
129
+
130
+ ```bash
131
+ paperstack review init <key> --id arxiv:NNNN.NNNNN --title "Verbatim title" --editor <name>
132
+ ```
133
+
134
+ This initializes an ungraded scaffold only. The review itself remains a reading and judgment task. Review files are
135
+ stored internally as `entries/<key>.md`. See the [review guide](#review-guide) for editorial guidance.
136
+
137
+ - Name files `<first-author surname><arXiv v1 year><first significant title word>`, lowercase; suffix collisions with `a`, `b`, and so on.
138
+ - Use the established method name or full title as the `#` heading.
139
+ - Use a registered CURIE (`arxiv:`, `doi:`, `hdl:`, `isbn:`) for `id`, or a URL when none exists.
140
+ - `tags` are lowercase and singular. Reuse before inventing.
141
+ - Include only verified affiliations in `lab`. Use a person's name in `editor`, or `model effort (harness)` for an agent.
142
+ - Use CommonMark with GFM, including tables for tabular results.
143
+
144
+ Run `make check`; `make style` adds prose-length warnings. The scripts require `jq` and mikefarah's `yq` (`go-yq` on conda-forge).
145
+
146
+ ### Getting the source in front of you
147
+
148
+ The source fetcher uses `latexpand` from PATH, falling back to a vendored copy via Perl.
149
+
150
+ ```bash
151
+ paperstack paper read arxiv:2604.23073 # the complete LaTeX body
152
+ paperstack paper read arxiv:2604.23073 --outline # the section outline
153
+ paperstack paper read arxiv:2604.23073 --section 6
154
+ paperstack paper pdf arxiv:cs/9301101 # when there is no LaTeX source
155
+ yt-dlp --skip-download --write-auto-subs --sub-lang en -o talk <url>
156
+ ```
157
+
158
+ - Prefer the LaTeX source; use the PDF fallback only when no source exists.
159
+ - Cross-check malformed tables with `pdftotext -layout <pdf> -`.
160
+ - Treat commented-out results as evidence only when their surviving values match the published version; a mismatched baseline may be an earlier run.
161
+ - Strip timestamps and duplicate cues from video captions.
162
+
163
+ ## Review guide
164
+
165
+ Read the body, then write the critical read, paper summary, reason to read, and one-liner, in that order.
166
+
167
+ - Keep the review body within 2,500 visible non-whitespace characters. Use prose for argument, bullets for independent points, and tables for repeated comparisons.
168
+ - Make the summary self-contained: a reader who has not read the paper should understand its core problem, approach, evidence, and findings. Choose the form that reads best; five bullets is one option, not a target.
169
+ - In the critical read, consult prior and subsequent work as needed, then focus on what matters for interpreting, trusting, or using the paper.
170
+ - Keep the one-liner to one sentence and `Why read it` to two. `Why read it` captures significance, originality, or practical value; use `none` when there is no reason.
171
+ - `Quality` measures how much of the title and abstract's main claim survives the evidence:
172
+ - `excellent`: the claim stands and has lasting importance
173
+ - `good`: the claim stands
174
+ - `fair`: only a narrower claim stands
175
+ - `poor`: the claim is not established
176
+ - Grade the advertised claim, not the narrower verdict. Use `fair` only when narrowing scope preserves the core claim; materially replacing it is `poor`.
177
+ - For SOTA or efficiency claims, check the strongest comparable result and name the denominator.
178
+ - If an official protocol fits the claim, an unjustified custom replacement caps `Quality` at `fair` unless matched, interpretable anchors restore comparability.
179
+ - Disclosed weaknesses still count when the paper claims past them. Side contributions do not raise the grade.
180
+ - On `fair` or `poor`, add `Read it anyway.` only with a checked citation count from Hugging Face or Semantic Scholar.
181
+
182
+ ### For robotics papers
183
+
184
+ See [What Are We Actually Benchmarking in Robot Manipulation?](entries/jiang2026benchmarking.md).
185
+
186
+ - A benchmark counts only when success requires the claimed capability. LIBERO-only evidence caps Quality at `poor`; so does an uncounted real-world result added to it.
187
+ - Judge the hardest benchmark, note omissions, and account for benchmark age and test-set proximity.
188
+ - Treat margins within evaluation noise as ties; check SOTA claims against the [VLA Evaluation Harness](https://allenai.github.io/vla-evaluation-harness/leaderboard/).
189
+ - Recover exact values and trial counts where possible; otherwise state that they are unavailable.
190
+ - Compare baselines only under the same training and evaluation protocol.
@@ -0,0 +1,41 @@
1
+ [project]
2
+ name = "paperstack-cli"
3
+ version = "0.1.0"
4
+ description = "Review, inspect, and retrieve research papers from one CLI"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11.4" # First 3.11 release with tarfile.extractall(filter=).
7
+ dependencies = [
8
+ "polars>=1.43",
9
+ "python-dotenv>=1.2.2",
10
+ "pyyaml>=6",
11
+ ]
12
+
13
+ [project.optional-dependencies]
14
+ pdf = ["pymupdf4llm>=0.0.17"]
15
+
16
+ [dependency-groups]
17
+ lint = ["ruff>=0.12"]
18
+ test = ["pytest>=8"]
19
+ dev = [
20
+ { include-group = "lint" },
21
+ { include-group = "test" },
22
+ ]
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/MilkClouds/my-paperstack"
26
+
27
+ [project.scripts]
28
+ paperstack = "paperstack.entrypoint:main"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/paperstack"]
32
+
33
+ [tool.hatch.build.targets.sdist]
34
+ include = ["/src/paperstack", "/README.md", "/pyproject.toml"]
35
+
36
+ [tool.ruff]
37
+ line-length = 119
38
+
39
+ [build-system]
40
+ requires = ["hatchling"]
41
+ build-backend = "hatchling.build"
@@ -0,0 +1 @@
1
+ """Paperstack command-line package."""
@@ -0,0 +1,97 @@
1
+ """Batch citation-count updates for the review corpus."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import urllib.parse
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+
12
+ from . import metadata
13
+
14
+ S2_BATCH_API = "https://api.semanticscholar.org/graph/v1/paper/batch"
15
+ BATCH_SIZE = 500
16
+
17
+
18
+ def arxiv_id(raw: object) -> str | None:
19
+ """Return an unversioned arXiv ID from a CURIE or arxiv.org URL."""
20
+ value = str(raw or "").strip()
21
+ match = re.fullmatch(r"arxiv:([^?#]+)", value, re.IGNORECASE)
22
+ if not match:
23
+ match = re.search(r"arxiv\.org/(?:abs|pdf)/([^?#]+?)(?:\.pdf)?(?:[?#]|$)", value, re.IGNORECASE)
24
+ if not match:
25
+ return None
26
+ value = re.sub(r"v\d+$", "", match.group(1))
27
+ try:
28
+ return metadata.PaperRef.parse(f"arxiv:{value}").value
29
+ except ValueError:
30
+ return None
31
+
32
+
33
+ def collect(entries: list[dict]) -> list[str]:
34
+ return sorted({paper_id for entry in entries if (paper_id := arxiv_id(entry.get("id")))})
35
+
36
+
37
+ def fetch(arxiv_ids: list[str]) -> dict[str, int]:
38
+ """Fetch citation counts in aligned Semantic Scholar batches."""
39
+ counts: dict[str, int] = {}
40
+ headers = {"Content-Type": "application/json"}
41
+ if api_key := os.environ.get("SEMANTIC_SCHOLAR_API_KEY"):
42
+ headers["x-api-key"] = api_key
43
+
44
+ for start in range(0, len(arxiv_ids), BATCH_SIZE):
45
+ batch = arxiv_ids[start : start + BATCH_SIZE]
46
+ query = urllib.parse.urlencode({"fields": "citationCount"})
47
+ payload = json.dumps({"ids": [f"ARXIV:{paper_id}" for paper_id in batch]}).encode()
48
+ response = json.loads(metadata.request(f"{S2_BATCH_API}?{query}", headers=headers, data=payload))
49
+ if not isinstance(response, list):
50
+ detail = response.get("error") if isinstance(response, dict) else None
51
+ raise TypeError(f"unexpected Semantic Scholar batch response{f': {detail}' if detail else ''}")
52
+ for paper_id, paper in zip(batch, response, strict=True):
53
+ if paper is not None and not isinstance(paper, dict):
54
+ raise TypeError("unexpected paper in Semantic Scholar batch response")
55
+ if paper is not None and isinstance(paper.get("citationCount"), int):
56
+ counts[paper_id] = paper["citationCount"]
57
+ return counts
58
+
59
+
60
+ def load(path: Path) -> dict:
61
+ if not path.is_file():
62
+ return {"last_updated": None, "papers": {}}
63
+ value = json.loads(path.read_text(encoding="utf-8"))
64
+ if not isinstance(value, dict) or not isinstance(value.get("papers"), dict):
65
+ raise TypeError(f"{path} must contain a papers object")
66
+ last_updated = value.get("last_updated")
67
+ if last_updated is not None and not isinstance(last_updated, str):
68
+ raise TypeError(f"{path} last_updated must be a string or null")
69
+ papers = value["papers"]
70
+ if any(
71
+ not isinstance(paper_id, str) or not isinstance(count, int) or isinstance(count, bool) or count < 0
72
+ for paper_id, count in papers.items()
73
+ ):
74
+ raise TypeError(f"{path} papers must map IDs to non-negative integers")
75
+ return {"last_updated": last_updated, "papers": papers}
76
+
77
+
78
+ def update(root: Path, entries: list[dict], *, live: bool) -> tuple[dict, int]:
79
+ """Refresh or prune citation data and return the document and change count."""
80
+ path = root / "citations.json"
81
+ cached = load(path)
82
+ cached_papers = cached["papers"]
83
+ paper_ids = collect(entries)
84
+ fetched = fetch(paper_ids) if live else {}
85
+ papers = {
86
+ paper_id: fetched.get(paper_id, cached_papers.get(paper_id))
87
+ for paper_id in paper_ids
88
+ if paper_id in fetched or paper_id in cached_papers
89
+ }
90
+ changed = sum(cached_papers.get(paper_id) != count for paper_id, count in papers.items())
91
+ changed += sum(paper_id not in papers for paper_id in cached_papers)
92
+ document = {
93
+ "last_updated": datetime.now(UTC).date().isoformat() if live and fetched else cached["last_updated"],
94
+ "papers": papers,
95
+ }
96
+ path.write_text(json.dumps(document, indent=2, sort_keys=True) + "\n", encoding="utf-8")
97
+ return document, changed