erga 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.
Files changed (49) hide show
  1. erga-0.1.0/.github/workflows/ci.yml +24 -0
  2. erga-0.1.0/.github/workflows/release.yml +39 -0
  3. erga-0.1.0/.gitignore +16 -0
  4. erga-0.1.0/CLAUDE.md +61 -0
  5. erga-0.1.0/LICENSE +21 -0
  6. erga-0.1.0/PKG-INFO +91 -0
  7. erga-0.1.0/README.md +70 -0
  8. erga-0.1.0/docs/requirements-v1.md +349 -0
  9. erga-0.1.0/docs/todo.md +22 -0
  10. erga-0.1.0/pyproject.toml +60 -0
  11. erga-0.1.0/src/erga/__init__.py +7 -0
  12. erga-0.1.0/src/erga/cli.py +96 -0
  13. erga-0.1.0/src/erga/config.py +139 -0
  14. erga-0.1.0/src/erga/crossref.py +50 -0
  15. erga-0.1.0/src/erga/curation.py +300 -0
  16. erga-0.1.0/src/erga/dedup.py +120 -0
  17. erga-0.1.0/src/erga/errors.py +13 -0
  18. erga-0.1.0/src/erga/http.py +108 -0
  19. erga-0.1.0/src/erga/model.py +100 -0
  20. erga-0.1.0/src/erga/normalize.py +132 -0
  21. erga-0.1.0/src/erga/openalex.py +167 -0
  22. erga-0.1.0/src/erga/output.py +71 -0
  23. erga-0.1.0/src/erga/pipeline.py +142 -0
  24. erga-0.1.0/src/erga/verify.py +59 -0
  25. erga-0.1.0/tests/conftest.py +43 -0
  26. erga-0.1.0/tests/fixtures/crossref/cracked.json +7 -0
  27. erga-0.1.0/tests/fixtures/golden/erga.yml +11 -0
  28. erga-0.1.0/tests/fixtures/golden/expected-publications.json +233 -0
  29. erga-0.1.0/tests/fixtures/golden/manual.yml +15 -0
  30. erga-0.1.0/tests/fixtures/golden/overrides.yml +7 -0
  31. erga-0.1.0/tests/fixtures/golden/previous-publications.json +11 -0
  32. erga-0.1.0/tests/fixtures/golden/tags.yml +4 -0
  33. erga-0.1.0/tests/fixtures/openalex/authors-a5000000002.json +6 -0
  34. erga-0.1.0/tests/fixtures/openalex/authors-orcid.json +11 -0
  35. erga-0.1.0/tests/fixtures/openalex/works-page1.json +137 -0
  36. erga-0.1.0/tests/fixtures/openalex/works-page2.json +186 -0
  37. erga-0.1.0/tests/test_config.py +106 -0
  38. erga-0.1.0/tests/test_crossref.py +45 -0
  39. erga-0.1.0/tests/test_curation.py +238 -0
  40. erga-0.1.0/tests/test_dedup.py +130 -0
  41. erga-0.1.0/tests/test_golden.py +93 -0
  42. erga-0.1.0/tests/test_http.py +58 -0
  43. erga-0.1.0/tests/test_model.py +57 -0
  44. erga-0.1.0/tests/test_normalize.py +165 -0
  45. erga-0.1.0/tests/test_openalex.py +143 -0
  46. erga-0.1.0/tests/test_output.py +35 -0
  47. erga-0.1.0/tests/test_package.py +5 -0
  48. erga-0.1.0/tests/test_verify.py +83 -0
  49. erga-0.1.0/uv.lock +622 -0
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v7
17
+ - uses: astral-sh/setup-uv@v9.0.0
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: uv sync --locked
21
+ - run: uv run ruff check
22
+ - run: uv run ruff format --check
23
+ - run: uv run mypy
24
+ - run: uv run pytest
@@ -0,0 +1,39 @@
1
+ name: Release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v7
12
+ - uses: astral-sh/setup-uv@v9.0.0
13
+ - run: uv sync --locked
14
+ - run: uv run pytest
15
+ - name: Check tag matches package version
16
+ run: |
17
+ version="$(uv run python -c 'import erga; print(erga.__version__)')"
18
+ test "v${version}" = "${{ github.event.release.tag_name }}"
19
+ - run: uv build
20
+ - run: uvx twine check --strict dist/*
21
+ - uses: actions/upload-artifact@v4
22
+ with:
23
+ name: dist
24
+ path: dist/
25
+
26
+ publish:
27
+ needs: build
28
+ runs-on: ubuntu-latest
29
+ environment:
30
+ name: pypi
31
+ url: https://pypi.org/p/erga
32
+ permissions:
33
+ id-token: write
34
+ steps:
35
+ - uses: actions/download-artifact@v4
36
+ with:
37
+ name: dist
38
+ path: dist/
39
+ - uses: pypa/gh-action-pypi-publish@release/v1
erga-0.1.0/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ # Local workspace — never tracked, never public
2
+ local/
3
+
4
+ # Python
5
+ __pycache__/
6
+ *.py[cod]
7
+ .venv/
8
+ dist/
9
+ build/
10
+ *.egg-info/
11
+
12
+ # Tool caches
13
+ .pytest_cache/
14
+ .mypy_cache/
15
+ .ruff_cache/
16
+ .coverage
erga-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,61 @@
1
+ # erga
2
+
3
+ Automated publications pipeline for academic websites. One config file lists
4
+ authors (ORCID iDs); erga fetches their works from OpenAlex, normalizes and
5
+ deduplicates them across registrars, applies the maintainer's curation files,
6
+ backfills venues from Crossref, and writes a canonical `publications.json`
7
+ for any static site to render. Delivered as a GitHub Action and a CLI.
8
+
9
+ Design principle: the fetch is disposable, the curated JSON is the durable,
10
+ reviewable artifact. Curation (manual additions, per-record overrides,
11
+ highlights) lives in separate files that survive every automated refresh.
12
+
13
+ ## Status
14
+
15
+ Pre-0.1. The pipeline is implemented end-to-end (config, fetch, normalize,
16
+ dedup, curation, Crossref backfill, deterministic output, `build`/`verify`
17
+ CLI) with unit suites plus a byte-exact golden test, and validated against
18
+ live OpenAlex: smoke-tested on real author profiles and run in parallel
19
+ with the origin lab site's embedded pipeline to full convergence (187/187
20
+ records, no field diffs, 2026-08-05). Not yet released. See
21
+ `docs/requirements-v1.md` for the v1 design and `docs/todo.md` for open
22
+ work.
23
+
24
+ Module map (`src/erga/`): `config` (erga.yml), `http` (injectable transport
25
+ + retry), `openalex`/`crossref` (clients), `normalize` (raw work → canonical
26
+ record), `dedup` (DOI + title clustering), `curation` (manual/overrides/
27
+ tags), `pipeline` (stage orchestration), `output` (deterministic JSON),
28
+ `verify` (disambiguation report), `cli`.
29
+
30
+ ## Commands
31
+
32
+ - `uv sync`: install the dev environment
33
+ - `uv run pytest`: run tests
34
+ - `uv run ruff check` / `uv run ruff format`: lint / format
35
+ - `uv run mypy`: type-check (strict)
36
+
37
+ ## Layout
38
+
39
+ - `src/erga/`: package source (src layout)
40
+ - `tests/`: pytest suite; fixtures are handcrafted records plus recorded
41
+ OpenAlex responses (synthetic or CC0 data only, never real curated
42
+ personal data)
43
+ - `docs/`: design docs and `todo.md`
44
+ - `local/`: gitignored scratch space (session notes, reference material).
45
+ Session continuity lives here: `local/next-session-prompt.md`, never a
46
+ tracked `feedback/` — public-from-commit-1 discipline, no session state
47
+ in the repo or its history
48
+
49
+ ## Conventions
50
+
51
+ - Python ≥ 3.10; CI runs the matrix 3.10–3.13 on GitHub Actions
52
+ - uv manages the environment and lockfile; ruff lints and formats; mypy is
53
+ strict; all four checks must pass in CI
54
+ - Releases: GitHub Releases only, no CHANGELOG file; semver from 0.x
55
+ (v1.0 = the JSON schema is declared stable)
56
+ - PyPI publishing via Trusted Publishing (OIDC) from a release workflow;
57
+ no stored tokens
58
+ - Config samples, docs, and fixtures use placeholder mailto/ORCID values,
59
+ never real contact details
60
+ - v1 non-goals: no rendering or UI, no Google Scholar scraping, no database,
61
+ no hosted service, no sources beyond OpenAlex + manual entries
erga-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thomas Kogias
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
erga-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: erga
3
+ Version: 0.1.0
4
+ Summary: Keep a website's academic publications list current: fetch from OpenAlex, deduplicate across registrars, apply curation that survives every refresh.
5
+ Project-URL: Repository, https://github.com/belalik/erga
6
+ Author: Thomas Kogias
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: academic,bibliography,openalex,orcid,publications,static-site
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: pyyaml>=6
19
+ Requires-Dist: requests>=2.32
20
+ Description-Content-Type: text/markdown
21
+
22
+ # erga
23
+
24
+ Keep a website's academic publications list current, automatically, without
25
+ giving up control of the data.
26
+
27
+ **Status: alpha (v0.1).** The CLI pipeline works end-to-end and its
28
+ output has converged with a production lab site's existing pipeline in a
29
+ parallel run against live OpenAlex (187/187 records, zero field diffs).
30
+ The JSON schema may still change before v1.0.
31
+
32
+ ## What it does
33
+
34
+ You list your authors (ORCID iDs) in one config file. erga fetches their works
35
+ from OpenAlex, normalizes and deduplicates them across registrars (arXiv,
36
+ Zenodo, publisher records), applies your curation files, and writes a
37
+ canonical `publications.json` into your site repository. Your site (Jekyll,
38
+ Astro, Hugo, anything) renders it however it likes.
39
+
40
+ - **Curation that survives refresh**: manual additions, per-record overrides,
41
+ and highlights live in their own files and are re-applied on every
42
+ automated run.
43
+ - **Proper APIs, no scraping**: OpenAlex (CC0 data) plus Crossref venue
44
+ backfill, with API etiquette built in (keys, delays, retries).
45
+ - **Git-owned data**: the output is a diffable, PR-reviewable file in your
46
+ repo: no hosted embed, no runtime dependency, publications present in the
47
+ initial HTML.
48
+ - **Delivery**: a pip-installable CLI today; GitHub Action packaging is
49
+ planned once the CLI is proven on consumer sites.
50
+
51
+ The name: έργα, "works" — the same term OpenAlex uses for publications.
52
+
53
+ ## Usage
54
+
55
+ Install with `uv tool install erga` or `pip install erga` (or run one-off
56
+ with `uvx erga`). Write an `erga.yml`:
57
+
58
+ ```yaml
59
+ mailto: you@example.org # identifies requests to Crossref/OpenAlex
60
+ authors:
61
+ - name: Josiah Carberry
62
+ orcid: 0000-0002-1825-0097
63
+ - name: Another Person
64
+ openalex_id: A5000000000 # alternative when ORCID is missing/wrong
65
+
66
+ openalex:
67
+ api_key_env: OPENALEX_API_KEY # optional; env var name, never the key itself
68
+
69
+ output:
70
+ path: publications.json
71
+ ```
72
+
73
+ Then:
74
+
75
+ - `erga build [--config PATH] [--dry-run]` runs the pipeline and writes
76
+ `publications.json`. With `--dry-run` it prints a summary (fetched, merged,
77
+ deduplicated, excluded, backfilled) without writing.
78
+ - `erga verify [--config PATH]` prints the author-disambiguation report:
79
+ what each configured author resolves to on OpenAlex, with warnings for
80
+ split profiles, zero-work authors, and implausible works counts. Run it
81
+ once when setting up, and whenever a build looks off.
82
+
83
+ Three optional curation files next to the config survive every refresh:
84
+ `manual.yml` (records the APIs miss), `overrides.yml` (per-record patches,
85
+ exclusions, dedup exemptions), and `tags.yml` (tag name to DOI/id lists;
86
+ tag semantics are entirely yours). The full schema and pipeline design live
87
+ in [docs/requirements-v1.md](docs/requirements-v1.md).
88
+
89
+ ## License
90
+
91
+ MIT
erga-0.1.0/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # erga
2
+
3
+ Keep a website's academic publications list current, automatically, without
4
+ giving up control of the data.
5
+
6
+ **Status: alpha (v0.1).** The CLI pipeline works end-to-end and its
7
+ output has converged with a production lab site's existing pipeline in a
8
+ parallel run against live OpenAlex (187/187 records, zero field diffs).
9
+ The JSON schema may still change before v1.0.
10
+
11
+ ## What it does
12
+
13
+ You list your authors (ORCID iDs) in one config file. erga fetches their works
14
+ from OpenAlex, normalizes and deduplicates them across registrars (arXiv,
15
+ Zenodo, publisher records), applies your curation files, and writes a
16
+ canonical `publications.json` into your site repository. Your site (Jekyll,
17
+ Astro, Hugo, anything) renders it however it likes.
18
+
19
+ - **Curation that survives refresh**: manual additions, per-record overrides,
20
+ and highlights live in their own files and are re-applied on every
21
+ automated run.
22
+ - **Proper APIs, no scraping**: OpenAlex (CC0 data) plus Crossref venue
23
+ backfill, with API etiquette built in (keys, delays, retries).
24
+ - **Git-owned data**: the output is a diffable, PR-reviewable file in your
25
+ repo: no hosted embed, no runtime dependency, publications present in the
26
+ initial HTML.
27
+ - **Delivery**: a pip-installable CLI today; GitHub Action packaging is
28
+ planned once the CLI is proven on consumer sites.
29
+
30
+ The name: έργα, "works" — the same term OpenAlex uses for publications.
31
+
32
+ ## Usage
33
+
34
+ Install with `uv tool install erga` or `pip install erga` (or run one-off
35
+ with `uvx erga`). Write an `erga.yml`:
36
+
37
+ ```yaml
38
+ mailto: you@example.org # identifies requests to Crossref/OpenAlex
39
+ authors:
40
+ - name: Josiah Carberry
41
+ orcid: 0000-0002-1825-0097
42
+ - name: Another Person
43
+ openalex_id: A5000000000 # alternative when ORCID is missing/wrong
44
+
45
+ openalex:
46
+ api_key_env: OPENALEX_API_KEY # optional; env var name, never the key itself
47
+
48
+ output:
49
+ path: publications.json
50
+ ```
51
+
52
+ Then:
53
+
54
+ - `erga build [--config PATH] [--dry-run]` runs the pipeline and writes
55
+ `publications.json`. With `--dry-run` it prints a summary (fetched, merged,
56
+ deduplicated, excluded, backfilled) without writing.
57
+ - `erga verify [--config PATH]` prints the author-disambiguation report:
58
+ what each configured author resolves to on OpenAlex, with warnings for
59
+ split profiles, zero-work authors, and implausible works counts. Run it
60
+ once when setting up, and whenever a build looks off.
61
+
62
+ Three optional curation files next to the config survive every refresh:
63
+ `manual.yml` (records the APIs miss), `overrides.yml` (per-record patches,
64
+ exclusions, dedup exemptions), and `tags.yml` (tag name to DOI/id lists;
65
+ tag semantics are entirely yours). The full schema and pipeline design live
66
+ in [docs/requirements-v1.md](docs/requirements-v1.md).
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,349 @@
1
+ # erga v1 Requirements
2
+
3
+ Status: adopted 2026-08-03. Maintained as a living spec: current state only,
4
+ superseded content is replaced rather than appended. Product identity and
5
+ scope were fixed at kickoff; this document records the research-backed design
6
+ decisions and the concrete v1 surface. Sources: OpenAlex API documentation and live API
7
+ verification (2026-08-03), CSL-JSON schema analysis, GitHub Action delivery
8
+ research, and an audit of the origin pipeline (a production Jekyll lab site
9
+ that this tool generalizes).
10
+
11
+ ## 1. Purpose
12
+
13
+ erga keeps a website's academic publications list current while the
14
+ maintainer stays in control of the data. One config file lists authors
15
+ (ORCID iDs). The tool fetches their works from OpenAlex, normalizes and
16
+ deduplicates them, applies curation files that survive every refresh,
17
+ backfills missing venues from Crossref, and writes a canonical
18
+ `publications.json` into the site repository. The site renders it however it
19
+ likes; erga renders nothing.
20
+
21
+ The fetch is disposable. The curated JSON is the durable, reviewable
22
+ artifact.
23
+
24
+ ## 2. Decisions on the kickoff open questions
25
+
26
+ ### 2.1 Canonical schema: own minimal JSON, with emitters later
27
+
28
+ CSL-JSON was evaluated as a candidate canonical format and rejected:
29
+
30
+ - Five fields this tool needs most have no first-class CSL-JSON home:
31
+ open-access URL, citation count, ORCID on author names (the name schema
32
+ forbids extra properties), keyword arrays (CSL `keyword` is a single
33
+ string), and curation flags. All would land in the unstructured `custom`
34
+ bag.
35
+ - No `preprint` item type exists in CSL 1.0.2.
36
+ - CSL dates are `date-parts` nested arrays; Jekyll, Astro, and Hugo
37
+ templates all want plain ISO strings and would each need glue code.
38
+ - The Jekyll academic ecosystem (jekyll-scholar, al-folio) consumes BibTeX,
39
+ not CSL-JSON, so canonical CSL-JSON would buy no adoption there anyway.
40
+ - OpenAlex offers no native CSL-JSON output and no reusable mapping library
41
+ exists in Python, so the mapping is hand-written under either choice.
42
+
43
+ Decision: a purpose-built minimal schema (section 4) is canonical.
44
+ CSL-JSON and BibTeX emitters come after v0.1 as strategic outputs that turn
45
+ the existing theme ecosystem into potential adopters. v1.0 means the
46
+ canonical schema is declared stable.
47
+
48
+ ### 2.2 Featured/tags: generalize to tags
49
+
50
+ The origin pipeline had a `featured` boolean fed by a flat DOI list. v1
51
+ generalizes this to a single mechanism: every record carries `tags`
52
+ (list of strings), and a curation file maps tag names to DOI/id lists.
53
+ Manual entries may declare their own tags inline. Tag names carry no
54
+ semantics for erga: sites decide what a tag means and whether it exists at
55
+ all. A "featured" highlight list is one pattern a site can implement (the
56
+ origin site does); the docs present it as an example, never a default.
57
+
58
+ ### 2.3 Action output mode: compose, do not embed
59
+
60
+ Research across comparable data-updating Actions shows two viable patterns:
61
+ built-in output-mode switches (lowlighter/metrics) and composition with
62
+ single-purpose commit/PR actions (the dominant pattern for thin tool
63
+ wrappers). Decision: the erga Action only produces the output file and
64
+ exits. Delivery is composed in the user's workflow, and the docs ship two
65
+ copy-paste recipes:
66
+
67
+ 1. Commit-back inside the site's existing build workflow (default recipe):
68
+ run erga, commit the JSON if changed, then build and deploy in the same
69
+ run. Production-proven by the origin site; sidesteps the GITHUB_TOKEN
70
+ restriction that bot pushes never trigger downstream workflows, because
71
+ the build happens in the same workflow.
72
+ 2. Pull-request mode (cautious recipe): peter-evans/create-pull-request.
73
+ Repeated runs update one branch and PR; quiet weeks produce no noise;
74
+ merges are normal pushes so separately-triggered deploy workflows fire.
75
+ Right default for maintainers who want a review gate, and the only clean
76
+ path for protected branches.
77
+
78
+ Workflow guidance the docs must include: declare `permissions` explicitly,
79
+ use a concurrency group, schedule cron off the top of the hour, and note
80
+ GitHub's 60-day auto-disable of scheduled workflows on inactive repos.
81
+
82
+ ### 2.4 Department scale: batching, cursor pagination, cost budget
83
+
84
+ For 10 to 100 authors and thousands of works per run:
85
+
86
+ - Batch authors with the OR-pipe filter (`author.id:A1|A2|...`), at most
87
+ 100 values per filter, and deduplicate fetched works by OpenAlex id
88
+ (co-authored works arrive once per matching batch).
89
+ - `per-page=100` with cursor pagination (`cursor=*`, then
90
+ `meta.next_cursor`). Basic paging caps at 10,000 results; cursor does not.
91
+ The live API still accepts `per-page=200` but the documented maximum is
92
+ 100; the spec uses 100.
93
+ - Trim payloads with `select=` (root-level fields only).
94
+ - Cost: a 100-author department at roughly 10,000 works is on the order of
95
+ 100 to 200 list calls per run, about $0.02 against the $1/day allowance of
96
+ a free API key. A small lab fits even the keyless $0.10/day allowance.
97
+ Runtime is bounded by politeness delays, not data volume.
98
+
99
+ ## 3. OpenAlex operational facts (verified 2026-08-03)
100
+
101
+ The landscape changed materially in 2025-2026; these facts supersede the
102
+ founding documents where they conflict.
103
+
104
+ - **API keys are the access model** (since 2026-02-13). The mailto "polite
105
+ pool" no longer affects OpenAlex rate limits. Free key: $1/day usage
106
+ allowance. Keyless: $0.10/day. List/filter calls cost $0.0001. Verified
107
+ live: keyless requests succeed and return `x-ratelimit-limit-usd: 0.1`
108
+ headers. Hard throttle 100 req/s. The key is passed as `api_key` query
109
+ parameter; erga reads it from an env var and must never write it to disk.
110
+ Keyless quota can only be tracked per IP, and GitHub-hosted runners share
111
+ IPs, so CI runs must not rely on the keyless allowance; the Action docs
112
+ treat a free key as required setup.
113
+ Crossref, unlike OpenAlex, still operates a mailto polite pool, so the
114
+ config mailto remains first-class for the venue-backfill stage.
115
+ - **XPAC subset**: the Nov 2025 backend rewrite added ~190M works (DataCite,
116
+ institutional repositories) that are excluded from queries by default;
117
+ `include_xpac=true` opts in, and works carry an `is_xpac` field (verified
118
+ live). Default off in erga, configurable, because XPAC metadata quality is
119
+ explicitly lower and a publications page wants precision over recall.
120
+ Revisit with department-scale evidence.
121
+ - **Type vocabulary drifts**: a July 2026 reclassification changed `type` on
122
+ ~10% of the catalog and added first-class `conference-paper`, `software`
123
+ and `software-paper`; the classifier re-runs daily. `type_crossref` and
124
+ `raw_type` are absent from live responses (verified). Consequence: rely
125
+ on `type` alone, expect drift, and make drift visible — a raw type that
126
+ is neither mapped nor deliberately "other" raises a build warning
127
+ (a silent catch-all misfiled sixty conference papers on the origin site
128
+ for months). `software-paper` at a journal source is classified `journal`:
129
+ it is a peer-reviewed article about software (SoftwareX, JOSS), not a
130
+ software artifact. Gotcha, should stats ever use it: `group_by=type`
131
+ returns full URI keys (`https://openalex.org/types/...`) while `type` on
132
+ works is the bare string.
133
+ - **OpenAlex already merges many cross-registrar copies**: a single work can
134
+ carry multiple `locations[]` (publisher, DOAJ, Zenodo deposits) with the
135
+ top-level DOI pointing at the published version (verified live on a work
136
+ with 5 locations). erga's own dedup remains necessary for what OpenAlex
137
+ misses, for manual entries, and as a guard, but it is a second line of
138
+ defense rather than the only one.
139
+ - **Abstracts** still arrive as `abstract_inverted_index`; standard
140
+ positional reconstruction is unchanged. Coverage is uneven and skews
141
+ recent.
142
+ - **Authorships** carry `author.id`, `display_name`, `orcid`, plus
143
+ `raw_author_name`; only ~30% of recent works have publisher-asserted
144
+ ORCID data, so ORCID cannot be the only identity signal. `is_retracted`
145
+ is available and reliable (Retraction Watch data).
146
+
147
+ ## 4. Canonical output schema
148
+
149
+ Top-level object, not a bare array, so the schema version has a home:
150
+
151
+ ```json
152
+ {
153
+ "schema_version": 1,
154
+ "works": [ ... ]
155
+ }
156
+ ```
157
+
158
+ Per work, all keys always present:
159
+
160
+ | field | type | notes |
161
+ |---|---|---|
162
+ | `id` | string | OpenAlex work id without host (`"W4406028178"`), or `"manual-<slug>"` |
163
+ | `title` | string | |
164
+ | `authors` | array | `{ "name": str, "orcid": str\|null, "tracked": bool }`; `tracked` = matches a configured author by resolved OpenAlex id, ORCID, or name/alias |
165
+ | `year` | int \| null | |
166
+ | `date` | string \| null | ISO publication date `"2026-01-15"` |
167
+ | `venue` | string \| null | null when unknown (origin pipeline used `""`) |
168
+ | `type` | string | `journal`, `conference`, `book`, `book-chapter`, `thesis`, `preprint`, `dataset`, `software`, `other` |
169
+ | `doi` | string \| null | full `https://doi.org/...` URL |
170
+ | `cited_by_count` | int | |
171
+ | `abstract` | string \| null | reconstructed plaintext |
172
+ | `open_access` | object \| null | `{ "url": str }`; object form leaves room for license/version later |
173
+ | `tags` | array of string | from the tags curation file and manual entries |
174
+ | `is_retracted` | bool | |
175
+ | `source` | string | `"openalex"` or `"manual"` |
176
+
177
+ Output is deterministic: sorted by year descending then id, UTF-8,
178
+ 2-space indent, `ensure_ascii=False`, trailing newline. Unchanged inputs
179
+ produce a byte-identical file, so "did anything change" is exactly
180
+ `git diff`.
181
+
182
+ Schema changes vs the origin pipeline (its consumer migrates with a
183
+ template tweak during the parallel run): `featured` boolean replaced by
184
+ `tags`, `is_lab_member` renamed `tracked`, empty-string venue becomes null,
185
+ `date` and `is_retracted` added, `dissertation` renamed `thesis`, `software`
186
+ type added, top-level wrapper added.
187
+
188
+ ## 5. Configuration
189
+
190
+ One YAML file, default `erga.yml`. All examples use placeholder contacts
191
+ (0000-0002-1825-0097 is ORCID's fictitious researcher Josiah Carberry).
192
+
193
+ ```yaml
194
+ mailto: you@example.org # identifies requests to Crossref/OpenAlex
195
+ authors:
196
+ - name: Josiah Carberry
197
+ orcid: 0000-0002-1825-0097
198
+ aliases: ["J. S. Carberry"] # optional, for matching manual entries
199
+ - name: Another Person
200
+ openalex_id: A5000000000 # alternative when ORCID is missing/wrong
201
+ - name: Third Person # no ids at all: tracked by name only
202
+
203
+ openalex:
204
+ api_key_env: OPENALEX_API_KEY # optional; env var name, never the key itself
205
+ include_xpac: false
206
+
207
+ output:
208
+ path: publications.json
209
+
210
+ curation: # optional; defaults shown, relative to config
211
+ manual: manual.yml
212
+ overrides: overrides.yml
213
+ tags: tags.yml
214
+ ```
215
+
216
+ Author resolution: ORCID resolves via the OpenAlex authors endpoint
217
+ (singleton lookups are free). An author entry may pin `openalex_id`
218
+ explicitly, and both may coexist (some profiles are split across multiple
219
+ OpenAlex author IDs). An entry with neither id is a tracking-only author:
220
+ it contributes its name and aliases to the `tracked` flag and to manual-
221
+ entry matching but resolves and fetches nothing (for authors without any
222
+ registrar identity, or whose works OpenAlex misassigns to a conflated
223
+ homonym profile that must not be fetched).
224
+
225
+ ## 6. Curation files
226
+
227
+ All three survive every refresh; a missing file means "none".
228
+
229
+ - **`manual.yml`**: list of records the APIs miss. Fields mirror the output
230
+ schema loosely: `title`, `authors` (string or list), `venue`, `year`,
231
+ `doi`, `type`, `tags`. Authors are matched to configured authors by
232
+ name/alias for the `tracked` flag.
233
+ - **`overrides.yml`**: list of patches keyed by `doi` (case-insensitive) or
234
+ `id`. Any other key overwrites that field on the merged record. Special
235
+ keys: `exclude: true` drops the record; `keep_distinct: true` exempts it
236
+ from title clustering. A field patch that no longer changes anything
237
+ (upstream caught up) raises a build warning, measured against the
238
+ pre-patch record — measuring against the output would be circular. The
239
+ warning is information, not an instruction to delete: a redundant
240
+ override may stay as insurance against upstream regressing.
241
+ - **`tags.yml`**: mapping of tag name to list of DOIs/ids:
242
+
243
+ ```yaml
244
+ featured:
245
+ - https://doi.org/10.5555/12345678
246
+ ```
247
+
248
+ Tag names are arbitrary; "featured" above is only an example.
249
+
250
+ ## 7. Pipeline stages
251
+
252
+ Ported from the production origin pipeline with generalization deltas noted.
253
+
254
+ 1. Load config and curation files.
255
+ 2. Resolve authors to OpenAlex author IDs (section 5).
256
+ 3. Fetch works: OR-pipe author batches, `select=` trimmed fields,
257
+ `per-page=100`, cursor pagination, retry with backoff on 429/5xx,
258
+ politeness delay between calls. Deduplicate by work id. Any fetch
259
+ failure aborts the run without touching existing output; a transient
260
+ API failure must never shrink a published list.
261
+ 4. Normalize to the canonical schema: type mapping, abstract
262
+ reconstruction, OA URL from `best_oa_location`/`open_access.oa_url`,
263
+ author `tracked` flags.
264
+ 5. Merge manual entries; their DOIs seed the dedup set so manual always
265
+ wins.
266
+ 6. DOI-level dedup, case-insensitive.
267
+ 7. Title-cluster dedup. Normalize (NFKD, lowercase, fold dash variants,
268
+ strip non-alphanumerics, collapse whitespace); group by
269
+ (normalized title, is-dataset) so datasets never merge with papers;
270
+ titles under 12 normalized characters and `keep_distinct` records bypass
271
+ clustering. Rank within a cluster: manual first, then version-of-record
272
+ over repository deposits (known repository DOI prefixes: arXiv, Zenodo,
273
+ figshare, Research Square, bio/medRxiv, SSRN, OSF, Fraunhofer publica,
274
+ and `preprint` type), then has-DOI, then citation count, then newest
275
+ OpenAlex record (numeric W-id; publication dates deliberately play no
276
+ part — within a same-title cluster they differ by deposit-version
277
+ artifacts and favor the wrong copies). The winner inherits `abstract`
278
+ and `open_access` from absorbed copies when it lacks them.
279
+ 8. Apply overrides (patch or exclude).
280
+ 9. Crossref venue backfill with the last-known-good ratchet: reuse venues
281
+ from the previous output first, then query Crossref (polite mailto
282
+ User-Agent) only for records still lacking one; DataCite DOIs 404 there
283
+ and are skipped silently.
284
+ 10. Apply tags.
285
+ 11. Sort deterministically and write.
286
+
287
+ ## 8. CLI
288
+
289
+ `erga` console entry point, two subcommands in v1:
290
+
291
+ - `erga build [--config PATH] [--dry-run]`: run the pipeline. `--dry-run`
292
+ prints a summary (fetched, merged, deduplicated, excluded, backfilled)
293
+ without writing. Exit 0 on success (changed or not; change detection is
294
+ git's job), nonzero on any failure.
295
+ - `erga verify [--config PATH]`: the author-disambiguation report, a
296
+ first-class feature because OpenAlex author IDs split and conflate
297
+ people. Per configured author: resolved ID(s), works count, name
298
+ variants, most recent titles; warnings for ORCIDs resolving to multiple
299
+ author IDs, zero-work authors, and implausible works counts.
300
+
301
+ Python >= 3.10. Runtime dependencies: `requests` and `PyYAML` only.
302
+
303
+ ## 9. GitHub Action
304
+
305
+ A composite action in this repo (`action.yml`): pinned `setup-uv`, then
306
+ `uvx erga==<version> build`. Inputs: `config` (path), `version`. No commit
307
+ or PR logic inside the action (section 2.3). Full semver tags plus a moving
308
+ `v1` major tag, actions/checkout convention. The README pitch stays "one
309
+ workflow file plus one config file", with the two delivery recipes.
310
+
311
+ Action packaging lands after the CLI is proven (see milestones); the
312
+ consumer sites can run the CLI directly in their workflows meanwhile.
313
+
314
+ ## 10. Testing and fixtures
315
+
316
+ - Two-tier fixtures, per the scaffold decisions: small handcrafted records
317
+ exercising dedup/curation logic (they double as documentation of the
318
+ ranking rules), plus a few recorded OpenAlex responses for the fetch
319
+ layer. Synthetic or clearly-public CC0 data only; never the origin
320
+ site's curated real-people data.
321
+ - The fetch layer takes an injectable transport so recorded fixtures need
322
+ no HTTP mocking library.
323
+ - One end-to-end golden test: fixture config plus recorded responses in,
324
+ byte-exact `publications.json` out.
325
+ - CI: ruff, ruff format, mypy strict, pytest across Python 3.10-3.13
326
+ (already in place).
327
+
328
+ ## 11. Milestones
329
+
330
+ - **v0.1**: CLI end-to-end (config in, correct curated JSON out), tested,
331
+ documented. No Action, no emitters.
332
+ - **v0.2**: consumer #1, the origin Jekyll lab site (14 authors), runs erga
333
+ in parallel with its embedded pipeline until outputs match, then
334
+ switches.
335
+ - **v0.3**: GitHub Action packaging; consumer #2, an Astro 5 department
336
+ site (tens of authors), stress-tests scale and disambiguation.
337
+ - **CSL-JSON and BibTeX emitters** slot in after v0.1 as demand warrants,
338
+ before the public flip.
339
+ - **Public + v1.0**: strong README (before/after dedup story, head-on
340
+ "why not BibBase" answer), schema declared stable, PyPI via Trusted
341
+ Publishing, promotion in the channels where the demand already sits.
342
+
343
+ ## 12. Non-goals (v1)
344
+
345
+ No rendering or UI components, no Google Scholar (scraping is the failure
346
+ mode this tool exists to replace), no database, no hosted service, no
347
+ sources beyond OpenAlex plus manual entries. Multi-source merging (PubMed,
348
+ ADS, DBLP) stays a documented architectural possibility only.
349
+