% ============================================================================
% Appendix H — Reproducibility
% Written by Agent L (appendices A/E/H instance), 2026-11-03. Sources of truth: reproducibility/RUNBOOK.md,
% reproducibility/environment_M3U96b.txt, data/raw/README.md, reports/INCIDENTS.md, protocol/amendments.csv,
% protocol/PROTOCOL.md §P8, README.md, .gitignore, ingestion/*.py (ledger schemas). Every number here is a
% provenance fact (file counts, sizes, wall times, versions) taken from those files; no corpus result appears.
% ============================================================================
\section{Reproducibility}
\label{app:repro}
The mandate requires that the corpus be reproducible from immutable raw snapshots and that every quantitative
claim trace to a script, an input artefact and a result file. This appendix records what a reader needs to
re-run the pipeline: the computing environment and the locked package versions, the ordered steps with their
commands, inputs, outputs and measured wall times, the mechanisms by which provenance is kept (manifests,
hash ledgers, verbatim raw pages, API ledgers), the incidents that occurred during the first run and what
they imply for a second one, the protocol amendments, and the licence of each source together with what is
and is not redistributed. The runbook (\texttt{reproducibility/\allowbreak RUNBOOK.\allowbreak md}) is the operational version of
this appendix and is updated whenever a step changes; where the two differ, the runbook is more recent.
\subsection{Environment}
\label{app:repro-env}
Everything described in the runbook was executed on one node of the MacLustr cluster, M3U96b (Mac Studio,
Apple M3~Ultra, 28~cores, 96~GB of unified memory, 103{,}079{,}215{,}104 bytes as recorded by the environment
capture), in the working directory \texttt{\textasciitilde/\allowbreak wp44\_\allowbreak uqo}, under Python 3.12.15 in a virtual
environment created with \texttt{uv} from \texttt{requirements.\allowbreak txt} and then frozen to
\texttt{reproducibility/\allowbreak environment\_\allowbreak M3U96b.\allowbreak txt} (92 lines: 88 pinned packages, the interpreter version, the
operating-system version string, the processor and the memory size). The operating-system version string
captured in that file is 26.4, whereas the runbook header states macOS~27.0; the discrepancy is recorded here
rather than resolved, since the node was updated between captures and neither value affects the computation.
Two further nodes were used after the incidents of Section~\ref{app:repro-incidents}: M5M36 for the second
snapshot pass and M4M64c and M3U96a for embedding shards; they run the same locked environment.
Table~\ref{tab:appH-packages} lists the packages on which results depend; the complete list is the lock file.
No step requires credentials except the two LLM raters of the taxonomy validation and the knowledge-graph
extraction (an Anthropic API key read from a file outside the repository, never stored in it).
\begin{table}[htbp]
\begin{singlespace}
\centering
\caption{Key packages of the locked environment (\texttt{reproducibility/\allowbreak environment\_\allowbreak M3U96b.\allowbreak txt}, Python 3.12.15).}
\label{tab:appH-packages}
\small
\begin{tabularx}{\textwidth}{l X l}
\toprule
Role & Package & Version \\
\midrule
Analytical database & \texttt{duckdb} \citep{raasveldt2019duckdb} & 2.0.0 \\
Columnar I/O & \texttt{pyarrow} & 26.0.0 \\
Data frames & \texttt{polars} / \texttt{pandas} & 2.1.0 / 3.1.0 \\
Numerics & \texttt{numpy} / \texttt{scipy} & 2.5.4 / 1.18.1 \\
Statistics, time series & \texttt{statsmodels} / \texttt{formulaic} & 0.15.0 / 1.2.2 \\
Change-point detection & \texttt{ruptures} \citep{truong2020ruptures} & 1.1.10 \\
Machine learning & \texttt{scikit-learn} & 1.9.1 \\
Embeddings & \texttt{torch} / \texttt{transformers} / \texttt{sentence-transformers} \citep{reimers2019sbert} & 2.15.0 / 5.22.1 / 6.2.0 \\
Model hub client & \texttt{huggingface-hub} & 2.4.0 \\
Dimensionality reduction, clustering & \texttt{umap-learn} / \texttt{hdbscan} / \texttt{pynndescent} & 0.5.12 / 0.8.44 / 0.7.0 \\
Networks & \texttt{networkx} / \texttt{igraph} & 3.7 / 1.0.0 \\
Parsing & \texttt{lxml} / \texttt{orjson} / \texttt{zstandard} / \texttt{regex} & 6.1.3 / 3.13.0 / 0.25.0 / 2026.9.29 \\
Ontology file & \texttt{pyyaml} & 6.0.3 \\
HTTP & \texttt{requests} / \texttt{httpx2} & 2.34.2 / 2.16.0 \\
LLM raters, extraction & \texttt{anthropic} & 2.4.0 \\
Figures & \texttt{matplotlib} & 3.11.2 \\
Tests & \texttt{pytest} & 9.2.0 \\
\bottomrule
\end{tabularx}
\end{singlespace}
\end{table}
\subsection{Steps, commands and measured times}
\label{app:repro-steps}
Table~\ref{tab:appH-steps} reproduces the runbook's step table. Commands are given relative to the repository
root with the virtual environment active; \texttt{\ldots} abbreviates arguments that are spelled out in the
runbook. Wall times are those measured during the first run and are reported as orientation, not as
guarantees: the snapshot download depends on network throughput (about 108~MB/s was sustained from the node
with eight parallel streams), and the steps marked ``to be measured'' had not completed at the time the
runbook was last updated because of the node freezes described below. Steps~1--5 are pure acquisition and
can be run independently and in any order; steps 6--10 are sequential; step~9 is a second acquisition pass
whose need is discovered at step~8 and which then repeats steps 6--8 and 10--11 with the suffix \texttt{\_\allowbreak p2}
(\texttt{ingestion/\allowbreak run\_\allowbreak pass2\_\allowbreak chain.\allowbreak sh}, which waits for a flag file written when the pass-2 outputs have
been copied back to the node). The API increment of Table~\ref{tab:appH-raw} (works published after the
snapshot cut-off) is converted to the snapshot's part format by \texttt{ingestion/\allowbreak ingest\_\allowbreak api\_\allowbreak increment.\allowbreak py}
and normalised by the same step~6; it is not yet a numbered step of the runbook.
\begin{singlespace}
\begin{footnotesize}
\begin{longtable}{@{}p{0.5cm} p{5.6cm} p{2.3cm} p{4.6cm} p{2.3cm}@{}}
\caption{Pipeline steps (\texttt{reproducibility/\allowbreak RUNBOOK.\allowbreak md}), with the command, the inputs, the outputs and the wall time measured on M3U96b.}
\label{tab:appH-steps}\\
\toprule
\# & Step and command & Inputs & Outputs & Wall time \\
\midrule
\endfirsthead
\caption[]{Pipeline steps (continued).}\\
\toprule
\# & Step and command & Inputs & Outputs & Wall time \\
\midrule
\endhead
\midrule
\multicolumn{5}{r}{continued on next page}\\
\endfoot
\bottomrule
\endlastfoot
1 & OpenAlex works snapshot, pass~1 (CS filter + slim table): \texttt{python ingestion/\allowbreak openalex\_\allowbreak snapshot.\allowbreak py --out data/\allowbreak raw/\allowbreak openalex/\allowbreak works --manifest data/\allowbreak raw/\allowbreak openalex/\allowbreak works\_\allowbreak manifest\_\allowbreak 2026-10-14.\allowbreak json --workers 8} & public S3 bucket, release 2026-10-14 & \texttt{works/\allowbreak cs/\allowbreak *.\allowbreak jsonl.\allowbreak zst} (15{,}490{,}159 records), \texttt{works/\allowbreak slim/\allowbreak *.\allowbreak parquet} (464{,}765{,}485 rows), \texttt{ledger.\allowbreak sqlite} & 1~h 46~min at $\approx$108~MB/s \\
2 & OpenAlex entities: \texttt{python ingestion/\allowbreak openalex\_\allowbreak entities.\allowbreak py --out data/\allowbreak raw/\allowbreak openalex} & S3 & \texttt{topics/\allowbreak }, \ldots, \texttt{institutions/\allowbreak }; \texttt{entities\_\allowbreak ledger.\allowbreak json} & 5~min \\
3 & DBLP: \texttt{python ingestion/\allowbreak dblp\_\allowbreak parse.\allowbreak py --xml data/\allowbreak raw/\allowbreak dblp/\allowbreak dblp.\allowbreak xml.\allowbreak gz --out data/\allowbreak raw/\allowbreak dblp} & dump of 2026-11-02, MD5 verified & \texttt{dblp\_\allowbreak publications.\allowbreak parquet} (8{,}840{,}866 records), parse report & 4~min 45~s \\
4 & arXiv OAI-PMH: \texttt{python ingestion/\allowbreak arxiv\_\allowbreak oai.\allowbreak py --out data/\allowbreak raw/\allowbreak arxiv --from 2018-01-01 --until 2026-11-02} & OAI feed, set \texttt{cs} & raw XML pages (758), \texttt{arxiv\_\allowbreak cs.\allowbreak parquet} (915{,}115 records) & $\approx$1~h \\
5 & ACL Anthology: \texttt{python ingestion/\allowbreak acl\_\allowbreak parse.\allowbreak py --bib data/\allowbreak raw/\allowbreak acl/\allowbreak anthology+abstracts.\allowbreak bib.\allowbreak gz --out data/\allowbreak raw/\allowbreak acl} & BibTeX export & \texttt{acl\_\allowbreak anthology.\allowbreak parquet} (138{,}796 entries) & 10~s \\
6 & Normalise: \texttt{python ingestion/\allowbreak build\_\allowbreak corpus.\allowbreak py --cs-dir data/\allowbreak raw/\allowbreak openalex/\allowbreak works/\allowbreak cs --out data/\allowbreak processed --workers 12} & step 1 & \texttt{tables/\allowbreak
/\allowbreak *.\allowbreak parquet} & 2~min 10~s \\
7 & Compact: \texttt{ulimit -n 65536; python ingestion/\allowbreak compact\_\allowbreak tables.\allowbreak py \textasciitilde/\allowbreak wp44\_\allowbreak uqo} & step 6 & \texttt{compact/\allowbreak .\allowbreak parquet} & 30~s \\
8 & Link: \texttt{python ingestion/\allowbreak link\_\allowbreak sources.\allowbreak py --root \textasciitilde/\allowbreak wp44\_\allowbreak uqo} & steps 3--7 & \texttt{wp44.\allowbreak duckdb} with \texttt{link\_\allowbreak *}; \texttt{data/\allowbreak metadata/\allowbreak linkage\_\allowbreak report.\allowbreak json}; \texttt{needed\_\allowbreak oa\_\allowbreak ids.\allowbreak csv} & 10~s \\
9 & Second pass (DBLP- and arXiv-linked works outside the CS-topic filter): \texttt{python ingestion/\allowbreak openalex\_\allowbreak snapshot.\allowbreak py --out data/\allowbreak raw/\allowbreak openalex/\allowbreak works\_\allowbreak pass2 --manifest \ldots --workers 8 --id-list data/\allowbreak metadata/\allowbreak needed\_\allowbreak oa\_\allowbreak ids.\allowbreak csv}, then \texttt{ingestion/\allowbreak run\_\allowbreak pass2\_\allowbreak chain.\allowbreak sh} (steps 6--8 with suffix \texttt{\_\allowbreak p2}, then 10--11) & S3, step 8 & \texttt{works\_\allowbreak pass2/\allowbreak cs/\allowbreak *.\allowbreak jsonl.\allowbreak zst}, \texttt{compact/\allowbreak \_\allowbreak p2.\allowbreak parquet} & $\approx$1~h 50~min \\
10 & Corpus definition: \texttt{python systematic\_\allowbreak review/\allowbreak define\_\allowbreak corpus.\allowbreak py --root \textasciitilde/\allowbreak wp44\_\allowbreak uqo} & step 8 & \texttt{corpus\_\allowbreak works}, \texttt{data/\allowbreak metadata/\allowbreak prisma\_\allowbreak counts.\allowbreak json} & 2~min \\
11 & Volume, collaboration, citations, graph: \texttt{bibliometrics/\allowbreak 01\_\allowbreak volume\_\allowbreak series.\allowbreak py}, \texttt{02\_\allowbreak collaboration.\allowbreak py}, \texttt{03\_\allowbreak citation\_\allowbreak impact.\allowbreak py}, \texttt{knowledge\_\allowbreak graph/\allowbreak 01\_\allowbreak citation\_\allowbreak graph.\allowbreak py} & step 10 & \texttt{results/\allowbreak bibliometrics/\allowbreak *.\allowbreak csv}, \texttt{results/\allowbreak graph/\allowbreak *}, \texttt{data/\allowbreak processed/\allowbreak graph/\allowbreak *} & minutes \\
12 & Classification: \texttt{python taxonomy/\allowbreak classify.\allowbreak py --root \ldots --ontology taxonomy/\allowbreak ontology.\allowbreak yaml --prepare --run} (in practice \texttt{taxonomy/\allowbreak run\_\allowbreak classify\_\allowbreak shards.\allowbreak sh}, see incident I-3) & steps 8, 10; \texttt{ontology.\allowbreak yaml} & \texttt{classification/\allowbreak cats/\allowbreak *.\allowbreak parquet}, \texttt{classification/\allowbreak primary/\allowbreak *.\allowbreak parquet}, report & to be measured \\
13 & Validation sample and raters: \texttt{python taxonomy/\allowbreak validate\_\allowbreak sample.\allowbreak py --root \ldots --draw}; \texttt{--rate --model claude-haiku-5-5}; \texttt{--rate --model claude-sonnet-5-5}; \texttt{--metrics} & step 12 & \texttt{taxonomy/\allowbreak validation/\allowbreak *} & to be measured \\
14 & Embeddings: \texttt{python embeddings/\allowbreak embed\_\allowbreak corpus.\allowbreak py --root \ldots --prepare}; \texttt{--run --model allenai/\allowbreak specter2\_\allowbreak base} (sharded over nodes) & step 10 & \texttt{embeddings/\allowbreak /\allowbreak shard\_\allowbreak *.\allowbreak npy} & $\approx$300 texts/s per M3~Ultra \\
15 & Structural change, emerging detection, figures, tables: \texttt{econometrics/\allowbreak run\_\allowbreak structural\_\allowbreak change.\allowbreak py}, \texttt{experiments/\allowbreak emerging\_\allowbreak detection.\allowbreak py}, \texttt{figures/\allowbreak make\_\allowbreak figures.\allowbreak py}, \texttt{tables/\allowbreak make\_\allowbreak numbers.\allowbreak py} & steps 11--14 & \texttt{results/\allowbreak econometrics}, \texttt{results/\allowbreak emerging}, \texttt{figures/\allowbreak }, \texttt{manuscript/\allowbreak tables/\allowbreak numbers.\allowbreak tex} & minutes \\
16 & Manuscript: \texttt{cd manuscript \&\& make} (\texttt{latexmk}) & all & \texttt{manuscript/\allowbreak main.\allowbreak pdf} & --- \\
\end{longtable}
\end{footnotesize}
\end{singlespace}
The test suite (\texttt{python -m pytest -q tests/\allowbreak }) covers the structural-change toolkit and the
classification scoring rule (\texttt{tests/\allowbreak test\_\allowbreak structural\_\allowbreak change.\allowbreak py}, \texttt{tests/\allowbreak test\_\allowbreak classify\_\allowbreak rules.\allowbreak py});
the second fixes the implemented aggregation of equation~\eqref{eq:score} against the toy ontology it
declares, so that a change in \texttt{classify.\allowbreak py} that altered the rule would fail before any corpus were
classified.
\subsection{Provenance mechanisms}
\label{app:repro-provenance}
Provenance is kept at the level of files and jobs, not of individual records, by six mechanisms.
\paragraph{Manifests and hash ledgers for the OpenAlex snapshot.} The snapshot's own manifest
(\texttt{works\_\allowbreak manifest\_\allowbreak 2026-10-14.\allowbreak json}) lists every part file with its content length and record count.
The ingestion script keeps a SQLite ledger (\texttt{works/\allowbreak ledger.\allowbreak sqlite}) with a \texttt{files} table (one
row per part: URL, local name, expected length, manifest record count, status, SHA-256 of the gzip bytes,
records parsed, records retained by the filter, lines that failed to parse, download and total time, number
of attempts, start and end timestamps, error text) and a \texttt{runs} table (start time, manifest date,
manifest record and byte totals, number of workers, host name and command line of every invocation). A
part is downloaded with resume and retries until its size equals the manifest's, hashed, parsed, and only
then marked done; outputs are written to a temporary name and renamed atomically, so an interrupted run
leaves no partial output that a resumed run would mistake for a complete one. The raw gzip files are deleted
after a successful parse unless \texttt{--keep-raw} is given: the snapshot is a public, versioned release, and
the manifest together with the per-file hashes allows any later copy to be verified against what was
ingested. For the first pass the ledger records 1{,}898 of 1{,}898 files done, 0 parse errors, and a sum of
per-file record counts equal to the manifest total.
\paragraph{Entity ledger.} The small entity snapshots (topics, subfields, fields, domains, sources,
institutions, publishers, funders, concepts) are listed in \texttt{entities\_\allowbreak ledger.\allowbreak json} with, per file, the
entity, the local path, the byte count, the SHA-256 and the status.
\paragraph{DBLP checksum.} The DBLP dump is distributed with an \texttt{.md5} file; the downloaded
\texttt{dblp.\allowbreak xml.\allowbreak gz} was verified against it (\texttt{ff1c424c3b4cef36582f647c36df27c0}). The parse report
(\texttt{dblp\_\allowbreak parse\_\allowbreak report.\allowbreak json}) additionally records the input path, its size in bytes, an MD5 of its
first GiB (the file is 1.12~GB, so this digest is not the published one and is a convenience for detecting a
changed input), the parse timestamp, per-type and per-year record counts, and timings.
\paragraph{Verbatim OAI-PMH pages.} The arXiv harvest stores every XML response page as received under
\texttt{data/\allowbreak raw/\allowbreak arxiv/\allowbreak raw/\allowbreak /\allowbreak page\_\allowbreak NNNN.\allowbreak xml} (758 pages for the monthly windows from 2018-01 to
2026-11), together with a harvest state file that allows resumption by window and a parse report; the
Parquet table can be rebuilt from the pages alone with \texttt{--parse-only}. Because OAI selects by record
\emph{datestamp}, a record updated in several windows appears in several pages; the parser keeps the latest.
\paragraph{API ledger.} The OpenAlex API increment (works with a CS topic published between 2026-10-01 and
2026-11-02, fetched after the snapshot) stores every JSON page verbatim under \texttt{api\_\allowbreak increment/\allowbreak pages/\allowbreak }
and a ledger (\texttt{api\_\allowbreak increment/\allowbreak ledger.\allowbreak json}) with the slices requested (day $\times$ CS-primary or not),
the number of calls and the cost charged against the public budget; without an API key the harvest is paged
by day with at most 10{,}000 results per slice and spread over several days.
\paragraph{Step reports and generated numbers.} Each processing step writes a machine-readable report:
\texttt{linkage\_\allowbreak report.\allowbreak json} (counts of keys, links by method, unlinked records by type, identifiers needed
for the second pass), \texttt{prisma\_\allowbreak counts.\allowbreak json} (every PRISMA stage, with an identity check that the
records loaded equal the sum of sequential exclusions and the remainder), \texttt{classification\_\allowbreak report.\allowbreak json}
(ontology version, works scored, works classified, timestamp), one embedding manifest per host (model,
maximum sequence length, device, and per shard the number of texts, the seconds taken and the timestamp),
and the validation metrics. The macros of \texttt{manuscript/\allowbreak tables/\allowbreak numbers.\allowbreak tex} are generated from these
files by \texttt{tables/\allowbreak make\_\allowbreak numbers.\allowbreak py}; a number whose source file is missing yields an undefined macro
and therefore a compilation error, which is intended: the prose cannot cite a number that no result file
contains.
\subsection{Incident log and its implications}
\label{app:repro-incidents}
Six incidents were recorded during the first run (\texttt{reports/\allowbreak INCIDENTS.\allowbreak md}); none caused data loss,
because every step is resumable from its ledger or its shard files, but three of them changed how the
pipeline must be run and are therefore part of the reproduction instructions.
\begin{table}[htbp]
\begin{singlespace}
\centering
\caption{Incident log of the first run (\texttt{reports/\allowbreak INCIDENTS.\allowbreak md}; times in Eastern Time, 2026-11-03).}
\label{tab:appH-incidents}
\footnotesize
\begin{tabularx}{\textwidth}{l l X X}
\toprule
\# & When & What happened & Impact and recovery \\
\midrule
I-1 & $\approx$15:45 & Second snapshot pass with \texttt{--id-list}: worker processes had an empty identifier set, because the macOS \texttt{spawn} start method does not inherit module globals & 68 part files marked in error, no data loss; a pool initialiser now loads the identifier set in each worker; ledger reset and rerun \\
I-2 & $\approx$15:50 & DuckDB ``too many open files'' when reading 1{,}898 Parquet files per table & Linkage step failed; \texttt{compact\_\allowbreak tables.\allowbreak py} (one file per table) and \texttt{ulimit -n 65536} \\
I-3 & $\approx$16:30 & \texttt{multiprocessing.\allowbreak Pool} with pyarrow in spawned workers hung (0\,\% CPU) in the classifier & Test stalled 10~min; shard mode: independent processes started by \texttt{run\_\allowbreak classify\_\allowbreak shards.\allowbreak sh} \\
I-4 & $\approx$16:58 & M3U96b froze (no ping, SSH ``host is down'') while running the second pass (8 download-and-parse workers), the SPECTER2 embedding job on the GPU and earlier DuckDB jobs concurrently & Second pass ($\approx$1{,}400 of 1{,}898 files done), the chained corpus v2 and classification, and the node's embedding share interrupted; other embedding nodes unaffected; all jobs resumable; rule adopted: never run ingestion, embeddings and classification concurrently on one node \\
I-5 & 17:16 & M3U96b froze again two minutes after the second pass was restarted alone on the rebooted node & Pass~2 moved to the idle node M5M36 (10 workers, ledger copied); M3U96b reserved for DuckDB and classification; the chain now waits for a flag raised when the pass-2 outputs are copied back; Apple diagnostics show performance auto-bug-capture reports, no kernel panic found \\
I-6 & 17:17 & M4M64a froze a second time within two minutes of restarting its embedding share (batch 32); M3U96b rebooted in the same minute while running pass~2 alone & M4M64a embedding share lost again; M4M64a and M3U96b withdrawn from GPU and bulk-download work; shard ranges reassigned to M4M64c and M3U96a; pass~2 on M5M36 \\
\bottomrule
\end{tabularx}
\end{singlespace}
\end{table}
Three implications follow for anyone reproducing the work. First, the parallelism in the scripts assumes
the \texttt{spawn} start method of macOS: worker state must be built in initialisers, and the classifier
should be run as independent shard processes rather than through a pool (\texttt{classify.\allowbreak py --shard i/\allowbreak N});
on Linux, where \texttt{fork} is the default, the pool mode may work but has not been exercised. Second, the
per-part Parquet layout exceeds default file-descriptor limits; the compaction step is mandatory, not an
optimisation. Third, the two freezes of the primary node occurred under concurrent download, parse, GPU
and database load, and their cause was not established from the system logs; the operational rule adopted
afterwards (one heavy job per node at a time) costs wall time but is the configuration under which the
reported outputs were produced, and the measured times of Table~\ref{tab:appH-steps} assume it. Because
every job writes its outputs atomically and records what it has completed, the state of the pipeline after
an interruption is recoverable from the ledgers alone, and the outputs of a resumed run are identical to
those of an uninterrupted one for all deterministic steps (ingestion, normalisation, linkage, corpus
definition, classification). The embedding step is the exception: shards were computed on several nodes and
devices, and the per-host manifests record for each shard where it was computed, so that any numerical
difference between devices can be traced; the embeddings are stored in half precision and are not claimed
to be bit-identical across hardware.
\subsection{Protocol amendments}
\label{app:repro-amendments}
The protocol was frozen on 2026-11-02 before any analysis of the corpus, and every later change is an
amendment with an identifier, a reason and an approver (\texttt{protocol/\allowbreak amendments.\allowbreak csv}). All six amendments
of Table~\ref{tab:appH-amendments} are approved by the executing agent and pending the principal
investigator's signature at the checkpoint of the corresponding phase; the manuscript marks the affected
conclusions accordingly.
\begin{table}[htbp]
\begin{singlespace}
\centering
\caption{Protocol amendments (\texttt{protocol/\allowbreak amendments.\allowbreak csv}).}
\label{tab:appH-amendments}
\footnotesize
\begin{tabularx}{\textwidth}{l l l X X}
\toprule
Id & Date & Phase & Change & Reason \\
\midrule
A-000 & 2026-11-02 & 0 & \S14 of the mandate completed from \S1--\S13 & The pasted mandate was truncated by the terminal \\
A-001 & 2026-11-03 & 1 & Eligibility type list corrected: conference-paper and software-paper are kept; DBLP-linked records are kept whatever their type unless hard-excluded; XPAC records kept when DBLP-linked (\texttt{xpac\_\allowbreak kept\_\allowbreak dblp}) & The first run excluded 2.5~M conference-paper records (1.09~M DBLP-linked) through an outdated type list \\
A-002 & 2026-11-03 & 1 & Records whose primary source is a generic deposit platform are excluded unless DBLP- or arXiv-linked (E7); \texttt{date\_\allowbreak precision} added, monthly series use month-precision records only & Zenodo alone contributed 138{,}533 otherwise eligible 2026 records (deposits of slides, datasets and reports typed as articles); 18--28\,\% of records carry a year-only date \\
A-003 & 2026-11-03 & 2 & LLM rating budget for the taxonomy validation fixed at 25~USD (two raters, structured outputs) & Mandate \S7 requires agent-based validation with human checkpoints; the PI rates 200 works \\
A-004 & 2026-11-03 & 3 & Knowledge-graph extraction from abstracts of the high-priority set with \texttt{claude-sonnet-5-5}, provenance spans and machine verification of spans; budget 150~USD & Mandate \S6.4 requires provenance, confidence and verification status on every inferred relationship \\
A-005 & 2026-11-03 & 1 & Exclusion code E7 redefined as the generic-deposit exclusion; the CS penumbra is a membership flag, not an exclusion code & The Part~IV writer found the protocol and code labels inconsistent \\
\bottomrule
\end{tabularx}
\end{singlespace}
\end{table}
\subsection{Sources, licences and what is redistributed}
\label{app:repro-licences}
Table~\ref{tab:appH-raw} lists each raw source as obtained, with its size, integrity record and licence.
Nothing under \texttt{data/\allowbreak raw/\allowbreak } or \texttt{data/\allowbreak processed/\allowbreak } is committed to the repository
(\texttt{.gitignore} excludes both, together with every Parquet and JSONL file, except the provenance
\texttt{README} and the metadata directory); the copies live on the compute node. What the released
repository contains is the code, the ontology and its audit table, the manifests, ledgers and hashes, the
PRISMA counts and the linkage report, the aggregated result tables, the figures, the generated number macros
and the manuscript (protocol \S P8). Record-level derived tables may be released where the source licence
permits: OpenAlex is CC0, DBLP is ODC-BY~1.0, arXiv metadata obtained through OAI-PMH is under the CC0
terms of that feed, and the ACL Anthology is CC~BY~4.0; a derived record table therefore carries the
attribution required by ODC-BY and CC~BY for the DBLP and ACL fields it contains. Full texts are never
redistributed and were never obtained through paywalls. The Semantic Scholar Academic Graph was not used:
bulk access requires an API key that was not available and anonymous calls were refused, and the mandate's
instruction to record inaccessible data explicitly is satisfied by this note; influential-citation and
citation-intent signals derived from that graph are consequently absent from the analyses.
\begin{table}[htbp]
\begin{singlespace}
\centering
\caption{Raw sources as obtained (\texttt{data/\allowbreak raw/\allowbreak README.\allowbreak md}), with integrity record and licence.}
\label{tab:appH-raw}
\footnotesize
\begin{tabularx}{\textwidth}{p{2.6cm} X p{3.6cm} p{2.0cm}}
\toprule
Source & What was obtained, when, size & Integrity & Licence \\
\midrule
OpenAlex works snapshot, release 2026-10-14 & \texttt{data/\allowbreak jsonl/\allowbreak works/\allowbreak }: 1{,}898 part files, 464{,}765{,}485 works, 675.3~GB gzip; streamed 2026-11-02 21:19--23:05~ET over HTTPS with 8 parallel streams; retained: CS subset 20~GB (15{,}490{,}159 records) and slim table 9.3~GB (all works) & manifest; per-file content length and SHA-256 in \texttt{works/\allowbreak ledger.\allowbreak sqlite} (1{,}898/1{,}898 ok, 0 parse errors, record total = manifest total) & CC0 \\
OpenAlex entity snapshots & topics 4{,}516; subfields 252; fields 26; domains 4; sources 268{,}629; institutions 143{,}455; publishers 10{,}713; funders 41{,}799; concepts 65{,}026; 2026-11-02; 0.6~GB & \texttt{entities\_\allowbreak ledger.\allowbreak json} (SHA-256 per file) & CC0 \\
OpenAlex API increment & works with any CS topic, publication date 2026-10-01 to 2026-11-02, sliced by day and CS-primary status, basic paging $\le$10{,}000 per slice; from 2026-11-03 (public budget 0.10~USD/day, several days) & raw JSON pages; \texttt{api\_\allowbreak increment/\allowbreak ledger.\allowbreak json} (counts, cost) & CC0 \\
DBLP & \texttt{dblp.\allowbreak xml.\allowbreak gz} (2026-11-02 22:39~UTC), \texttt{dblp.\allowbreak dtd}, \texttt{.md5}; 1.12~GB & MD5 \texttt{ff1c424c\ldots27c0} verified; parse report (8{,}840{,}866 records) & ODC-BY 1.0 \\
arXiv OAI-PMH & set \texttt{cs}, metadata format \texttt{arXiv}, datestamps 2018-01-01 to 2026-11-02 in monthly windows, raw XML pages kept; 2026-11-02/03 & 758 pages, 915{,}115 unique records; \texttt{harvest\_\allowbreak state.\allowbreak json}, parse report & CC0 (metadata) \\
ACL Anthology & \texttt{anthology+abstracts.\allowbreak bib.\allowbreak gz}; 2026-11-02; 46.7~MB, 138{,}796 entries & SHA-256 \texttt{44c3f7e9\ldots58e3} & CC BY 4.0 \\
\bottomrule
\end{tabularx}
\end{singlespace}
\end{table}
\subsection{Minimal reproduction sequence}
\label{app:repro-minimal}
A reader with a macOS or Linux machine, disk for the retained outputs (the CS subset and the slim table
occupy about 30~GB; the 675.3~GB of gzip parts are streamed and deleted after parsing unless
\texttt{--keep-raw} is given), memory for the DuckDB steps as configured (the scripts set
\texttt{memory\_\allowbreak limit} to 60--70~GB, to be lowered on smaller machines at the cost of time) and no
credentials can reproduce the corpus and the
bibliometric tables by creating the environment (\texttt{uv venv --python 3.\allowbreak 12 .\allowbreak venv \&\& uv pip install -r
requirements.\allowbreak txt}), downloading the manifest of the 2026-10-14 release, and running steps~1--11 of
Table~\ref{tab:appH-steps} in order, with the second pass of step~9 between steps~8 and~10. The snapshot
release is versioned, so a later release will not reproduce the counts; the manifest date is the parameter
that pins them. Steps~12 and~14 need no credentials but benefit from several nodes; step~13 and the
knowledge-graph extraction of amendment A-004 need an Anthropic API key and a budget of 25 and 150~USD
respectively, and their outputs (ratings, extracted edges) are released so that the downstream statistics
can be recomputed without re-rating. Step~15 regenerates every figure, table and number macro from the
result files, and step~16 compiles the manuscript; a missing result file surfaces as an undefined macro at
compilation, which is the intended way of detecting that a number has no source.