WP44: phase 0 (feasibility, plan, protocol) + phase 1 ingestion pipelines
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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 | +[](manuscript/) | ||||
| 7 | +[](requirements.txt) | ||||
| 8 | +[](data/raw/README.md) | ||||
| 9 | +[](reproducibility/) | ||||
| 10 | +[](https://uqo.ca) | ||||
| 11 | +[](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