SPB Git

spb/wp44_uqo

Public

WP44: phase 0 (feasibility, plan, protocol) + phase 1 ingestion pipelines

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Nov 3, 2026) Browse files

28 changed files +10,294 −0

added .gitignore +54 −0
@@ -0,0 +1,54 @@
1+# Raw & processed data — never committed (see data/raw/README.md for provenance)
2+data/raw/*
3+!data/raw/README.md
4+data/processed/*
5+!data/processed/.gitkeep
6+
7+# Python
8+__pycache__/
9+*.pyc
10+.pytest_cache/
11+.venv/
12+*.egg-info/
13+
14+# LaTeX build artifacts (paper/main.pdf IS committed)
15+paper/*.aux
16+paper/*.log
17+paper/*.out
18+paper/*.toc
19+paper/*.lof
20+paper/*.lot
21+paper/*.fls
22+paper/*.fdb_latexmk
23+paper/*.bbl
24+paper/*.blg
25+paper/*.spl
26+paper/*.synctex.gz
27+paper/*.run.xml
28+paper/*.bcf
29+
30+# Logs
31+logs/
32+*.log
33+
34+# macOS
35+.DS_Store
36+
37+# WP44 specifics
38+data/metadata/*.sqlite
39+logs/
40+*.jsonl.zst
41+*.parquet
42+!data/metadata/**/*.parquet
43+manuscript/*.aux
44+manuscript/*.log
45+manuscript/*.out
46+manuscript/*.toc
47+manuscript/*.lof
48+manuscript/*.lot
49+manuscript/*.fls
50+manuscript/*.fdb_latexmk
51+manuscript/*.bbl
52+manuscript/*.blg
53+manuscript/*.synctex.gz
54+.venv/
added CLAUDE.md +527 −0
@@ -0,0 +1,527 @@
1+# CLAUDE.md — UQO Working Paper 44
2+
3+> Mandate pasted verbatim by the principal investigator on 2026-11-02 ("fait ce projet rigoureusement"). The paste was
4+> truncated by the terminal inside §14 (after "The corpus is documented and reproduc"); the executing agent completed §14
5+> from the acceptance criteria implied by §1–§13 and recorded the completion in `protocol/amendments.csv` (A-000).
6+> Operational decisions are in `RESEARCH_PLAN.md`; progress in `PROJECT_STATUS.md`.
7+
8+## Project Identity
9+
10+**Working Paper:** WP44 — UQO Working Paper Series
11+
12+**Proposed Title:** *The Post-ChatGPT Computer Science Revolution: A Comprehensive Systematic Mapping, Bibliometric Analysis, and Knowledge Graph of Computer Science Research, 2022–2026*
13+
14+**Research period:** November 30, 2022 – November 2, 2026
15+
16+**Historical baseline:** January 1, 2018 – November 29, 2022
17+
18+**Primary language:** English
19+
20+**Target manuscript:** 300–500 pages, including extensive technical appendices.
21+
22+**Scientific objective:** Produce one of the most comprehensive, rigorously documented, reproducible computational mappings of contemporary computer science research.
23+
24+The ambition is to build an original scientific contribution, not merely a long literature summary.
25+
26+Never claim that this is the largest review ever conducted without independently establishing that claim.
27+
28+---
29+
30+# 1. NON-NEGOTIABLE SCIENTIFIC REQUIREMENTS
31+
32+The project must:
33+
34+1. Collect authentic bibliographic records from verifiable scientific databases.
35+2. Build a unified, deduplicated corpus with persistent identifiers.
36+3. Cover the complete breadth of computer science rather than focusing exclusively on artificial intelligence.
37+4. Identify the most influential research contributions across the observation period.
38+5. Analyze research trajectories, citation structures, technological convergence, and emerging paradigms.
39+6. Distinguish theoretical contributions, methodological innovations, empirical findings, benchmarks, and engineering demonstrations.
40+7. Critically evaluate conflicting findings and limitations.
41+8. Produce an evidence-linked scientific manuscript.
42+9. Make the methodology reproducible.
43+10. Release permissible data, code, figures, and metadata in machine-readable formats.
44+
45+**Strict integrity rules:**
46+
47+- Never fabricate references, DOI identifiers, citation counts, experiments, or results.
48+- Never invent paper content when only metadata is available.
49+- Never treat an abstract as equivalent to a full-text review.
50+- Never classify unverified model-generated summaries as scientific evidence.
51+- Never interpret temporal association as causation.
52+- Never describe an unpublished preprint as peer-reviewed.
53+- Never fabricate statistical significance.
54+- Record unavailable or inaccessible data explicitly.
55+- Maintain human-review checkpoints for scientific conclusions.
56+
57+# 2. PROJECT SCALE
58+
59+Initial engineering targets:
60+
61+- 1,000,000+ candidate bibliographic records, subject to actual availability.
62+- 100,000+ deeply classified research records.
63+- 10,000+ high-priority papers for detailed evidence extraction, subject to lawful full-text access.
64+- 1,000+ landmark papers for intensive critical synthesis.
65+- 30+ computer science research categories.
66+- 100+ publication-quality figures.
67+- 100+ analytical tables.
68+- 300–500 manuscript pages including appendices.
69+
70+These are planning targets, not guaranteed outcomes.
71+
72+Corpus size must never take precedence over validity, relevance, or quality.
73+
74+# 3. SCIENTIFIC DATA SOURCES
75+
76+Prioritize:
77+
78+- DBLP
79+- OpenAlex
80+- Semantic Scholar Academic Graph
81+- arXiv
82+- Crossref
83+- ACL Anthology
84+- Open-access conference proceedings
85+- Institutional repositories
86+- Publicly available benchmark repositories
87+
88+Before ingestion, verify current API specifications, rate limits, licensing, authentication, bulk-access policies, and redistribution permissions.
89+
90+Prefer official snapshots and documented bulk datasets for large-scale ingestion.
91+
92+Never bypass access controls or publisher restrictions.
93+
94+# 4. RESEARCH TAXONOMY
95+
96+Build a hierarchical taxonomy including:
97+
98+1. Large Language Models
99+2. Foundation Models
100+3. Multimodal AI
101+4. Generative AI
102+5. AI Agents and Autonomous Systems
103+6. Reinforcement Learning
104+7. Machine Learning Theory
105+8. Deep Learning Architectures
106+9. Computer Vision
107+10. Natural Language Processing
108+11. Speech and Audio Computing
109+12. Robotics
110+13. Human-Computer Interaction
111+14. Software Engineering
112+15. Programming Languages
113+16. Compilers
114+17. Formal Verification
115+18. Algorithms and Data Structures
116+19. Computational Complexity
117+20. Database Systems
118+21. Information Retrieval
119+22. Distributed Computing
120+23. Cloud Computing
121+24. Edge Computing
122+25. Computer Networks
123+26. Cybersecurity
124+27. Applied Cryptography
125+28. Blockchain Systems
126+29. Computer Architecture
127+30. Accelerators and AI Hardware
128+31. Operating Systems
129+32. Quantum Computing
130+33. Scientific Computing
131+34. Computational Biology
132+35. Computer Graphics
133+36. Privacy-Preserving Computation
134+37. Federated Learning
135+38. AI Safety and Alignment
136+39. Sustainable Computing
137+40. Autonomous Scientific Discovery
138+
139+Allow papers to belong to multiple categories.
140+
141+Construct an extensible hierarchical ontology rather than a flat keyword classification.
142+
143+# 5. SCIENTIFIC QUESTIONS
144+
145+The paper must investigate:
146+
147+**RQ1:** How has the volume and composition of computer science research evolved since November 2022?
148+
149+**RQ2:** Which subfields have experienced the strongest acceleration or structural change?
150+
151+**RQ3:** Which publications introduced the most influential methodological or theoretical contributions?
152+
153+**RQ4:** How have citation networks and interdisciplinary knowledge flows evolved?
154+
155+**RQ5:** Has the emergence of foundation models changed the relationship between previously distinct computer science disciplines?
156+
157+**RQ6:** How have research institutions, countries, and collaboration networks changed?
158+
159+**RQ7:** Which research areas demonstrate genuine breakthroughs versus incremental benchmark improvements?
160+
161+**RQ8:** Which scientific problems remain unresolved despite rapid publication growth?
162+
163+**RQ9:** How have reproducibility, open-source availability, and evaluation standards evolved?
164+
165+**RQ10:** Can computational models detect emerging scientific paradigms before they become dominant?
166+
167+# 6. RESEARCH METHODOLOGY
168+
169+Implement multiple complementary analytical approaches.
170+
171+## 6.1 Systematic Mapping
172+
173+Follow a preregistered protocol where practical.
174+
175+Use PRISMA 2020 reporting principles where applicable.
176+
177+Document:
178+
179+- Search strategies
180+- Database coverage
181+- Eligibility criteria
182+- Deduplication
183+- Screening decisions
184+- Full-text availability
185+- Exclusion reasons
186+- Quality assessment
187+- Final synthesis populations
188+
189+Separate broad bibliometric mapping from focused systematic reviews.
190+
191+## 6.2 Bibliometric Analysis
192+
193+Estimate:
194+
195+- Publication growth
196+- Citation growth
197+- Field-normalized impact
198+- Citation velocity
199+- Collaboration networks
200+- Institutional influence
201+- Country-level participation
202+- Venue concentration
203+- Citation inequality
204+- Research topic diversification
205+
206+Account for citation-age bias, database coverage, and preprint duplication.
207+
208+## 6.3 Semantic Research Mapping
209+
210+Use scientific document embeddings, topic modeling, hierarchical clustering, and temporal semantic analysis.
211+
212+Compare multiple embedding approaches and validate topic stability.
213+
214+Construct a dynamic semantic atlas of computer science.
215+
216+## 6.4 Scientific Knowledge Graph
217+
218+Create nodes for:
219+
220+- Papers
221+- Authors
222+- Institutions
223+- Venues
224+- Research topics
225+- Algorithms
226+- Models
227+- Datasets
228+- Benchmarks
229+- Scientific claims
230+
231+Create evidence-supported relationships including:
232+
233+- Cites
234+- Extends
235+- Evaluates
236+- Contradicts
237+- Reproduces
238+- Uses dataset
239+- Introduces method
240+- Addresses research problem
241+
242+Every inferred relationship must include provenance, confidence, and verification status.
243+
244+## 6.5 Structural Change Analysis
245+
246+Use interrupted time-series models, changepoint detection, dynamic network models, and appropriate robustness checks.
247+
248+The November 2022 boundary is a historical reference point, not proof of a causal ChatGPT effect.
249+
250+Include pre-period trend diagnostics, placebo breakpoints, alternative event dates, and sensitivity analysis.
251+
252+## 6.6 Emerging Research Detection
253+
254+Develop an original computational framework for identifying emerging scientific fields.
255+
256+Candidate signals:
257+
258+- Abnormal publication growth
259+- Semantic novelty
260+- Citation acceleration
261+- Interdisciplinary connectivity
262+- Methodological adoption
263+- Benchmark proliferation
264+- Open-source ecosystem activity
265+
266+Evaluate the framework retrospectively using temporally separated training and test periods.
267+
268+Avoid look-ahead bias.
269+
270+# 7. AUTONOMOUS RESEARCH AGENTS
271+
272+Use Claude Code subagents with explicit responsibilities.
273+
274+**Agent A — Research Director:** Coordinates the scientific program and validates deliverables.
275+
276+**Agent B — Bibliographic Acquisition:** Collects metadata through legitimate APIs and snapshots.
277+
278+**Agent C — Data Engineering:** Builds normalized databases and reproducible ingestion pipelines.
279+
280+**Agent D — Taxonomy Architect:** Constructs the computer science ontology.
281+
282+**Agent E — AI Research Specialist:** Reviews AI, machine learning, foundation models, and agents.
283+
284+**Agent F — Systems Specialist:** Reviews distributed systems, operating systems, databases, and hardware.
285+
286+**Agent G — Theory Specialist:** Reviews algorithms, complexity, formal methods, and programming languages.
287+
288+**Agent H — Security Specialist:** Reviews cybersecurity, privacy, and cryptography.
289+
290+**Agent I — Bibliometrician:** Conducts citation and collaboration analyses.
291+
292+**Agent J — Econometrician:** Designs temporal models, statistical inference, and robustness tests.
293+
294+**Agent K — Scientific Critic:** Challenges unsupported claims and methodological weaknesses.
295+
296+**Agent L — Academic Writer:** Produces publication-quality scientific prose.
297+
298+**Agent M — LaTeX Engineer:** Manages equations, references, tables, figures, and compilation.
299+
300+**Agent N — Reproducibility Auditor:** Independently validates computations and evidence.
301+
302+**Agent O — Visualization Specialist:** Creates publication-quality research graphics.
303+
304+Agents must exchange structured artifacts, not undocumented conversational summaries.
305+
306+No agent may approve its own unverified scientific findings.
307+
308+# 8. MACLUSTR INFRASTRUCTURE
309+
310+Discover the available compute nodes, CPU, GPU, memory, storage, networking, and existing services before implementation.
311+
312+Use distributed execution only when it materially improves throughput.
313+
314+Suggested stack:
315+
316+- Python
317+- DuckDB
318+- PostgreSQL
319+- Parquet and Apache Arrow
320+- Polars
321+- NetworkX or igraph
322+- Scientific embedding models
323+- Vector search
324+- PyTorch where appropriate
325+- LaTeX
326+- Git
327+- Containerized reproducible pipelines
328+
329+Use incremental ingestion, checkpointing, resumable processing, and content hashes.
330+
331+Preserve immutable raw-source snapshots.
332+
333+Implement job-level provenance, retry logic, and resource monitoring.
334+
335+Do not assume access to credentials or services before testing them.
336+
337+# 9. MANUSCRIPT STRUCTURE
338+
339+Produce the following major parts.
340+
341+**Part I — Introduction and Scientific Motivation**
342+
343+Explain the research problem, historical context, contributions, and scope.
344+
345+**Part II — The Evolution of Computer Science**
346+
347+Provide the conceptual and historical background needed to interpret post-2022 developments.
348+
349+**Part III — Data and Systematic Review Protocol**
350+
351+Explain corpus construction, source coverage, deduplication, eligibility, and research integrity.
352+
353+**Part IV — Computational Methodology**
354+
355+Develop the mathematical framework for bibliometrics, semantic mapping, networks, and structural-change analysis.
356+
357+**Part V — The Artificial Intelligence Transformation**
358+
359+Critically synthesize foundation models, multimodal learning, agents, reasoning, efficiency, evaluation, and safety.
360+
361+**Part VI — Systems and Computing Infrastructure**
362+
363+Review hardware, distributed systems, databases, networks, and operating systems.
364+
365+**Part VII — Theory, Algorithms, and Security**
366+
367+Review theoretical computer science, formal methods, cryptography, and cybersecurity.
368+
369+**Part VIII — Scientific Knowledge Networks**
370+
371+Analyze authors, institutions, citations, interdisciplinarity, and knowledge diffusion.
372+
373+**Part IX — Empirical Results**
374+
375+Present all verified quantitative findings with publication-quality figures and tables.
376+
377+**Part X — The Next Research Frontiers**
378+
379+Identify unresolved scientific problems, competing hypotheses, and testable research directions.
380+
381+**Part XI — Discussion and Conclusion**
382+
383+Explain contributions, limitations, generalizability, and implications for computer science.
384+
385+**Appendices A–Z**
386+
387+Include detailed taxonomy, search strategies, mathematical derivations, robustness checks, data dictionaries, supplementary tables, figure documentation, and reproducibility instructions.
388+
389+Do not pad the manuscript to meet page targets.
390+
391+# 10. ACADEMIC WRITING STANDARD
392+
393+Write in formal, precise, technically sophisticated academic English.
394+
395+Target the scholarly standards of leading computer science journals and major survey venues.
396+
397+The writing must:
398+
399+- Critically compare competing approaches.
400+- Distinguish established findings from speculation.
401+- Present meaningful mathematical notation.
402+- Explain assumptions and limitations.
403+- Integrate citations into the scientific argument.
404+- Avoid promotional or exaggerated language.
405+- Avoid repetitive AI-generated prose.
406+- Include genuine original synthesis.
407+
408+All references must be resolved to verified bibliographic records.
409+
410+# 11. FIGURES AND TABLES
411+
412+Create publication-quality visualizations including:
413+
414+- Annual and monthly publication trends
415+- Research domain growth
416+- Dynamic topic maps
417+- Citation networks
418+- Scientific influence distributions
419+- Institutional collaboration networks
420+- Country-level research activity
421+- Topic emergence and decline
422+- Interdisciplinary knowledge flows
423+- Methodological genealogy diagrams
424+- Structural-break estimates
425+- Benchmark evolution
426+- Research reproducibility indicators
427+- Uncertainty and robustness visualizations
428+
429+All figures must be generated from reproducible scripts.
430+
431+Use vector formats when appropriate.
432+
433+No decorative or fabricated empirical graphics.
434+
435+# 12. OUTPUT STRUCTURE
436+
437+Create:
438+
439+```text
440+WP44/
441+├── CLAUDE.md
442+├── README.md
443+├── manuscript/
444+│ ├── main.tex
445+│ ├── sections/
446+│ ├── appendices/
447+│ └── references.bib
448+├── data/
449+│ ├── raw/
450+│ ├── processed/
451+│ └── metadata/
452+├── ingestion/
453+├── taxonomy/
454+├── embeddings/
455+├── knowledge_graph/
456+├── bibliometrics/
457+├── econometrics/
458+├── systematic_review/
459+├── literature_synthesis/
460+├── figures/
461+├── tables/
462+├── experiments/
463+├── agents/
464+├── tests/
465+├── reports/
466+└── reproducibility/
467+```
468+
469+# 13. EXECUTION PHASES
470+
471+**Phase 0 — Feasibility and Scientific Protocol**
472+
473+Audit available infrastructure, verify data sources, define inclusion criteria, and prepare the research protocol.
474+
475+**Phase 1 — Bibliographic Corpus**
476+
477+Build ingestion pipelines and create the deduplicated research database.
478+
479+**Phase 2 — Scientific Taxonomy**
480+
481+Classify the corpus and validate the ontology using manually reviewed samples.
482+
483+**Phase 3 — Knowledge Graph**
484+
485+Construct citation, semantic, institutional, and methodological relationships.
486+
487+**Phase 4 — Statistical Research**
488+
489+Estimate bibliometric trends, network measures, and temporal models.
490+
491+**Phase 5 — Critical Literature Synthesis**
492+
493+Generate evidence-grounded disciplinary reviews and reconcile conflicting findings.
494+
495+**Phase 6 — Original Scientific Contribution**
496+
497+Develop and validate the emerging-research detection framework.
498+
499+**Phase 7 — Manuscript Construction**
500+
501+Write the full LaTeX paper and comprehensive appendices.
502+
503+**Phase 8 — Independent Scientific Audit**
504+
505+Use independent review agents to verify citations, statistical results, claims, equations, and reproducibility.
506+
507+**Phase 9 — Final Research Release**
508+
509+Compile the final PDF, archive reproducibility artifacts, prepare the dataset documentation, and produce a release-ready research repository.
510+
511+Do not advance to the next phase until required validation checks pass.
512+
513+# 14. ACCEPTANCE CRITERIA
514+
515+The project is complete only when:
516+
517+- The corpus is documented and reproduc[ible — paste truncated here. Completed by the executing agent (A-000):]
518+- The corpus is documented and reproducible from the immutable raw snapshots, with record counts at every PRISMA stage.
519+- Every quantitative claim in the manuscript traces to a script, an input artefact and a result file.
520+- Every reference in `references.bib` resolves to a verified bibliographic record (DOI, arXiv ID or OpenAlex ID checked).
521+- The taxonomy has been validated on a manually reviewed sample with reported agreement statistics.
522+- Structural-change results include pre-trend diagnostics, placebo breakpoints and sensitivity analyses.
523+- The emerging-research detection framework has been evaluated out-of-time without look-ahead.
524+- Independent audit agents have verified citations, statistics, equations and reproducibility, and their reports are archived.
525+- The LaTeX manuscript compiles without unresolved references, and the release repository contains code, metadata,
526+ figures, tables and documentation under an explicit licence.
527+- Human-review checkpoints for scientific conclusions are recorded and any unreviewed conclusion is flagged as such.
added PROJECT_STATUS.md +14 −0
@@ -0,0 +1,14 @@
1+# PROJECT_STATUS — WP44 (updated by the executing agent; times ET)
2+
3+| Phase | Status | Evidence |
4+|---|---|---|
5+| 0 Feasibility & protocol | done 2026-11-02 (pending PI checkpoint H-0) | `reports/PHASE0_FEASIBILITY.md`, `RESEARCH_PLAN.md`, `protocol/PROTOCOL.md` |
6+| 1 Bibliographic corpus | in progress — OpenAlex snapshot ingestion running on M3U96b; DBLP parsed; ACL parsed; arXiv harvest running | `logs/` on M3U96b, `data/metadata/` |
7+| 2 Taxonomy | in progress — Agent D building `taxonomy/ontology.yaml` | `agents/briefs/AGENT_D_TAXONOMY.md` |
8+| 3 Knowledge graph | pending | |
9+| 4 Statistics | pending | |
10+| 5 Literature synthesis | pending | |
11+| 6 Emerging-research detection | pending | |
12+| 7 Manuscript | pending | |
13+| 8 Independent audit | pending | |
14+| 9 Release | pending | |
added README.md +75 −0
@@ -0,0 +1,75 @@
1+<!-- Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca -->
2+# 🧭 The Post-ChatGPT Computer Science Revolution
3+
4+**UQO Working Paper No. 44** — *A Comprehensive Systematic Mapping, Bibliometric Analysis, and Knowledge Graph of Computer Science Research, 2022–2026*
5+
6+[![Paper](https://img.shields.io/badge/paper-in%20preparation-lightgrey?logo=latex&logoColor=white)](manuscript/)
7+[![Python](https://img.shields.io/badge/python-3.12-3776AB?logo=python&logoColor=white)](requirements.txt)
8+[![Data](https://img.shields.io/badge/data-OpenAlex%202026--10--14%20·%20DBLP%20·%20arXiv%20·%20ACL-4051b5)](data/raw/README.md)
9+[![Reproducible](https://img.shields.io/badge/reproducible-ledgers%20%2B%20hashes-2e7d32)](reproducibility/)
10+[![Institution](https://img.shields.io/badge/UQO-D%C3%A9pt.%20des%20sciences%20administratives-16365c)](https://uqo.ca)
11+[![Author](https://img.shields.io/badge/author-Simon--Pierre%20Boucher-16365c)](mailto:simon-pierre.boucher@uqo.ca)
12+
13+> **TL;DR** — Field-level systematic mapping of computer-science research from 2018-01-01 to 2026-11-02 (baseline
14+> 2018 → 2022-11-29; post-ChatGPT window 2022-11-30 → 2026-11-02) built from the full OpenAlex public snapshot
15+> (release 2026-10-14; 464.8 M works scanned, 15.49 M with a computer-science topic in 2018–2026), the DBLP dump of
16+> 2026-11-02 (8.84 M records), the arXiv `cs` OAI-PMH harvest (915 k records) and the ACL Anthology (139 k entries),
17+> linked, deduplicated and classified into a 40-category ontology; bibliometric, network, semantic, structural-change and
18+> emerging-topic analyses; knowledge graph with provenance; preregistered protocol (`protocol/PROTOCOL.md`).
19+> Status: Phase 1 (corpus) in progress — see `PROJECT_STATUS.md`.
20+
21+---
22+
23+## 📌 Status
24+
25+| Item | Value |
26+|---|---|
27+| Version | 0.1 (November 2026, in preparation) |
28+| SSRN | not yet submitted |
29+| Journal | — |
30+| Code & data | reproducible from `ingestion/` → `bibliometrics/` → `manuscript/` (see Quick start); raw snapshots not redistributed (manifests, ledgers and SHA-256 hashes are) |
31+
32+## 🎯 What this paper does
33+
34+Mandate: `CLAUDE.md` (verbatim). Design: `RESEARCH_PLAN.md`. Protocol: `protocol/PROTOCOL.md` (frozen 2026-11-02;
35+amendments in `protocol/amendments.csv`). Feasibility audit: `reports/PHASE0_FEASIBILITY.md`.
36+
37+## 🗂 Repository layout
38+
39+```
40+wp44/ (spbgit: wp44_uqo.git)
41+├── CLAUDE.md · RESEARCH_PLAN.md · PROJECT_STATUS.md · protocol/{PROTOCOL.md,amendments.csv}
42+├── ingestion/ openalex_snapshot.py (S3 snapshot → CS JSONL + slim table, SHA-256 ledger), openalex_entities.py,
43+│ openalex_api_increment.py, dblp_parse.py, dblp_streams.py, arxiv_oai.py, acl_parse.py,
44+│ build_corpus.py (normalised Parquet tables), link_sources.py (record linkage, DuckDB)
45+├── taxonomy/ ontology.yaml (40 leaves, 8 areas), topic_map.csv, ONTOLOGY.md, src/ (mapping data), build_ontology.py
46+├── embeddings/ · knowledge_graph/ · bibliometrics/ · econometrics/ · systematic_review/ · literature_synthesis/
47+├── experiments/ emerging-research detection framework
48+├── agents/ briefs/ (one per agent), REGISTER.md (artefacts exchanged, who verified what)
49+├── data/ raw/README.md (provenance), metadata/ (manifests, ledgers, PRISMA counts, linkage report), processed/ (not in git)
50+├── figures/ · tables/ · reports/ · tests/ · reproducibility/
51+└── manuscript/ main.tex, sections/, appendices/, references.bib, figures/, tables/ (mandate §12 layout)
52+```
53+
54+## 🚀 Quick start (compute node M3U96b, `~/wp44_uqo`)
55+
56+```bash
57+uv venv --python 3.12 .venv && . .venv/bin/activate && uv pip install -r requirements.txt
58+python ingestion/openalex_snapshot.py --out data/raw/openalex/works --manifest data/raw/openalex/works_manifest_2026-10-14.json --workers 8
59+python ingestion/openalex_entities.py --out data/raw/openalex
60+python ingestion/dblp_parse.py --xml data/raw/dblp/dblp.xml.gz --out data/raw/dblp
61+python ingestion/arxiv_oai.py --out data/raw/arxiv --from 2018-01-01 --until 2026-11-02
62+python ingestion/acl_parse.py --bib data/raw/acl/anthology+abstracts.bib.gz --out data/raw/acl
63+python ingestion/build_corpus.py --cs-dir data/raw/openalex/works/cs --out data/processed --workers 12
64+python ingestion/link_sources.py --root ~/wp44_uqo
65+```
66+
67+## 🗃 Data
68+
69+See `data/raw/README.md` and `reports/PHASE0_FEASIBILITY.md`. Licences: OpenAlex CC0; DBLP ODC-BY 1.0; arXiv metadata
70+(CC0 terms of the OAI-PMH feed); ACL Anthology CC BY 4.0. Semantic Scholar was not used (API key required; recorded).
71+
72+## 📝 Notes (deviations from the UQO working-paper standard)
73+
74+- The manuscript lives in `manuscript/` (mandate §12) instead of `paper/`; the title page and preamble follow the UQO template.
75+- Phase status and human-review checkpoints: `PROJECT_STATUS.md`, `protocol/PROTOCOL.md` §P7.
added RESEARCH_PLAN.md +131 −0
@@ -0,0 +1,131 @@
1+# WP44 — Research plan (operational design, v1.0, 2026-11-02)
2+
3+This document turns the mandate (`CLAUDE.md`) into the concrete design implemented by the code, the protocol and the
4+manuscript. Decisions the mandate left open are listed in §10 with their rationale; they are the executing agent's until
5+the principal investigator (PI) confirms them (human-review checkpoint H-0).
6+
7+## 1. Scientific object and estimands
8+
9+Population: the computer-science research literature indexed by at least one of DBLP, OpenAlex (any topic in field 17,
10+Computer Science), arXiv (set `cs`), or the ACL Anthology, first published between 2018-01-01 and 2026-11-02.
11+Two windows: **baseline** B = [2018-01-01, 2022-11-29] and **post** P = [2022-11-30, 2026-11-02]. The boundary is the public
12+release of ChatGPT; it is a reference date for description and for interrupted-time-series designs, *not* a causal
13+treatment (CLAUDE.md §6.5). Monthly series are the primary temporal unit; years are secondary.
14+
15+Primary descriptive estimands (RQ1, RQ2): monthly counts of works per category c, N_{c,t}; composition shares
16+s_{c,t} = N_{c,t}/N_t; growth rates and their pre/post structural-break statistics; citation-based influence (field- and
17+age-normalised); collaboration/interdisciplinarity indices (RQ4–RQ6). Inferential estimands (RQ2, RQ10): breakpoint
18+locations and magnitudes with placebo distributions; out-of-time precision/recall of the emerging-topic detector.
19+
20+## 2. Sources, access and what each contributes (verified 2026-11-02, see `reports/PHASE0_FEASIBILITY.md`)
21+
22+| Source | Access used | Licence | Role in the corpus |
23+|---|---|---|---|
24+| OpenAlex public snapshot, release 2026-10-14 | S3 public bucket, JSONL, 1,898 works files (675 GB), ~110 MB/s from M3U96b | CC0 | backbone: works, authorships + institutions + countries, venues, topics (field/subfield/topic), `referenced_works` (citation graph), abstracts (inverted index), OA status, FWCI, counts_by_year |
25+| OpenAlex API (no key, $0.10/day shared budget) | REST, polite `mailto` | CC0 | incremental works 2026-10-14 → 2026-11-02; spot checks; free single-record lookups for reference verification |
26+| DBLP XML dump (2026-11-02, 1.12 GB gz, MD5 verified) | HTTPS | ODC-BY 1.0 | CS-scope gold standard, venue streams, author PIDs, DOI links; defines "CS venue" |
27+| arXiv OAI-PMH, set `cs`, 2018-01 → 2026-11 | OAI-PMH, 3.5 s/request, raw XML kept | arXiv metadata terms (CC0 for metadata) | preprints, categories (cs.XX, cross-lists), versions, licences; preprint–publication linking |
28+| ACL Anthology BibTeX (anthology+abstracts.bib, 138,796 entries) | HTTPS | CC BY 4.0 | NLP venue coverage check, abstracts for ACL papers |
29+| Crossref REST API (polite pool) | REST | open metadata | reference verification, `is-referenced-by-count` cross-check sample, publisher/type cross-check |
30+| Semantic Scholar Academic Graph | **not available**: API key required for bulk/dataset access; anonymous calls return HTTP 429 | — | recorded as unavailable (CLAUDE.md §1: record inaccessible data explicitly); influential-citation and citation-intent signals are therefore not used |
31+| Papers with Code / benchmark repositories, GitHub | HTTPS, public dumps where available | varies | RQ7/RQ9 (benchmark proliferation, code availability) — scoped in Phase 3 |
32+| Full texts | arXiv e-prints (rate-limited, licence-respecting), OA locations from OpenAlex/Unpaywall | per-paper licence | ≤ 10,000 high-priority papers; never through paywalls |
33+
34+No credential was assumed: the OpenAlex key, Semantic Scholar key and Kaggle credentials were tested as absent.
35+
36+## 3. Corpus construction (Phase 1) — PRISMA-style stages
37+
38+S0 Identification: all OpenAlex works with any topic in field 17 and `publication_year` 2018–2026 (full records kept),
39+plus DBLP records 2018–2026, arXiv `cs` records created 2018–2026, ACL entries 2018–2026. A *slim* table of **every**
40+OpenAlex work (465 M rows: id, DOI, year, type, primary topic/subfield/field/domain, cited_by_count, flags) is kept to resolve
41+cited works outside CS.
42+S1 Record linkage: OpenAlex ↔ DBLP by DOI (lower-cased), else by arXiv ID (DBLP `ee` → arxiv.org/abs), else by normalised
43+title + year + first-author surname (blocking on title hash); OpenAlex ↔ arXiv by `locations[].landing_page_url`/`ids`
44+and DOI (10.48550/arXiv.*); ACL by DOI/URL. Linkage precision is estimated on a 500-pair manually reviewed sample (H-1).
45+S2 Deduplication: one node per *work*; preprint and published version are merged into a single node with
46+`preprint_date`, `publication_date`, `version_relationship` kept (CLAUDE.md §6.2: preprint duplication).
47+S3 Eligibility: (i) `publication_year` in window; (ii) type in {article, preprint, book-chapter, proceedings-article,
48+dissertation, report, book} — exclude paratext, editorials, errata, retracted (kept with flag `is_retracted`), XPAC
49+expansion records (`is_xpac`); (iii) CS membership: DBLP-indexed **or** OpenAlex primary field = CS **or** (secondary CS
50+topic **and** arXiv primary category cs.*). Secondary-only CS topics without DBLP/arXiv support form a *penumbra* set
51+reported separately. Sensitivity: results re-estimated on the strict core (DBLP ∩ OpenAlex primary CS).
52+S4 Classification: 40-category hierarchical ontology (`taxonomy/ontology.yaml`), multi-label, assigned by rules over
53+OpenAlex topics/subfields, arXiv categories, DBLP venue streams and keyword patterns; validated on manually reviewed
54+samples (Phase 2, H-2) and by an LLM-based second rater with reported agreement.
55+S5 Synthesis populations: (a) mapping corpus (all eligible); (b) deeply classified set (≥ 100 k: abstracts + embeddings +
56+full feature vectors); (c) high-priority set (≥ 10 k: citation-ranked within category × year with OA full text);
57+(d) landmark set (≥ 1 k: top field-normalised impact, reviewed by specialist agents and flagged for PI review, H-3).
58+
59+Every stage writes counts to `data/metadata/prisma_counts.json`, rendered as the PRISMA flow figure.
60+
61+## 4. Taxonomy (Phase 2)
62+
63+`taxonomy/ontology.yaml`: 40 leaf categories under 8 top-level areas (AI & ML; Vision, Language & Perception; Systems &
64+Infrastructure; Theory & Foundations; Security, Privacy & Cryptography; Software & Human Factors; Emerging Computing
65+Paradigms; Computing & Science). Each leaf has: definition, parent, OpenAlex topic IDs (weighted), subfield IDs, arXiv
66+categories, DBLP streams, keyword regexes with weights, exclusions, and provenance of the mapping. Assignment score per
67+category = weighted sum of matched signals; multi-label with threshold; `primary_category` = argmax. Validation: 2,000
68+stratified works (50 per category) rated by two independent LLM raters with the rubric in `taxonomy/VALIDATION.md`,
69+Cohen's κ and per-category precision/recall; a 200-work subsample is flagged for PI review (H-2).
70+
71+## 5. Knowledge graph (Phase 3)
72+
73+Nodes: works, authors (OpenAlex A-ids), institutions, countries, venues (sources), topics, taxonomy categories, arXiv
74+categories; *method/model/dataset/benchmark* and *claim* nodes only for the high-priority set, extracted from abstracts
75+(and full text where lawfully available) with a schema-constrained LLM extractor; each extracted edge carries
76+`provenance` (work id + text span), `confidence`, `verification_status` ∈ {unverified, machine-verified, human-verified}.
77+Citation edges come from `referenced_works` (verified by construction). Semantic edges from embeddings (k-NN, cosine).
78+Storage: Parquet tables + DuckDB views; graph analytics with igraph (python-igraph) on M3U96b (96 GB RAM).
79+
80+## 6. Statistics (Phase 4) and structural change
81+
82+- Bibliometrics: monthly/annual counts; shares; CAGR; field-normalised citation score FNCS = c / E[c | field, year, type];
83+ citation velocity (citations within 12/24 months, computed from `counts_by_year`); Gini/Hirsch-type inequality; venue
84+ concentration (HHI); author/institution/country collaboration networks; Rao–Stirling and Leinster–Cobbold diversity.
85+- Citation-age bias: all cross-period comparisons use fixed citation windows and year-normalisation; FWCI used as a
86+ secondary measure with its known limits.
87+- Structural change: interrupted time series on log monthly counts with seasonality (OLS-HAC, Newey–West), Bai–Perron
88+ multiple-breakpoint estimation, PELT/binary-segmentation changepoints, Bayesian online changepoint detection; pre-trend
89+ diagnostics (placebo breaks at every month of the baseline), alternative event dates (2020-05 GPT-3, 2022-04 DALL·E 2,
90+ 2022-11 ChatGPT, 2023-03 GPT-4, 2024-09 o1), donut windows; sensitivity to OpenAlex indexing lag (right-truncation
91+ corrected by indexing-delay profiles estimated from `created_date`).
92+- Dynamic networks: yearly topic co-occurrence and citation-flow matrices between taxonomy categories; modularity and
93+ cross-area flow shares; ERGM-style descriptives are out of scope.
94+
95+## 7. Emerging-research detection (Phase 6, original contribution)
96+
97+Unit: topic candidates = (i) OpenAlex topics, (ii) semantic clusters (HDBSCAN on embeddings, re-estimated per
98+training cutoff without look-ahead), (iii) keyword n-grams. Signals at month t using data ≤ t only: growth anomaly,
99+semantic novelty (distance to pre-t centroid), citation acceleration, interdisciplinary connectivity, methodological
100+adoption (share of works citing a method node), benchmark proliferation, open-source activity. Label at t+h: top-decile
101+growth and citation share at horizon h ∈ {12, 24} months. Training cutoffs 2019–2022; test cutoffs 2023–2024 with
102+outcomes observed up to 2026-11. Models: regularised logistic, gradient boosting, and a simple z-score baseline;
103+metrics: AUC, precision@k, lead time. Known-emergence validation set: LLMs, diffusion models, NeRF, agents, RAG,
104+state-space models, vision-language models, mechanistic interpretability (defined before estimation, listed in
105+`protocol/PROTOCOL.md`).
106+
107+## 8. Agents and artefacts
108+
109+The executing session is Agent A (Research Director). Subagents are spawned with written briefs (`agents/briefs/`),
110+return structured artefacts (JSON/CSV/Markdown with declared schema) and never approve their own findings: every
111+scientific artefact is checked by a different agent (K or N) before it enters the manuscript (`agents/REGISTER.md`).
112+
113+## 9. Infrastructure
114+
115+Primary compute/storage: **M3U96b** (M3 Ultra 28 c, 96 GB, 375 GB free; `~/wp44_uqo/`, venv Python 3.12 via uv:
116+orjson, pyarrow, polars, duckdb, zstandard, lxml, requests). Secondary: M4M64a (540 GB free) for embeddings; other LAN
117+nodes for embarrassingly parallel embedding/extraction batches when it materially helps. Laptop = controller and git
118+repository (`Recherche-UQO/informatique/wp44`, origin `gitsrv:srv/git/wp44_uqo.git`). Data are not in git; ledgers,
119+manifests, hashes and PRISMA counts are.
120+
121+## 10. Decisions taken by the executing agent (pending PI confirmation, H-0)
122+
123+| # | Decision | Rationale |
124+|---|---|---|
125+| D1 | Snapshot over API for OpenAlex | mandate §3 prefers snapshots; the API budget without a key is $0.10/day (≈ 200 k works/day) |
126+| D2 | Keep a slim table of all 465 M works | required for interdisciplinary citation flows (RQ4) and cited-work normalisation |
127+| D3 | Semantic Scholar not used | key required; recorded as unavailable rather than scraped |
128+| D4 | CS membership rule (§3 S3) with strict-core sensitivity | avoids both AI-only bias and over-inclusion from secondary topics |
129+| D5 | Manuscript directory `manuscript/` (mandate §12) instead of the UQO `paper/` convention | mandate is explicit; deviation noted in README |
130+| D6 | Historical raw snapshot kept as filtered subset + per-file SHA-256 + manifest, not the 675 GB bucket copy | public versioned release; hashes allow verification; storage |
131+| D7 | LLM raters (Claude) used for taxonomy validation and knowledge-graph extraction, with PI review checkpoints | mandate §1 requires human checkpoints; PI is the human |
added agents/briefs/AGENT_D_TAXONOMY.md +77 −0
@@ -0,0 +1,77 @@
1+# Brief — Agent D (Taxonomy Architect), WP44
2+
3+Goal: produce the extensible hierarchical ontology of computer-science research used to classify every work of the WP44
4+corpus into the 40 leaf categories of CLAUDE.md §4 (multi-label), with a deterministic mapping from the signals available
5+in the corpus. The mapping is *data*, not code: it must be complete, explicit and justified so that another agent can audit it.
6+
7+Inputs (read-only):
8+- `taxonomy/openalex_topics_2026-10-14.csv` — the 4,516 OpenAlex topics (topic_id, display_name, subfield, field, domain,
9+ works_count, keywords, description). 302 topics are in field 17 (Computer Science); relevant topics also exist in
10+ Mathematics (26), Engineering (22), Decision Sciences (18), Physics (31: quantum), Biochemistry (13: bioinformatics), etc.
11+- `taxonomy/dblp_streams_2018_2026.csv` — 3,570 DBLP venue streams (journal/conference series keys) with ≥ 100 records
12+ in 2018–2026, counts and names. `journals/corr` = arXiv CoRR (not a venue signal).
13+- arXiv categories: cs.AI cs.AR cs.CC cs.CE cs.CG cs.CL cs.CR cs.CV cs.CY cs.DB cs.DC cs.DL cs.DM cs.DS cs.ET cs.FL cs.GL
14+ cs.GR cs.GT cs.HC cs.IR cs.IT cs.LG cs.LO cs.MA cs.MM cs.MS cs.NA cs.NE cs.NI cs.OH cs.OS cs.PF cs.PL cs.RO cs.SC cs.SD
15+ cs.SE cs.SI cs.SY, plus cross-listed stat.ML, eess.AS, eess.IV, eess.SP, eess.SY, quant-ph, math.OC, math.IT, math.ST,
16+ q-bio.QM, q-bio.GN, physics.comp-ph.
17+
18+Deliverables (write these files; nothing else):
19+1. `taxonomy/ontology.yaml` with this schema:
20+ ```yaml
21+ version: "1.0"
22+ created: "2026-11-02"
23+ areas: # 8 top-level areas (choose names; every leaf belongs to exactly one area)
24+ - id: ai_ml
25+ name: ...
26+ leaves: [llm, foundation_models, ...]
27+ categories: # exactly 40 leaves, ids snake_case, in the order of CLAUDE.md §4
28+ - id: llm
29+ number: 1
30+ name: Large Language Models
31+ area: ai_ml
32+ definition: one or two precise sentences (what is in, what is out)
33+ openalex_topics: # list of {id: T10028, weight: 1.0, note: ...}; weight in (0,1]; include topics from any field
34+ openalex_subfields: # list of {id: 1702, weight: 0.3} — coarse fallback only
35+ arxiv_categories: # list of {id: cs.CL, weight: 0.5}
36+ dblp_streams: # list of {key: conf/acl, weight: 1.0}
37+ keywords: # list of {pattern: "(?i)\\blarge language model", weight: 1.0, field: title|abstract|both}
38+ exclusions: # optional list of regex patterns that veto the category
39+ related: [foundation_models, nlp] # ontology edges (is-related-to); also use `parent_of`/`child_of` where a leaf is a specialisation of another (e.g. llm child_of foundation_models; federated_learning child_of privacy_preserving_computation)
40+ provenance: short note on how the mapping was built
41+ assignment_rule:
42+ score: "sum of weights of matched signals, each signal type capped at 1.0, then sum over signal types"
43+ threshold: 1.0 # a work belongs to a category if score >= threshold
44+ primary: "argmax of score; ties broken by the more specific category (child before parent)"
45+ ```
46+2. `taxonomy/ONTOLOGY.md` — human-readable description: the 8 areas, the 40 leaves with definitions, the hierarchy
47+ (parent/child edges), coverage statistics you can compute from the inputs (how many OpenAlex CS topics are mapped,
48+ unmapped CS topics listed explicitly with a reason), and the known ambiguities (e.g. LLM vs foundation model vs
49+ generative AI; cloud vs distributed vs edge; cryptography vs security vs privacy).
50+3. `taxonomy/topic_map.csv` — one row per (openalex topic_id, category_id, weight) used in the YAML (for auditing).
51+
52+Constraints:
53+- Every one of the 302 OpenAlex CS topics must appear in `topic_map.csv` or in the "unmapped" list of ONTOLOGY.md.
54+- The six AI categories that OpenAlex topics cannot separate well (LLM, foundation models, multimodal, generative AI,
55+ agents, AI safety) and the small categories (federated learning, sustainable computing, autonomous scientific discovery,
56+ privacy-preserving computation, blockchain, quantum, edge) MUST have keyword patterns (title and abstract) strong enough
57+ to reach the threshold on their own; the broad categories (CV, NLP, SE, networks, ...) can rely on topics/venues/arXiv.
58+- Keyword regexes must be valid Python `re` patterns (double-escape backslashes in YAML strings) and should be
59+ conservative (word boundaries, avoid single ambiguous tokens such as "agent" or "model").
60+- Map at least the 600 largest DBLP streams; the mapping may give a stream to several categories.
61+- Do not invent topic ids: use only ids present in the CSV. Validate the YAML by loading it with Python (`yaml.safe_load`)
62+ and compiling every regex before you finish; report counts (topics mapped per category, keywords per category).
63+- Write nothing in the manuscript. Report back a short summary (≤ 30 lines) with the counts and the open ambiguities.
64+
65+## Addendum (2026-11-03, after a first attempt failed on output size)
66+
67+The first attempt tried to emit `ontology.yaml` in one response and exceeded the 64,000-token output limit. Work
68+incrementally and keep every single file write under ~6,000 tokens:
69+- Put the mapping data in Python modules, one per area: `taxonomy/src/area_ai_ml.py`, `taxonomy/src/area_perception.py`,
70+ … each defining `CATEGORIES = [ {...}, ... ]` dicts with the schema of the brief (ids, weights, patterns as raw strings).
71+ Write each module with its own Write call; if an area is large, split it into two modules.
72+- Write `taxonomy/build_ontology.py` that imports the modules, validates (40 leaves, unique ids, topic ids exist in the
73+ CSV, regexes compile, weights in (0,1]), and generates `taxonomy/ontology.yaml`, `taxonomy/topic_map.csv` and the
74+ coverage statistics section of `taxonomy/ONTOLOGY.md` (the prose part of ONTOLOGY.md you write by hand, briefly).
75+- Run `python3 taxonomy/build_ontology.py` (pyyaml: `/opt/homebrew/bin/python3 -m pip install --user pyyaml` or use
76+ `uv pip install` in a venv) and fix errors until it passes. Never print whole CSVs to your context: use grep/awk/python
77+ to look up topics by keyword.
added data/raw/README.md +10 −0
@@ -0,0 +1,10 @@
1+# data/raw — provenance (nothing here is committed except this file; copies live on M3U96b:~/wp44_uqo/data/raw)
2+
3+| Source | What | Obtained | Size | Integrity |
4+|---|---|---|---|---|
5+| OpenAlex works snapshot, release 2026-10-14 | `data/jsonl/works/` 1,898 part files, 464,765,485 works, 675.3 GB gz | 2026-11-02 21:19 → 23:05 ET, HTTPS from `openalex.s3.amazonaws.com`, 8 parallel streams | filtered CS subset `works/cs/*.jsonl.zst` 20 GB (15,490,159 records), slim table `works/slim/*.parquet` 9.3 GB (all works) | manifest (`works_manifest_2026-10-14.json`), per-file content length and SHA-256 in `works/ledger.sqlite` (1,898/1,898 ok, 0 parse errors, record total = manifest total) |
6+| 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 | `entities_ledger.json` (SHA-256 per file) |
7+| OpenAlex API increment | works with any CS topic, publication_date 2026-10-01 → 2026-11-02, sliced by day × (CS primary / non-CS primary), basic paging ≤ 10,000 per slice | 2026-11-03 (public budget $0.10/day → several days) | raw JSON pages `api_increment/pages/` | `api_increment/ledger.json` (counts, cost) |
8+| DBLP | `dblp.xml.gz` (2026-11-02 22:39 UTC), `dblp.dtd`, `.md5` | 2026-11-02 | 1.12 GB | MD5 `ff1c424c3b4cef36582f647c36df27c0` verified; parse report `dblp_parse_report.json` (8,840,866 records) |
9+| arXiv OAI-PMH | set `cs`, metadataPrefix `arXiv`, datestamp 2018-01-01 → 2026-11-02, monthly windows, raw XML pages kept | 2026-11-02/03 | 758 pages, 915,115 unique records | `harvest_state.json`, `arxiv_parse_report.json` |
10+| ACL Anthology | `anthology+abstracts.bib.gz` | 2026-11-02 | 46.7 MB, 138,796 entries | SHA-256 `44c3f7e907f221b740bea09069818a3d7c0dfd88b2da3b750d9f02955efc58b3` |
added ingestion/acl_parse.py +83 −0
@@ -0,0 +1,83 @@
1+#!/usr/bin/env python3
2+"""WP44 — parse the ACL Anthology BibTeX export (anthology+abstracts.bib.gz) into Parquet."""
3+import argparse, gzip, hashlib, json, os, re, datetime as dt
4+import pyarrow as pa, pyarrow.parquet as pq
5+
6+FIELDS = ["title", "author", "editor", "booktitle", "journal", "month", "year", "address", "publisher", "url", "doi", "pages", "abstract", "volume", "number"]
7+SCHEMA = pa.schema([("acl_id", pa.string()), ("entry_type", pa.string())] + [(f, pa.string()) for f in FIELDS])
8+
9+
10+def entries(txt):
11+ i = 0; n = len(txt)
12+ while True:
13+ j = txt.find("@", i)
14+ if j < 0:
15+ return
16+ k = txt.find("{", j)
17+ et = txt[j + 1:k].strip().lower()
18+ depth = 0; p = k
19+ while p < n:
20+ c = txt[p]
21+ if c == "{":
22+ depth += 1
23+ elif c == "}":
24+ depth -= 1
25+ if depth == 0:
26+ break
27+ p += 1
28+ body = txt[k + 1:p]; i = p + 1
29+ if et in ("comment", "string", "preamble"):
30+ continue
31+ yield et, body
32+
33+
34+def parse_body(body):
35+ key, _, rest = body.partition(",")
36+ out = {"acl_id": key.strip()}
37+ # fields of the form name = {value} or name = "value", values may contain nested braces
38+ pos = 0
39+ while True:
40+ m = re.search(r"(\w+)\s*=\s*", rest[pos:])
41+ if not m:
42+ break
43+ name = m.group(1).lower(); p = pos + m.end()
44+ if p >= len(rest):
45+ break
46+ if rest[p] == "{":
47+ depth = 0; q = p
48+ while q < len(rest):
49+ if rest[q] == "{": depth += 1
50+ elif rest[q] == "}":
51+ depth -= 1
52+ if depth == 0: break
53+ q += 1
54+ val = rest[p + 1:q]; pos = q + 1
55+ elif rest[p] == '"':
56+ q = rest.find('"', p + 1); val = rest[p + 1:q]; pos = q + 1
57+ else:
58+ q = rest.find(",", p); q = len(rest) if q < 0 else q; val = rest[p:q]; pos = q
59+ val = re.sub(r"\s+", " ", val).strip()
60+ val = val.replace("{", "").replace("}", "")
61+ if name in FIELDS:
62+ out[name] = val
63+ return out
64+
65+
66+def main():
67+ ap = argparse.ArgumentParser(); ap.add_argument("--bib", required=True); ap.add_argument("--out", required=True)
68+ a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)
69+ raw = open(a.bib, "rb").read(); sha = hashlib.sha256(raw).hexdigest()
70+ txt = gzip.decompress(raw).decode("utf-8", "replace")
71+ rows = {n: [] for n in SCHEMA.names}; n = 0; types = {}
72+ for et, body in entries(txt):
73+ d = parse_body(body); d["entry_type"] = et; types[et] = types.get(et, 0) + 1
74+ for k in SCHEMA.names:
75+ rows[k].append(d.get(k))
76+ n += 1
77+ pq.write_table(pa.Table.from_pydict(rows, schema=SCHEMA), os.path.join(a.out, "acl_anthology.parquet"), compression="zstd")
78+ rep = {"input": a.bib, "sha256": sha, "bytes": len(raw), "entries": n, "types": types, "parsed_at": dt.datetime.now(dt.UTC).isoformat()}
79+ json.dump(rep, open(os.path.join(a.out, "acl_parse_report.json"), "w"), indent=1); print(json.dumps(rep, indent=1))
80+
81+
82+if __name__ == "__main__":
83+ main()
added ingestion/arxiv_oai.py +134 −0
@@ -0,0 +1,134 @@
1+#!/usr/bin/env python3
2+"""WP44 — harvest arXiv metadata (set=cs) through OAI-PMH (metadataPrefix=arXiv).
3+
4+Harvests by monthly datestamp windows from --from to --until; each window's raw
5+XML pages are stored verbatim (immutable raw snapshot) under <out>/raw/<window>/
6+page_NNNN.xml and parsed into <out>/arxiv_cs.parquet at the end (``--parse-only``
7+re-parses). Politeness: >= 3 s between requests, honours Retry-After on 503.
8+Note: OAI ``from``/``until`` filter on the record *datestamp* (last update), so a
9+window contains every record updated in that window; ``created`` gives the first
10+submission date. Duplicates across windows are resolved at parse time (latest
11+datestamp wins).
12+"""
13+import argparse, glob, json, os, re, time, datetime as dt, calendar
14+import requests
15+from lxml import etree
16+import pyarrow as pa, pyarrow.parquet as pq
17+
18+BASE = "https://oaipmh.arxiv.org/oai"
19+NS = {"oai": "http://www.openarchives.org/OAI/2.0/", "ax": "http://arxiv.org/OAI/arXiv/"}
20+UA = "WP44-UQO-systematic-mapping/0.1 (academic research; contact simon-pierre.boucher@uqo.ca)"
21+SCHEMA = pa.schema([
22+ ("arxiv_id", pa.string()), ("datestamp", pa.string()), ("created", pa.string()), ("updated", pa.string()),
23+ ("title", pa.string()), ("abstract", pa.string()), ("authors", pa.list_(pa.string())), ("categories", pa.list_(pa.string())),
24+ ("primary_category", pa.string()), ("comments", pa.string()), ("journal_ref", pa.string()), ("doi", pa.string()),
25+ ("license", pa.string()), ("report_no", pa.string()), ("msc_class", pa.string()), ("acm_class", pa.string()), ("set_specs", pa.list_(pa.string())),
26+])
27+
28+
29+def month_windows(start, end):
30+ y, m = start.year, start.month
31+ while (y, m) <= (end.year, end.month):
32+ last = calendar.monthrange(y, m)[1]
33+ a = dt.date(y, m, 1); b = min(dt.date(y, m, last), end)
34+ yield a, b
35+ m += 1
36+ if m == 13:
37+ y, m = y + 1, 1
38+
39+
40+def fetch(session, params, log):
41+ for attempt in range(12):
42+ try:
43+ r = session.get(BASE, params=params, timeout=180, headers={"User-Agent": UA})
44+ except requests.RequestException as e:
45+ log(f" network error {e}; sleep 30"); time.sleep(30); continue
46+ if r.status_code == 503:
47+ wait = int(r.headers.get("Retry-After", "30")); log(f" 503 retry-after {wait}"); time.sleep(wait + 1); continue
48+ if r.status_code != 200:
49+ log(f" HTTP {r.status_code}; sleep 30"); time.sleep(30); continue
50+ return r.text
51+ raise RuntimeError("too many failures")
52+
53+
54+def harvest(a, log):
55+ s = requests.Session()
56+ state_path = os.path.join(a.out, "harvest_state.json")
57+ state = json.load(open(state_path)) if os.path.exists(state_path) else {"windows": {}}
58+ for w0, w1 in month_windows(dt.date.fromisoformat(a.frm), dt.date.fromisoformat(a.until)):
59+ key = f"{w0}_{w1}"
60+ if state["windows"].get(key, {}).get("status") == "done":
61+ continue
62+ d = os.path.join(a.out, "raw", key); os.makedirs(d, exist_ok=True)
63+ params = {"verb": "ListRecords", "metadataPrefix": "arXiv", "set": a.set, "from": str(w0), "until": str(w1)}
64+ page = 0; nrec = 0; t0 = time.time()
65+ while True:
66+ xml = fetch(s, params, log)
67+ with open(os.path.join(d, f"page_{page:04d}.xml"), "w") as fh:
68+ fh.write(xml)
69+ root = etree.fromstring(xml.encode())
70+ err = root.find("oai:error", NS)
71+ if err is not None:
72+ if err.get("code") == "noRecordsMatch":
73+ break
74+ raise RuntimeError(f"OAI error {err.get('code')}: {err.text}")
75+ nrec += len(root.findall(".//oai:record", NS))
76+ tok = root.find(".//oai:resumptionToken", NS)
77+ page += 1
78+ if tok is None or not (tok.text or "").strip():
79+ break
80+ params = {"verb": "ListRecords", "resumptionToken": tok.text.strip()}
81+ time.sleep(a.delay)
82+ state["windows"][key] = {"status": "done", "pages": page, "records": nrec, "seconds": round(time.time() - t0),
83+ "when": dt.datetime.now(dt.UTC).isoformat()}
84+ json.dump(state, open(state_path, "w"), indent=1)
85+ log(f"window {key}: {page} pages, {nrec} records, {time.time()-t0:.0f}s")
86+ time.sleep(a.delay)
87+
88+
89+def parse(a, log):
90+ recs = {}
91+ files = sorted(glob.glob(os.path.join(a.out, "raw", "*", "page_*.xml")))
92+ for f in files:
93+ root = etree.parse(f).getroot()
94+ for rec in root.findall(".//oai:record", NS):
95+ h = rec.find("oai:header", NS)
96+ if h.get("status") == "deleted":
97+ continue
98+ m = rec.find("oai:metadata/ax:arXiv", NS)
99+ if m is None:
100+ continue
101+ g = lambda t: (m.findtext(f"ax:{t}", namespaces=NS) or "").strip() or None
102+ aid = g("id"); ds = h.findtext("oai:datestamp", namespaces=NS)
103+ if aid in recs and recs[aid]["datestamp"] >= ds:
104+ continue
105+ cats = (g("categories") or "").split()
106+ authors = []
107+ for au in m.findall("ax:authors/ax:author", NS):
108+ kn = au.findtext("ax:keyname", namespaces=NS) or ""; fn = au.findtext("ax:forenames", namespaces=NS) or ""
109+ authors.append((fn + " " + kn).strip())
110+ recs[aid] = dict(arxiv_id=aid, datestamp=ds, created=g("created"), updated=g("updated"),
111+ title=re.sub(r"\s+", " ", g("title") or ""), abstract=re.sub(r"\s+", " ", g("abstract") or ""),
112+ authors=authors, categories=cats, primary_category=cats[0] if cats else None,
113+ comments=g("comments"), journal_ref=g("journal-ref"), doi=(g("doi") or "").lower() or None, license=g("license"),
114+ report_no=g("report-no"), msc_class=g("msc-class"), acm_class=g("acm-class"),
115+ set_specs=[x.text for x in h.findall("oai:setSpec", NS)])
116+ cols = {n: [r[n] for r in recs.values()] for n in SCHEMA.names}
117+ pq.write_table(pa.Table.from_pydict(cols, schema=SCHEMA), os.path.join(a.out, "arxiv_cs.parquet"), compression="zstd")
118+ rep = {"files": len(files), "records": len(recs), "parsed_at": dt.datetime.now(dt.UTC).isoformat(), "set": a.set, "from": a.frm, "until": a.until}
119+ json.dump(rep, open(os.path.join(a.out, "arxiv_parse_report.json"), "w"), indent=1); log(json.dumps(rep))
120+
121+
122+def main():
123+ ap = argparse.ArgumentParser(); ap.add_argument("--out", required=True); ap.add_argument("--set", default="cs")
124+ ap.add_argument("--from", dest="frm", default="2018-01-01"); ap.add_argument("--until", default="2026-11-02")
125+ ap.add_argument("--delay", type=float, default=3.5); ap.add_argument("--parse-only", action="store_true")
126+ a = ap.parse_args(); os.makedirs(a.out, exist_ok=True)
127+ log = lambda s: print(f"[{dt.datetime.now():%H:%M:%S}] {s}", flush=True)
128+ if not a.parse_only:
129+ harvest(a, log)
130+ parse(a, log)
131+
132+
133+if __name__ == "__main__":
134+ main()
added ingestion/build_corpus.py +197 −0
@@ -0,0 +1,197 @@
1+#!/usr/bin/env python3
2+"""WP44 — normalise the filtered OpenAlex CS records (cs/*.jsonl.zst) into Parquet tables.
3+
4+Tables (one Parquet file per input part under <out>/tables/<table>/<part>.parquet, resumable):
5+ works, authorships, work_topics, work_keywords, references, abstracts, locations, grants, counts_by_year
6+Run on the compute node after (or while) ingestion/openalex_snapshot.py runs; parts whose outputs exist are skipped.
7+"""
8+import argparse, glob, io, multiprocessing as mp, os, re, time, datetime as dt
9+import orjson, zstandard as zstd
10+import pyarrow as pa, pyarrow.parquet as pq
11+
12+ARXIV_RE = re.compile(r"arxiv\.org/(?:abs|pdf)/([0-9]{4}\.[0-9]{4,5}(?:v\d+)?|[a-z\-]+(?:\.[A-Z]{2})?/[0-9]{7}(?:v\d+)?)", re.I)
13+ARXIV_DOI_RE = re.compile(r"^10\.48550/arxiv\.(.+)$", re.I)
14+
15+S = {
16+ "works": pa.schema([("id", pa.int64()), ("doi", pa.string()), ("title", pa.string()), ("publication_year", pa.int32()),
17+ ("publication_date", pa.string()), ("created_date", pa.string()), ("updated_date", pa.string()), ("type", pa.string()),
18+ ("type_crossref", pa.string()), ("language", pa.string()), ("cited_by_count", pa.int32()), ("fwci", pa.float32()),
19+ ("citation_percentile", pa.float32()), ("is_top1", pa.bool_()), ("is_top10", pa.bool_()), ("is_oa", pa.bool_()), ("oa_status", pa.string()),
20+ ("license", pa.string()), ("is_retracted", pa.bool_()), ("is_paratext", pa.bool_()), ("is_xpac", pa.bool_()), ("n_refs", pa.int32()),
21+ ("n_authors", pa.int32()), ("n_institutions", pa.int32()), ("n_countries", pa.int32()), ("countries", pa.list_(pa.string())),
22+ ("institution_types", pa.list_(pa.string())), ("primary_topic_id", pa.int32()), ("primary_subfield_id", pa.int32()),
23+ ("primary_field_id", pa.int32()), ("primary_domain_id", pa.int32()), ("cs_primary", pa.bool_()), ("cs_any", pa.bool_()),
24+ ("source_id", pa.int64()), ("source_name", pa.string()), ("source_type", pa.string()), ("host_organization", pa.string()),
25+ ("is_in_doaj", pa.bool_()), ("arxiv_id", pa.string()), ("pmid", pa.string()), ("mag", pa.string()), ("has_abstract", pa.bool_()),
26+ ("has_fulltext", pa.bool_()), ("n_locations", pa.int32()), ("indexed_in", pa.list_(pa.string())), ("referenced_works_count", pa.int32()),
27+ ("n_grants", pa.int32()), ("apc_paid_usd", pa.int32()), ("is_authors_truncated", pa.bool_()), ("part", pa.string())]),
28+ "authorships": pa.schema([("work_id", pa.int64()), ("position", pa.int32()), ("author_position", pa.string()), ("author_id", pa.int64()),
29+ ("author_name", pa.string()), ("orcid", pa.string()), ("is_corresponding", pa.bool_()), ("institution_ids", pa.list_(pa.int64())),
30+ ("institution_names", pa.list_(pa.string())), ("institution_types", pa.list_(pa.string())), ("countries", pa.list_(pa.string())),
31+ ("n_raw_affiliations", pa.int32())]),
32+ "work_topics": pa.schema([("work_id", pa.int64()), ("rank", pa.int16()), ("topic_id", pa.int32()), ("score", pa.float32()),
33+ ("subfield_id", pa.int32()), ("field_id", pa.int32()), ("domain_id", pa.int32())]),
34+ "work_keywords": pa.schema([("work_id", pa.int64()), ("keyword", pa.string()), ("score", pa.float32())]),
35+ "references": pa.schema([("work_id", pa.int64()), ("ref_id", pa.int64())]),
36+ "abstracts": pa.schema([("work_id", pa.int64()), ("abstract", pa.string()), ("n_words", pa.int32())]),
37+ "locations": pa.schema([("work_id", pa.int64()), ("rank", pa.int16()), ("is_primary", pa.bool_()), ("is_best_oa", pa.bool_()), ("source_id", pa.int64()),
38+ ("source_name", pa.string()), ("source_type", pa.string()), ("landing_page_url", pa.string()), ("pdf_url", pa.string()), ("is_oa", pa.bool_()),
39+ ("license", pa.string()), ("version", pa.string()), ("is_accepted", pa.bool_()), ("is_published", pa.bool_())]),
40+ "grants": pa.schema([("work_id", pa.int64()), ("funder_id", pa.int64()), ("funder_name", pa.string()), ("award_id", pa.string())]),
41+ "counts_by_year": pa.schema([("work_id", pa.int64()), ("year", pa.int16()), ("cited_by_count", pa.int32())]),
42+}
43+
44+
45+def oid(u):
46+ if not u:
47+ return None
48+ t = u.rsplit("/", 1)[-1]
49+ i = 0
50+ while i < len(t) and not t[i].isdigit():
51+ i += 1
52+ try:
53+ return int(t[i:])
54+ except ValueError:
55+ return None
56+
57+
58+def abstract_text(inv):
59+ if not inv:
60+ return None
61+ pos = []
62+ for w, ps in inv.items():
63+ for p in ps:
64+ pos.append((p, w))
65+ pos.sort()
66+ return " ".join(w for _, w in pos)
67+
68+
69+def arxiv_from(w):
70+ ids = w.get("ids") or {}
71+ if ids.get("arxiv"):
72+ return str(ids["arxiv"]).rsplit("/", 1)[-1]
73+ doi = w.get("doi") or ""
74+ m = ARXIV_DOI_RE.match(doi.replace("https://doi.org/", ""))
75+ if m:
76+ return m.group(1)
77+ for loc in (w.get("locations") or []):
78+ for k in ("landing_page_url", "pdf_url"):
79+ u = loc.get(k) or ""
80+ m = ARXIV_RE.search(u)
81+ if m:
82+ return re.sub(r"v\d+$", "", m.group(1))
83+ return None
84+
85+
86+def process(args):
87+ path, out, cs_field = args
88+ part = os.path.basename(path).replace(".jsonl.zst", "")
89+ outs = {t: os.path.join(out, "tables", t, part + ".parquet") for t in S}
90+ if all(os.path.exists(p) for p in outs.values()):
91+ return part, "skip", 0
92+ for p in outs.values():
93+ os.makedirs(os.path.dirname(p), exist_ok=True)
94+ rows = {t: {c: [] for c in S[t].names} for t in S}
95+ n = 0
96+ with open(path, "rb") as fh, zstd.ZstdDecompressor().stream_reader(fh) as r:
97+ for line in io.TextIOWrapper(r, encoding="utf-8"):
98+ if not line.strip():
99+ continue
100+ w = orjson.loads(line); n += 1
101+ wid = oid(w["id"])
102+ doi = (w.get("doi") or "").replace("https://doi.org/", "").lower() or None
103+ pt = w.get("primary_topic") or {}
104+ topics = w.get("topics") or []
105+ cs_any = any(((t.get("field") or {}).get("id") == cs_field) for t in topics) or ((pt.get("field") or {}).get("id") == cs_field)
106+ loc = w.get("primary_location") or {}
107+ src = loc.get("source") or {}
108+ best = w.get("best_oa_location") or {}
109+ oa = w.get("open_access") or {}
110+ auths = w.get("authorships") or []
111+ inst_ids = set(); countries = set(); itypes = []
112+ for i, a in enumerate(auths):
113+ ins = a.get("institutions") or []
114+ ii = [oid(x.get("id")) for x in ins]
115+ inst_ids.update(x for x in ii if x)
116+ cc = [c for c in (a.get("countries") or []) if c]
117+ countries.update(cc)
118+ itypes.extend(x.get("type") for x in ins if x.get("type"))
119+ au = a.get("author") or {}
120+ R = rows["authorships"]
121+ R["work_id"].append(wid); R["position"].append(i); R["author_position"].append(a.get("author_position"))
122+ R["author_id"].append(oid(au.get("id"))); R["author_name"].append(au.get("display_name")); R["orcid"].append(au.get("orcid"))
123+ R["is_corresponding"].append(bool(a.get("is_corresponding"))); R["institution_ids"].append(ii)
124+ R["institution_names"].append([x.get("display_name") for x in ins]); R["institution_types"].append([x.get("type") for x in ins])
125+ R["countries"].append(cc); R["n_raw_affiliations"].append(len(a.get("raw_affiliation_strings") or []))
126+ for k, t in enumerate(topics):
127+ R = rows["work_topics"]
128+ R["work_id"].append(wid); R["rank"].append(k); R["topic_id"].append(oid(t.get("id"))); R["score"].append(t.get("score"))
129+ R["subfield_id"].append(oid((t.get("subfield") or {}).get("id"))); R["field_id"].append(oid((t.get("field") or {}).get("id")))
130+ R["domain_id"].append(oid((t.get("domain") or {}).get("id")))
131+ for kw in (w.get("keywords") or []):
132+ R = rows["work_keywords"]; R["work_id"].append(wid); R["keyword"].append(kw.get("display_name")); R["score"].append(kw.get("score"))
133+ refs = w.get("referenced_works") or []
134+ for rr in refs:
135+ rows["references"]["work_id"].append(wid); rows["references"]["ref_id"].append(oid(rr))
136+ ab = abstract_text(w.get("abstract_inverted_index"))
137+ if ab:
138+ R = rows["abstracts"]; R["work_id"].append(wid); R["abstract"].append(ab); R["n_words"].append(len(ab.split()))
139+ locs = w.get("locations") or []
140+ best_url = best.get("landing_page_url") if best else None
141+ for k, L in enumerate(locs):
142+ s_ = L.get("source") or {}
143+ R = rows["locations"]
144+ R["work_id"].append(wid); R["rank"].append(k); R["is_primary"].append(L.get("landing_page_url") == loc.get("landing_page_url") and L.get("pdf_url") == loc.get("pdf_url"))
145+ R["is_best_oa"].append(bool(best_url) and L.get("landing_page_url") == best_url)
146+ R["source_id"].append(oid(s_.get("id"))); R["source_name"].append(s_.get("display_name")); R["source_type"].append(s_.get("type"))
147+ R["landing_page_url"].append(L.get("landing_page_url")); R["pdf_url"].append(L.get("pdf_url")); R["is_oa"].append(bool(L.get("is_oa")))
148+ R["license"].append(L.get("license")); R["version"].append(L.get("version")); R["is_accepted"].append(bool(L.get("is_accepted"))); R["is_published"].append(bool(L.get("is_published")))
149+ for g in (w.get("grants") or []):
150+ R = rows["grants"]; R["work_id"].append(wid); R["funder_id"].append(oid(g.get("funder"))); R["funder_name"].append(g.get("funder_display_name")); R["award_id"].append(g.get("award_id"))
151+ for c in (w.get("counts_by_year") or []):
152+ R = rows["counts_by_year"]; R["work_id"].append(wid); R["year"].append(c.get("year")); R["cited_by_count"].append(c.get("cited_by_count"))
153+ cnp = w.get("citation_normalized_percentile") or {}
154+ ids = w.get("ids") or {}
155+ apc = (w.get("apc_paid") or {}).get("value_usd") if isinstance(w.get("apc_paid"), dict) else None
156+ R = rows["works"]
157+ vals = dict(id=wid, doi=doi, title=w.get("title") or w.get("display_name"), publication_year=w.get("publication_year"), publication_date=w.get("publication_date"),
158+ created_date=w.get("created_date"), updated_date=(w.get("updated_date") or "")[:10] or None, type=w.get("type"), type_crossref=w.get("type_crossref"),
159+ language=w.get("language"), cited_by_count=w.get("cited_by_count"), fwci=w.get("fwci"), citation_percentile=cnp.get("value"),
160+ is_top1=bool(cnp.get("is_in_top_1_percent")), is_top10=bool(cnp.get("is_in_top_10_percent")), is_oa=bool(oa.get("is_oa")), oa_status=oa.get("oa_status"),
161+ license=(best.get("license") if best else None) or loc.get("license"), is_retracted=bool(w.get("is_retracted")), is_paratext=bool(w.get("is_paratext")),
162+ is_xpac=bool(w.get("is_xpac")), n_refs=len(refs), n_authors=w.get("authors_count") if w.get("authors_count") is not None else len(auths),
163+ n_institutions=len(inst_ids), n_countries=len(countries), countries=sorted(countries), institution_types=sorted(set(itypes)),
164+ primary_topic_id=oid(pt.get("id")), primary_subfield_id=oid((pt.get("subfield") or {}).get("id")), primary_field_id=oid((pt.get("field") or {}).get("id")),
165+ primary_domain_id=oid((pt.get("domain") or {}).get("id")), cs_primary=(pt.get("field") or {}).get("id") == cs_field, cs_any=bool(cs_any),
166+ source_id=oid(src.get("id")), source_name=src.get("display_name"), source_type=src.get("type"), host_organization=src.get("host_organization_name"),
167+ is_in_doaj=bool(src.get("is_in_doaj")), arxiv_id=arxiv_from(w), pmid=(ids.get("pmid") or "").rsplit("/", 1)[-1] or None, mag=str(ids.get("mag")) if ids.get("mag") else None,
168+ has_abstract=ab is not None, has_fulltext=bool(w.get("has_fulltext") or w.get("has_content")), n_locations=len(locs), indexed_in=w.get("indexed_in") or [],
169+ referenced_works_count=w.get("referenced_works_count") if w.get("referenced_works_count") is not None else len(refs), n_grants=len(w.get("grants") or []),
170+ apc_paid_usd=apc, is_authors_truncated=bool(w.get("is_authors_truncated")), part=part)
171+ for k, v in vals.items():
172+ R[k].append(v)
173+ for t, sch in S.items():
174+ tbl = pa.Table.from_pydict(rows[t], schema=sch)
175+ tmp = outs[t] + ".part"
176+ pq.write_table(tbl, tmp, compression="zstd"); os.replace(tmp, outs[t])
177+ return part, "done", n
178+
179+
180+def main():
181+ ap = argparse.ArgumentParser(); ap.add_argument("--cs-dir", required=True); ap.add_argument("--out", required=True)
182+ ap.add_argument("--workers", type=int, default=8); ap.add_argument("--limit", type=int, default=0)
183+ a = ap.parse_args()
184+ files = sorted(glob.glob(os.path.join(a.cs_dir, "*.jsonl.zst")))
185+ if a.limit:
186+ files = files[: a.limit]
187+ t0 = time.time(); tot = 0; done = 0
188+ with mp.Pool(a.workers, maxtasksperchild=40) as pool:
189+ for part, st, n in pool.imap_unordered(process, [(f, a.out, "https://openalex.org/fields/17") for f in files]):
190+ tot += n; done += 1
191+ if st == "done" or done % 100 == 0:
192+ print(f"[{dt.datetime.now():%H:%M:%S}] {st} {part} n={n} | {done}/{len(files)} files, {tot} works, {time.time()-t0:.0f}s", flush=True)
193+ print("finished", done, "files", tot, "works")
194+
195+
196+if __name__ == "__main__":
197+ main()
added ingestion/dblp_parse.py +85 −0
@@ -0,0 +1,85 @@
1+#!/usr/bin/env python3
2+"""WP44 — parse the DBLP XML dump (dblp.xml.gz + dblp.dtd) into Parquet.
3+
4+Output: <out>/dblp_publications.parquet with one row per publication record
5+(article, inproceedings, proceedings, book, incollection, phdthesis, mastersthesis,
6+www is skipped) and <out>/dblp_parse_report.json (counts, md5 of input, timings).
7+All years are kept (the historical baseline needs 2018–2022; counts per year are
8+reported for the whole archive).
9+"""
10+import argparse, gzip, hashlib, json, os, time, datetime as dt
11+from lxml import etree
12+import pyarrow as pa, pyarrow.parquet as pq
13+
14+TYPES = {"article", "inproceedings", "proceedings", "book", "incollection", "phdthesis", "mastersthesis", "data"}
15+SCHEMA = pa.schema([
16+ ("key", pa.string()), ("rec_type", pa.string()), ("mdate", pa.string()), ("publtype", pa.string()),
17+ ("title", pa.string()), ("year", pa.int32()), ("authors", pa.list_(pa.string())), ("author_pids", pa.list_(pa.string())),
18+ ("editors", pa.list_(pa.string())), ("journal", pa.string()), ("booktitle", pa.string()), ("volume", pa.string()),
19+ ("number", pa.string()), ("pages", pa.string()), ("publisher", pa.string()), ("series", pa.string()),
20+ ("doi", pa.string()), ("ee", pa.list_(pa.string())), ("url", pa.string()), ("crossref", pa.string()),
21+ ("stream", pa.string()),
22+])
23+
24+
25+def text(el):
26+ return "".join(el.itertext()).strip() if el is not None else None
27+
28+
29+def main():
30+ ap = argparse.ArgumentParser(); ap.add_argument("--xml", required=True); ap.add_argument("--out", required=True)
31+ ap.add_argument("--min-year", type=int, default=0)
32+ a = ap.parse_args(); a.xml = os.path.abspath(a.xml); a.out = os.path.abspath(a.out); os.makedirs(a.out, exist_ok=True)
33+ t0 = time.time()
34+ md5 = hashlib.md5(open(a.xml, "rb").read(1 << 30)).hexdigest() # first GB hash (file is 1.1 GB) + size recorded
35+ size = os.path.getsize(a.xml)
36+ # lxml needs the DTD next to the XML for entity resolution; parse the gz stream
37+ parser = etree.XMLParser(load_dtd=True, resolve_entities=True, huge_tree=True)
38+ counts = {}; years = {}
39+ rows = {n: [] for n in SCHEMA.names}; n = 0
40+ writer = pq.ParquetWriter(os.path.join(a.out, "dblp_publications.parquet"), SCHEMA, compression="zstd")
41+
42+ def flush():
43+ nonlocal rows
44+ if rows["key"]:
45+ writer.write_table(pa.Table.from_pydict(rows, schema=SCHEMA)); rows = {k: [] for k in SCHEMA.names}
46+
47+ # iterparse over the gzip stream; the DTD is referenced relatively (dblp.dtd) -> chdir
48+ os.chdir(os.path.dirname(os.path.abspath(a.xml)))
49+ with gzip.open(a.xml, "rb") as fh:
50+ ctx = etree.iterparse(fh, events=("end",), tag=list(TYPES | {"www"}), load_dtd=True, resolve_entities=True, huge_tree=True)
51+ for _, el in ctx:
52+ t = el.tag
53+ if t in TYPES:
54+ counts[t] = counts.get(t, 0) + 1
55+ yr = el.findtext("year"); yr = int(yr) if yr and yr.isdigit() else None
56+ years[yr] = years.get(yr, 0) + 1
57+ if yr is not None and yr >= a.min_year:
58+ ees = [e.text for e in el.findall("ee") if e.text]
59+ doi = next((e[16:].lower() for e in ees if e.startswith("https://doi.org/")), None)
60+ stream = el.get("key", "").rsplit("/", 1)[0] if "/" in el.get("key", "") else None
61+ vals = dict(key=el.get("key"), rec_type=t, mdate=el.get("mdate"), publtype=el.get("publtype"),
62+ title=text(el.find("title")), year=yr,
63+ authors=[text(x) for x in el.findall("author")], author_pids=[x.get("pid") for x in el.findall("author")],
64+ editors=[text(x) for x in el.findall("editor")], journal=el.findtext("journal"), booktitle=el.findtext("booktitle"),
65+ volume=el.findtext("volume"), number=el.findtext("number"), pages=el.findtext("pages"),
66+ publisher=el.findtext("publisher"), series=text(el.find("series")), doi=doi, ee=ees,
67+ url=el.findtext("url"), crossref=el.findtext("crossref"), stream=stream)
68+ for k, v in vals.items():
69+ rows[k].append(v)
70+ n += 1
71+ if n % 200000 == 0:
72+ flush(); print(f"{n} rows, {time.time()-t0:.0f}s", flush=True)
73+ el.clear()
74+ while el.getprevious() is not None:
75+ del el.getparent()[0]
76+ flush(); writer.close()
77+ rep = {"input": os.path.abspath(a.xml), "input_bytes": size, "input_md5_first_GiB": md5, "parsed_at": dt.datetime.now(dt.UTC).isoformat(),
78+ "rows_written": n, "min_year": a.min_year, "counts_by_type": counts, "counts_by_year": {str(k): v for k, v in sorted(years.items(), key=lambda kv: (kv[0] is None, kv[0]))},
79+ "seconds": round(time.time() - t0)}
80+ json.dump(rep, open(os.path.join(a.out, "dblp_parse_report.json"), "w"), indent=1)
81+ print(json.dumps({k: v for k, v in rep.items() if k != "counts_by_year"}, indent=1))
82+
83+
84+if __name__ == "__main__":
85+ main()
added ingestion/dblp_streams.py +9 −0
@@ -0,0 +1,9 @@
1+#!/usr/bin/env python3
2+"""Venue streams (journals / conference series) of DBLP 2018-2026 with counts, for the taxonomy mapping."""
3+import duckdb, sys
4+src, out = sys.argv[1], sys.argv[2]
5+con = duckdb.connect()
6+con.sql(f"""copy (select stream, rec_type, count(*) n, min(year) y0, max(year) y1, any_value(journal) journal, any_value(booktitle) booktitle
7+ from read_parquet('{src}') where year between 2018 and 2026 and rec_type in ('article','inproceedings')
8+ group by 1,2 having n >= 100 order by n desc) to '{out}' (header)""")
9+print(con.sql(f"select count(*) from read_csv_auto('{out}')").fetchall(), con.sql(f"select rec_type, count(*) from read_parquet('{src}') where year between 2018 and 2026 group by 1 order by 2 desc").fetchall())
added ingestion/link_sources.py +121 −0
@@ -0,0 +1,121 @@
1+#!/usr/bin/env python3
2+"""WP44 — Stage S1 record linkage: OpenAlex CS records ↔ DBLP ↔ arXiv ↔ ACL Anthology, in DuckDB.
3+
4+Creates/refreshes <db> with views over the Parquet tables and the linkage tables
5+ link_dblp_oa(dblp_key, oa_id, method), link_arxiv_oa(arxiv_id, oa_id, method), link_acl_oa(acl_id, oa_id, method)
6+and writes data/metadata/linkage_report.json. Also writes the list of OpenAlex ids of DBLP-indexed works (2018–2026)
7+that are in the slim table but NOT in the CS-topic subset (needed for the second snapshot pass).
8+"""
9+import argparse, json, os, time, datetime as dt
10+import duckdb
11+
12+
13+def main():
14+ ap = argparse.ArgumentParser(); ap.add_argument("--root", required=True); ap.add_argument("--db", default=None)
15+ a = ap.parse_args(); R = a.root; db = a.db or os.path.join(R, "data/processed/wp44.duckdb")
16+ con = duckdb.connect(db); con.execute("SET threads TO 24; SET memory_limit='60GB'; SET preserve_insertion_order=false;")
17+ T = os.path.join(R, "data/processed/tables"); t0 = time.time(); rep = {"created": dt.datetime.now(dt.UTC).isoformat()}
18+ def log(m): print(f"[{time.time()-t0:7.0f}s] {m}", flush=True)
19+ for t in ["works", "authorships", "work_topics", "work_keywords", "references", "abstracts", "locations", "grants", "counts_by_year"]:
20+ con.execute(f"CREATE OR REPLACE VIEW oa_{t} AS SELECT * FROM read_parquet('{T}/{t}/*.parquet')")
21+ con.execute(f"CREATE OR REPLACE VIEW oa_slim AS SELECT * FROM read_parquet('{R}/data/raw/openalex/works/slim/*.parquet')")
22+ con.execute(f"CREATE OR REPLACE VIEW arxiv_raw AS SELECT * FROM read_parquet('{R}/data/raw/arxiv/arxiv_cs.parquet')")
23+ con.execute(f"CREATE OR REPLACE VIEW acl_raw AS SELECT * FROM read_parquet('{R}/data/raw/acl/acl_anthology.parquet')")
24+ con.execute(f"CREATE OR REPLACE VIEW dblp_raw AS SELECT * FROM read_parquet('{R}/data/raw/dblp/dblp_publications.parquet')")
25+ log("views created")
26+ # --- normalised keys ---
27+ con.execute("""CREATE OR REPLACE TABLE oa_keys AS
28+ SELECT id AS oa_id, doi, lower(regexp_replace(arxiv_id, 'v[0-9]+$', '')) AS arxiv_id, publication_year AS year,
29+ regexp_replace(lower(title), '[^a-z0-9]+', '', 'g') AS ntitle, length(title) AS tlen, type
30+ FROM oa_works""")
31+ con.execute("""CREATE OR REPLACE TABLE dblp_keys AS
32+ SELECT key AS dblp_key, rec_type, year, doi,
33+ lower(regexp_replace(regexp_extract(array_to_string(ee, ' '), 'arxiv\\.org/abs/([^ ]+)', 1), 'v[0-9]+$', '')) AS arxiv_id,
34+ regexp_replace(lower(title), '[^a-z0-9]+', '', 'g') AS ntitle, length(title) AS tlen, stream
35+ FROM dblp_raw WHERE year BETWEEN 2018 AND 2026 AND rec_type IN ('article','inproceedings','incollection','book','phdthesis','data')""")
36+ con.execute("UPDATE dblp_keys SET arxiv_id = NULL WHERE arxiv_id = ''")
37+ con.execute("""CREATE OR REPLACE TABLE arxiv_keys AS
38+ SELECT lower(arxiv_id) AS arxiv_id, created, substr(created,1,4)::INT AS created_year, doi, primary_category, categories,
39+ regexp_replace(lower(title), '[^a-z0-9]+', '', 'g') AS ntitle FROM arxiv_raw""")
40+ con.execute("""CREATE OR REPLACE TABLE acl_keys AS
41+ SELECT acl_id, try_cast(year AS INT) AS year, lower(doi) AS doi, url, regexp_replace(lower(title), '[^a-z0-9]+', '', 'g') AS ntitle,
42+ entry_type FROM acl_raw WHERE entry_type IN ('inproceedings','article')""")
43+ log("keys built")
44+ # --- DBLP ↔ OpenAlex ---
45+ con.execute("""CREATE OR REPLACE TABLE link_dblp_oa AS
46+ SELECT d.dblp_key, o.oa_id, 'doi' AS method FROM dblp_keys d JOIN oa_keys o ON d.doi = o.doi WHERE d.doi IS NOT NULL""")
47+ con.execute("""INSERT INTO link_dblp_oa
48+ SELECT d.dblp_key, o.oa_id, 'arxiv' FROM dblp_keys d JOIN oa_keys o ON d.arxiv_id = o.arxiv_id
49+ WHERE d.arxiv_id IS NOT NULL AND o.arxiv_id IS NOT NULL AND d.dblp_key NOT IN (SELECT dblp_key FROM link_dblp_oa)
50+ AND o.oa_id NOT IN (SELECT oa_id FROM link_dblp_oa)""")
51+ # title+year: unique normalised titles on both sides, length >= 25, |year diff| <= 1
52+ con.execute("""CREATE OR REPLACE TEMP TABLE d_un AS SELECT ntitle, any_value(dblp_key) dblp_key, any_value(year) year FROM dblp_keys
53+ WHERE tlen >= 25 AND dblp_key NOT IN (SELECT dblp_key FROM link_dblp_oa) GROUP BY ntitle HAVING count(*) = 1""")
54+ con.execute("""CREATE OR REPLACE TEMP TABLE o_un AS SELECT ntitle, any_value(oa_id) oa_id, any_value(year) year FROM oa_keys
55+ WHERE tlen >= 25 AND oa_id NOT IN (SELECT oa_id FROM link_dblp_oa) GROUP BY ntitle HAVING count(*) = 1""")
56+ con.execute("""INSERT INTO link_dblp_oa SELECT d.dblp_key, o.oa_id, 'title_year' FROM d_un d JOIN o_un o ON d.ntitle = o.ntitle
57+ WHERE abs(d.year - o.year) <= 1""")
58+ log("dblp linked")
59+ # --- arXiv ↔ OpenAlex ---
60+ con.execute("""CREATE OR REPLACE TABLE link_arxiv_oa AS
61+ SELECT x.arxiv_id, o.oa_id, 'arxiv_id' AS method FROM arxiv_keys x JOIN oa_keys o ON x.arxiv_id = o.arxiv_id""")
62+ con.execute("""INSERT INTO link_arxiv_oa SELECT x.arxiv_id, o.oa_id, 'doi' FROM arxiv_keys x JOIN oa_keys o ON x.doi = o.doi
63+ WHERE x.doi IS NOT NULL AND x.arxiv_id NOT IN (SELECT arxiv_id FROM link_arxiv_oa)""")
64+ con.execute("""CREATE OR REPLACE TEMP TABLE x_un AS SELECT ntitle, any_value(arxiv_id) arxiv_id, any_value(created_year) year FROM arxiv_keys
65+ WHERE length(ntitle) >= 25 AND arxiv_id NOT IN (SELECT arxiv_id FROM link_arxiv_oa) GROUP BY ntitle HAVING count(*) = 1""")
66+ con.execute("""CREATE OR REPLACE TEMP TABLE o_un2 AS SELECT ntitle, any_value(oa_id) oa_id, any_value(year) year FROM oa_keys
67+ WHERE tlen >= 25 GROUP BY ntitle HAVING count(*) = 1""")
68+ con.execute("""INSERT INTO link_arxiv_oa SELECT x.arxiv_id, o.oa_id, 'title_year' FROM x_un x JOIN o_un2 o ON x.ntitle = o.ntitle
69+ WHERE abs(x.year - o.year) <= 2 AND o.oa_id NOT IN (SELECT oa_id FROM link_arxiv_oa)""")
70+ log("arxiv linked")
71+ # --- ACL ↔ OpenAlex ---
72+ con.execute("""CREATE OR REPLACE TABLE link_acl_oa AS
73+ SELECT c.acl_id, o.oa_id, 'doi' AS method FROM acl_keys c JOIN oa_keys o ON c.doi = o.doi WHERE c.doi IS NOT NULL""")
74+ con.execute("""INSERT INTO link_acl_oa SELECT c.acl_id, l.work_id, 'url' FROM acl_keys c
75+ JOIN (SELECT DISTINCT work_id, regexp_extract(landing_page_url, 'aclanthology\\.org/([^/ ]+)', 1) AS aid FROM oa_locations
76+ WHERE landing_page_url LIKE '%aclanthology.org/%') l ON l.aid = c.acl_id OR l.aid = c.acl_id || '/'
77+ WHERE c.acl_id NOT IN (SELECT acl_id FROM link_acl_oa)""")
78+ con.execute("""INSERT INTO link_acl_oa SELECT c.acl_id, o.oa_id, 'title_year' FROM
79+ (SELECT ntitle, any_value(acl_id) acl_id, any_value(year) year FROM acl_keys WHERE length(ntitle) >= 25 AND acl_id NOT IN (SELECT acl_id FROM link_acl_oa) GROUP BY ntitle HAVING count(*)=1) c
80+ JOIN o_un2 o ON c.ntitle = o.ntitle WHERE abs(c.year - o.year) <= 1 AND o.oa_id NOT IN (SELECT oa_id FROM link_acl_oa)""")
81+ log("acl linked")
82+ # --- report ---
83+ q = lambda s: con.execute(s).fetchall()
84+ rep["oa_cs_records"] = q("SELECT count(*) FROM oa_keys")[0][0]
85+ rep["oa_cs_distinct_ids"] = q("SELECT count(DISTINCT oa_id) FROM oa_keys")[0][0]
86+ rep["dblp_2018_2026"] = q("SELECT count(*) FROM dblp_keys")[0][0]
87+ rep["dblp_with_doi"] = q("SELECT count(*) FROM dblp_keys WHERE doi IS NOT NULL")[0][0]
88+ rep["dblp_with_arxiv"] = q("SELECT count(*) FROM dblp_keys WHERE arxiv_id IS NOT NULL")[0][0]
89+ rep["link_dblp_oa"] = dict(q("SELECT method, count(*) FROM link_dblp_oa GROUP BY 1"))
90+ rep["dblp_linked_distinct"] = q("SELECT count(DISTINCT dblp_key) FROM link_dblp_oa")[0][0]
91+ rep["dblp_unlinked"] = rep["dblp_2018_2026"] - rep["dblp_linked_distinct"]
92+ rep["dblp_unlinked_by_type"] = dict(q("SELECT rec_type, count(*) FROM dblp_keys WHERE dblp_key NOT IN (SELECT dblp_key FROM link_dblp_oa) GROUP BY 1"))
93+ rep["dblp_unlinked_with_doi_in_slim"] = q("""SELECT count(*) FROM dblp_keys d JOIN oa_slim s ON d.doi = s.doi
94+ WHERE d.dblp_key NOT IN (SELECT dblp_key FROM link_dblp_oa)""")[0][0]
95+ rep["arxiv_cs_records"] = q("SELECT count(*) FROM arxiv_keys")[0][0]
96+ rep["arxiv_created_2018_2026"] = q("SELECT count(*) FROM arxiv_keys WHERE created_year BETWEEN 2018 AND 2026")[0][0]
97+ rep["link_arxiv_oa"] = dict(q("SELECT method, count(*) FROM link_arxiv_oa GROUP BY 1"))
98+ rep["arxiv_linked_distinct"] = q("SELECT count(DISTINCT arxiv_id) FROM link_arxiv_oa")[0][0]
99+ rep["acl_records"] = q("SELECT count(*) FROM acl_keys")[0][0]
100+ rep["acl_2018_2026"] = q("SELECT count(*) FROM acl_keys WHERE year BETWEEN 2018 AND 2026")[0][0]
101+ rep["link_acl_oa"] = dict(q("SELECT method, count(*) FROM link_acl_oa GROUP BY 1"))
102+ rep["acl_linked_distinct_2018_2026"] = q("SELECT count(DISTINCT l.acl_id) FROM link_acl_oa l JOIN acl_keys c USING(acl_id) WHERE c.year BETWEEN 2018 AND 2026")[0][0]
103+ # ids needed for the second snapshot pass: DBLP-indexed (2018–2026) works in slim but not in the CS subset; plus arXiv cs works in slim via DOI not in subset
104+ con.execute("""CREATE OR REPLACE TABLE needed_oa_ids AS
105+ SELECT DISTINCT s.id AS oa_id, 'dblp_doi' AS why FROM dblp_keys d JOIN oa_slim s ON d.doi = s.doi
106+ WHERE d.dblp_key NOT IN (SELECT dblp_key FROM link_dblp_oa) AND s.id NOT IN (SELECT oa_id FROM oa_keys)
107+ UNION
108+ SELECT DISTINCT s.id, 'arxiv_doi' FROM arxiv_keys x JOIN oa_slim s ON x.doi = s.doi
109+ WHERE x.doi IS NOT NULL AND x.arxiv_id NOT IN (SELECT arxiv_id FROM link_arxiv_oa) AND s.id NOT IN (SELECT oa_id FROM oa_keys)""")
110+ rep["needed_oa_ids_second_pass"] = dict(q("SELECT why, count(*) FROM needed_oa_ids GROUP BY 1"))
111+ rep["slim_total"] = q("SELECT count(*) FROM oa_slim")[0][0]
112+ rep["slim_cs_any_2018_2026"] = q("SELECT count(*) FROM oa_slim WHERE cs_any_topic AND publication_year BETWEEN 2018 AND 2026")[0][0]
113+ rep["seconds"] = round(time.time() - t0)
114+ os.makedirs(os.path.join(R, "data/metadata"), exist_ok=True)
115+ json.dump(rep, open(os.path.join(R, "data/metadata/linkage_report.json"), "w"), indent=1, default=str)
116+ con.execute(f"COPY (SELECT oa_id FROM needed_oa_ids) TO '{R}/data/metadata/needed_oa_ids.csv' (HEADER)")
117+ print(json.dumps(rep, indent=1, default=str))
118+
119+
120+if __name__ == "__main__":
121+ main()
added ingestion/openalex_api_increment.py +66 −0
Large diff (66 lines) is not loaded by default to keep the page fast.Load diff
added ingestion/openalex_entities.py +41 −0
Large diff (41 lines) is not loaded by default to keep the page fast.Load diff
added ingestion/openalex_snapshot.py +245 −0
Large diff (245 lines) is not loaded by default to keep the page fast.Load diff
added ingestion/openalex_topics_flat.py +17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added manuscript/.latexmkrc +5 −0
Large diff (5 lines) is not loaded by default to keep the page fast.Load diff
added manuscript/Makefile +11 −0
Large diff (11 lines) is not loaded by default to keep the page fast.Load diff
added manuscript/sections/titlepage.tex +77 −0
Large diff (77 lines) is not loaded by default to keep the page fast.Load diff
added manuscript/sections/titlepage_anon.tex +53 −0
Large diff (53 lines) is not loaded by default to keep the page fast.Load diff
added manuscript/uq_logo.jpg +0 −0

Binary file not shown.

added protocol/PROTOCOL.md +96 −0
Large diff (96 lines) is not loaded by default to keep the page fast.Load diff
added protocol/amendments.csv +2 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added reports/PHASE0_FEASIBILITY.md +54 −0
Large diff (54 lines) is not loaded by default to keep the page fast.Load diff
added requirements.txt +22 −0
Large diff (22 lines) is not loaded by default to keep the page fast.Load diff
added taxonomy/dblp_streams_2018_2026.csv +3571 −0
Large diff (3,571 lines) is not loaded by default to keep the page fast.Load diff
added taxonomy/openalex_topics_2026-10-14.csv +4517 −0
Large diff (4,518 lines) is not loaded by default to keep the page fast.Load diff
Menu

Theme