Compare commits

..

24 Commits

Author SHA1 Message Date
codex-bot fe3e2502a5 Add read-only source identity candidate audit with evidence shards
Metadata integrity checks / metadata (push) Successful in 2m42s
2026-11-10 02:55:27 +08:00
Zayden-Jung b675a38b97 Merge pull request '适配AAMAS2026官方目录并保留撤稿与撤回证据' (#49) from codex/aamas2026 into main
Metadata integrity checks / metadata (push) Successful in 2m45s
Reviewed-on: #49
2026-11-09 18:49:37 +00:00
Zayden-Jung cb875b8eef Merge pull request '封装NeurIPS2026节目采集,分离投稿与重复展示' (#50) from codex/neurips-program into main
Metadata integrity checks / metadata (push) Successful in 2m34s
Reviewed-on: #50
2026-11-09 18:49:29 +00:00
codex-bot 6df904e6d8 Collect official NeurIPS 2026 program with forum-level presentation deduplication
Metadata integrity checks / metadata (push) Successful in 2m40s
2026-11-10 02:18:38 +08:00
codex-bot 57a557c66b Merge current main and preserve scope and AAMAS documentation entries
Metadata integrity checks / metadata (push) Successful in 2m27s
2026-11-10 01:12:45 +08:00
codex-bot 5b5e606f0c Correct independent affiliation inventory and target section heading mutation
Metadata integrity checks / metadata (push) Successful in 2m22s
2026-11-10 01:09:25 +08:00
Zayden-Jung 3ff9ee9a94 Merge pull request '限定2020起补漏,保留2000年以来来源与恢复清单' (#48) from codex/coverage-handoff-2020 into main
Metadata integrity checks / metadata (push) Successful in 2m18s
Reviewed-on: #48
2026-11-09 17:05:50 +00:00
codex-bot 9f84fd7caf Separate italic title content from publisher status notices
Metadata integrity checks / metadata (push) Failing after 2m6s
2026-11-10 01:04:46 +08:00
codex-bot bf05d3cdc8 Keep day-only withdrawal evidence through native ingestion
Metadata integrity checks / metadata (push) Failing after 2m6s
2026-11-10 01:02:54 +08:00
codex-bot 930df2a4d5 Preserve explicit withdrawal and byte-exact AAMAS fixture evidence
Metadata integrity checks / metadata (push) Failing after 2m4s
2026-11-10 01:00:58 +08:00
codex-bot 37380e1d3b Collect reviewed AAMAS 2026 tracks and hold explicit retraction evidence
Metadata integrity checks / metadata (push) Failing after 2m10s
2026-11-10 00:57:09 +08:00
codex-bot 25ed4559ca docs: set 2020 backfill scope and preserve recovery inventory since 2000
Metadata integrity checks / metadata (push) Successful in 2m9s
2026-11-10 00:50:15 +08:00
Zayden-Jung e0ac62464a Merge pull request '封装有来源核验的 Springer 会后选集采集器' (#47) from codex/springer-selected into main
Metadata integrity checks / metadata (push) Successful in 2m22s
Reviewed-on: #47
2026-11-09 16:47:08 +00:00
Zayden-Jung e37fa46e42 Merge pull request '补入精确的 ACL 1963 AMTCL 年会摘要卷' (#46) from codex/acl1963 into main
Metadata integrity checks / metadata (push) Successful in 2m9s
Reviewed-on: #46
2026-11-09 16:47:02 +00:00
Zayden-Jung 70463c0250 Merge pull request '修复DBLP family候选漏采并提供快照恢复命令' (#45) from codex/dblp-ifaamas-discovery into main
Metadata integrity checks / metadata (push) Successful in 2m12s
Reviewed-on: #45
2026-11-09 16:46:56 +00:00
codex-bot 5c78a36fd5 Scope chapter publication identities and test explicit work anchors
Metadata integrity checks / metadata (push) Successful in 2m20s
2026-11-10 00:04:35 +08:00
codex-bot 7facde6c94 Handle section front matter and preserve abstract paragraph boundaries
Metadata integrity checks / metadata (push) Failing after 2m3s
2026-11-10 00:00:30 +08:00
codex-bot c06981b244 Collect reviewed Springer postconference selected volumes
Metadata integrity checks / metadata (push) Failing after 2m4s
2026-11-09 23:51:25 +08:00
codex-bot 8b1e983e7d Recognize the exact 1963 AMTCL annual meeting volume
Metadata integrity checks / metadata (push) Successful in 2m10s
2026-11-09 23:21:53 +08:00
codex-bot fe0f08a550 Recover omitted DBLP discovery families from pinned snapshots
Metadata integrity checks / metadata (push) Successful in 2m11s
2026-11-09 22:28:50 +08:00
Zayden-Jung b226b3b98b Merge pull request '限定ACL1978与TINLAP卷的来源关系及非论文条目' (#42) from codex/acl-early into main
Metadata integrity checks / metadata (push) Successful in 2m18s
Reviewed-on: #42
2026-11-09 13:42:18 +00:00
Zayden-Jung 05ada8daf8 Merge pull request '支持 AAMAS 2025 官方出版目录元数据采集' (#44) from codex/aamas2025 into main
Metadata integrity checks / metadata (push) Successful in 3m20s
Reviewed-on: #44
2026-11-09 13:42:00 +00:00
codex-bot 13408869c2 Add reviewed AAMAS 2025 publisher metadata adapter
Metadata integrity checks / metadata (push) Successful in 2m11s
2026-11-09 20:50:29 +08:00
codex-bot 443a79dce2 Preserve edition-specific ACL and TINLAP 1978 source evidence
Metadata integrity checks / metadata (push) Successful in 2m6s
2026-11-09 17:20:27 +08:00
51 changed files with 19202 additions and 9973 deletions
+37 -2
View File
@@ -12,7 +12,7 @@ jobs:
- name: Verify metadata storage and query commands
run: |
python3 -B - <<'PY'
import io, os, pathlib, re, subprocess, tarfile, tempfile, urllib.request
import gzip, io, json, os, pathlib, re, subprocess, tarfile, tempfile, urllib.request
sha = os.environ['ARCHIVE_SHA']
if not re.fullmatch(r'[0-9a-fA-F]{40,64}', sha):
raise SystemExit('Invalid source commit')
@@ -50,8 +50,42 @@ jobs:
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','cvf','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','www-archive','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','aamas-proceedings','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','neurips-program','--help'],cwd=outside,check=True)
neurips_snapshots = root/'neurips2026-program-snapshots'
neurips_snapshots.mkdir()
(neurips_snapshots/'program-json.response').write_bytes(gzip.decompress((src/'tests/fixtures/neurips2026-program.json.gz').read_bytes()))
(neurips_snapshots/'receipts.json').write_bytes((src/'tests/fixtures/neurips2026-receipts.json').read_bytes())
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','neurips-program','--snapshots',str(neurips_snapshots),'--out',str(root/'installed-neurips2026-program')],cwd=outside,check=True)
neurips_report = json.loads((root/'installed-neurips2026-program/neurips-program-2026.json').read_text())
assert neurips_report['records'] == 9004 and len(neurips_report['held_presentations']) == 139
assert len(neurips_report['exact_duplicate_event_rows']) == 1 and neurips_report['folded_extra_presentation_rows'] == 132
assert not neurips_report['database_written'] and not neurips_report['pdfs_downloaded']
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','dblp-family','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','aamas-proceedings','--year','2025','--snapshots',str(src/'tests/fixtures/aamas2025'),'--out',str(root/'installed-aamas2025')],cwd=outside,check=True)
aamas2026 = json.loads(gzip.decompress((src/'tests/fixtures/aamas2026-fixture.json.gz').read_bytes()))
aamas2026_snapshots = root/'aamas2026-snapshots'
aamas2026_snapshots.mkdir()
(aamas2026_snapshots/'contents.html').write_text(aamas2026['contents'])
(aamas2026_snapshots/'receipts.json').write_text(json.dumps(aamas2026['receipts']))
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','aamas-proceedings','--year','2026','--snapshots',str(aamas2026_snapshots),'--out',str(root/'installed-aamas2026')],cwd=outside,check=True)
aamas2026_report = json.loads((root/'installed-aamas2026/aamas-proceedings-2026.json').read_text())
assert aamas2026_report['records'] == 639 and len(aamas2026_report['held_publications']) == 1
assert aamas2026_report['status_counts'] == {'accepted': 638, 'withdrawn': 1}
assert not aamas2026_report['database_written'] and not aamas2026_report['pdfs_downloaded']
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','iclr-archive','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','rss-proceedings','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','acl','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','springer-selected','--help'],cwd=outside,check=True)
springer = json.loads(gzip.decompress((src/'tests/fixtures/springer-aistats1993-fixture.json.gz').read_bytes()))
for group in ('toc','chapters'):
folder = root/'springer-snapshots'/group
folder.mkdir(parents=True)
for name, content in springer[group].items():
(folder/name).write_text(content)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','springer-selected','--toc-snapshots',str(root/'springer-snapshots/toc'),'--chapter-snapshots',str(root/'springer-snapshots/chapters'),'--out',str(root/'installed-springer')],cwd=outside,check=True)
springer_report = json.loads((root/'installed-springer/collection-report.json').read_bytes())
assert springer_report['records'] == 49 and not springer_report['database_written']
springer_manifest, = springer_report['files']
assert springer_manifest['status'] == 'partial' and springer_manifest['missing_abstract_count'] == 0
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','postgres','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','edition-tracks','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','maintain','joint-editions','--help'],cwd=outside,check=True)
@@ -62,6 +96,7 @@ jobs:
subprocess.run([python,'-I','-B','-m','paper_atlas','audit','public-openreview','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','coverage','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','audit','histories','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','audit','source-identities','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','curate','identifiers','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-c','from paper_atlas.postgres.writer import PostgresStore, WriteConnection'],cwd=outside,check=True)
smoke = "from paper_atlas.db.store import Store; from paper_atlas.config import DEFAULT_REGISTRY; import json; assert {'COLM','MM','MLSys'} <= {v['acronym'] for v in json.loads(DEFAULT_REGISTRY.read_text())['venues']}; s=Store('installed.sqlite'); s.conn.close()"
+14 -2
View File
@@ -6,12 +6,18 @@ Paper Atlas 将 arXiv、AI 会议论文集及公开投稿决定整理到可追
项目仍在建设中。候选范围包含 43 个会议(含 ACM MM、MLSys),尚未完成全部历史数据和最终去重复核。实际进度以数据库、来源清单和覆盖报告为准,而不是仓库文件数量。
当前主动补漏从 **2020 年**起;2000 年起已发现的名单、缺口与待办保存在[补漏恢复清单](docs/coverage/backfill-handoff-2026-11-10.md),下次从这里接续。已有旧数据保留,范围排除不算补齐。
AAMAS 2026 的官方 HTML 目录由 `collect aamas-proceedings --year 2026` 适配,区分 track 和发表形式,keynote 排除、撤稿证据单独保留;[采集与恢复说明](docs/aamas-proceedings.md)。采集不写数据库,仍需身份核验和增量导入。
历史 ICLR 2014–2016 官方录用页由 `collect iclr-archive` 适配,保留主会/workshop及重复展示证据;运行和身份核验边界见 [历史档案说明](docs/coverage/iclr-archive-2014-2016.md)。
RSS 2025 的 `collect rss-proceedings` 读取官方逐篇标题、完整作者、摘要和 DOI,并核对录用与演示节目单;不下载 PDF。15 个演示 ID 有明确来源,其余逐篇投稿类型保持未明确,不用标题或会话主题代替 track。运行、证据恢复和限制见 [RSS 元数据说明](docs/rss-proceedings.md)。
`collect springer-selected` 核验限定的会后选集,分开保存会议年份、出版年份及选集事件身份。目前支持 AISTATS 1993 对应的 1994 年出版选集,原始会议决定仍须单独核查;配置、证据和恢复见 [会后选集说明](docs/springer-selected-volumes.md)。
## 项目结构
ACL 1963 的 AMTCL 年会摘要与 1978 的 TINLAP-2 来源关系均按精确卷适配,保留原系列、出版形式和不明逐篇角色;不将 EarlyMT 全系列映射到 ACL,1965 的替代关系也不转成全局别名。来源、排除条目和恢复边界见 [ACL 早期来源核查](docs/coverage/acl-early-sources-2026-11-09.md)。
```text
paper_atlas/
cli.py 统一命令入口
@@ -64,11 +70,15 @@ paper-atlas collect colm --cache /data/paper-index/raw/http-cache \
--out /data/paper-index/collected
```
NeurIPS 2026 的[官方节目采集器](docs/neurips-program.md)按 main、evaluations/datasets、position 分开保存,合并同一 forum 的口头与海报展示,保留期刊等未决关系;运行 `paper-atlas collect neurips-program`。它只生成元数据文件,身份核验和生产导入另行执行。
早期主系列的Workshop名称按[ICML/IROS精确届次证据](docs/coverage/historical-primary-series-2026-11-09.md)识别,保留原名与范围证据;代码支持不代表已补齐生产数据。
EWSL、EPS、ECP、EWSP 的[具体前身卷与完整快照要求](docs/coverage/historical-predecessors-2026-11-09.md)单独核验,保留原始系列及事件/出版年份;旧候选文件缺少 ECP/EWSP 时会明确提示,避免静默遗漏。
AAMAS 的[官方论文集采集器](docs/aamas-proceedings.md)目前核验了 2012 版式,可从缓存或已校验快照采集标题、作者及页面提供的摘要;发表形式单独保存,全文/扩展摘要中未明确的投稿 track 保持待明确,不根据会话主题推定主会轨道。
AAMAS 的[官方论文集采集器](docs/aamas-proceedings.md)支持已核验的 2012、2025 版式,可从缓存或已校验快照采集元数据。2025 年目录含 476 条非 keynote 出版物,保留完整作者和机构,HTML 未提供摘要时留空;发表形式与投稿 track 分开,不根据主题推定主会轨道或研究类型。
旧 DBLP 候选文件可能漏掉新命名空间。`collect dblp-family` 可从已校验的固定压缩快照和完整目录中[恢复指定 family 的原始候选](docs/dblp-family-recovery.md),例如 `ifaamas`;恢复文件保留卷、keynote 和 workshop 等原始条目,需另做来源范围与身份核验。
`paper-atlas collect --help` 查看来源;`paper-atlas import --help` 查看导入方式。完整批次步骤见[使用指南](docs/usage.md)。
@@ -106,3 +116,5 @@ CI 在已授权的远程 runner 上运行测试、构建 wheel,并从源码目
整合后的运行版本依赖、交互约束和生产切换顺序见 [运行版本整合](docs/research-runtime.md)。
已导入整卷的逐条轨道修正见 [edition-tracks 维护流程](docs/edition-tracks.md):按完整证据生成只读计划,保留原投稿和决定 ID,事务性修正 edition、标识与覆盖,并防止旧整卷刷新覆盖分类。
入库前可使用通用的[来源身份只读预检](docs/source-identity-audit.md),导出候选论文、稳定标识和既有投稿决定证据;候选结果需要审核,不会自动批准导入。
+34 -1
View File
@@ -1,6 +1,39 @@
# AAMAS 官方论文集元数据
`paper_atlas.collectors.aamas` 是按需运行的采集适配器,通过统一入口 `collect aamas-proceedings` 使用。目前只支持已独立核验的 **AAMAS 2012** HTML 版式;其他年份需先核对官方目录和逐篇结构,再增加适配,不能仅替换年份。
`paper_atlas.collectors.aamas` 是按需运行的采集适配器,通过统一入口 `collect aamas-proceedings` 使用。支持已独立核验的 **AAMAS 2012、2025、2026** HTML 版式;其他年份需先核对官方目录和逐篇结构,再增加适配,不能仅替换年份。2025、2026 解析分别封装在 `paper_atlas.collectors.aamas2025`、`paper_atlas.collectors.aamas2026`。
## AAMAS 2026
2026 只读取[官方 HTML 目录](https://www.ifaamas.org/Proceedings/aamas2026/forms/contents.htm),不下载 PDF。冻结目录 SHA256 `ce2bc7fc5da340930343901506a9ce37c0334d4068f07546431c9deee3f13de9`,643 个条目:keynote 3、research 338、extended abstract 185、AAAI 19、Blue Sky 17、doctoral 34、demo 44、JAAMAS 3。
排除 3 个 keynote;research 的 p238 明确标注 `Retracted June 8, 2026`,完整题名、作者、栏目、日期、原文和来源回执保存在采集报告 `held_publications` 中,**不会输出 accepted 或 withdrawn 投稿记录**。当前决定模型没有独立 retracted 状态,后续须单独建模和核验;原始证据不能删除。任何新增或改变的撤稿标记都会停止采集,等待人工核查。
另一个 extended abstract p3178 明确标注 `Withdrawn June 8, 2026`,输出 `withdrawn`,在 metadata 保留日期 `2026-06-08` 和日精度;原网页没有时区/时刻,不能伪造决定 timestamp,不补造其此前的接受决定或 PDF 链接。因此 639 个输出条目是 **638 accepted、1 withdrawn**,按六个 track 分组:`proceedings_unspecified` 522、`aaai_transfer` 19、`blue_sky` 17、`doctoral` 34、`demo` 44、`jaamas` 3。全文/扩展摘要是发表形式,不是研究类型;research 栏目不能证明每篇原始 main/special 投稿轨道。JAAMAS 保留期刊展示语义,不制造另一次独立审稿录用。
[AAAI 转投详细政策](https://cyprusconferences.org/aamas2026/call-for-papers-aaai-track/) 要求基于 AAAI-26 rejected 稿件;[总览](https://cyprusconferences.org/aamas2026/calls/) 却写 AAAI-25,须保留冲突。采集器保留 AAAI 栏目归属,不据此生成原 AAAI 投稿 ID、年份或逐篇拒稿决定。目录不是原始决定 note,未来关联仍需逐篇证据。
```sh
paper-atlas collect aamas-proceedings --year 2026 \
--snapshots /data/paper-index/collected/aamas2026-primary-evidence-v1 \
--out /data/paper-index/collected/aamas2026-normalized-v1
```
在线模式将 `--snapshots` 换成 `--cache /data/paper-index/cache/aamas2026`。冻结模式 `receipts.json` 的 `receipts` 至少包含官方目录 URL、相对 `file`、`bytes`、`sha256`、`status=200` 和带时区 `finished_at`。拒绝越界路径、越界符号链接、失败状态、哈希/大小不符的页面。这里只读取目录对应回执,其他已冻结政策网页继续保留。
输出六组 JSONL/manifest、原始 HTML 与最后发布的 `aamas-proceedings-2026.json`;统一入口沿用输出目录锁和暂存目录。manifest 全部为 `partial`,639 条 HTML 摘要缺失如实记录。发布编号使用年份和起始页,随机 PDF 文档 ID 单独保留,不能沿用 2025 的 pN.pdf 规则。
首次部署先核对来源哈希和生产状态,再在新输出目录运行采集、独立比对 643 条标题/作者/栏目分区,随后做 PostgreSQL 身份预检与增量导入。采集、远程 CI 通过或目录含某个年份均不代表生产已导入、摘要齐全或最终去重完成。较早年份不自动启动补采,当前主动补漏范围按用户要求从 2020 年起。
## AAMAS 2025
2025 版式使用[官方首页](https://www.ifaamas.org/Proceedings/aamas2025/)和[出版目录](https://www.ifaamas.org/Proceedings/aamas2025/forms/contents.htm)。原始目录共 479 条:3 keynote、260 research paper、148 extended abstract、12 Blue Sky、25 doctoral、22 Demo 栏目、9 JAAMAS。排除 keynote,输出 476 条;Demo 栏目中的 p3000 是明确标题的 COMP24 竞赛报告,发表年份仍为 2025,格式记 `competition_report`、投稿 track 留作 `proceedings_unspecified`,保留原 Demo 栏目证据。其余输出按 track 分组为 409 unspecified、12 blue_sky、25 doctoral、21 demo、9 jaamas。
作者按逐篇 BR 行解析,斜体机构原文另存,保留 Unicode、顺序及完整名单。HTML 没有提供摘要或逐篇 DOI,故 `abstract=null`、标识字典为空;只保存 PDF 链接,不下载研究 PDF。不能把 Research Paper Track 的所有条目推成 main,不能把目录缺席推为 rejected/withdrawn,也不能把出版格式标签当研究类型。
2025 用法:`paper-atlas collect aamas-proceedings --year 2025 --snapshots /data/review/aamas2025 --out /data/paper-index/collected/aamas-2025`;在线模式改用 `--cache /data/paper-index/cache/aamas2025`,只请求上述两个 HTML 页面。快照目录包含原始 `index.html`、`contents.html` 和 `receipts.json`,凭据必须列出各原始 URL、文件、字节数、SHA256、`http_status=200` 和带时区 `at`。已恢复的凭据需保留 `at_precision=filesystem_write_time_recovered` 等说明,不能伪造 HTTP 头或请求时间。2026-11-09 保存的两个页面通过哈希校验;独立录用清单 URL 返回 403,失败保留,不绕过或依赖其完成采集。
2025 命令同样使用目录锁、临时目录、最后写入采集报告。五个 JSONL/manifest 均保持 `partial`,记录摘要缺失数量;它不连接数据库。首次采集需独立核验全部标题/作者/栏目和 source IDs,随后做 PostgreSQL 身份预检、必要的逐条版本审核、原生导入与独立验收。若失败且没有完成报告,应先检查原始响应和错误,再使用新输出目录重跑,不重放已提交批次。代码验证、原始目录盘点和采集成功均不代表已导入生产或最终去重完成。
## 来源与范围
+2
View File
@@ -1,5 +1,7 @@
# 会议覆盖与公开决定核查
**当前工作范围:2020 年起主动补漏。** 2000 年起已发现资料保留,恢复工作从 [补漏范围与恢复清单(2026-11-10)](backfill-handoff-2026-11-10.md) 开始;下文旧日期快照不改写,较早年份排除不算补齐。
这是 2026-11-08 的首份核查基线,**完整逐年政策普查和最终去重仍未完成**。收录 43 个目标,不能把“有记录”当作“完整覆盖”。结构化清单见 [inventory-2026-11-08.json](inventory-2026-11-08.json)。清单中的 `policy_checks` 均限定所列年份和轨道;空数组表示尚未核实,绝不表示没有公开拒稿。
## 快照与复核
@@ -0,0 +1,64 @@
# ACL 早期年份:会议存在、出版目录与逐篇资料分开核验
固定普查中 ACL 1963–1978 仍有年份缺口。不能因为会议年表连续,就假设每年都有独立的正式论文集;也不能因为当前目录始于 1979,就断言此前没有摘要、通知或其他公开资料。本次不修改固定的 1,266 个目标年份基准。
## 1978 年:限定 T78-1 的来源关系
[同期年会通知](https://aclanthology.org/J77-4001.pdf)的 PDF 第 9–10 页(印刷页码 9–10)明确说明,1978 年 ACL 年会采用第二届 Theoretical Issues in Natural Language Processing 的形式,地点为 Urbana/Champaign,日期为 7 月 25–27 日。通知中的 ACM 联合赞助确认仍处于待确认状态,不能将该预告改写为已完成的行政决定。
[1982 年 Donald E. Walker 的历史回顾](https://aclanthology.org/www.mt-archive.info/70/ACL-1982-Walker-1.pdf)第 3 页(印刷页码 91)的年会表及缩写说明,独立对应了 1978 年的时间、地点与 TNLP/TINLAP-2 关系。两份扫描件相关页已渲染并逐页目视核对,不仅依靠 OCR。
[官方卷目录](https://aclanthology.org/volumes/T78-1/)与固定 XML `T78.xml` 保留原系列名 TINLAP、完整卷名、1978 年及编辑 David L. Waltz。适配仅接受精确的 collection `T78`、volume `1` 和上述字段组合;关键元数据变化会拒绝处理,等待重新核查。
- 规范事件名使用 `ACL+TINLAP`,原系列名 `TINLAP`、原始 XML 的 `venue_codes: [tinlap]` 及证据均保留。
- ACL/TINLAP 别名只挂在这一规范事件系列上,不把所有 TINLAP 年份都算成 ACL。T75、T87 及 T78 其他卷没有此例外。
- 通知描述了邀请与提交论文两种参与方式,XML 没有逐篇角色。统一暂存为 `proceedings_unspecified`,不猜测每篇属于 invited、main 或 workshop。
- XML 共 36 条条目,其中 T78-1036 是无作者的场次讨论问题列表。精确识别并在 manifest 的 `excluded_materials` 保留其标识、标题与原因;其余 35 条作为具名贡献整理。该排除不推广到所有无作者条目,已识别条目发生变化也须重新核查。
- 全部 35 条 XML 均无摘要。出版目录中的 accepted 是来源观察;通知提及录用/不录用通知时间,不提供任何可识别的拒稿决定,不能制造 rejected 或 withdrawn。
采集器仅生成元数据文件,不写业务数据库。真实来源逐条验收、PostgreSQL 身份预检、串行导入与独立验收仍是后续门槛。35 条来源不等于 35 个新增论文实体,更不等于完成全库去重。
## 1963 年:AMTCL 旧名称与 EarlyMT 分类分开保留
[ACL 官方历史介绍](https://2023.aclweb.org/participants/about_acl/)确认 AMTCL 是协会旧名称,1968 年改称 ACL。[1963 官方卷目录](https://aclanthology.org/volumes/1963.earlymt-1/)和固定 XML `1963.earlymt.xml` 明确记录 AMTCL 年会、Denver、1963 年 8 月 25–26 日。源目录将该卷归在 EarlyMT;这是资料分类,不能把其他 EarlyMT 会议全部变成 ACL。
适配仅接受 collection `1963.earlymt`、volume `1`,并核对旧名称全称、年份、地点、日期和原始 venue code。规范会议为 ACL,保留 AMTCL 别名及原始 `earlymt` 分类。关键字段变化或重复时拒绝处理,等待重新审核。
34 条均有作者和摘要,来源是出版摘要清单,记录 `published_abstract` 出版形式及 `acl_anthology:publication_format` 标签;逐篇 track 未提供,使用 `proceedings_unspecified`。`accepted` 只表示被列入该出版清单,不推断现代 main track、原始评审决定或任何拒稿记录。
XML commit 与下文相同,SHA256 为 `4a4626d792ac507de0281cb358769f7246f4a0d6b6a9176ac02e61545c1635fc`。原始 fixture 为 `tests/fixtures/acl-1963-earlymt.xml`。官方历史 HTML SHA256 为 `0f6bc260c2b34c9e2a51d971aeb43c3270a5b15b6db1d6d41e50669f316345f3`,响应回执与固定 XML 保存在服务器 `/data/paper-index/collected/acl1963-scope-evidence-v1`。另有卷目录 HTML 回执保存在 `/data/paper-index/collected/historical-primary-candidates-20261109-v1`。未下载论文 PDF。
使用下文相同采集命令,将 `--collection` 改为 `1963.earlymt`,输出到新的独立目录。采集结果需逐条验收和 PostgreSQL 身份预检,再进入增量导入;34 条来源不等于 34 个新增论文实体,代码测试通过也不代表生产覆盖已补齐。
## 1965 年:替代关系不改写为别名
Walker 回顾第 1 页(印刷页码 89)说明,1965 年 ICCL(后来称 COLING)替代了当年的年会。[官方 C65-1 目录](https://aclanthology.org/volumes/C65-1/)及 XML 仍以 COLING 标记该卷。这是限定年份的历史关系,不证明 COLING 与 ACL 是两个可全局互换的名字。
本次保留 C65 原有 `COLING` 归属,不添加 ACL 全局别名、不重放已导入记录、不将某一年的日历关系推广到整个系列,也不据此悄悄减少固定普查缺口。其他 1963–1977 年资料仍须逐年核查;本次不能证明这些年份整体属于非归档例外。
## 固定来源与恢复
服务器证据目录:`/data/paper-index/collected/acl-early-source-evidence-v1`。五份官方响应均 HTTP 200,另有两份来自既有官方 XML checkout 的文件,已与 Git 对象逐字节核对。未下载论文全文集合;两份 PDF 是本次历史/行政取证资料,不进入论文全文库。
| 文件 | SHA256 |
|---|---|
| `acl-venue.html` | `c60f64cb4d72e677aeb84a5844cde28a8d50579e1995f08e8c7801eed1e3fac5` |
| `acl-history-1982.pdf` | `35c21bd4983975bcaccbb94bd8ab91c9019fc190feec1791255407cf18a7e49a` |
| `acl-1978-notice.pdf` | `14b5deb471c55afb07d789529b95d0462a2a1a77df3dfba7fa1f5d3ab75eb411` |
| `tinlap1978-volume.html` | `7e20d709f72f415819e4d17e3674b19dc7d871eabcf0cf8b4935f0392d775729` |
| `coling1965-volume.html` | `0d2f2f49f2a311f9c0b80a8832f6476eb32f94f35abf36f6d2f2e10063ac880f` |
| `T78.xml` | `f712f94cff0cba05f786b7ac31dad2ea9408fde2a17a04531130df623f57dd84` |
| `C65.xml` | `572791610e20b7c6045d984d968e73a18a144bcb5c3da64112001c781826808e` |
XML commit:`a7be623d01c48c61405dec31c871121cdbed5a01`。测试 fixture `tests/fixtures/acl-t78.xml` 是该原始文件,不是手工模拟论文;不包含 PDF。PDF 哈希建立完整性,不单独证明语义,语义依据是上述具体页面。
在服务器使用既有命令,固定 checkout 并输出到新目录:
```sh
paper-atlas collect acl \
--checkout /data/paper-index/raw/metadata-checkouts/acl-anthology \
--collection T78 \
--out /data/paper-index/collected/acl-t78-source-v1
```
该命令会校验/更新官方 metadata checkout,实际 commit 必须以生成清单为准;严格重现本次取证时,使用已冻结的 XML 和上述 commit,不把更新后的内容冒充旧快照。不要将无 collection 限制的全部历史输出直接作为增量队列。采集完成不代表导入完成,恢复前先检查当前单元和报告,不因日志安静重启已执行的写入。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
# 补漏范围与恢复清单(2026-11-10)
用户确定:**后续主动补漏从 2020 年起**。2000 年起已经发现的名单、缺口、资料组合和身份核查记录保留在项目中,供下次接着做;已有旧数据继续保留。停止补采的年份标为范围外或延期,不算作已补齐。
本页是下一次工作的入口,结构化记录见 [backfill-handoff-2026-11-10.json](backfill-handoff-2026-11-10.json)。它保存 43 个会议的 2000–2026 已观察年份、缺年、官方目录文件入口、近期待办与恢复证据。旧的日期快照不改写,也不能当实时数据库。
## 当前范围和年份差集
依据生产只读快照 `2026-11-09 16:26:41.688509+00:00`,原全历史口径为 1266 个目标年份槽位:1208 已观察、43 个历史缺年、14 个 2026 缺年,以及 1 个 SysML 2018 非存档例外。原 135 个历史缺口中有 92 个通过数据补入解决;其余 43 个现在停止补采,不能因此宣称全部解决。
- **2020–2025**:没有整年完全缺记录的目标槽位,但逐篇、track、摘要、公开负面决定仍不完整。
- **2026**:NeurIPS、ECCV、RecSys、CIKM、ICDM、SDM、IROS、RSS、CoRL、ICAPS、AAMAS、ICASSP、ISMIR、ACM MM,共 14 个槽位。须逐个核查是否已经公开出版;不能将尚未发布的资料说成漏采。
- **2000–2019 延期线索**:WWW 2000、ICAPS 2001 两个整年缺口;官方历届记录分别见 [WWW](www-editions-2026-11-08.json)、[ICAPS](icaps-editions-2026-11-08.json)。SysML 2018 是非存档首届,见 [MLSys](mlsys-editions-2026-11-08.json),不能制造正式出版记录。
- **2000 年前**:剩余 41 个历史缺年停止补采。旧来源文件继续保留作证据,不再作为当前工作队列。
以上只判断目标会议年份是否至少有一条投稿记录,绝不等于目录完整、独立论文数量或最终去重验收。
## 近期资料与恢复位置
服务器采集根目录为 `/data/paper-index/collected`,本地证据备份为 `/Users/zzyang/Documents/Codex/2026-11-06/paper-index`。这些目录提供大体积原始文件,仓库 JSON 提供路径、状态和可用哈希;恢复前先验证文件存在、哈希和生产实际状态。
| 资料 | 当前状态 | 下一步与证据入口 |
| --- | --- | --- |
| AAMAS 2026 | 官方目录 643 条,3 个 keynote 排除;640 个非 keynote 中含 1 个明确撤稿。尚未导入 | `aamas2026-primary-evidence-v1/` 保存官方 HTML、回执、逐项目录、审计和校验清单。八个标题差异/撤稿前缀条目需核查。先扩展正式 collector 并远程验证,再做新 PostgreSQL 身份预检。 |
| CVPR 2026 Findings | 941 条摘要已核验,尚未导入 | `cvf-2026-verified/recovery-audit.json`;原 404 与 RVLF/FVLF 路径更正证据保留。先核对 manifests、track 和实体关系。 |
| AAMAS 2025 | 476 个非 keynote 来源中,240 已验收、236 待身份核查 | `aamas2025-import225-v1/` 与 `aamas2025-import15-v1/` 是已消费批次,禁止重放。15 条最近验收报告哈希见 JSON。余下资料从当前身份候选与新只读基线继续。 |
| RSS 2025 | 163 条目录中 162 已验收,`2025/p117` 待核查 | 保留 spinal/RMCC 关系问题;查看工作状态键 `rss2025_held7_primary_evidence_v1`,不能重跑已消费的 6/88/54/14 批次。 |
| 近期公开负面决定首批 | 1625 条已验收:1624 rejected、1 withdrawn | `public-negative-postgresql-import-postcheck-v1.json`;仅该批次验收,不是公开拒稿全集,不要重放。 |
AAMAS 2026 官方目录分区:research 338、extended abstracts 185、AAAI 19、Blue Sky 17、doctoral 34、demo 44、JAAMAS 3,另有 keynote 3。JAAMAS 是期刊展示关系,不能凭目录再造一次独立审稿录用。官方撤稿条目在 research 分区,不能把全目录都标为当前 accepted。
AAAI 转投政策有年份冲突:[详细征稿页](https://cyprusconferences.org/aamas2026/call-for-papers-aaai-track/) 指 AAAI-26 rejected,[总览](https://cyprusconferences.org/aamas2026/calls/) 写 AAAI-25。保留两份证据;19 个 AAAI track 条目不能直接转成 19 条有原始 note 的拒稿决定。原投稿 ID、逐篇决定和版本关系仍需核实。
## 2000–2019 已发现但延期的身份与名单工作
- **AAMAS 2012**:官方 334 条,319 已独立验收、15 个身份条目保留待查。验收报告 `aamas2012-import-v1/postgresql-postcheck-v1.json`,哈希在 JSON;已验收 319 条及被替代的 316 条计划均不得重放。来源布局与用法见 [AAMAS 适配说明](../aamas-proceedings.md)。
- **UAI 2008**:官方 programme 与归档资料确认了四篇独立论文,但部分原始 DBLP 键的标题、作者、页码、出版链接对应仍有冲突。JSON 保留四组候选、arXiv ID、官方题名及归档报告哈希。之前仅根据页码推断题名/作者错误的结论已撤回,未应用纠正;恢复时不得据此覆盖或合并。
- **其他名单**:本目录全部 `*-editions-2026-11-08.json` 保留各会议官方年份及来源;[历史缺口](historical-gaps-2026-11-09.md)、[来源候选](historical-source-candidates-2026-11-09.json)、[来源范围](historical-source-scope-2026-11-09.md)、[主系列](historical-primary-series-2026-11-09.md)、[前身会议](historical-predecessors-2026-11-09.md)、[ICLR 档案](iclr-archive-2014-2016.md)继续留档。这些旧报告的“缺失”可能已被后续导入解决,恢复时必须对照当前状态,不能直接当可执行队列。
## 公开拒稿资料不能遗漏
结构化清单保留最近已查的政策 URL、限定年份和可用性结论。优先继续 UAI 2024–2026 的 opt-in 公开拒稿来源、EMNLP 2023 的公开集合范围及其他目标的逐年核查;平台使用 OpenReview 不能证明 rejected 公开。访问受限如实记录,不绕过 Challenge。
保留 rejected、desk rejected、withdrawn、retracted、resubmit 的区别;评审建议不等于最终决定。COLM 2026 的不公开政策只适用于已核实年份,不能推广到其他会议。PaperCopilot 与原 OpenReview、镜像间有重叠,候选计数不能相加。论文先被 A 拒绝再被 B 录用时,两次投稿、决定、来源与证据全部保留。
## 下次启动顺序
1. 读取本页和 JSON,再核实服务器工作状态、明确的 systemd invocation 与终态报告。权威工作状态 `/Users/zzyang/Documents/Codex/2026-11-06/paper-index/work-state.json` 中部分旧字段会过期;以最新验收和实际查询为准。
2. 当前主动范围为 2020 起,2000–2019 只恢复线索,不自动启动其采集。先检查新来源是否已导入,验收过的队列禁止重放。
3. 使用现有 `foundry-postgres` 的 `foundry_data.research`。当前已验证 writer 为提交 `714f745bd5c3fbc742be1efae0b31ab37a55e21f` 的固定 release;恢复时重新确认,不使用旧 `/data/paper-index/code` 替代。服务配置仅由服务器受保护文件读取,不复制密码。
4. 对待导入资料做新只读身份预检,处理稳定标识、短标题、作者/版本差异、track 与跨会历史,再生成小范围增量队列。每批独立验收,SQLite 只读备份保持保护。
5. 编译、测试或 wheel 构建仅在已确认 `ces-control:host` runner 执行。代码按工作分支交付 PR,等待用户手动合并。范围内导入结束后仍须全库去重、近似标题/作者冲突、来源-年份-track、摘要、约束、实际全文查询验收;不能用单批导入或 CI 成功替代。
+37
View File
@@ -0,0 +1,37 @@
# DBLP family 候选恢复
DBLP 的会议命名空间会变化。旧候选过滤器包含 `atal`、`aamas`,遗漏了 `ifaamas`;2026-11-01 固定快照的完整目录实际已有 `conf/ifaamas/2025` 和 `conf/ifaamas/2026`。新目录盘点已把 `ifaamas` 纳入广泛候选范围,已有快照也可按需恢复,无需重新下载或重建完整目录。
## 运行
在已有元数据的服务器运行:
```sh
paper-atlas collect dblp-family \
--archive /data/paper-index/raw/dblp-2026-11-01.xml.gz \
--dtd /data/paper-index/raw/dblp-2023-06-28.dtd \
--catalogue /data/paper-index/collected/dblp/dblp-catalogue.sqlite \
--family ifaamas \
--out /data/paper-index/collected/ifaamas-recovered
```
`--family` 可以重复,但仅接受项目明确列出的候选 family。`--out` 必须尚不存在。同名输出由文件锁防止并发恢复;任务运行中应观测原任务,不能因为日志间隔长就重复启动。完整扫描会定期打印进度。
命令重新验证固定压缩包 SHA256、DTD MD5 与大小,并只读打开目录。目录旁的 `dblp-ai-candidates.manifest.json` 必须属于同一个快照和 DTD,包含完整记录类型计数。旧候选文件不作为完整性依据,也不会被改写。
恢复扫描使用项目原有 XML 流式解析器,仅允许已校验的本地 DTD,不联网读取 XML 实体。最终会比对完整快照的记录总数、逐 family 数量和全部选中卷的原始元数据。全部一致后才原子发布输出目录;失败时不发布半成品,也不覆盖既有文件。排查明确终止的失败任务后,可用尚不存在的输出目录重新运行。
## 输出与后续流程
- `dblp-family-candidates.jsonl`:原始 `inproceedings` 和 `proceedings`,保留书目 key、原始作者、标题、年份、卷引用、出版链接等字段。
- `recovery-manifest.json`:快照、DTD、目录凭据哈希,完整扫描数量、逐 family 数量、选中卷、旧候选文件的已知数量及输出哈希。状态为 `recovered_raw_candidates_pending_scope_review`。
恢复结果没有新增规范化的 `venue`、`track` 或决定标签,也不是 `import records` 的输入队列。`ifaamas` 的整卷可能混有研究论文、扩展摘要、keynote、博士生、演示和 JAAMAS 展示。目录过滤修复不会把这些条目统一标成 main;规范化的范围映射仍需第一方逐栏目证据。
后续先用官方目录核对标题、页码、作者及发表形式,再查数据库稳定标识和候选实体,保留每次投稿、决定和标签来源。只有审核后的新增分区才能原生导入并独立验收;已验收的来源记录不能重放。单个年份有记录不代表逐篇、track、摘要或公开拒稿覆盖完整。
## 2026-11-09 核查实例
对既有固定快照的独立原始元数据恢复发现 `ifaamas` 有 1,122 条出版物候选及 2 卷:2025 年 479 条、2026 年 643 条。2025 年的 479 条标题与起始页均对应官方目录,其中 3 条 keynote 应排除,余下 476 条出版物需逐项处理。该数字包含已经验收的 225 条来源,不是 1,122 篇可新增论文。
此实例说明过滤遗漏的影响,不代表恢复命令已完成生产导入。2026 年仍需核验官方出版清单、栏目、身份及逐次决定;缺少录用记录不能推断为拒稿。最终全库去重、覆盖和查询审计继续单独进行。
+56
View File
@@ -0,0 +1,56 @@
# NeurIPS 官方节目元数据
`paper-atlas collect neurips-program` 是按需运行的采集器,当前核验范围是 2026 年的[官方 orals/posters JSON](https://neurips.cc/static/virtual/data/neurips-2026-orals-posters.json)。它只请求这个固定公开元数据文件,不连接数据库,不请求 OpenReview API,不下载 PDF、视频或个人资料页。模块位于 `paper_atlas.collectors.neurips_program`,通过统一 CLI 使用。
## 已核验快照
2026-11-09T17:57:40Z 冻结的原始响应共 20,447,021 字节,SHA256 `879293a863057fa05370443b3b5ddee12a10eeb37c810abc6c1d53f71f545997`。原始 `count=9276` 与结果数组长度一致,`next`、`previous` 均为空;这只证明此份公开文件已完整取得,不是全部会议出版物或公开拒稿全集。
按官方逐条 `sourceurl` 中的明确投稿来源分开处理:
| 原始 OpenReview group | 数据库 track | 不同 forum 数 |
| --- | --- | ---: |
| `NeurIPS.cc/2026/Conference` | `main` | 7896 |
| `NeurIPS.cc/2026/Evaluations_and_Datasets_Track` | `evaluations_datasets` | 971 |
| `NeurIPS.cc/2026/Position_Paper_Track` | `position` | 137 |
| 合计 | | 9004 |
只接受已核验的显式 `Accept`、`Accept (poster/spotlight/oral)` 标签,保留原始大小写和文本。未知来源、决定、展示类型、可见性或无法验证的 forum 链接进入 `held_presentations`,不推成 accepted 或 rejected。空标题、缺少作者、相同 event ID 不同内容、同一 forum 的题名/作者/来源/决定冲突会停止采集。
原 9276 行中,event 154866 有一条内容完全相同的重复行,单独保留其原始行号;另外 132 个口头展示与同 forum 的海报合并为一条投稿候选,完整保留两种展示及时间、场次、原始带符号 `sourceid`。负数 `sourceid` 不能被解释为拒稿或另一次投稿,也不转换为全局论文标识。
其余 139 行单独留档:87 个期刊展示、36 个 Bridging the Future 展示、16 个未提供明确投稿来源的条目。这些不因缺少链接而被推为拒稿,也不能被冒充为独立主会审稿录用。后续须核实原期刊发表、认证、复现或其他展示关系。
9004 条是不同 forum 的来源记录,不是已经新增 9004 篇独立科学论文。不同 forum 即使题名和作者相同也保留各自投稿;跨会及版本关系另行核验。题名、完整作者顺序和机构取自原始 JSON,口头与海报不是研究类型;展示标签独立使用 `neurips_program:presentation_format` namespace。节目中的未来展示时间不冒充决定时间。
## 运行与恢复
在线模式只请求一个固定元数据 URL,并复用受限大小的响应缓存:
```sh
paper-atlas collect neurips-program \
--cache /data/paper-index/cache/neurips2026-program \
--out /data/paper-index/collected/neurips2026-program-YYYYMMDD
```
冻结模式只读 `receipts.json` 对应的原始文件:
```sh
paper-atlas collect neurips-program \
--snapshots /data/paper-index/collected/neurips2026-program-probe-v1 \
--out /data/paper-index/collected/neurips2026-program-normalized-v1
```
快照回执包含精确 `url`、相对 `file`、`bytes`、`sha256`、`status=200` 和带时区的 `finished_at`。拒绝越界路径/符号链接、重复回执、失败响应、大小或哈希不符。在线模式沿用项目 HTTP 缓存和限速,验证回执时间后保存原始 JSON。输出目录有互斥锁,完成报告最后发布;已有完成报告时要求新建输出目录,避免覆盖后误重放。
输出三个 track 的 JSONL/manifest、原始 JSON 证据和 `neurips-program-2026.json`。manifest 始终为 `partial`;原始节目 JSON 没有摘要,9004 条 `abstract=null` 和缺失原因如实保存,不能用别篇摘要填充。命令终端只输出数量与路径,完整待处理展示与原始行号在报告中。
恢复时先校验文件和回执,使用同一完整快照生成新输出;若已有完成报告或导入回执,先核实实际来源绑定和 checkpoint,不重放已消费队列。生成文件后还需 PostgreSQL 身份预检、关联审核、增量队列、原生导入和独立验收;源文件成功、CI 通过均不能替代生产导入或最终全库去重。
## 目录差异与公开拒稿限制
同期[官网 Downloads 页面](https://neurips.cc/Downloads/2026)的活动总数不是本 JSON 的论文总数。逐个 poster ID/题名对照后,网页另有 event 155165(Lemon)未出现在本 JSON 中,须单独取得并审核元数据,不能补造其 track 或 forum。16 个未提供来源的条目在 JSON 中使用通用 session URL,而网页有各自 poster URL;原值保留,不能因 URL 映射差异制造新论文。
[2026 Main Track Handbook](https://neurips.cc/Conferences/2026/MainTrackHandbook)明确允许 rejected 作者选择公开。当前无账号公开 notes API 请求返回 `ChallengeRequiredError`、HTTP 403,失败原文和回执保留,不绕过验证;因此不能把本 accepted 节目文件当作拒稿数据源或据此推断拒稿是否存在。官网下载表单的 JSON 导出也曾超时,未取得导出内容,已改用独立公开静态节目文件并保留失败证据,不重试超时导出。
当前主动补漏范围从 2020 年起。期刊/展示关系、网页独有条目、摘要和公开负面决定仍有待办;冻结资料和代码适配不代表完整会议覆盖。
-38
View File
@@ -1,38 +0,0 @@
# RSS 官方论文元数据
这是长期维护、按需运行的 package collector,不是定时服务或一次性导入脚本。目前仅接受独立核对过的 2025 年版式;不把 2026 年或其他年份套用同一分类规则。
## 数据范围和证据
- [RSS XXI 官方论文集](https://www.roboticsproceedings.org/rss21/)列出 163 个不同的逐篇 HTML 链接。每篇有 citation 元数据、BibTeX 和摘要区;PDF 链接作为原始元数据保存,不请求全文。
- [官方 Accepted Papers](https://roboticsconference.org/2025/program/papers/)按论文 ID、标题和 session 对账。网页作者列包含隐藏的重复展示,逐篇有序 citation_author 与论文集目录作者完整对比后才采用;不截断作者、不将重复展示当新论文。
- [官方 Demos](https://roboticsconference.org/2025/program/demos/)明确列出 15 个论文 ID,包括标题不以 Demonstrating 开头的 ID 93。这些条目标为 `demo`,保留整行节目证据。其他 148 条使用 `proceedings_unspecified`:没有逐篇类型依据时,不因缺席演示表就自动推断 main。
- [作者说明](https://roboticsconference.org/2025/information/authorinfo/)描述 Science/Systems 与 Demo 两类投稿,两类都进入论文集。分类属于该来源的投稿/发表语义,不自动生成通用 research_type 标签。
`rss_proceedings:session` 保留官方 session,`rss_proceedings:program_role` 保留明确的 demonstration。二者与其他平台标签隔离;session 主题不等于投稿 track。单篇来源 ID 是 `2025/pNNN`,DOI 同时保留原始 BibTeX 大小写和标准化身份键,显式的 `publication_doi` 供原生导入器在同一会议年度/track 中关联其他正式发表来源;不能据此合并不同 track 或不同会议的投稿经历。
只有已发表、被官方录用表列出的条目才产生 accepted 记录。采集观察时间来自响应回执;出版日期单独留在 metadata,不伪造原始决定日期。来源未列出的论文不能推断为 rejected、desk rejected 或 withdrawn;公开负面决定是另一个待核范围。
## 运行
构建、安装和项目测试仅使用已确认的 `ces-control:host` 远程 runner。本机不能执行隐式构建的验证。服务器已安装环境运行示例:
```sh
paper-atlas collect rss-proceedings --year 2025 \
--snapshots /data/paper-index/collected/rss2025-full-evidence-v1 \
--out /data/paper-index/collected/rss2025-source-v1
```
也可使用 `--cache /data/paper-index/raw/rss-http` 请求限定官方 HTML 元数据。两种输入互斥。冻结快照的 `receipts.json` 每项必须包含 `file`、准确 `url/final_url`、`http_status: 200`、`content_type: text/html`、`bytes`、`sha256` 和含时区的观察时间 `at`。路径必须位于快照内,不能通过符号链接逃逸。所有输入逐个核验字节数与哈希,UTF-8 解码;重定向、未知版式和异常 ID 需要先核查。
采集只输出 JSONL、每个 track 的 manifest、完整 HTML 证据和 `rss-2025.json` 来源报告,不连接生产数据库。两份清单状态保持 `partial`;即使该卷 163 个链接全部解析,也不表示独立作品去重、逐篇类型或公开负面决定全集已完成。明确为空的摘要保存 `null` 和原因;缺失或重复摘要边界属于版式异常,停止输出。
## 冻结与恢复
2026-11-09 在服务器冻结 167 个官方 HTML 响应:目录、录用表、演示表、作者说明和 163 个逐篇页面。回执 SHA256:`f427d93a113702030864e789efe1176d69f71d2c567e81536d155e53d9513a93`。六个未改写样例位于 `tests/fixtures/rss2025/`,附其真实响应回执;完整原件保存在服务器来源目录与独立审阅备份,不作为运行时下载依赖。
整个采集持有输出目录的排他锁。全部解析和交叉检查通过后才发布 JSONL、manifest 和完成报告;完成报告最后发布。已有完成报告或 JSONL 时拒绝覆盖。若发布中断,先核对目录、回执和所有文件哈希,保留现场,再换新输出目录重采;不能看到日志安静就重启或重复导入。新的官方页面内容或计数与冻结样本不同,必须另留快照、解释变化并验证后再更新采集器。
生产导入前必须另外做来源独立核验和 PostgreSQL 只读身份预检。DOI、同标题候选和既有 arXiv 实体需要逐项核查,保留跨会的所有投稿和决定关系;发现冲突时隔离,不能只按标题强合并。已导入队列不得重放。生产独立验收通过后才重新统计年度缺口;导入成功和 CI 不能替代最终全库去重、摘要/标签及约束和实际查询审计。
本说明中的 163/15 是冻结官方页面的来源清单,不是当前生产库导入结果。历史缺口在本批生产验收前仍为 47。
+21
View File
@@ -0,0 +1,21 @@
# 来源身份只读预检
`audit source-identities` 在 PostgreSQL 的同一 `REPEATABLE READ READ ONLY` 快照中,检查规范化 JSONL 的来源绑定、稳定标识及精确标题候选,并导出候选论文的原始来源和投稿决定历史。用于入库前评审,不执行导入、合并或标识修改。
```sh
PGSERVICEFILE=/etc/paper-atlas/pg_service.conf paper-atlas audit source-identities \
--pg-service foundry-research --pg-schema research \
--files /data/paper-index/collected/example/main.jsonl \
/data/paper-index/collected/example/position.jsonl \
--out /data/paper-index/collected/example-identity-preflight-v1
```
每个 JSONL 必须有同名 `.manifest.json`,包含 `source`、`venue`、`year`、`track`、`count` 和 `file_sha256`。逐行校验届次、显式决定状态、标识类型和跨文件来源主键唯一性;文件哈希和数量必须相符。只读取调用者指定的文件,不跟随 manifest 中的绝对 `file` 路径;原始行哈希包含换行符。原始 JSONL 和 manifest 应与报告一起保留。
输出目录必须不存在,防止复用旧报告或覆盖证据。失败会保留 `failed.json` 及已完成分片,不能把部分输出当成功;排查后使用新的输出目录重新核查。成功报告 `postgresql-preflight.json` 最后原子写入,记录输入哈希、数据库快照和每条候选。实体详情存为 `entities/<分片>/<paper_id>.json.gz`,报告中列出压缩文件的 SHA-256;包含完整 paper、标识和限量原始来源、投稿、决定及出处。每篇最多保留 100 条来源、200 次投稿,每次投稿最多 200 条决定,每组决定或投稿出处最多 100 条,所有截断均显式标记。精确标题最多取 21 个候选;达到上限即标记,不能据此推算总数。这里没有计算全表精确数量。
候选分类用于安排审核:既有来源记录需要核对并避免重放,多个绑定冲突必须暂缓;单个 DOI、arXiv 或 OpenReview 绑定也可能错误,必须核对原文、标题、作者和历史。作者比较保留完整规范化姓名的重复次数,但姓名相同不能证明是同一个人。未找到精确标题或标识不能证明是新作品,仍需近似标题、版本和跨会历史核查。
所有条目固定为 `not_approved_for_import`,批准数量为 0。不要直接把此报告当成 curation 计划或导入队列。后续获批导入前应重新检查实时来源绑定;来源摘要缺失不得覆盖已有摘要,旧拒稿与新录用应作为不同投稿经历保留。本命令不能代替所有导入结束后的全库去重、覆盖和约束验收。
构建与测试仅在已确认的 `ces-control:host` 远程 runner 执行。集成测试使用已有 PostgreSQL 服务做真实只读候选查询,不往生产库添加测试记录。
+36
View File
@@ -0,0 +1,36 @@
# 会后选集的元数据采集
`collect springer-selected` 读取已冻结、带 HTTP 回执的公开 HTML,不访问 PDF、不写业务库。目前仅支持核验过的 *Selecting Models from Data: Artificial Intelligence and Statistics IV*,书籍 DOI 为 `10.1007/978-1-4612-2660-4`。增加其他卷前必须另行核对会议关系、目录、年份和字段格式。
[会议官方旧站](https://www.gatsby.ucl.ac.uk/aistats/workshops.html)区分现场分发的 Preliminary Papers 与之后修订、再次评审的选集。[出版社卷说明](https://link.springer.com/book/10.1007/978-1-4612-2660-4)将本书关联到 1993 年 1 月的第四届 workshop,书籍出版年份为 1994。三页目录共有 49 个章节 DOI;分区 Front Matter 不作为章节论文导入。
规范记录保留:
- 会议年份 1993、独立 track `selected_volume` 与事件标识 `aistats-1993-selected-volume-1994`;出版年份单独记为 1994。
- 章节本身的 DOI、作者原始引用写法及顺序、姓名首字母、标题、摘要、页码和逐页原始证据。不扩写作者首字母。
- 明确提供本章 `publication_doi` 投稿出版标识,不把它转移到旧的原始会议评审记录,也不因短标题相同而强制合并实体。
- `springer_selected_volume:publication_format` 标签与独立来源 namespace。
- `accepted` 仅表示章节已列入这本出版选集,原始 1993 投稿评审决定、决定日期和全部录用清单均未知。后续原会议 main、拒稿或撤稿证据可以独立保留。
不能据此宣称会议全部录用、全部投稿或公开拒稿覆盖完整,也不能把出版社机器添加的关键词视为作者标签。未命中选集不等于被拒。
## 按需运行与恢复
输入分别是卷目录/旧站说明和章节页的快照目录。每个目录的 `receipts.json` 包含 `responses` 列表,每条保留 `url`、`final_url`、`file`、`http_status`、`headers`、`bytes`、`sha256`、`at`。需要三页目录、官方旧站说明及全部 49 个章节页面。
```sh
paper-atlas collect springer-selected \
--toc-snapshots /data/paper-index/collected/aistats1993-book-evidence-v1 \
--chapter-snapshots /data/paper-index/collected/aistats1993-chapter-evidence-v1 \
--out /data/paper-index/collected/aistats1993-source-v1
```
采集器验证回执和内容哈希、文件路径、HTTP 类型、目录/章节 DOI 和标题、作者格式、出版年份及页码。Springer 对公开元数据返回的同路径 `cookies_not_supported` 诊断参数会原样保留;不接受其他域、路径、分页变化或未知查询参数。官方旧站说明固定哈希为 `6db205b9dbc82b4a096556efd2aa67e065cad8e0bd6351176b1962ffd1fca179`,变化时须重新审核。
全部文件先核验,输出使用排他锁和暂存目录,完成报告最后发布。已有输出拒绝覆盖;发现不完整或已消费文件,应先查运行状态与报告,另建明确版本目录,不能重放生产导入。
输出 JSONL 配套 manifest 始终为 `partial`。收集完成后还必须做 PostgreSQL 稳定标识/标题/作者身份预检、排除已导入记录、准备限定增量文件、保存旧数据保护基线、原生导入和独立验收。49 条来源不代表 49 个新增论文实体,全部来源处理后仍须全库去重审计。
## 固定测试资料
`tests/fixtures/springer-aistats1993-fixture.json.gz` 保存 4 份范围/目录 HTML、49 份章节 HTML、原始请求回执及独立原始字段审计的预期元数据,SHA256 为 `94028fd37080f9d8027e5790e379cc151ee6eab17ed0d1dcc0f59e6e75f9d4d0`。摘要按 HTML 段落边界分隔;未修改原始 HTML 或回执。它是压缩的公开元数据测试资料,不包含论文 PDF。测试覆盖全部真实章节字段、缺失和漂移输入、来源安全边界、幂等导入及旧拒稿历史保留。构建和测试只在已授权远程 runner 执行。
+252
View File
@@ -0,0 +1,252 @@
"""Read-only, evidence-preserving identity candidates for normalized source files.
A candidate is never permission to import or merge. Absence of an exact match is
never proof of a new work. Run again against a fresh snapshot before curation.
"""
import argparse
from collections import Counter
import gzip
import hashlib
import json
from pathlib import Path
from paper_atlas.db.store import author_keys, normalized
from paper_atlas.db.submissions import timestamp
from paper_atlas.postgres.client import ReadConnection
STATUSES = {'accepted', 'rejected', 'desk_rejected', 'withdrawn', 'under_review', 'unknown'}
def digest(data):
return hashlib.sha256(data).hexdigest()
def authors_multiset(authors):
# author_keys intentionally removes duplicates; identity review must retain them.
return Counter(key for author in authors for key in author_keys([author]))
def load_inputs(files):
records, inputs, seen = [], [], set()
for filename in files:
path = Path(filename).resolve()
manifest_path = path.with_suffix('.manifest.json')
raw_manifest = manifest_path.read_bytes()
manifest = json.loads(raw_manifest)
hasher, count = hashlib.sha256(), 0
with path.open('rb') as stream:
for line_number, line in enumerate(stream, 1):
hasher.update(line)
row = json.loads(line)
for key in ('source', 'external_id', 'title', 'venue', 'track'):
if not isinstance(row.get(key), str) or not row[key].strip():
raise ValueError(f'Invalid {key}: {path.name}:{line_number}')
if type(row.get('year')) is not int or row.get('status') not in STATUSES:
raise ValueError('Invalid year or explicit source status')
for key in ('source', 'venue', 'year', 'track'):
if row[key] != manifest.get(key):
raise ValueError(f'Manifest scope mismatch: {key}')
authors = row.get('authors')
if not isinstance(authors, list) or any(not isinstance(a, (str, list, dict)) for a in authors):
raise ValueError('Invalid authors')
identifiers = row.get('identifiers', {})
if not isinstance(identifiers, dict) or any(
not isinstance(k, str) or not k.strip() or not isinstance(v, str) or not v.strip()
for k, v in identifiers.items()):
raise ValueError('Identifiers must contain singular nonempty strings')
key = (row['source'], row['external_id'])
if key in seen:
raise ValueError('Duplicate source/external_id across inputs')
seen.add(key)
record = {k: row[k] for k in ('source', 'external_id', 'title', 'authors',
'venue', 'year', 'track', 'status')}
record.update(identifiers=identifiers, file=str(path), line=line_number,
raw_line_sha256=digest(line), source_record_hits=[],
identifier_hits=[], title_candidate_ids=[])
records.append(record)
count += 1
if not count or count != manifest.get('count') or hasher.hexdigest() != manifest.get('file_sha256'):
raise ValueError('Input count/hash mismatch or empty input')
inputs.append({'file': str(path), 'file_sha256': hasher.hexdigest(), 'count': count,
'manifest_sha256': digest(raw_manifest), 'manifest': manifest})
if not records:
raise ValueError('At least one nonempty input is required')
return records, inputs
def _lookup(connection, values, columns, lateral):
"""Bound scalar VALUES allow the existing text codec to encode each key."""
width = len(columns)
for start in range(0, len(values), 100):
batch = values[start:start + 100]
sql = 'WITH keys(' + ','.join(columns) + ') AS (VALUES ' + ','.join(
'(' + ','.join(['?'] * width) + ')' for _ in batch) + ') SELECT keys.n,hit.* FROM keys JOIN LATERAL (' + lateral + ') hit ON true'
for row in connection.execute(sql, tuple(value for item in batch for value in item)):
yield dict(row)
def find_candidates(connection, records):
source_keys = [(i, row['source'], row['external_id']) for i, row in enumerate(records)]
for hit in _lookup(connection, source_keys, ('n', 'source', 'external_id'), '''
SELECT r.id,r.paper_id FROM sources s JOIN source_records r ON r.source_id=s.id
WHERE s.name=keys.source AND r.external_id=keys.external_id'''):
records[hit.pop('n')]['source_record_hits'].append(hit)
identifiers = []
for i, row in enumerate(records):
pairs = {(row['source'], row['external_id']), *row['identifiers'].items()}
identifiers.extend((i, scheme, value) for scheme, value in sorted(pairs))
for hit in _lookup(connection, identifiers, ('n', 'scheme', 'value'), '''
SELECT scheme,value,paper_id FROM identifiers
WHERE scheme=keys.scheme AND value=keys.value'''):
records[hit.pop('n')]['identifier_hits'].append(hit)
title_keys = [(i, normalized(row['title'])) for i, row in enumerate(records)]
for hit in _lookup(connection, title_keys, ('n', 'title_key'), '''
SELECT id AS paper_id FROM papers WHERE title_key=keys.title_key ORDER BY id LIMIT 21'''):
records[hit['n']]['title_candidate_ids'].append(hit['paper_id'])
def entity_packet(connection, paper_id):
paper = connection.execute('SELECT * FROM papers WHERE id=?', (paper_id,)).fetchone()
if paper is None:
raise RuntimeError('Candidate disappeared from read-only snapshot')
sources = [dict(r) for r in connection.execute('''SELECT r.*,s.name AS source
FROM source_records r JOIN sources s ON s.id=r.source_id
WHERE r.paper_id=? ORDER BY r.id LIMIT 101''', (paper_id,))]
attempts = [dict(r) for r in connection.execute('''SELECT s.*,v.acronym AS venue,e.year,e.track,e.event_key
FROM submissions s JOIN editions e ON e.id=s.edition_id JOIN venues v ON v.id=e.venue_id
WHERE s.paper_id=? ORDER BY s.id LIMIT 201''', (paper_id,))]
packet = {'paper': dict(paper), 'identifiers': [dict(r) for r in connection.execute(
'SELECT * FROM identifiers WHERE paper_id=? ORDER BY scheme,value', (paper_id,))],
'source_records': sources[:100], 'source_records_truncated': len(sources) > 100,
'attempts': attempts[:200], 'attempts_truncated': len(attempts) > 200}
for attempt in packet['attempts']:
observations = [dict(r) for r in connection.execute(
'SELECT * FROM decisions WHERE submission_id=? ORDER BY id LIMIT 201', (attempt['id'],))]
attempt['decisions'], attempt['decisions_truncated'] = observations[:200], len(observations) > 200
provenance = [dict(r) for r in connection.execute('''SELECT ss.*,r.external_id,s.name AS source
FROM submission_sources ss JOIN source_records r ON r.id=ss.source_record_id
JOIN sources s ON s.id=r.source_id WHERE ss.submission_id=?
ORDER BY ss.source_record_id,ss.source_submission_id LIMIT 101''', (attempt['id'],))]
attempt['sources'], attempt['sources_truncated'] = provenance[:100], len(provenance) > 100
for decision in attempt['decisions']:
provenance = [dict(r) for r in connection.execute('''SELECT ds.*,r.external_id,s.name AS source
FROM decision_sources ds JOIN source_records r ON r.id=ds.source_record_id
JOIN sources s ON s.id=r.source_id WHERE ds.decision_id=?
ORDER BY ds.source_record_id,ds.source_decision_id LIMIT 101''', (decision['id'],))]
decision['sources'], decision['sources_truncated'] = provenance[:100], len(provenance) > 100
return packet
def classify(record, entities):
anchors = {r['paper_id'] for r in record['source_record_hits'] + record['identifier_hits']}
titles = record['title_candidate_ids']
flags = []
own_authors = authors_multiset(record['authors'])
if not own_authors:
flags.append('missing_source_authors')
if any(n > 1 for n in own_authors.values()):
flags.append('duplicate_normalized_author_names')
if len(normalized(record['title'])) < 25:
flags.append('short_title')
comparisons = []
for pid in sorted(anchors | set(titles)):
entity = entities[pid]
other = authors_multiset(entity['authors'])
comparisons.append({'paper_id': pid,
'normalized_title_equal': normalized(record['title']) == normalized(entity['title']),
'nonempty_author_multiset_equal': bool(own_authors) and own_authors == other,
'author_name_overlap': sorted(own_authors.keys() & other.keys())})
if len(anchors) > 1:
kind = 'conflicting_existing_bindings_hold'
elif record['source_record_hits']:
kind = 'existing_source_record_reconcile_no_replay'
elif anchors:
kind = 'existing_identifier_binding_requires_review'
elif len(titles) >= 21:
kind = 'title_candidate_limit_reached_hold'
elif not titles:
kind = 'no_exact_match_not_proof_of_novelty'
elif len(titles) > 1:
kind = 'multiple_exact_title_entities_hold'
elif comparisons[0]['nonempty_author_multiset_equal']:
kind = 'one_exact_title_author_multiset_requires_review'
elif comparisons[0]['author_name_overlap']:
kind = 'partial_author_overlap_requires_review'
else:
kind = 'no_author_agreement_hold'
return {'kind': kind, 'flags': flags, 'comparisons': comparisons,
'title_candidates_truncated': len(titles) >= 21,
'decision': 'not_approved_for_import'}
def _atomic_json(path, value):
temporary = path.with_suffix(path.suffix + '.part')
with temporary.open('w', encoding='utf-8') as stream:
json.dump(value, stream, ensure_ascii=False, indent=2)
stream.write('\n')
temporary.replace(path)
def audit(files, out, *, pg_service, pg_schema):
# Reject input corruption before opening a database connection or reserving output.
records, inputs = load_inputs(files)
out = Path(out)
out.mkdir(parents=True, exist_ok=False)
(out / 'audit.lock').write_text(timestamp() + '\n')
connection = None
try:
connection = ReadConnection(pg_service, pg_schema)
connection.execute("SET LOCAL statement_timeout='60s'")
if connection.execute('SHOW transaction_read_only').fetchone()[0] != 'on':
raise RuntimeError('A read-only transaction is required')
snapshot = dict(connection.execute('''SELECT current_database() AS database,current_schema() AS schema,
transaction_timestamp()::text AS transaction_at,pg_current_snapshot()::text AS snapshot''').fetchone())
find_candidates(connection, records)
ids = sorted({hit['paper_id'] for row in records for hit in row['source_record_hits'] + row['identifier_hits']} |
{pid for row in records for pid in row['title_candidate_ids']})
entities, index = {}, {}
for number, pid in enumerate(ids, 1):
packet = entity_packet(connection, pid)
relative = Path('entities') / str(pid // 10000) / f'{pid}.json.gz'
target = out / relative
target.parent.mkdir(parents=True, exist_ok=True)
raw = gzip.compress(json.dumps(packet, ensure_ascii=False).encode(), mtime=0)
target.write_bytes(raw)
index[str(pid)] = {'file': relative.as_posix(), 'sha256': digest(raw)}
entities[pid] = {'title': packet['paper']['title'],
'authors': json.loads(packet['paper']['authors_json'])}
if number % 250 == 0 or number == len(ids):
print(json.dumps({'phase': 'entity_evidence', 'completed': number, 'total': len(ids)}), flush=True)
kinds = Counter()
for row in records:
row.update(classify(row, entities))
kinds[row['kind']] += 1
report = {'status': 'completed_read_only_candidates_not_import_approval',
'started_snapshot': snapshot, 'completed_at': timestamp(), 'transaction_read_only': True,
'database_written': False, 'approved_for_import': 0, 'records': len(records),
'kind_counts': dict(kinds), 'inputs': inputs, 'entities': index, 'entries': records,
'limitations': ['Exact keys only; no match does not establish a new work.',
'Name agreement does not prove person or scientific-work identity.',
'Bound identifiers can be incorrect; review titles, authors and original evidence.',
'Source/history limits are explicit in compressed entity packets.',
'Not a final whole-database deduplication or coverage audit.']}
_atomic_json(out / 'postgresql-preflight.json', report)
return report
except Exception as error:
_atomic_json(out / 'failed.json', {'status': 'failed_no_import_approval',
'error_type': type(error).__name__, 'database_written': False})
raise
finally:
if connection is not None:
connection.close()
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog, description=__doc__)
parser.add_argument('--files', nargs='+', required=True, type=Path)
parser.add_argument('--pg-service', required=True)
parser.add_argument('--pg-schema', required=True)
parser.add_argument('--out', required=True, type=Path)
args = parser.parse_args(argv)
report = audit(args.files, args.out, pg_service=args.pg_service, pg_schema=args.pg_schema)
print(json.dumps({k: report[k] for k in ('status', 'records', 'kind_counts', 'approved_for_import')}, ensure_ascii=False))
+4 -1
View File
@@ -13,8 +13,8 @@ COMMANDS = {
('download',): ('paper_atlas.io.snapshots', {}, '下载并校验固定版本快照'),
('collect','pmlr'): ('paper_atlas.collectors.pmlr', {}, 'PMLR 官方元数据'),
('collect','acl'): ('paper_atlas.collectors.acl', {}, 'ACL Anthology XML'),
('collect','springer-selected'): ('paper_atlas.collectors.springer_selected', {}, '核验限定的会后选集章节与原会议关系'),
('collect','iclr-archive'): ('paper_atlas.collectors.iclr_archive', {}, '核对ICLR 2014–2016官方历史录用档案'),
('collect','rss-proceedings'): ('paper_atlas.collectors.rss', {}, '核对RSS 2025官方论文摘要与演示证据'),
('collect','iclr-program'): ('paper_atlas.collectors.iclr', {}, 'ICLR 官方公开程序'),
('collect','iclr-snapshot'): ('paper_atlas.collectors.openreview', {}, 'ICLR 公开投稿与决定快照'),
('collect','public-openreview'): ('paper_atlas.collectors.public_openreview', {}, '核验其他会议的公开拒稿及撤稿标签'),
@@ -26,8 +26,10 @@ COMMANDS = {
('collect','cvf'): ('paper_atlas.collectors.vision', {'source':'cvf'}, 'CVPR / ICCV 官方开放目录'),
('collect','ecva'): ('paper_atlas.collectors.vision', {'source':'ecva'}, 'ECCV 官方开放目录'),
('collect','aamas-proceedings'): ('paper_atlas.collectors.aamas', {}, '按官方目录区分 AAMAS 发表形式和摘要'),
('collect','neurips-program'): ('paper_atlas.collectors.neurips_program', {}, '按公开节目来源区分 NeurIPS 2026 投稿与重复展示'),
('collect','www-archive'): ('paper_atlas.collectors.www_archive', {}, '核验早期WWW官方档案并分开待核查条目'),
('collect','dblp'): ('paper_atlas.collectors.dblp_records', {}, '核对并整理 DBLP 历史发表记录'),
('collect','dblp-family'): ('paper_atlas.collectors.dblp_recovery', {}, '从固定快照恢复指定 DBLP family 的原始候选'),
('catalogue','dblp'): ('paper_atlas.collectors.dblp', {}, '盘点 DBLP 历史会议目录'),
('import','arxiv'): ('paper_atlas.ingestion.arxiv', {}, '导入 arXiv 元数据 ZIP'),
('import','records'): ('paper_atlas.ingestion.records', {}, '导入核验过的会议文件'),
@@ -44,6 +46,7 @@ COMMANDS = {
('audit','arxiv'): ('paper_atlas.audits.arxiv', {}, '核验 arXiv 原始编号、重复行与 ZIP 完整性'),
('audit','arxiv-tags'): ('paper_atlas.audits.arxiv_tags', {}, '逐记录核验 arXiv 类别及出处完整性'),
('audit','public-openreview'): ('paper_atlas.audits.public_openreview', {}, '核对公开负面决定与已有投稿历史'),
('audit','source-identities'): ('paper_atlas.audits.source_identity', {}, '只读核对来源文件的身份候选与既有投稿历史'),
('audit','histories'): ('paper_atlas.audits.histories', {}, '只读核对跨会议投稿历史及决定来源关系'),
}
+9 -3
View File
@@ -136,6 +136,12 @@ def read_pages(cache=None, snapshots=None):
def _collect(year, out, *, cache=None, snapshots=None):
if year == 2026:
from paper_atlas.collectors.aamas2026 import collect as collect_2026
return collect_2026(out, cache=cache, snapshots=snapshots)
if year == 2025:
from paper_atlas.collectors.aamas2025 import collect as collect_2025
return collect_2025(out, cache=cache, snapshots=snapshots)
if year != 2012:
raise ValueError('Only the independently reviewed AAMAS 2012 layout is supported')
pages, receipts = read_pages(cache, snapshots)
@@ -190,8 +196,8 @@ def _collect(year, out, *, cache=None, snapshots=None):
def collect(year, out, *, cache=None, snapshots=None):
if year != 2012:
raise ValueError('Only the independently reviewed AAMAS 2012 layout is supported')
if year not in (2012, 2025, 2026):
raise ValueError('Only independently reviewed AAMAS 2012/2025/2026 layouts are supported')
out = Path(out)
out.mkdir(parents=True, exist_ok=True)
with (out/'aamas.collect.lock').open('a') as lock:
@@ -201,7 +207,7 @@ def collect(year, out, *, cache=None, snapshots=None):
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--year', type=int, choices=[2012], required=True)
parser.add_argument('--year', type=int, choices=[2012, 2025, 2026], required=True)
parser.add_argument('--out', required=True)
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument('--cache')
+183
View File
@@ -0,0 +1,183 @@
"""Reviewed AAMAS 2025 publisher TOC; HTML metadata only, never paper PDFs."""
from collections import Counter, defaultdict
import hashlib
import json
from pathlib import Path
import re
import tempfile
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.html import Document, Node, clean
from paper_atlas.io.http import fetch
ROOT = 'https://www.ifaamas.org/Proceedings/aamas2025/'
URLS = {'index': ROOT, 'contents': ROOT+'forms/contents.htm'}
TITLE = 'AAMAS 2025: Proceedings of the 2025 International Conference on Autonomous Agents and Multiagent Systems'
SECTIONS = {
'1': ('Keynote Talks', 'keynote', 'keynote', 3),
'2': ('Research Paper Track', 'full_paper', 'proceedings_unspecified', 260),
'3': ('Extended Abstracts', 'extended_abstract', 'proceedings_unspecified', 148),
'4': ('Blue Sky Ideas', 'blue_sky_paper', 'blue_sky', 12),
'5': ('Doctoral Consortium', 'doctoral_abstract', 'doctoral', 25),
'6': ('Demo Track', 'demo', 'demo', 22),
'7': ('JAAMAS Track', 'journal_presentation', 'jaamas', 9),
}
COMPETITION_TITLE = '[COMP24] The Automated Negotiating Agents Competition (ANAC) 2024 Challenges and Results'
PAPER = re.compile(r'\.\./pdfs/p([0-9]+)\.pdf')
def one(items, label):
if len(items) != 1:
raise ValueError('Expected one '+label)
return items[0]
def parse_contents(html, evidence):
doc = Document(html)
if [clean(n.text()) for n in doc.root.walk() if n.tag == 'title'] != [TITLE]:
raise ValueError('Unexpected AAMAS proceedings title/year')
for paragraph in doc.root.walk():
if paragraph.tag != 'p' or any(n is not paragraph and n.tag == 'p' for n in paragraph.walk()):
continue
anchors = [n.attrs['name'] for n in paragraph.walk() if n.tag == 'a' and n.attrs.get('name') in SECTIONS]
if anchors and (len(anchors) != 1 or clean(paragraph.text()) != SECTIONS[anchors[0]][0]):
raise ValueError('Publisher section heading differs from reviewed layout')
section = None
headers = []
records, excluded, counts, seen = [], [], Counter(), set()
for node in doc.root.walk():
if node.tag == 'a' and node.attrs.get('name') in SECTIONS:
section = node.attrs['name']
headers.append(section)
if node.tag != 'p' or any(n is not node and n.tag == 'p' for n in node.walk()):
continue
links = [n for n in node.walk() if n.tag == 'a' and PAPER.fullmatch(n.attrs.get('href', ''))]
if not links:
continue
link = one(links, 'paper link per paragraph')
if section not in SECTIONS:
raise ValueError('Paper outside a reviewed publisher section')
page = int(PAPER.fullmatch(link.attrs['href'])[1])
if page in seen:
raise ValueError('Repeated publisher paper/page ID')
seen.add(page)
title = clean(link.text())
if not title:
raise ValueError('Empty paper title')
# Names are outside italicized affiliations and separated by explicit BRs.
font = one([n for n in node.walk() if n.tag == 'font' and n.attrs.get('color', '').lower() == '#222222'], 'ordered author block')
authors, affiliations = [], []
name, affiliation = '', ''
def flush():
nonlocal name, affiliation
if clean(name) or clean(affiliation):
if not clean(name):
raise ValueError('Affiliation without author')
authors.append(clean(name)); affiliations.append(clean(affiliation))
name, affiliation = '', ''
for child in font.children:
if isinstance(child, str):
name += child
elif child.tag == 'br':
flush()
elif child.tag == 'i':
affiliation += child.text()
else:
raise ValueError('Unreviewed author markup: '+child.tag)
flush()
if not authors:
raise ValueError('Empty author list')
heading, publication_format, track, _ = SECTIONS[section]
counts[section] += 1
if section == '1':
excluded.append({'publisher_page': page, 'title': title, 'authors': authors, 'reason': 'publisher keynote talk, not a research-paper submission'})
continue
note = 'Explicit publisher section; section topics are not research types.'
if track == 'proceedings_unspecified':
note = 'Published format is known; individual main/special submission track is not provided.'
if page == 3000:
if section != '6' or title != COMPETITION_TITLE:
raise ValueError('Competition report differs from reviewed publisher entry')
publication_format, track = 'competition_report', 'proceedings_unspecified'
note = 'Publisher places this competition report in Demo Track; no individual demonstration submission is inferred.'
records.append({
'source': 'aamas_proceedings', 'external_id': '2025/p'+str(page),
'url': URLS['contents']+'#'+section, 'title': title, 'abstract': None,
'authors': authors, 'identifiers': {}, 'venue': 'AAMAS', 'venue_aliases': [],
'year': 2025, 'track': track, 'status': 'accepted',
'observed_at': evidence['observed_at'],
'raw_decision': 'listed in official AAMAS 2025 published proceedings '+heading,
'tags': [{'namespace': 'aamas_proceedings:publication_format', 'value': publication_format}],
'metadata': {'publisher_page': page, 'paper_url': ROOT+'pdfs/p'+str(page)+'.pdf',
'section_url': URLS['contents'], 'section_anchor': section,
'section_heading': heading, 'publication_format': publication_format,
'author_affiliations': affiliations, 'source_evidence': evidence,
'track_scope_note': note, 'decision_date_precision': 'not_supplied',
'missing_abstract_reason': 'Publisher HTML supplies no abstract; research PDF not downloaded.',
'publication_evidence': 'Published proceedings entry, not an original decision note.'}})
if headers != list(SECTIONS) or dict(counts) != {k: v[3] for k, v in SECTIONS.items()}:
raise ValueError('Publisher section inventory differs from reviewed 2025 layout')
if len(records) != 476 or len(excluded) != 3 or sum(r['metadata']['publication_format'] == 'competition_report' for r in records) != 1:
raise ValueError('Unexpected publication partition')
return records, excluded
def read_pages(cache, snapshots):
if bool(cache) == bool(snapshots):
raise ValueError('Choose exactly one cache or frozen snapshot directory')
frozen = json.loads((Path(snapshots)/'receipts.json').read_text())['receipts'] if snapshots else []
pages = {}
for key, url in URLS.items():
if snapshots:
r = one([r for r in frozen if r['url'] == url], 'frozen response receipt')
filename = Path(r['file']); base = Path(snapshots).resolve()
if filename.is_absolute() or '..' in filename.parts or not (base/filename).resolve().is_relative_to(base) or r.get('http_status') != 200:
raise ValueError('Unsafe or unsuccessful publisher response')
data = (base/filename).read_bytes()
if len(data) > 8*1024**2 or len(data) != r['bytes'] or hashlib.sha256(data).hexdigest() != r['sha256']:
raise ValueError('Frozen publisher response checksum/size mismatch')
observed = r['at']
else:
data = fetch(url, cache, 8*1024**2, delay=1)
r = json.loads((Path(cache)/hashlib.sha256(url.encode()).hexdigest()).with_suffix('.json').read_text())
observed = r['retrieved_at']
timestamp(observed)
pages[key] = data, {'url': url, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.html',
'observed_at': observed, 'http_status': 200, 'response_receipt': r}
return pages
def collect(out, *, cache=None, snapshots=None):
pages = read_pages(cache, snapshots)
if [clean(n.text()) for n in Document(pages['index'][0].decode('utf-8')).root.walk() if n.tag == 'title'] != [TITLE]:
raise ValueError('Unexpected AAMAS 2025 introduction')
records, excluded = parse_contents(pages['contents'][0].decode('utf-8'), pages['contents'][1])
grouped = defaultdict(list)
for row in records: grouped[row['track']].append(row)
out = Path(out); out.mkdir(parents=True, exist_ok=True); files = []
with tempfile.TemporaryDirectory(prefix='.aamas2025-', dir=out) as temporary:
stage = Path(temporary); (stage/'evidence').mkdir()
for data, e in pages.values(): (stage/e['file']).write_bytes(data)
for track, group in sorted(grouped.items()):
p = stage/f'aamas-proceedings-2025-{track}.jsonl'
p.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
m = {'source': 'aamas_proceedings', 'parser_version': 2, 'venue': 'AAMAS', 'year': 2025,
'track': track, 'count': len(group), 'file': str(out/p.name), 'file_sha256': hashlib.sha256(p.read_bytes()).hexdigest(),
'list_url': URLS['contents'], 'status': 'partial', 'missing_abstract_count': len(group),
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in group)),
'note': 'Complete published TOC partition; HTML abstracts absent, identities and submission-track scope require independent review.'}
p.with_suffix('.manifest.json').write_text(json.dumps(m, ensure_ascii=False, indent=2)+'\n'); files.append(m)
result = {'source': 'aamas_proceedings', 'year': 2025, 'finished_at': timestamp(),
'status': 'collected_pending_identity_review', 'records': len(records), 'files': files,
'excluded_publications': excluded, 'source_receipts': [e for _, e in pages.values()],
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in records)),
'database_written': False, 'pdfs_downloaded': False,
'note': 'Neither absence from proceedings nor aggregate rejection/withdrawal counts identify an individual negative decision.'}
report = 'aamas-proceedings-2025.json'; (stage/report).write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
(out/'evidence').mkdir(exist_ok=True)
for p in (stage/'evidence').iterdir(): p.replace(out/'evidence'/p.name)
for p in stage.iterdir():
if p.is_file() and p.name != report: p.replace(out/p.name)
(stage/report).replace(out/report)
return result
+219
View File
@@ -0,0 +1,219 @@
"""Reviewed AAMAS 2026 HTML metadata; retractions stay outside accepted queues."""
from collections import Counter, defaultdict
import hashlib
import json
from pathlib import Path
import re
import tempfile
from paper_atlas.collectors.aamas2025 import one
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.html import Document, Node, clean
from paper_atlas.io.http import fetch
ROOT = 'https://www.ifaamas.org/Proceedings/aamas2026/'
URL = ROOT+'forms/contents.htm'
TITLE = 'AAMAS 2026: Proceedings of the 2026 International Conference on Autonomous Agents and Multiagent Systems'
SECTIONS = {
'K': ('Keynote Talks', 'keynote', 'keynote', 3),
'R': ('Research Paper Track', 'full_paper', 'proceedings_unspecified', 338),
'1': ('Extended Abstracts', 'extended_abstract', 'proceedings_unspecified', 185),
'A': ('AAAI Track', 'full_paper', 'aaai_transfer', 19),
'B': ('Blue Sky Ideas Track', 'blue_sky_paper', 'blue_sky', 17),
'C': ('Doctoral Consortium', 'doctoral_abstract', 'doctoral', 34),
'D': ('Demonstration Track', 'demo', 'demo', 44),
'J': ('JAAMAS Track', 'journal_presentation', 'jaamas', 3),
}
PDF = re.compile(r'\.\./pdfs/([A-Z]{4}[0-9]{4})\.pdf')
RETRACTION_TITLE = 'Unifying Model-Free Efficiency and Model-Based Representations via Latent Dynamics'
WITHDRAWAL_TITLE = 'Cognitive Heterogeneity and Behavioral Biases in Multi-Stage Supply Chains: Evidence from LLM-Based Agent Simulations'
def parse_authors(paragraph):
font = one([n for n in paragraph.walk() if n.tag == 'font' and
n.attrs.get('color', '').lower() == '#222222'], 'ordered author block')
authors, affiliations = [], []
name, affiliation = '', ''
def flush():
nonlocal name, affiliation
if clean(name) or clean(affiliation):
if not clean(name):
raise ValueError('Affiliation without author')
authors.append(clean(name)); affiliations.append(clean(affiliation))
name, affiliation = '', ''
for child in font.children:
if isinstance(child, str):
name += child
elif child.tag == 'br':
flush()
elif child.tag == 'i':
affiliation += child.text()
else:
raise ValueError('Unreviewed author markup: '+child.tag)
flush()
if not authors:
raise ValueError('Empty author list')
return authors, affiliations
def parse_contents(html, evidence):
doc = Document(html)
if [clean(n.text()) for n in doc.root.walk() if n.tag == 'title'] != [TITLE]:
raise ValueError('Unexpected AAMAS proceedings title/year')
section = None
headers, records, excluded, held = [], [], [], []
counts, seen_pages, seen_pdf_ids = Counter(), set(), set()
for node in doc.root.walk():
if node.tag != 'p' or any(n is not node and n.tag == 'p' for n in node.walk()):
continue
anchors = [n.attrs['name'] for n in node.walk() if n.tag == 'a' and n.attrs.get('name') in SECTIONS]
if anchors:
section = one(anchors, 'section anchor')
if clean(node.text()) != SECTIONS[section][0]:
raise ValueError('Publisher section heading differs from reviewed layout')
headers.append(section)
page_fonts = [n for n in node.walk() if n.tag == 'font' and
n.attrs.get('color', '').lower() == '#444444']
if not page_fonts:
continue
page_text = clean(one(page_fonts, 'page marker').text())
page_match = re.fullmatch(r'\(Page ([1-9][0-9]*)\)', page_text)
if not page_match or section not in SECTIONS:
raise ValueError('Invalid page or paper outside reviewed sections')
page = int(page_match[1])
if page in seen_pages:
raise ValueError('Repeated publisher page ID')
seen_pages.add(page)
title_node = one([n for n in node.walk() if n.tag == 'strong'], 'paper title')
title = clean(title_node.text())
if not title:
raise ValueError('Empty paper title')
authors, affiliations = parse_authors(node)
link = one([n for n in node.walk() if n.tag == 'a' and 'href' in n.attrs], 'paper link')
href = link.attrs['href']
title_nodes = {id(n) for n in title_node.walk()}
retractions = [clean(n.text()) for n in link.walk() if n.tag == 'i' and id(n) not in title_nodes]
heading, publication_format, track, _ = SECTIONS[section]
counts[section] += 1
metadata = {'publisher_page': page, 'section_url': URL, 'section_anchor': section,
'section_heading': heading, 'publication_format': publication_format,
'author_affiliations': affiliations, 'source_evidence': evidence,
'decision_date_precision': 'not_supplied',
'missing_abstract_reason': 'Publisher HTML supplies no abstract; research PDF not downloaded.',
'publication_evidence': 'Published proceedings entry, not an original decision note.'}
withdrawn = retractions == ['Withdrawn June 8, 2026']
if withdrawn:
if (section, page, title, href) != ('1', 3178, WITHDRAWAL_TITLE, ''):
raise ValueError('Unreviewed withdrawal notice')
metadata.update({'publication_state': 'withdrawn',
'withdrawal_date': '2026-06-08',
'raw_publication_state': retractions[0],
'decision_date_precision': 'day',
'publication_evidence': 'Explicit dated publisher withdrawal notice, not an original review decision note.'})
elif retractions:
if (section, page, title, href, retractions) != ('R', 238, RETRACTION_TITLE, '', ['Retracted June 8, 2026']):
raise ValueError('Unreviewed retraction or publication status markup')
held.append({'source': 'aamas_proceedings', 'external_id': '2026/p238',
'title': title, 'authors': authors, 'year': 2026, 'track': track,
'url': URL+'#R', 'abstract': None, 'identifiers': {},
'observed_at': evidence['observed_at'], 'metadata': metadata,
'publication_state': 'retracted', 'retraction_date': '2026-06-08',
'raw_publication_state': retractions[0],
'hold_reason': 'Explicit retraction; not an accepted/withdrawn submission assertion. Dedicated decision modeling and identity review required.'})
continue
if not withdrawn:
match = PDF.fullmatch(href)
if not match or match[1] in seen_pdf_ids:
raise ValueError('Invalid or repeated publisher PDF ID')
seen_pdf_ids.add(match[1])
metadata.update({'publisher_document_id': match[1], 'paper_url': ROOT+'pdfs/'+match[1]+'.pdf'})
if section == 'K':
excluded.append({'publisher_page': page, 'title': title, 'authors': authors,
'metadata': metadata, 'reason': 'Publisher keynote, not a research submission.'})
continue
metadata['track_scope_note'] = ('Published format known; individual main/special submission track not established.'
if track == 'proceedings_unspecified' else 'Explicit publisher section.')
if section == 'A':
metadata['prior_submission_note'] = 'AAAI transfer section does not supply original submission IDs or individual prior decision notes; none inferred.'
if section == 'J':
metadata['track_scope_note'] = 'Journal presentation; not another independent journal review or research-track acceptance.'
records.append({'source': 'aamas_proceedings', 'external_id': '2026/p'+str(page),
'url': URL+'#'+section, 'title': title, 'abstract': None,
'authors': authors, 'identifiers': {}, 'venue': 'AAMAS', 'venue_aliases': [],
'year': 2026, 'track': track, 'status': 'withdrawn' if withdrawn else 'accepted',
'observed_at': evidence['observed_at'],
'raw_decision': retractions[0] if withdrawn else 'listed in official AAMAS 2026 published proceedings '+heading,
'metadata': metadata,
'tags': [{'namespace': 'aamas_proceedings:publication_format', 'value': publication_format}]})
if headers != list(SECTIONS) or dict(counts) != {k: v[3] for k, v in SECTIONS.items()}:
raise ValueError('Publisher section inventory differs from reviewed 2026 layout')
if (len(records), len(excluded), len(held)) != (639, 3, 1):
raise ValueError('Unexpected accepted/keynote/retraction partition')
if Counter(r['status'] for r in records) != {'accepted': 638, 'withdrawn': 1}:
raise ValueError('Unexpected publisher publication-state partition')
return records, excluded, held
def read_page(cache, snapshots):
if bool(cache) == bool(snapshots):
raise ValueError('Choose exactly one cache or frozen snapshot directory')
if snapshots:
base = Path(snapshots).resolve()
frozen = json.loads((base/'receipts.json').read_text())['receipts']
r = one([r for r in frozen if r['url'] == URL], 'frozen response receipt')
filename = Path(r['file'])
if (filename.is_absolute() or '..' in filename.parts or
not (base/filename).resolve().is_relative_to(base) or r.get('status') != 200):
raise ValueError('Unsafe or unsuccessful publisher response')
data = (base/filename).read_bytes()
if len(data) > 8*1024**2 or len(data) != r['bytes'] or hashlib.sha256(data).hexdigest() != r['sha256']:
raise ValueError('Frozen publisher response checksum/size mismatch')
observed = r['finished_at']
else:
data = fetch(URL, cache, 8*1024**2, delay=1)
r = json.loads((Path(cache)/hashlib.sha256(URL.encode()).hexdigest()).with_suffix('.json').read_text())
observed = r['retrieved_at']
timestamp(observed)
return data, {'url': URL, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.html',
'observed_at': observed, 'http_status': 200, 'response_receipt': r}
def collect(out, *, cache=None, snapshots=None):
data, evidence = read_page(cache, snapshots)
records, excluded, held = parse_contents(data.decode('utf-8'), evidence)
grouped = defaultdict(list)
for row in records:
grouped[row['track']].append(row)
out = Path(out); out.mkdir(parents=True, exist_ok=True); files = []
with tempfile.TemporaryDirectory(prefix='.aamas2026-', dir=out) as temporary:
stage = Path(temporary); (stage/'evidence').mkdir()
(stage/evidence['file']).write_bytes(data)
for track, group in sorted(grouped.items()):
p = stage/f'aamas-proceedings-2026-{track}.jsonl'
p.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
manifest = {'source': 'aamas_proceedings', 'parser_version': 3, 'venue': 'AAMAS', 'year': 2026,
'track': track, 'count': len(group), 'file': str(out/p.name),
'file_sha256': hashlib.sha256(p.read_bytes()).hexdigest(), 'list_url': URL,
'status': 'partial', 'missing_abstract_count': len(group),
'status_counts': dict(Counter(r['status'] for r in group)),
'publication_format_counts': dict(Counter(r['metadata']['publication_format'] for r in group)),
'note': 'Reviewed TOC partition excluding keynote/retracted entries; identity and full submission history remain unverified.'}
p.with_suffix('.manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2)+'\n')
files.append(manifest)
result = {'source': 'aamas_proceedings', 'year': 2026, 'finished_at': timestamp(),
'status': 'collected_pending_identity_review', 'records': len(records), 'files': files,
'excluded_publications': excluded, 'held_publications': held,
'status_counts': dict(Counter(r['status'] for r in records)),
'source_receipts': [evidence], 'database_written': False, 'pdfs_downloaded': False,
'note': 'Retraction evidence held separately; publication is not an original decision note, and absence is not rejection.'}
report = 'aamas-proceedings-2026.json'
(stage/report).write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
(out/'evidence').mkdir(exist_ok=True)
for p in (stage/'evidence').iterdir():
p.replace(out/'evidence'/p.name)
for p in stage.iterdir():
if p.is_file() and p.name != report:
p.replace(out/p.name)
(stage/report).replace(out/report)
return result
+18 -3
View File
@@ -6,6 +6,7 @@ import xml.etree.ElementTree as ET
from pathlib import Path
from paper_atlas.io.git_metadata import checkout_metadata
from paper_atlas.collectors.acl_historical import event_scope, material_reason
VENUES={'acl':'ACL','emnlp':'EMNLP','naacl':'NAACL','eacl':'EACL','coling':'COLING','conll':'CoNLL'}
@@ -29,7 +30,10 @@ def paper_id(collection,volume,number):
return f'{collection}-{int(volume):0{width}d}{int(number):0{digits}d}'
def volume_scope(meta,volume):
def volume_scope(meta,volume,collection=None):
historical=event_scope(meta,volume,collection)
if historical:
return historical['canonical_venue'],historical['venue_aliases'],historical['track']
codes=[text(n).lower() for n in meta.findall('venue')]
book=text(meta.find('booktitle'))
lower=book.lower()
@@ -59,7 +63,7 @@ def collect_xml(path,out,commit):
vid=volume.attrib['id']
meta=volume.find('meta')
if meta is None:raise ValueError('Volume metadata missing')
scope=volume_scope(meta,vid)
scope=volume_scope(meta,vid,collection)
key=f'{collection}-{vid}'
if scope is None:
results.append({'volume':key,'status':'excluded_by_venue','venue_codes':[text(n) for n in meta.findall('venue')],'booktitle':text(meta.find('booktitle')),'year':text(meta.find('year'))})
@@ -70,12 +74,18 @@ def collect_xml(path,out,commit):
year=int(year_text)
records=[]
frontmatter=0
excluded_materials=[]
historical=event_scope(meta,vid,collection)
for paper in volume.findall('paper'):
pid=paper.attrib['id']
if int(pid)==0:
frontmatter+=1
continue
identifier=paper_id(collection,vid,pid)
reason=material_reason(collection,vid,paper)
if reason:
excluded_materials.append({'external_id':identifier,'title':text(paper.find('title')),'reason':reason})
continue
doi=text(paper.find('doi'))
if doi.startswith('10.18653/v1/') and doi.split('10.18653/v1/',1)[1].casefold()!=identifier.casefold():
raise ValueError('Official DOI disagrees with constructed Anthology ID: '+identifier)
@@ -83,6 +93,11 @@ def collect_xml(path,out,commit):
if not title:raise ValueError('Missing paper title: '+identifier)
authors=[{'given':text(a.find('first')),'family':text(a.find('last'))} for a in paper.findall('author')]
records.append({'source':'acl_anthology','external_id':identifier,'url':f'https://aclanthology.org/{identifier}/','title':title,'abstract':text(paper.find('abstract')) or None,'authors':authors,'identifiers':{'acl':identifier,'doi':doi},'venue':venue,'venue_aliases':aliases if len(aliases)>1 else [],'year':year,'track':track,'status':'accepted','raw_decision':'listed in authoritative ACL Anthology proceedings XML','metadata':{'collection':collection,'volume':vid,'booktitle':text(meta.find('booktitle')),'venue_codes':[text(n) for n in meta.findall('venue')],'pages':text(paper.find('pages')),'publisher':text(meta.find('publisher')),'metadata_commit':commit,'source_xml':f'https://github.com/acl-org/acl-anthology/blob/{commit}/data/xml/{collection}.xml'}})
if historical:
records[-1]['metadata']['historical_event_scope']=historical
if historical.get('publication_format'):
records[-1]['metadata']['publication_format']=historical['publication_format']
records[-1]['tags']=[{'namespace':'acl_anthology:publication_format','value':historical['publication_format']}]
if not records:
results.append({'volume':key,'status':'unavailable','reason':'no research paper elements after excluding front matter'})
continue
@@ -93,7 +108,7 @@ def collect_xml(path,out,commit):
for record in records:output.write(json.dumps(record,ensure_ascii=False)+'\n')
part.replace(target)
missing=sum(not row['abstract'] for row in records)
manifest={'source':'acl_anthology','venue':venue,'year':year,'track':track,'count':len(records),'xml_paper_elements':len(volume.findall('paper')),'excluded_frontmatter':frontmatter,'missing_abstract_count':missing,'list_url':f'https://aclanthology.org/{key}/','metadata_commit':commit,'file':str(target),'status':'partial' if missing else 'collected'}
manifest={'source':'acl_anthology','venue':venue,'year':year,'track':track,'count':len(records),'xml_paper_elements':len(volume.findall('paper')),'excluded_frontmatter':frontmatter,'excluded_materials':excluded_materials,'missing_abstract_count':missing,'list_url':f'https://aclanthology.org/{key}/','metadata_commit':commit,'file':str(target),'status':'partial' if missing else 'collected'}
target.with_suffix('.manifest.json').write_text(json.dumps(manifest,indent=2))
results.append(manifest)
print(json.dumps(manifest),flush=True)
+76
View File
@@ -0,0 +1,76 @@
"""Reviewed edition-specific scope; never alias entire source classifications."""
HISTORY_URL = 'https://aclanthology.org/www.mt-archive.info/70/ACL-1982-Walker-1.pdf'
NOTICE_URL = 'https://aclanthology.org/J77-4001.pdf'
BOOK = 'Theoretical Issues in Natural Language Processing-2'
MATERIAL_TITLE = 'List of questions suggested for consideration in each session'
def text(node):
return ' '.join(''.join(node.itertext()).split()) if node is not None else ''
def event_scope(meta, volume, collection):
if (collection, volume) == ('1963.earlymt', '1'):
return amtcl1963_scope(meta)
if (collection, volume) != ('T78', '1'):
return None
if ([text(n) for n in meta.findall('venue')] != ['tinlap']
or [text(n) for n in meta.findall('year')] != ['1978']
or [text(n) for n in meta.findall('booktitle')] != [BOOK]
or [(text(n.find('first')), text(n.find('last'))) for n in meta.findall('editor')]
!= [('David L.', 'Waltz')]):
raise ValueError('Reviewed T78-1 scope metadata changed; independent review required')
return {
'collection': 'T78', 'volume': '1', 'original_venue': 'TINLAP',
'event_year': 1978, 'event_dates': ['1978-07-25', '1978-07-27'],
'relationship': 'ACL annual meeting took the form of TINLAP-2',
'canonical_venue': 'ACL+TINLAP', 'venue_aliases': ['ACL', 'TINLAP'],
'track': 'proceedings_unspecified',
'track_scope_note': 'Invited and submitted contributions are described at volume level; individual roles are not identified in XML.',
'evidence': [
{'url': NOTICE_URL, 'sha256': '14b5deb471c55afb07d789529b95d0462a2a1a77df3dfba7fa1f5d3ab75eb411',
'pdf_pages': [9, 10], 'printed_pages': ['9', '10'],
'scope': 'Contemporary annual meeting notice, not individual decision notes; ACM confirmation was pending in this notice.'},
{'url': HISTORY_URL, 'sha256': '35c21bd4983975bcaccbb94bd8ab91c9019fc190feec1791255407cf18a7e49a',
'pdf_pages': [3], 'printed_pages': ['91'],
'scope': 'Retrospective annual meeting table and TNLP abbreviation confirm the 1978 event association.'},
],
}
def amtcl1963_scope(meta):
expected = {
'venue': ['earlymt'], 'year': ['1963'],
'booktitle': ['Proceedings of the Annual meeting of the Association for Machine Translation and Computational Linguistics'],
'address': ['Denver, Colorado'], 'month': ['25-26 August'],
}
if any([text(n) for n in meta.findall(field)] != values
for field, values in expected.items()):
raise ValueError('Reviewed AMTCL 1963 scope metadata changed; independent review required')
return {
'collection': '1963.earlymt', 'volume': '1', 'original_venue': 'EarlyMT',
'original_event_name': 'Association for Machine Translation and Computational Linguistics',
'event_year': 1963, 'event_dates': ['1963-08-25', '1963-08-26'],
'relationship': 'Annual meeting under the former name of ACL; EarlyMT is the source classification, not a global ACL alias.',
'canonical_venue': 'ACL', 'venue_aliases': ['ACL', 'AMTCL'],
'track': 'proceedings_unspecified',
'track_scope_note': 'Published abstracts with named authors; individual submission tracks and original decision notes are not supplied.',
'publication_format': 'published_abstract',
'evidence': [
{'url': 'https://2023.aclweb.org/participants/about_acl/',
'sha256': '0f6bc260c2b34c9e2a51d971aeb43c3270a5b15b6db1d6d41e50669f316345f3',
'scope': 'Official ACL history identifies AMTCL as the former name, changed to ACL in 1968.'},
{'url': 'https://github.com/acl-org/acl-anthology/blob/a7be623d01c48c61405dec31c871121cdbed5a01/data/xml/1963.earlymt.xml',
'sha256': '4a4626d792ac507de0281cb358769f7246f4a0d6b6a9176ac02e61545c1635fc',
'scope': 'Official XML supplies the exact annual meeting title, dates, location and 34 contributions; hash is XML, not HTML.'},
],
}
def material_reason(collection, volume, paper):
if (collection, volume, paper.attrib.get('id')) != ('T78', '1', '36'):
return None
if text(paper.find('title')) != MATERIAL_TITLE or paper.findall('author'):
raise ValueError('Reviewed T78-1036 material changed; independent review required')
return 'Session question list, not an authored research contribution; no decision inferred.'
+1 -1
View File
@@ -20,7 +20,7 @@ DTD_MD5='96e4283e71886dec2173718957293648'
DTD_NAMES={'dblp.dtd','dblp-2023-06-28.dtd'}
RELEASE='https://doi.org/10.4230/dblp.xml.2026-11-01'
# Broad discovery candidates, including historical names; not a venue mapping.
FAMILIES=set('nips icml iclr colm colt aistats uai clear acl emnlp naacl eacl coling conll cvpr iccv eccv aaai ijcai ecai kdd sigir www wsdm recsys cikm icdm sdm ecml pkdd ecmlpkdd icra iros rss corl aips icaps ecp ewsp kr atal aamas icassp interspeech eurospeech icslp ismir miccai fat facct lrec mm mlsys'.split())
FAMILIES=set('nips icml iclr colm colt aistats uai clear acl emnlp naacl eacl coling conll cvpr iccv eccv aaai ijcai ecai kdd sigir www wsdm recsys cikm icdm sdm ecml pkdd ecmlpkdd icra iros rss corl aips icaps ecp ewsp kr atal aamas ifaamas icassp interspeech eurospeech icslp ismir miccai fat facct lrec mm mlsys'.split())
def records(archive,dtd_bytes):
+156
View File
@@ -0,0 +1,156 @@
"""Recover raw family candidates from a pinned snapshot and its existing catalogue.
The old candidate file is not an exhaustive source: earlier discovery filters
can omit an entire namespace. This command checks the complete snapshot and
catalogue, without assigning venues, tracks, acceptance or paper identities.
"""
import argparse
from collections import Counter
from contextlib import closing
import fcntl
import json
from pathlib import Path
import re
import sqlite3
import tempfile
from paper_atlas.collectors.dblp import DTD_MD5, FAMILIES, RELEASE, XML_SHA, family, records
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.snapshots import digest_file
def catalogue_evidence(catalogue, families, expected_sha, expected_dtd_md5):
manifest_path = catalogue.with_name('dblp-ai-candidates.manifest.json')
manifest = json.loads(manifest_path.read_text())
if (manifest.get('snapshot_sha256') != expected_sha
or manifest.get('dtd_md5') != expected_dtd_md5):
raise ValueError('Catalogue belongs to a different snapshot or DTD')
totals = manifest.get('record_types', {})
if (set(totals) != {'proceedings', 'inproceedings'}
or any(type(n) is not int or n < 0 for n in totals.values())
or type(manifest.get('records_scanned')) is not int
or sum(totals.values()) != manifest['records_scanned']):
raise ValueError('Incomplete catalogue record totals')
counts = Counter()
volumes = {}
# The catalogue is evidence, never a writable recovery destination.
with closing(sqlite3.connect(catalogue.resolve().as_uri() + '?mode=ro', uri=True)) as c:
for name in families:
found = c.execute('SELECT record_type,count FROM family_counts WHERE family=?',
(name,)).fetchall()
if not found or not any(n for _, n in found):
raise ValueError('Family absent from complete catalogue: ' + name)
for kind, count in found:
if kind not in totals or type(count) is not int or count < 0:
raise ValueError('Invalid catalogue family count')
counts[(name, kind)] = count
for key, raw in c.execute('SELECT key,metadata_json FROM proceedings WHERE family=?',
(name,)):
row = json.loads(raw)
if (family(key) != name or row.get('key') != key
or row.get('record_type') != 'proceedings'):
raise ValueError('Invalid catalogue proceedings evidence')
volumes[key] = row
if len(volumes) != sum(counts[(name, 'proceedings')] for name in families):
raise ValueError('Catalogue proceedings count differs from volume records')
return manifest, digest_file(manifest_path)['sha256'], counts, volumes
def recover(archive, dtd, catalogue, out, families, *, expected_sha=XML_SHA,
expected_dtd_md5=DTD_MD5):
families = list(families)
if (not families or any(not isinstance(name, str)
or not re.fullmatch(r'[a-z][a-z0-9-]*', name)
or name not in FAMILIES for name in families)):
raise ValueError('Select an explicitly supported discovery family')
families = sorted(set(families))
archive, dtd, catalogue = Path(archive), Path(dtd), Path(catalogue)
output = Path(out).absolute()
if output.exists() or output.is_symlink():
raise ValueError('Recovery output must not already exist')
output.parent.mkdir(parents=True, exist_ok=True)
lock_path = output.parent / ('.' + output.name + '.recovery.lock')
with lock_path.open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
if output.exists() or output.is_symlink():
raise ValueError('Recovery output appeared before scan')
actual = digest_file(archive)
if actual['sha256'] != expected_sha:
raise ValueError('DBLP snapshot checksum mismatch')
if dtd.stat().st_size > 128 * 1024 or digest_file(dtd)['md5'] != expected_dtd_md5:
raise ValueError('DBLP DTD checksum mismatch or size limit')
manifest, manifest_sha, expected, volumes = catalogue_evidence(
catalogue, families, expected_sha, expected_dtd_md5)
totals, found = Counter(), Counter()
seen_keys, found_volumes = set(), set()
with tempfile.TemporaryDirectory(prefix='.dblp-family-', dir=output.parent) as tmp:
stage = Path(tmp) / 'recovered'
stage.mkdir()
target = stage / 'dblp-family-candidates.jsonl'
with target.open('w') as stream:
for row in records(archive, dtd.read_bytes()):
totals[row['record_type']] += 1
name = family(row['key'])
if name in families:
if row['key'] in seen_keys:
raise ValueError('Duplicate raw bibliographic key')
seen_keys.add(row['key'])
found[(name, row['record_type'])] += 1
if row['record_type'] == 'proceedings':
if volumes.get(row['key']) != row:
raise ValueError('Raw proceedings differs from catalogue evidence')
found_volumes.add(row['key'])
stream.write(json.dumps(row, ensure_ascii=False) + '\n')
scanned = sum(totals.values())
if scanned % 250000 == 0:
print(json.dumps({'status': 'scanning_raw_family_candidates',
'records_scanned': scanned,
'recovered_records': sum(found.values())}), flush=True)
if (totals != Counter(manifest['record_types'])
or sum(totals.values()) != manifest['records_scanned']):
raise ValueError('Complete snapshot totals differ from catalogue')
if found != expected or found_volumes != volumes.keys():
raise ValueError('Recovered family totals differ from catalogue')
report = {
'format_version': 1, 'source': 'dblp',
'status': 'recovered_raw_candidates_pending_scope_review',
'finished_at': timestamp(), 'source_release': RELEASE,
'snapshot_sha256': actual['sha256'], 'dtd_md5': expected_dtd_md5,
'catalogue_manifest_sha256': manifest_sha, 'families': families,
'records_scanned': sum(totals.values()), 'record_types': dict(totals),
'recovered_records': sum(found.values()),
'recovered_publications': sum(found[(n, 'inproceedings')] for n in families),
'recovered_volumes': len(found_volumes),
'family_counts': [{'family': name, 'record_type': kind, 'count': count}
for (name, kind), count in sorted(found.items())],
'old_candidate_family_counts': {
name: manifest.get('candidate_family_counts', {}).get(name, 0)
for name in families},
'catalogue_volumes': [volumes[k] for k in sorted(volumes)],
'file': str(output / target.name),
'file_sha256': digest_file(target)['sha256'], 'database_written': False,
'note': 'Raw bibliographic candidates, including volume records and possible '
'keynotes/workshops. No venue, track, acceptance or work identity '
'inferred. Requires primary-source scope and identity review; '
'not a normalized import queue. Existing imported sources must '
'not be replayed.',
}
# Deliberately not an import-records sidecar manifest.
(stage / 'recovery-manifest.json').write_text(
json.dumps(report, ensure_ascii=False, indent=2) + '\n')
if output.exists() or output.is_symlink():
raise ValueError('Recovery output appeared during scan')
stage.rename(output)
return report
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog, description=__doc__)
parser.add_argument('--archive', required=True, type=Path)
parser.add_argument('--dtd', required=True, type=Path)
parser.add_argument('--catalogue', required=True, type=Path)
parser.add_argument('--family', required=True, action='append', choices=sorted(FAMILIES))
parser.add_argument('--out', required=True, type=Path)
args = parser.parse_args(argv)
print(json.dumps(recover(args.archive, args.dtd, args.catalogue, args.out, args.family),
ensure_ascii=False))
+218
View File
@@ -0,0 +1,218 @@
"""Official NeurIPS program metadata, with presentations separate from submissions."""
import argparse
from collections import Counter, defaultdict
import fcntl
import hashlib
import json
from pathlib import Path
import re
import tempfile
from urllib.parse import parse_qs, urlsplit
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.http import fetch
URL = 'https://neurips.cc/static/virtual/data/neurips-2026-orals-posters.json'
GROUPS = {
'https://openreview.net/group?id=NeurIPS.cc/2026/Conference': 'main',
'https://openreview.net/group?id=NeurIPS.cc/2026/Evaluations_and_Datasets_Track': 'evaluations_datasets',
'https://openreview.net/group?id=NeurIPS.cc/2026/Position_Paper_Track': 'position',
}
ACCEPTED = {'Accept', 'Accept (poster)', 'Accept (spotlight)', 'Accept (oral)',
'Accept (Spotlight)', 'Accept (Oral)'}
LIMIT = 64*1024**2
def forum_id(url):
if not isinstance(url, str):
return None
try:
parsed = urlsplit(url)
except ValueError:
return None
query = parse_qs(parsed.query, keep_blank_values=True)
if (parsed.scheme != 'https' or parsed.netloc != 'openreview.net' or
parsed.path != '/forum' or parsed.fragment or set(query) != {'id'} or
len(query['id']) != 1 or not re.fullmatch(r'[A-Za-z0-9_-]{8,32}', query['id'][0])):
return None
return query['id'][0]
def parse_program(data, evidence):
root = json.loads(data)
if (not isinstance(root, dict) or not {'count', 'next', 'previous', 'results'} <= set(root) or
not isinstance(root.get('results'), list) or
type(root.get('count')) is not int or root['count'] != len(root['results']) or
root.get('next') is not None or root.get('previous') is not None):
raise ValueError('Expected one complete, nonpaginated public program snapshot')
if not root['results']:
raise ValueError('Empty program snapshot')
events, grouped, held, duplicates = {}, defaultdict(list), [], []
for index, event in enumerate(root['results']):
if not isinstance(event, dict) or type(event.get('id')) is not int or event['id'] <= 0:
raise ValueError('Missing stable event ID')
eid = event['id']
if eid in events:
first_index, first = events[eid]
if first != event:
raise ValueError('Conflicting payloads share one publisher event ID')
duplicates.append({'event_id': eid, 'first_row': first_index, 'duplicate_row': index})
continue
events[eid] = index, event
origin = event.get('sourceurl')
reason = None
if not isinstance(origin, str) or origin not in GROUPS:
reason = ('journal_presentation_requires_original_publication_relation'
if isinstance(origin, str) and origin.startswith('journal-track/') else
'bridging_future_scope_requires_review' if origin == 'neurips2026-bridging-the-future' else
'unreviewed_or_missing_submission_origin')
elif event.get('visible') is not True:
reason = 'program_visibility_requires_review'
elif not isinstance(event.get('decision'), str) or event['decision'] not in ACCEPTED:
reason = 'unreviewed_decision_label'
elif event.get('event_type') not in ('Poster', 'Oral'):
reason = 'unreviewed_presentation_type'
forum = forum_id(event.get('paper_url'))
if reason is None and forum is None:
reason = 'missing_or_invalid_original_submission_link'
if reason:
held.append({'row': index, 'event_id': eid, 'reason': reason, 'raw_event': event})
continue
if (not isinstance(event.get('name'), str) or not event['name'].strip() or
not isinstance(event.get('authors'), list) or not event['authors'] or
any(not isinstance(a, dict) or not isinstance(a.get('fullname'), str) or
not a['fullname'].strip() for a in event['authors'])):
raise ValueError('Empty title or incomplete ordered author list')
if not isinstance(event.get('virtualsite_url'), str) or not re.fullmatch(
r'/virtual/2026/(?:poster|oral)/'+str(eid), event['virtualsite_url']):
raise ValueError('Publisher event URL differs from year/type/ID')
grouped[forum].append((index, event))
records = []
folded = 0
for forum, observations in sorted(grouped.items()):
# Signed sourceid values are preserved verbatim in the presentation
# evidence; they are not independent submissions or scientific works.
signatures = {json.dumps([e['name'], e['authors'], e['sourceurl'], e['decision']],
ensure_ascii=False, sort_keys=True) for _, e in observations}
if len(signatures) != 1:
raise ValueError('Title, authors, origin or decision disagree for one forum')
posters = [e for _, e in observations if e['event_type'] == 'Poster']
event = posters[0] if posters else observations[0][1]
track = GROUPS[event['sourceurl']]
presentations = []
for index, e in observations:
presentations.append({'row': index, 'event_id': e['id'], 'uid': e.get('uid'),
'sourceid': e.get('sourceid'), 'event_type': e['event_type'],
'eventtype': e.get('eventtype'), 'program_url': e['virtualsite_url'],
'session': e.get('session'), 'starttime': e.get('starttime'),
'endtime': e.get('endtime'), 'parent_id': e.get('parent_id'),
'raw_decision': e['decision']})
formats = sorted({e['event_type'] for _, e in observations})
records.append({
'source': 'neurips_program', 'external_id': '2026/'+forum,
'url': event['paper_url'], 'title': event['name'], 'abstract': None,
'authors': [a['fullname'] for a in event['authors']],
'identifiers': {'openreview': forum}, 'venue': 'NeurIPS', 'venue_aliases': [],
'year': 2026, 'track': track, 'status': 'accepted',
'observed_at': evidence['observed_at'], 'raw_decision': event['decision'],
'metadata': {'source_group_url': event['sourceurl'], 'forum_id': forum,
'author_affiliations': [a.get('institution') for a in event['authors']],
'presentations': presentations, 'source_evidence': evidence,
'publication_evidence': 'Explicit accepted label in official conference program; not an original decision note or proof of proceedings publication.',
'decision_date_precision': 'not_supplied',
'missing_abstract_reason': 'Program JSON does not contain abstracts; paper HTML/PDF not fetched by this command.',
'identity_scope_note': 'One record per original forum, not one scientific work per event. Other venues/versions require independent identity review.'},
'tags': [{'namespace': 'neurips_program:presentation_format', 'value': value}
for value in formats]})
folded += len(observations)-1
if not records:
raise ValueError('No reviewed submission origins found')
if root['count'] != len(records)+len(held)+len(duplicates)+folded:
raise ValueError('Program partition failed')
return records, held, duplicates, folded
def read_program(cache, snapshots):
if bool(cache) == bool(snapshots):
raise ValueError('Choose exactly one cache or frozen snapshot directory')
if snapshots:
base = Path(snapshots).resolve()
matches = [r for r in json.loads((base/'receipts.json').read_text())['receipts'] if r['url'] == URL]
if len(matches) != 1:
raise ValueError('Expected one official program receipt')
r = matches[0]; filename = Path(r['file'])
if (filename.is_absolute() or '..' in filename.parts or
not (base/filename).resolve().is_relative_to(base) or r.get('status') != 200):
raise ValueError('Unsafe or unsuccessful frozen program response')
data = (base/filename).read_bytes()
if len(data) > LIMIT or len(data) != r['bytes'] or hashlib.sha256(data).hexdigest() != r['sha256']:
raise ValueError('Frozen program checksum/size mismatch')
observed = r['finished_at']
else:
data = fetch(URL, cache, LIMIT, delay=1)
r = json.loads((Path(cache)/hashlib.sha256(URL.encode()).hexdigest()).with_suffix('.json').read_text())
observed = r['retrieved_at']
timestamp(observed)
return data, {'url': URL, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.json',
'observed_at': observed, 'http_status': 200, 'response_receipt': r}
def collect(out, *, cache=None, snapshots=None):
out = Path(out); out.mkdir(parents=True, exist_ok=True)
report_name = 'neurips-program-2026.json'
with (out/'neurips-program.collect.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
if (out/report_name).exists():
raise ValueError('Completed collection exists; use a new directory and reconcile before refresh')
data, evidence = read_program(cache, snapshots)
records, held, duplicates, folded = parse_program(data, evidence)
groups = defaultdict(list)
for r in records:
groups[r['track']].append(r)
files = []
with tempfile.TemporaryDirectory(prefix='.neurips-program-', dir=out) as temporary:
stage = Path(temporary); (stage/'evidence').mkdir()
(stage/evidence['file']).write_bytes(data)
for track, group in sorted(groups.items()):
p = stage/f'neurips-program-2026-{track}.jsonl'
p.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
manifest = {'source': 'neurips_program', 'parser_version': 1, 'venue': 'NeurIPS',
'year': 2026, 'track': track, 'count': len(group), 'file': str(out/p.name),
'file_sha256': hashlib.sha256(p.read_bytes()).hexdigest(), 'list_url': URL,
'status': 'partial', 'missing_abstract_count': len(group),
'note': 'Reviewed forum-level program partition; not full current proceedings, abstracts, public rejection coverage or scientific-work dedup.'}
p.with_suffix('.manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2)+'\n')
files.append(manifest)
result = {'source': 'neurips_program', 'year': 2026, 'finished_at': timestamp(),
'status': 'collected_pending_identity_review', 'records': len(records), 'files': files,
'held_presentations': held, 'exact_duplicate_event_rows': duplicates,
'folded_extra_presentation_rows': folded, 'source_receipts': [evidence],
'database_written': False, 'pdfs_downloaded': False,
'note': 'Public program only; rejected opt-in availability needs a separate source. Journals, bridging and missing origins remain held; absence is not rejection.'}
(stage/report_name).write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
(out/'evidence').mkdir(exist_ok=True)
for p in (stage/'evidence').iterdir():
p.replace(out/'evidence'/p.name)
for p in stage.iterdir():
if p.is_file() and p.name != report_name:
p.replace(out/p.name)
(stage/report_name).replace(out/report_name)
return result
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--out', required=True)
source = parser.add_mutually_exclusive_group(required=True)
source.add_argument('--cache')
source.add_argument('--snapshots')
args = parser.parse_args(argv)
result = collect(args.out, cache=args.cache, snapshots=args.snapshots)
print(json.dumps({'source': result['source'], 'year': 2026, 'status': result['status'],
'records': result['records'], 'files': result['files'],
'held_presentations': len(result['held_presentations']),
'exact_duplicate_event_rows': len(result['exact_duplicate_event_rows']),
'folded_extra_presentation_rows': result['folded_extra_presentation_rows'],
'report_file': str(Path(args.out)/'neurips-program-2026.json'),
'database_written': False, 'pdfs_downloaded': False}, ensure_ascii=False))
-279
View File
@@ -1,279 +0,0 @@
"""Reviewed RSS 2025 proceedings HTML; no PDF or private review access."""
import argparse
from collections import Counter
from datetime import date
import fcntl
import hashlib
import json
from pathlib import Path
import re
import tempfile
import unicodedata
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.html import Document, Node, clean
from paper_atlas.io.http import fetch
SOURCE = 'rss_proceedings'
ROOT = 'https://www.roboticsproceedings.org/rss21/'
HEADING = 'Robotics: Science and Systems XXI'
PAGES = {'index': ROOT,
'accepted': 'https://roboticsconference.org/2025/program/papers/',
'demos': 'https://roboticsconference.org/2025/program/demos/',
'authorinfo': 'https://roboticsconference.org/2025/information/authorinfo/'}
EXPECTED = 163
DEMO_IDS = {2, 20, 21, 33, 58, 59, 62, 84, 86, 92, 93, 107, 129, 132, 141}
def one(values, description):
if len(values) != 1:
raise ValueError('Expected one '+description)
return values[0]
def comparable(text):
return unicodedata.normalize('NFC', clean(text))
def parse_index(html):
document = Document(html)
nodes = list(document.root.walk())
if [clean(n.text()) for n in nodes if n.tag == 'h1'] != [HEADING]:
raise ValueError('Unexpected RSS proceedings edition')
result = {}
for row in (n for n in nodes if n.tag == 'tr'):
links = [n for n in row.walk() if n.tag == 'a'
and re.fullmatch(r'p[0-9]{3}\.html', n.attrs.get('href', ''))]
if not links:
continue
link = one(links, 'proceedings entry link')
pid = int(link.attrs['href'][1:4])
title = clean(link.text())
authors = clean(one([n for n in row.walk() if n.tag == 'i'], 'index authors').text())
if pid in result or pid < 1 or not title or not authors:
raise ValueError('Empty or repeated RSS index entry')
result[pid] = {'title': title, 'authors_display': authors,
'url': ROOT+link.attrs['href']}
occurrences = sum(n.tag == 'a' and bool(re.fullmatch(r'p[0-9]{3}\.html', n.attrs.get('href', '')))
for n in nodes)
if not result or len(result) != occurrences:
raise ValueError('Unparsed or duplicate proceedings links')
return result
def parse_program(html, heading, columns):
nodes = list(Document(html).root.walk())
if [clean(n.text()) for n in nodes if n.tag == 'h1'] != [heading]:
raise ValueError('Unexpected RSS program page')
headers = [[clean(n.text()) for n in row.children if isinstance(n, Node) and n.tag == 'th']
for row in nodes if row.tag == 'tr']
if list(columns) not in headers:
raise ValueError('Unexpected RSS program columns')
result = {}
for row in (n for n in nodes if n.tag == 'tr'):
cells = [n for n in row.children if isinstance(n, Node) and n.tag == 'td']
if not cells:
continue
if len(cells) != len(columns):
raise ValueError('Unexpected RSS program row width')
raw_id = clean(cells[0].text())
if not re.fullmatch('[1-9][0-9]*', raw_id):
raise ValueError('Invalid program ID')
pid = int(raw_id)
if pid in result:
raise ValueError('Duplicate program ID')
result[pid] = {key: clean(node.text()) for key, node in zip(columns, cells)}
if not result[pid]['Title']:
raise ValueError('Missing program title')
if not result:
raise ValueError('No RSS program rows')
return result
def validate_program(index, accepted, demos, authorinfo):
if set(index) != set(accepted) or len(index) != EXPECTED or set(demos) != DEMO_IDS:
raise ValueError('RSS program and reviewed proceedings census differ')
text = clean(Document(authorinfo).root.text())
if ('There are two types of submissions to RSS' not in text
or 'Both types of papers appear in the RSS proceedings.' not in text):
raise ValueError('Submission category evidence changed')
for pid, item in index.items():
variants = [accepted[pid]['Title']] + ([demos[pid]['Title']] if pid in demos else [])
if any(comparable(title) != comparable(item['title']) for title in variants):
raise ValueError('Conflicting official program title for '+str(pid))
def parse_paper(html, pid, entry, accepted, demo, evidence):
nodes = list(Document(html).root.walk())
meta = {}
for node in nodes:
if node.tag == 'meta' and node.attrs.get('name', '').startswith('citation_'):
meta.setdefault(node.attrs['name'], []).append(node.attrs.get('content', ''))
title = one(meta.get('citation_title', []), 'citation title')
authors = meta.get('citation_author', [])
publication_date = one(meta.get('citation_publication_date', []), 'publication date')
if not re.fullmatch(r'2025/[0-9]{2}/[0-9]{2}', publication_date):
raise ValueError('Unexpected publication year/date')
date.fromisoformat(publication_date.replace('/', '-'))
if (one(meta.get('citation_conference_title', []), 'conference') != HEADING
or one(meta.get('citation_volume', []), 'volume') != '21'
or [clean(n.text()) for n in nodes if n.tag == 'h1'] != [HEADING]
or [comparable(n.text()) for n in nodes if n.tag == 'h3'] != [comparable(title)]
or comparable(title) != comparable(entry['title']) or not authors
or any(not clean(author) for author in authors)
or comparable(', '.join(authors)) != comparable(entry['authors_display'])):
raise ValueError('Proceedings/citation title, authors or edition disagree')
pdf = one(meta.get('citation_pdf_url', []), 'citation PDF URL')
if pdf not in (ROOT+f'p{pid:03d}.pdf', ROOT.replace('https:', 'http:')+f'p{pid:03d}.pdf'):
raise ValueError('Citation PDF ID differs; URL is metadata only')
bibtex = one([n.text() for n in nodes if n.tag == 'pre'], 'original BibTeX')
doi = one(re.findall(r'^\s*DOI\s*=\s*\{([^{}]+)\}', bibtex, re.I | re.M), 'BibTeX DOI')
year = one(re.findall(r'^\s*YEAR\s*=\s*\{([^{}]+)\}', bibtex, re.I | re.M), 'BibTeX year')
if doi != f'10.15607/RSS.2025.XXI.{pid:03d}' or year != '2025':
raise ValueError('BibTeX DOI/year differs from volume and paper ID')
abstract_parts = []
inside = False
markers = []
for node in (n for n in nodes if n.tag == 'p'):
labels = [clean(n.text()) for n in node.walk() if n.tag == 'b']
if 'Abstract:' in labels:
markers.append('start')
inside = True
value = clean(node.text()).removeprefix('Abstract:').strip()
if value:
abstract_parts.append(value)
elif 'Download:' in labels:
markers.append('end')
inside = False
elif inside:
abstract_parts.append(clean(node.text()))
if markers != ['start', 'end']:
raise ValueError('Abstract boundaries missing or repeated')
abstract = clean(' '.join(abstract_parts)) or None
track = 'demo' if demo else 'proceedings_unspecified'
tags = [{'namespace': SOURCE+':session', 'value': accepted['Session']}]
if demo:
tags.append({'namespace': SOURCE+':program_role', 'value': 'demonstration'})
return {'source': SOURCE, 'external_id': f'2025/p{pid:03d}', 'url': entry['url'],
'title': title, 'abstract': abstract, 'authors': authors,
'identifiers': {'doi': doi.lower()},
'submission_identifiers': [{'scheme': 'publication_doi', 'value': doi.lower()}],
'venue': 'RSS', 'venue_aliases': [], 'year': 2025, 'track': track,
'status': 'accepted', 'observed_at': evidence['paper']['observed_at'],
'raw_decision': 'listed in official RSS 2025 proceedings and accepted program',
'tags': tags,
'metadata': {'publisher_paper_id': pid, 'citation_metadata': meta,
'publication_date': publication_date, 'original_bibtex': bibtex,
'index_entry': entry, 'accepted_program_entry': accepted,
'demo_program_entry': demo, 'source_evidence': evidence,
'missing_abstract_reason': None if abstract else 'empty_official_abstract_section',
'track_scope_note': ('Explicit official demo program ID; not inferred from title.' if demo else
'Proceedings and accepted list do not individually identify submission type; session is not a track.'),
'decision_date_precision': 'not_supplied',
'publication_evidence': 'Published proceedings and accepted program; not an original decision note.'}}
class Pages:
def __init__(self, *, cache=None, snapshots=None):
if bool(cache) == bool(snapshots):
raise ValueError('Choose exactly one cache or frozen snapshot directory')
self.cache = Path(cache) if cache else None
self.root = Path(snapshots).resolve() if snapshots else None
self.receipts = json.loads((self.root/'receipts.json').read_bytes())['receipts'] if self.root else []
self.evidence = {}
self.data = {}
def read(self, url):
if self.root:
receipt = one([r for r in self.receipts if r['url'] == url], 'exact frozen response receipt')
filename = Path(receipt['file'])
path = self.root/filename
if (filename.is_absolute() or '..' in filename.parts or not path.resolve().is_relative_to(self.root)
or receipt['http_status'] != 200 or receipt['content_type'] != 'text/html'
or receipt.get('final_url', url) != url):
raise ValueError('Unsafe or unsuccessful frozen response')
data = path.read_bytes()
observed = receipt['at']
else:
data = fetch(url, self.cache, 2*1024**2, delay=1)
receipt = json.loads((self.cache/hashlib.sha256(url.encode()).hexdigest()).with_suffix('.json').read_bytes())
observed = receipt['retrieved_at']
if (len(data) > 2*1024**2 or len(data) != receipt['bytes']
or hashlib.sha256(data).hexdigest() != receipt['sha256'] or receipt['url'] != url):
raise ValueError('Response checksum, URL or size differs')
evidence = {'url': url, 'sha256': hashlib.sha256(data).hexdigest(), 'bytes': len(data),
'file': 'evidence/'+hashlib.sha256(data).hexdigest()+'.html',
'observed_at': timestamp(observed), 'http_status': 200, 'response_receipt': receipt}
self.evidence[url] = evidence
self.data[url] = data
return data.decode('utf-8'), evidence
def collect(year, out, *, cache=None, snapshots=None):
if year != 2025:
raise ValueError('Only the independently reviewed RSS 2025 edition is supported')
out = Path(out)
out.mkdir(parents=True, exist_ok=True)
with (out/'rss.collect.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
report_path = out/'rss-2025.json'
if report_path.exists() or list(out.glob('rss-2025-*.jsonl')):
raise FileExistsError('Inspect existing output; never overwrite a completed or partial queue')
pages = Pages(cache=cache, snapshots=snapshots)
context = {key: pages.read(url) for key, url in PAGES.items()}
index = parse_index(context['index'][0])
accepted = parse_program(context['accepted'][0], 'Accepted Papers', ('ID', 'Session', 'Title', 'Authors'))
demos = parse_program(context['demos'][0], 'Demos', ('ID', 'Location', 'Title', 'Day', 'Time'))
validate_program(index, accepted, demos, context['authorinfo'][0])
records = []
for pid, entry in sorted(index.items()):
html, evidence = pages.read(entry['url'])
sources = {key: item[1] for key, item in context.items()}
sources['paper'] = evidence
records.append(parse_paper(html, pid, entry, accepted[pid], demos.get(pid), sources))
counts = Counter(r['track'] for r in records)
if counts != {'demo': len(DEMO_IDS), 'proceedings_unspecified': EXPECTED-len(DEMO_IDS)}:
raise ValueError('Track census differs; review official source changes')
files = []
with tempfile.TemporaryDirectory(prefix='.rss-', dir=out) as temporary:
stage = Path(temporary)
(stage/'evidence').mkdir()
for url, data in pages.data.items():
(stage/pages.evidence[url]['file']).write_bytes(data)
for track in sorted(counts):
group = [r for r in records if r['track'] == track]
path = stage/f'rss-2025-{track}.jsonl'
path.write_text(''.join(json.dumps(r, ensure_ascii=False)+'\n' for r in group))
manifest = {'source': SOURCE, 'parser_version': 1, 'venue': 'RSS', 'year': 2025,
'track': track, 'count': len(group), 'file': str(out/path.name),
'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
'list_url': ROOT, 'status': 'partial',
'missing_abstract_count': sum(r['abstract'] is None for r in group)}
path.with_suffix('.manifest.json').write_text(json.dumps(manifest, ensure_ascii=False, indent=2)+'\n')
files.append(manifest)
result = {'source': SOURCE, 'year': 2025, 'records': len(records), 'files': files,
'source_evidence': list(pages.evidence.values()), 'database_written': False,
'pdfs_downloaded': False, 'status': 'collected_pending_identity_review',
'note': 'Only the published proceedings. Demo IDs have explicit program evidence; '
'other individual submission types remain unknown. No absent-based negative decisions. '
'Separate source identity review and independent production acceptance are required.'}
(stage/report_path.name).write_text(json.dumps(result, ensure_ascii=False, indent=2)+'\n')
(out/'evidence').mkdir(exist_ok=True)
for path in (stage/'evidence').iterdir():
path.replace(out/'evidence'/path.name)
for path in stage.iterdir():
if path.is_file() and path.name != report_path.name:
path.replace(out/path.name)
(stage/report_path.name).replace(report_path)
return result
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--year', type=int, choices=[2025], required=True)
parser.add_argument('--out', required=True)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('--cache')
group.add_argument('--snapshots')
args = parser.parse_args(argv)
print(json.dumps(collect(args.year, args.out, cache=args.cache, snapshots=args.snapshots), ensure_ascii=False))
+231
View File
@@ -0,0 +1,231 @@
"""Reviewed postconference selected volumes from frozen public HTML metadata."""
import argparse
from collections import defaultdict
import fcntl
import hashlib
import json
from pathlib import Path
import re
import tempfile
from urllib.parse import parse_qs, urlsplit
from paper_atlas.db.submissions import timestamp
from paper_atlas.io.html import Document, clean
SOURCE = 'springer_selected_volume'
BOOK = '10.1007/978-1-4612-2660-4'
BOOK_URL = 'https://link.springer.com/book/'+BOOK
HISTORY_URL = 'https://www.gatsby.ucl.ac.uk/aistats/workshops.html'
HISTORY_SHA = '6db205b9dbc82b4a096556efd2aa67e065cad8e0bd6351176b1962ffd1fca179'
SCOPE = {
'venue': 'AISTATS', 'year': 1993, 'track': 'selected_volume',
'event_key': 'aistats-1993-selected-volume-1994',
'publication_year': 1994, 'book_doi': BOOK,
'book_title': 'Selecting Models from Data', 'expected_chapters': 49,
'relationship': 'Selected workshop papers were revised and rereviewed for later publication; this is not the original complete workshop decision list.',
}
def snapshot_index(root):
root = Path(root).resolve()
receipts = json.loads((root/'receipts.json').read_bytes())['responses']
index = {}
for receipt in receipts:
url = receipt['url']
if url in index:
raise ValueError('Duplicate snapshot URL')
index[url] = (root, receipt)
return index
def read_page(index, url):
if url not in index:
raise ValueError('Missing required public HTML snapshot: '+url)
root, receipt = index[url]
filename = Path(receipt['file'])
path = root/filename
content_type = {k.lower():v for k,v in receipt.get('headers', {}).items()}.get('content-type', '')
requested, final = urlsplit(url), urlsplit(receipt.get('final_url', ''))
query = parse_qs(final.query, keep_blank_values=True)
original_query = parse_qs(requested.query, keep_blank_values=True)
# Springer returns the public metadata with an explicit cookie diagnostic.
# Retain that final URL verbatim; accept no other redirect or changed page.
if query != original_query and requested.netloc == 'link.springer.com':
if (query.pop('error', None) != ['cookies_not_supported']
or not re.fullmatch(r'[0-9a-f-]{36}', ''.join(query.pop('code', [])))):
raise ValueError('Unreviewed Springer redirect query')
if (filename.is_absolute() or '..' in filename.parts or not path.resolve().is_relative_to(root)
or receipt.get('http_status') != 200
or (final.scheme, final.netloc, final.path) != (requested.scheme, requested.netloc, requested.path)
or final.fragment or query != original_query
or 'text/html' not in content_type.lower()):
raise ValueError('Unsafe, redirected or unsuccessful public HTML snapshot')
raw = path.read_bytes()
digest = hashlib.sha256(raw).hexdigest()
if len(raw) > 2_000_000 or len(raw) != receipt['bytes'] or digest != receipt['sha256']:
raise ValueError('Snapshot checksum or size differs')
evidence = {'url':url, 'sha256':digest, 'bytes':len(raw),
'file':'evidence/'+digest+'.html', 'http_status':200,
'observed_at':timestamp(receipt['at']), 'response_receipt':receipt}
return raw, evidence
def parse_toc(html, *, excluded_materials=None):
entries = {}
for node in Document(html).root.walk():
if node.tag != 'li' or node.attrs.get('data-test') != 'chapter':
continue
links = [n for n in node.walk() if n.tag == 'a' and re.fullmatch(
r'/chapter/'+re.escape(BOOK)+r'_[1-9]\d*', n.attrs.get('href', ''))]
if not links:
headings = [clean(n.text()) for n in node.walk() if n.tag in ('h3','h4')]
frontmatter = [n.attrs.get('href','') for n in node.walk()
if n.tag == 'a' and n.attrs.get('data-test') == 'front-matter-pdf']
if headings == ['Front Matter'] and len(frontmatter) == 1:
if excluded_materials is not None:
excluded_materials.append({'title':'Front Matter', 'url':frontmatter[0],
'reason':'Section front matter without a chapter DOI; not an authored chapter.'})
continue
if len(links) != 1:
raise ValueError('Chapter entry lacks one reviewed-book DOI')
link, = links
doi = link.attrs['href'].removeprefix('/chapter/')
if doi in entries or not clean(link.text()):
raise ValueError('Duplicate DOI or missing chapter title')
entries[doi] = clean(link.text())
return entries
def parse_chapter(html, doi):
nodes = list(Document(html).root.walk())
meta = defaultdict(list)
for node in nodes:
if node.tag == 'meta' and node.attrs.get('name', '').startswith('citation_'):
meta[node.attrs['name']].append(node.attrs.get('content', ''))
def one(key):
values = meta[key]
if len(values) != 1 or not values[0]:
raise ValueError('Missing or ambiguous citation field: '+key)
return values[0]
if (one('citation_doi') != doi or one('citation_inbook_title') != SCOPE['book_title']
or one('citation_publication_date') != str(SCOPE['publication_year'])):
raise ValueError('Chapter DOI, book or publication year differs from reviewed scope')
raw_authors = meta['citation_author']
if not raw_authors:
raise ValueError('Missing named chapter authors')
authors = []
for raw in raw_authors:
if raw.count(',') != 1:
raise ValueError('Unreviewed citation author format')
family, given = [clean(s) for s in raw.split(',', 1)]
if not family or not given:
raise ValueError('Incomplete citation author')
authors.append({'given':given, 'family':family})
abstract_nodes = [n for n in nodes if n.attrs.get('id') == 'Abs1-content']
if len(abstract_nodes) > 1:
raise ValueError('Ambiguous abstract section')
abstract = clean(abstract_nodes[0].text()) if abstract_nodes else None
first, last = one('citation_firstpage'), one('citation_lastpage')
if not first.isdigit() or not last.isdigit() or int(first) > int(last):
raise ValueError('Invalid chapter pages')
return {'doi':doi, 'title':one('citation_title'), 'authors':authors,
'authors_citation_raw':list(raw_authors), 'abstract':abstract or None,
'first_page':first, 'last_page':last}
def record_for(chapter, evidence, scope_evidence):
return {
'source':SOURCE, 'external_id':chapter['doi'],
'url':'https://link.springer.com/chapter/'+chapter['doi'],
'title':chapter['title'], 'abstract':chapter['abstract'], 'authors':chapter['authors'],
'identifiers':{'doi':chapter['doi']},
'submission_identifiers':[{'scheme':'publication_doi','value':chapter['doi']}],
**{k:SCOPE[k] for k in ('venue','year','track','event_key')},
'status':'accepted', 'observed_at':evidence['observed_at'],
'raw_decision':'listed in the 1994 selected postconference volume; original 1993 review decision not supplied',
'tags':[{'namespace':SOURCE+':publication_format','value':'selected_volume_chapter'}],
'metadata':{
'publication_format':'selected_volume_chapter', 'bibliographic_year':1994,
'related_workshop_year':1993, 'book_doi':BOOK, 'book_title':SCOPE['book_title'],
'year_basis':'Reviewed workshop year; bibliographic publication year is separately retained.',
'first_page':chapter['first_page'], 'last_page':chapter['last_page'],
'authors_citation_raw':chapter['authors_citation_raw'],
'publication_evidence':'Published selected-volume chapter, not an original workshop decision note.',
'scope_relationship':SCOPE['relationship'], 'decision_date_precision':'not_supplied',
'source_evidence':evidence, 'scope_evidence':scope_evidence,
},
}
def collect(out, *, toc_snapshots, chapter_snapshots):
toc_index, chapter_index = snapshot_index(toc_snapshots), snapshot_index(chapter_snapshots)
frozen = {}
def read(index, url):
raw, evidence = read_page(index, url)
frozen[evidence['file']] = raw
return raw.decode('utf-8'), evidence
history, history_evidence = read(toc_index, HISTORY_URL)
if hashlib.sha256(history.encode()).hexdigest() != HISTORY_SHA:
raise ValueError('Reviewed selected-volume relationship evidence changed')
scope_evidence = [history_evidence]
titles, excluded = {}, []
for page in range(1, 4):
html, evidence = read(toc_index, BOOK_URL+('?page='+str(page) if page > 1 else ''))
scope_evidence.append(evidence)
entries = parse_toc(html, excluded_materials=excluded)
if titles.keys() & entries.keys():
raise ValueError('DOI repeated across TOC pages')
titles.update(entries)
expected = {BOOK+'_'+str(i) for i in range(1, 50)}
if titles.keys() != expected:
raise ValueError('Reviewed 49-chapter inventory changed')
chapter_urls = {'https://link.springer.com/chapter/'+doi for doi in expected}
if chapter_index.keys() != chapter_urls:
raise ValueError('Missing or extraneous chapter snapshot')
records = []
for doi in sorted(expected, key=lambda s:int(s.rsplit('_',1)[1])):
html, evidence = read(chapter_index, 'https://link.springer.com/chapter/'+doi)
chapter = parse_chapter(html, doi)
if chapter['title'] != titles[doi]:
raise ValueError('Chapter title disagrees with official TOC')
records.append(record_for(chapter, evidence, scope_evidence))
out = Path(out)
out.mkdir(parents=True, exist_ok=True)
with (out/'springer-selected.collect.lock').open('a') as lock:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
if any(p.name != 'springer-selected.collect.lock' for p in out.iterdir()):
raise FileExistsError('Existing output must be inspected, not overwritten')
with tempfile.TemporaryDirectory(prefix='.springer-selected-', dir=out) as temporary:
stage = Path(temporary)
(stage/'evidence').mkdir()
for filename, raw in frozen.items():
(stage/filename).write_bytes(raw)
path = stage/'springer-aistats-1993-selected-volume.jsonl'
path.write_text(''.join(json.dumps(r,ensure_ascii=False)+'\n' for r in records))
manifest = {'source':SOURCE, 'parser_version':1,
**{k:SCOPE[k] for k in ('venue','year','track','event_key')},
'count':len(records), 'file':str(out/path.name),
'file_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),
'list_url':BOOK_URL, 'status':'partial',
'excluded_materials':excluded,
'missing_abstract_count':sum(not r['abstract'] for r in records)}
path.with_suffix('.manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
report = {'status':'collected_pending_identity_review', 'records':len(records),
'files':[manifest], 'reviewed_scope':SCOPE, 'database_written':False,
'pdfs_downloaded':False, 'scope_evidence':scope_evidence}
(stage/'collection-report.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
for item in stage.iterdir():
if item.name != 'collection-report.json':
item.replace(out/item.name)
(stage/'collection-report.json').replace(out/'collection-report.json')
return report
def main(argv=None, *, prog=None):
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--toc-snapshots', required=True)
parser.add_argument('--chapter-snapshots', required=True)
parser.add_argument('--out', required=True)
args = parser.parse_args(argv)
print(json.dumps(collect(args.out, toc_snapshots=args.toc_snapshots,
chapter_snapshots=args.chapter_snapshots),ensure_ascii=False))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
<HTML><HEAD><TITLE>AAMAS 2025: Proceedings of the 2025 International Conference on Autonomous Agents and Multiagent Systems</TITLE>
<META http-equiv=Content-Type content="text/html; charset=utf-8"><LINK
href="forms/index_files/factemplate.css" type=text/css rel=stylesheet>
<META content="MSHTML 6.00.2900.3562" name=GENERATOR></HEAD>
<BODY bgcolor="#010101" bgproperties="fixed"><DIV style="TEXT-ALIGN: center" >
<table width="724" border="0" align="center" cellpadding="0" cellspacing="0" valign="top">
<td colspan="2" align="center" valign="top" bgcolor="#FFFFFF"><img src="forms/index_files/aamas25.png" width="750" height="300" alt=""/></td>
</tr>
<tr bgcolor="#FFFFFF">
<td width="125" align="center" valign="top" bgcolor="#FFFFFF"><table width="125" border="1" align="center" cellpadding="1" cellspacing="1" bordercolor="#444444" style="margin-bottom: 2">
<tr>
<td bgcolor="#FFFFFF"><p style="text-indent: 0; margin-left: 6; margin-right: 6; margin-top: 5; margin-bottom: 4" align="left"><A href="forms/index.htm">Main
Page</A></p></td>
</tr>
<tr>
<td bgcolor="#FFFFFF"><p style="text-indent: 0; margin-left: 6; margin-right: 6; margin-top: 5; margin-bottom: 4" align="left"><A href="forms/contents.htm">Table
of Contents</A></p></td>
</tr>
</table></td>
<td width="599"><p style="text-indent: 0; margin-left: 12; margin-right: 14; margin-top: 12; margin-bottom: 12" align="left"><font color="#010101" size="4" face="Arial, Helvetica, sans-serif"><strong>Welcome
to the Proceedings of AAMAS-2025</strong></font></p>
<p style="text-indent: 0; margin-left: 12; margin-right: 14; margin-top: 12; margin-bottom: 12;" align="left"><font color="#333333" size="2" face="Times New Roman, Times, serif">These
are the proceedings of the 24th International Conference on Autonomous
Agents and Multiagent Systems (AAMAS-2025). They are published by the
International Foundation for Autonomous Agents and Multiagent Systems
(IFAAMAS).</font></p></td>
</tr>
<tr bgcolor="#FFFFFF">
<td align="center" valign="middle" bgcolor="#FFFFFF">&nbsp;</td>
<td><table width="96%" border="0" cellspacing="2" cellpadding="2">
<tr align="left" valign="top">
<td> <p style="text-indent: 0; margin-left: 12; margin-right: 6; margin-top: 0; margin-bottom: 6;" align="left"><font color="#333333" face="Times New Roman, Times, serif"><font size="2"><strong>ISBN: 979-8-4007-1426-9</strong></font></font></p></td>
<td> <p style="text-indent: 0; margin-left: 12; margin-right: 6; margin-top: 0; margin-bottom: 6;" align="left"><font color="#333333" face="Times New Roman, Times, serif"></font></p></td>
</tr>
<tr>
<td width="42%"><div align="left"><img src="forms/index_files/ifaamas.jpg" width="256" height="104"></div></td>
<td width="58%"><div align="center"><img src="forms/index_files/ACM-In-Cooperation_medium.jpg" width="285" height="86"></div></td>
</tr>
</table>
<p style="text-indent: 0; margin-left: 12; margin-right: 14; margin-top: 12; margin-bottom: 12;" align="left"><font color="#333333" size="2" face="Times New Roman, Times, serif">Copyright
&copy; 2025 by International Foundation for Autonomous Agents and Multiagent
Systems (IFAAMAS). Permission to make digital or hard copies of portions
of this work for personal or classroom use is granted without fee provided
that copies are not made or distributed for profit or commercial advantage
and that copies bear this notice and the full citation on the first page.
Copyright for components of this work owned by others than IFAAMAS must
be honored. Abstracting with credit is permitted. To copy otherwise, to
republish, to post on servers or to redistribute to lists, requires prior
specific permission and/or a fee. </font></p></td>
</tr>
</table>
<p>&nbsp;</p>
</BODY></HTML>
+38
View File
@@ -0,0 +1,38 @@
{
"status": "two_successful_official_metadata_pages_recovered_third403_not_retried",
"receipts": [
{
"file": "index.html",
"url": "https://www.ifaamas.org/Proceedings/aamas2025/",
"bytes": 4107,
"sha256": "850b7825c66d0147ac8a8bbf03a554590e4a9e8f4b5c576e043999bbd43317f4",
"at": "2026-11-09T12:31:49.347577+00:00",
"at_precision": "filesystem_write_time_recovered",
"http_status": 200,
"content_type": "text/html",
"receipt_recovered": true,
"scope": "Collectorasserted200/htmlbeforewrite;request3acceptedprogram403;noheaders/finalredirectURLpersisted;no refetch or rawbytesmutation."
},
{
"file": "contents.html",
"url": "https://www.ifaamas.org/Proceedings/aamas2025/forms/contents.htm",
"bytes": 370650,
"sha256": "732847b51972f34c721a49d8bd963a80a029a48f3ec08022c9a0d5ddc2945549",
"at": "2026-11-09T12:31:52.006642+00:00",
"at_precision": "filesystem_write_time_recovered",
"http_status": 200,
"content_type": "text/html",
"receipt_recovered": true,
"scope": "Collectorasserted200/htmlbeforewrite;request3acceptedprogram403;noheaders/finalredirectURLpersisted;no refetch or rawbytesmutation."
}
],
"failed_url": "https://aamas2025.org/index.php/conference/program/accepted-papers/",
"not_attempted_url": "https://aamas2025.org/index.php/conference/calls/submission-instructions-main-technical-track/",
"original_error": {
"error": "HTTP Error 403: Forbidden",
"type": "HTTPError",
"downloaded": 2
},
"research_fulltext_downloaded": false,
"database_written": false
}
Binary file not shown.
+263
View File
@@ -0,0 +1,263 @@
<?xml version='1.0' encoding='UTF-8'?>
<collection id="1963.earlymt">
<volume id="1" ingest-date="2021-11-14" type="proceedings">
<!-- https://aclanthology.org/1963.earlymt-1/ -->
<meta>
<booktitle>Proceedings of the Annual meeting of the Association for Machine Translation and Computational Linguistics</booktitle>
<address>Denver, Colorado</address>
<month>25-26 August</month>
<year>1963</year>
<pdf hash="2ff28cb1"/>
<venue>earlymt</venue>
</meta>
<paper id="1">
<!-- https://aclanthology.org/1963.earlymt-1.1/ -->
<title>Necessity of introducing some information provided by transformational analysis into <fixed-case>MT</fixed-case> algorithms</title>
<author><first>Irena</first><last>Bellert</last></author>
<abstract>A few examples of ambiguous English constructions and their Polish equivalents are discussed in terms of the correlation between their respective phrase-marker representations and transformational analyses. It is shown by these examples that such an investigation can reveal interesting facts for MT, and therefore should be carried out for any pair of languages for which a given MT program is being constructed. If the phrase-marker of the English construction is set into one-to-one correspondence with the phrasemarker of the Polish equivalent construction, whatever particular transformational analysis of this construction is to be taken into account, then the ambiguous phrasemarker representation can be used as a syntactical model for MT algorithms with good results. If the phrase-marker of the English construction is set into one-to-many correspondence with the phrasemarkers of the Polish equivalents, according to the transformational analyses of this construction, then the ambiguous phrase-structure representation has to be resolved in terms of transformational analysis, for only then is it possible to assign the corresponding phrase structure representation to the Polish equivalents. A tentative scheme of syntactical recognition is provided for the multiply ambiguous adjectival construction in English1 (which proved to belong to the latter case) by means of introducing some information obtained from the transformational analysis of this construction.</abstract>
<bibkey>bellert-1963-necessity</bibkey>
</paper>
<paper id="2">
<!-- https://aclanthology.org/1963.earlymt-1.2/ -->
<title>The use of a random access device for dictionary lookup</title>
<author><first>Robert S.</first><last>Betz</last></author>
<author><first>Walter</first><last>Hoffman</last></author>
<abstract>The purpose of this paper will be to present a scheme to locate for single textual items and idioms in textual order their corresponding dictionary entries stored in an IBM 1301 random access mechanism. Textual items are considered to be 24 characters in length (left justified with following blanks). A dictionary entry consists of a 24 character Russian form, grammar information for the form and a set of translations for that form. Dictionary entries are packed into sequential tracks of the 1301. This paper will cover the method used for dictionary storage. The lookup for a textual item I first consists of a search for the first track that the dictionary entry E (if one exists) for I could be stored in. Once a track has been determined its contents are searched in core by a bisection convergence technique to find E. If E cannot be found, a “no entry” indication is made. If E is found a further search is made of the dictionary to find the longest sequence of text, starting with the first item I, that has a dictionary entry. The last such entry found is picked up. Included in the presentation will be examples of the dictionary lookup output for actual text.</abstract>
<bibkey>betz-hoffman-1963-use</bibkey>
</paper>
<paper id="3">
<!-- https://aclanthology.org/1963.earlymt-1.3/ -->
<title>Generative processes for <fixed-case>R</fixed-case>ussian impersonal sentences</title>
<author><first>C. G.</first><last>Borkowski</last></author>
<author><first>L. R.</first><last>Micklesen</last></author>
<abstract>Impersonal sentences of Russian are those traditionally construed to consist of predicates only. Ever since the first Russian grammar was compiled, they have continued to pose a problem for grammarians. This paper is intended to be a review and evaluation of all types of the so-called impersonal sentences in the Russian language. The investigation of these sentences has been conducted in terms of their relationships to basic (kernel) sentences. Our paper attempts to define the origin for such impersonal sentences, i.e., how such sentences might be derived within the framework of a generative grammar from a set of rules possessing maximal simplicity and maximal generative power. The long-range aim of this investigation involves the most efficient manipulation of such sentences in a recognition device for Russian-English MT.</abstract>
<bibkey>borkowski-micklesen-1963-generative</bibkey>
</paper>
<paper id="4">
<!-- https://aclanthology.org/1963.earlymt-1.4/ -->
<title>Concerning the role of sub-grammars in machine translation</title>
<author><first>Joyce M.</first><last>Brady</last></author>
<author><first>William B.</first><last>Estes</last></author>
<abstract>The comprehensive grammars being developed at the Linguistics Research Center of the University of Texas will be too large for easy access and manipulation in either experimental programs or practical translation. It is necessary, therefore, to devise some reliable method for selecting subsets of the grammar rules which will be reasonably adequate for a given purpose. Since the majority of the rules are dictionary rules, this problem is closely related both to the problem of constructing microglossaries and to the subsequent problem of choosing a particular microglossary suitable to a given text. Our current approach to this problem entails the construction of key word lists in the first stage of analysis which guide the computer in its choice of a previously constructed microglossary. Work to date indicates adaptations of this technique may not only contribute to the solution of storage and access problems but also facilitate analysis and simplify problems of semantic resolution.</abstract>
<bibkey>brady-estes-1963-concerning</bibkey>
</paper>
<paper id="5">
<!-- https://aclanthology.org/1963.earlymt-1.5/ -->
<title>Word-meaning and sentence-meaning</title>
<author><first>Elinor K.</first><last>Charney</last></author>
<abstract>A theory of semantics is presented which (1) defines the meanings of the most frequently occurring semantic morphemes (‘all’, ‘unless’, ‘only’, ‘if’, ‘not’, etc.), (2) explains their role, as semantically interdependent structural-constants, in giving rise to sentence-meanings, (3) suggests a possible approach to a sentenceby-sentence recognition program, and (4) offers a feasible method of coordinating among different language systems synonymous sentences whose grammatical features and structural-constants do not bear a one-to-one correspondence to one another. The theory applies only to morphemes that function as structural-constants and their interlocking relationships, denotative terms being treated as variables whose ranges alone have structural significance in sentencemeaning. The basic views underlying the theory are: In any given sentence, it is the particular configuration of structural-constants in combination with specific grammatical features which produces the sentencemeaning; the defined meaning of each individual structural-constant remains constant. The word-meanings of this type of morpheme, thus, must be carefully distinguished from the sentence-meanings that configuration of these morphemes produce. Sentence-synonymy is not based upon word-synonymy alone. Contrary to the popular view that the meanings of all of the individual words must be known before the sentencemeaning can be known, it is shown that one must comprehend the total configuration of structural-constants and syntactical features in a sentence in order to comprehend the correct sentence-meaning and that this understanding of the sentence as a whole must precede the determination of the correct semantic interpretation of these critical morphemes. In fact, the structural features that produce the sentence-meanings may restrict the possible meanings of even the denotative terms since a structural feature may demand, for example, a verbal rather than a noun phrase as an indispensable feature of the configuration. Two or more synonymous sentences whose denotative terms are everywhere the same but whose structural configurations are not isomorphic express the same fundamental sentence-meaning. The fundamental sentence-meanings can be explicitly formulated, and serve as the mapping functions to co-ordinate morphemically-unlike synonymous sentences within a language system or from one system to another. The research goal of the author is to establish empirically these translation rules that state formally the structural characteristics of the sentence configurations whose sentence-meanings, as wholes, are related as synonymous.</abstract>
<bibkey>charney-1963-word</bibkey>
</paper>
<paper id="6">
<!-- https://aclanthology.org/1963.earlymt-1.6/ -->
<title>Translating ordinary language into symbolic logic</title>
<author><first>Jared L.</first><last>Darlington</last></author>
<abstract>The paper describes a computer program, written in COMIT, for translating ordinary English into the notation of propositional logic and first-order functional logic. The program is designed to provide an ordinary language input to a COMIT program for the David-Putnam proof-procedure algorithm. The entire set of operations which are performed on an input sentence or argument are divided into three stages. In Stage I, an input sentence ‘S’, such as “The composer who wrote ‘Alcina’ wrote some operas in English,” is rewritten in a quasi-logical notation, “The X/A such that X/A is a composer and X/A wrote Alcina wrote some X/B such that X/B is an opera and X/B is in English.” The quasi-logical notation serves as an intermediate language between logic and ordinary English. In Stage II, S is translated into the logical notation of propositional functions and quantifiers, or of propositional logic, whichever is appropriate. In Stage III, S is run through the proof-procedure program and evaluated. (The sample sentence quoted is of course ‘invalid’, i.e. nontautological.) The COMIT program for Stage III is complete, that for Stage II is almost complete, and that for Stage I is incomplete. The paper describes the work done to date on the programs for Stages I and II.</abstract>
<bibkey>darlington-1963-translating</bibkey>
</paper>
<paper id="7">
<!-- https://aclanthology.org/1963.earlymt-1.7/ -->
<title>The graphic structure of word-breaking</title>
<author><first>J. L.</first><last>Dolby</last></author>
<author><first>H. L.</first><last>Resnikoff</last></author>
<abstract>In a recent paper1 the authors have shown that it is possible to determine the possible parts of speech of English words from an analysis of the written form. This determination depends upon the ability to determine the number of graphic syllables in the word. It is natural, then, to speculate as to the nature of graphic syllabification and the relation of this phenomenon to the practice of word-breaking in dictionaries and style manuals. It is not at all clear at the start that dictionary wordbreaking is subject to any fixed structure. In fact, certain forms cannot be broken uniquely in isolation since the dictionary provides different forms depending upon whether the word is used as a noun or a verb. However, it is shown in this paper that letter strings can be decomposed into 3 sets of roughly the same size in the following manner: in the first, strings are never broken in English words; in the second, the strings are always broken in English words; and in the third, both situations occur. Rules for breaking vowel strings are obtained by a study of the CVC forms. Breaks involving consonants can be determined by noting whether or not the consonant string occurs in penultimate position with the final c. The final e in compounds also serves to identify the forms that are generally split off from the rest of the word. A thorough analysis is made of the accuracy of the rules given when applied to the 12,000 words of the Government Printing Office Style Manual Supplement on word-breaking. Comparisons are also drawn between this source and several American dictionaries on the basis of a random sample of 500 words.</abstract>
<bibkey>dolby-resnikoff-1963-graphic</bibkey>
</paper>
<paper id="8">
<!-- https://aclanthology.org/1963.earlymt-1.8/ -->
<title>Writing of <fixed-case>C</fixed-case>hinese recognition grammar for machine translation</title>
<author><first>Ching-yi</first><last>Dougherty</last></author>
<abstract>Our approach to this problem is based on the stratificational grammar outlined and the procedures proposed by Dr. Sydney Lamb. How the theory and the procedures can be applied to written Chinese is briefly discussed. For the time being our research is limited to the particular kind of written Chinese found in chemical and biochemical journals. First the Chinese lexes are classified by detailed syntactical analysis, then binary grammar rules are constructed for joining two primary or constitute classes. How a more and more refined classification can eliminate one by one the ambiguity resulting from all possible constructions arising from juxtaposition of two distributional classes is discussed in detail.</abstract>
<bibkey>dougherty-1963-writing</bibkey>
</paper>
<paper id="9">
<!-- https://aclanthology.org/1963.earlymt-1.9/ -->
<title>The behavior of <fixed-case>E</fixed-case>nglish articles</title>
<author><first>H. P.</first><last>Edmundson</last></author>
<abstract>Machine translation has often been conceived as consisting of three steps: analysis of source-language sentence, transformation of analyzed pieces, and synthesis of target-language sentence. This paper is concerned with one aspect of the last step, namely, the rules of behavior of English articles. Since the classical definitions of definite and indefinite articles are operationally imprecise, proper mechanistic rules must be formulated in order to permit the automatic insertion or non-insertion of English articles. The rules discussed are of syntactic origin; however, note is also taken of their semantic aspects. This paper describes the methods used to derive these rules and offers ideas for further research.</abstract>
<bibkey>edmundson-1963-behavior</bibkey>
</paper>
<paper id="10">
<!-- https://aclanthology.org/1963.earlymt-1.10/ -->
<title>On representing syntactic structure</title>
<author><first>E. R.</first><last>Gammon</last></author>
<abstract>The idea of sentence depth of Yngve (A Model and an Hypothesis for Language Structure, Proc. Am. Phil. Soc., Vol. 104, No. 5, Oct. 1960) is extended to the notion of “distance” between constituents of a construction. The distance between constituents is defined as a weighted sum of the number of IC cuts separating them. Yngve’s depth is then a maximum distance from a sentence to any of its words. Various systems of weighting cuts are investigated. For example, in endocentric structures we may require that the distance from an attribute to the structure exceeds the distance from the head to the structure, and in exocentric structures that the distances from each constituent to the structure are equal. Representations of constructions are considered which preserve the distance between constituents. It is shown that it is impossible to represent some sentences in Euclidean space with exact distances, but a representation may be found if only relative order is preserved. If more general spaces are used then exact distances may be represented. It follows that for a wide class of sentence types, there is a weighting, and a space, in which the distance preserving representations are identical with the diagrams of traditional grammar.</abstract>
<bibkey>gammon-1963-representing</bibkey>
</paper>
<paper id="11">
<!-- https://aclanthology.org/1963.earlymt-1.11/ -->
<title>La traduction automatique et l’enseignement du russe</title>
<author><first>Yves</first><last>Gentilhomme</last></author>
<abstract>Les recherches effectuées depuis quelques années en vue de la Traduction Automatique ont conduit à des méthodes de travail et à des résultats intéressant la pédagogie des langues. Une expérience d’enseignement du russe a l’usage des scientifiques fondée sur ces données a été poursuivie pendant deux ans à Paris (Centre National de la Recherche Scientifique et Faculté des Sciences), et a abouti à la publication d’un manuel. Le present compte-rendu a pour objet de préciser les principes généraux utilisés, la réaction des étudiants et le rendement pédagogique obtenu. 1. Graphes morphologiques: Les mots d’une même famille. Notion de base. La double ramification. Les graphes abstraits. Les néologismes scientifiques. 2. Graphes syntaxiques: La double structure d’une phrase. Multiplicité des modèles. Point de vue psychologique. Notion de fonction. Continuité et discontinuité. 3. Les séparateurs: La segmentation d’une phrase. Le vocabulaire prioritaire. 4. Théorie de la valence: macro et microcontexte. Qu’est-ce-que “connaître un mot”? 5. Point de vue de l’étudiant; point de vue du traducteur humain; et point de vue de l’Enseignant.</abstract>
<bibkey>gentilhomme-1963-la</bibkey>
</paper>
<paper id="12">
<!-- https://aclanthology.org/1963.earlymt-1.12/ -->
<title>Word and context association by means of linear networks</title>
<author><first>Vincent E.</first><last>Giuliano</last></author>
<abstract>This paper is concerned with the use of electrical networks for the automatic recognition of statistical associations among words and contexts present in written text. A general mathematical theory is proposed for association by means of linear transformations, and it is shown that this theory can be realized through use of passive linear electrical networks. Several smallscale experimental associative networks have been built, and are briefly described in the paper; one such device will be demonstrated in the course of the oral presentation of the paper. Some of the devices generate measures of association among index terms used to characterize a document collection, and between the index terms and the documents themselves. Another uses syntactic proximity within sentences as a criterion for the generation of word association measures. Examples are given of associations produced by these network devices. It is conjectured that the networkproduced association measures reflect two distinct types of linguistic association—“synonymy” association which reflects similarity of meaning, and “contiguity” association which reflects real-world relationships among designata.</abstract>
<bibkey>giuliano-1963-word</bibkey>
</paper>
<paper id="13">
<!-- https://aclanthology.org/1963.earlymt-1.13/ -->
<title>A study of the combinatorial properties of <fixed-case>R</fixed-case>ussian nouns</title>
<author><first>Kenneth E.</first><last>Harper</last></author>
<abstract>A statistical study was made of the extent to which Russian nouns enter into certain kinds of syntactic combination. The basis of the study was a corpus of 180,000 running words of Russian physics text prepared for analysis by the Automatic Language Data Processing group at The Rand Corporation; for each sentence of text the syntactic dependency of each word had been previously coded. A data retrieval program was applied, showing for each noun in text the number of occurrences (a) with at least one genitive noun dependent, (b) with at least one adjective dependent, and (c) with either type of dependent. A listing of all nouns in text (64,026 occurrences of 2,993 nouns) was prepared, ordered by frequency, and showing counts for a, b, and c above. Separate listings were prepared, showing for each noun that occurred 50 times or more the probability P that it would be modified in each of these three ways; these listings were ordered on P. The data suggests, among others, the following conclusions: there is statistical significance in the variability with which nouns enter into the given combinations; the partial interchangeability of adjective and genitive noun modification is supported; a general correspondence exists between combinatorial groupings of nouns and morphological or semantic groupings (concrete nouns have low P for genitive complementation, abstract nouns have high P, etc); the use of words in a given field of discourse can be determined empirically (e.g., the use of deverbative nouns either to indicate a process or the result of a process). It is suggested that the distributional approach is a useful supplement to traditional syntactic and semantic classification schemes, and that it is of direct utility in automatic parsing programs.</abstract>
<bibkey>harper-1963-study</bibkey>
</paper>
<paper id="14">
<!-- https://aclanthology.org/1963.earlymt-1.14/ -->
<title>Connectability calculations, syntactic functions, and <fixed-case>R</fixed-case>ussian syntax</title>
<author><first>David G.</first><last>Hays</last></author>
<abstract>A program for sentence-structure determination can be divided into routines for analysis of word order and for testing the grammatical connectability of pairs of sentence members. The present paper describes a connectability-test routine that uses the technique called code matching. This technique requires elaborate descriptions of individual items, say the words or morphemes listed in a dictionary, but it avoids the use of large tables or complicated programs for testing connectability. Development of the technique also leads to a certain clarification of the linguistic concepts of function, exocentrism, and homography. In the present paper, a format for the description of Russian items is offered and a program for testing the connectability of pairs of Russian items is sketched. This system recognizes nine dominative functions: subjective; first, second, and third complementary; first, second, and third auxiliary; modifying; and predicative. The nature of a program for testing connectability with respect to coordinative functions (coordination, apposition, etc.) is suggested.</abstract>
<bibkey>hays-1963-connectability</bibkey>
</paper>
<paper id="15">
<!-- https://aclanthology.org/1963.earlymt-1.15/ -->
<title>Punctuation and automatic syntactic analysis</title>
<author><first>Lydia</first><last>Hirschberg</last></author>
<abstract>In this paper we discuss how algorithms for automatic analysis can take advantage of information carried by the punctuation marks. We neglect stylistic aspects of punctuation because they lack universality of usage and we restrict ourselves to those rules which any punctuation must observe in order to be intelligible. This involves a concept we call “coherence” of punctuation. In order to define “coherence”, we introduce two characteristics, which we prove to be mutually independent, namely “separating power” and “syntactic function”. The separating power is defined by three experimental laws expressing the fact that two punctuation marks of different separating power prevent to a different extent syntactic links from crossing them. These laws are defined independently of any particular grammatical character of the punctuation marks or of the attached grammatical syntagms. On the other hand, whichever grammatical system we choose, we may assimilate the punctuation marks to the ordinary words, to the extent that we can assign to them a known grammatical character and function, well defined in any particular context. They differ however from the other words by their large number of homographs and synonyms i.e. by the fact that almost every punctuation mark can occur with almost every grammatical value in each particular case, and in quite similar contexts. The syntactic functions, in general, and in particular those of the punctuation marks, can be ordered according to an arbitrary scale of decreasing “value” of syntactic links, where the “value” of a link is directly related to the number of syntactic conditions the links must satisfy. The law of coherence, then, shows that in a given context, a particular punctuation mark cannot indistinctly represent all its homographs, so that a certain number of assumptions about its syntactic nature and function can be discarded. This law can be stated as follows: “When moving from a punctuation mark to its immediate (left or right) neighbor in any text, the separating power cannot increase if the value of the syntactic function increases and vice-versa”. In addition we review two related topics, namely the stylistic character of punctuation and the necessity and existence of intrinsic criteria of grammatically, i.e. independent of punctuation. We propose such a criterion, and suggest a formalism related to the parenthesis free notation of logic.</abstract>
<bibkey>hirschberg-1963-punctuation</bibkey>
</paper>
<paper id="16">
<!-- https://aclanthology.org/1963.earlymt-1.16/ -->
<title>Application of decision tables to syntactic analysis</title>
<author><first>Walter</first><last>Hoffman</last></author>
<author><first>Amelia</first><last>Janiotis</last></author>
<author><first>Sidney</first><last>Simon</last></author>
<abstract>Decision tables have recently become an object of investigation as a possible means of improving problem formulation of data processing procedures. The initial emphasis for this new tool came from systems analysts who were primarily concerned with business data processing problems. The purpose of this paper is to investigate the suitability of decision tables as a means of expressing syntactic relations as an alternative to customary flow charting techniques. The history of decision tables will be briefly reviewed and several kinds of decision tables will be defined. As an example, parts of the predicative blocking routine developed at Wayne State University will be presented as formulated with the aid of decision tables. The aim of the predicative blocking routine is to group a predicative form together with its modal and temporal auxiliaries, infinitive complements, and negative particle, if any of these exist. The object of the search is to define such a syntactic block, but it may turn out instead that an infinitive phrase is defined or that a possible predicative form turns out to be an adverb.</abstract>
<bibkey>hoffman-etal-1963-application</bibkey>
</paper>
<paper id="17">
<!-- https://aclanthology.org/1963.earlymt-1.17/ -->
<title>Simultaneous computation of lexical and extralinguistic information measures in dialogue</title>
<author><first>Joseph</first><last>Jaffe</last></author>
<abstract>An approach to the study of information processing in verbal interaction is described. It compares patterns of two indices of dispersion in recorded dialogue. The lexical measure is the mean segmental type—token ratio, based on 25-word segments of the running conversation. It is computed from a key punched transcript of the dialogue without regard to the speaker of the words. The extralinguistic measure is the H statistic, computed from the temporal pattern of the interaction. The latter is prepared from a two-channel tape recording by a special analogue to digital converter (AVTA system) which key punches the state of the vocal transaction 200 times per minute. Probabilities of the four possible states (either A or B speaking, neither speaking, both speaking) are the basis for the computation. All analyses are done on the IBM 7090. The methodology is part of an investigation of information processing in dyadic systems, aimed toward the reclassification of pathological communication.</abstract>
<bibkey>jaffe-1963-simultaneous</bibkey>
</paper>
<paper id="18">
<!-- https://aclanthology.org/1963.earlymt-1.18/ -->
<title>Design of a generalized information system</title>
<author><first>Ronald W.</first><last>Jonas</last></author>
<abstract>While mechanical translation research involves the design of a computer system which simulates language processes, there is the associated problem of collecting the language data which are to be used in translation. Because large quantities of information will be needed, the computer may be useful for data accumulation and verification. A generalized information system should be able to accept the many types of data which a linguist encodes. A suitable means of communication between the linguist and the system has to be established. This may be achieved with a central input, called Linguistic Requests, and a central output, called Information Displays. The requests should be coordinated so that all possible inputs to the system are compatible, and the displays should be composed by the system such that they are clearly understandable. An information system should be interpretive of the linguist’s needs by allowing him to program the data manipulation. The key to such a scheme is that the linguist be permitted to classify his data freely and to retrieve it as he chooses. He should have at his disposal selecting, sorting, and displaying functions with which he can verify data, select data for introduction to a mechanical translation system, and perform other activities necessary in his research. Such an information system has been designed at the Linguistics Research Center of The University of Texas.</abstract>
<bibkey>jonas-1963-design</bibkey>
</paper>
<paper id="19">
<!-- https://aclanthology.org/1963.earlymt-1.19/ -->
<title>Some experiments performed with an automatic paraphraser</title>
<author><first>Sheldon</first><last>Klein</last></author>
<abstract>The automatic paraphrasing system used in the experiments described herein consisted of a phrase structure, grammatically correct nonsense generator coupled with a monitoring system that required the dependency relations of the sentence in production to be in harmony with those of a source text. The output sentences also appeared to be logically consistent with the content of that source. Dependency was treated as a binary relation, transitive except across most verbs and prepositions. Five experiments in paraphrasing were performed with this basic system. The first attempted to paraphrase without the operation of the dependency monitoring system, yielding grammatically correct nonsense. The second experiment included the operation of the monitoring system and yielded logically consistent paraphrases of the source text. The third and fourth experiments demanded that the monitoring system permit the production of only those sentences whose dependency relations were non-existent in the source text. While these latter outputs were seemingly nonsensical, they bore a special logical relationship to the source. The fifth experiment demanded that the monitoring system permit the production of sentences whose dependency relations were the converse of those in the source. This restriction was equivalent to turning the dependency tree of the source text upside down. The output of this experiment consisted only of kernel type sentences which, if read backwards, were logically consistent with the source. The results of these experiments determine some formal properties of dependency and engender some comments about the role of dependency in phrase structure and transformational models of language.</abstract>
<bibkey>klein-1963-experiments</bibkey>
</paper>
<paper id="20">
<!-- https://aclanthology.org/1963.earlymt-1.20/ -->
<title>Interlingual correspondence at the syntactic level</title>
<author><first>Edward S.</first><last>Klima</last></author>
<abstract>The paper will investigate a few major construction types in several related European languages: relative clauses, attributive phrases, and certain instances of coordinate conjunction involving these constructions. In each of the languages independently, the constructions will be described as resulting from syntactic mechanisms further analyzable into chains of partially ordered operations on more basic structures. Pairs of sentences equivalent in two languages will be examined. Sentences will be considered equivalent if they are acceptable translations of one another. The examples used will, in fact, be drawn primarily from standard translations of scholarly and literary prose. Equivalence between whole sentences can be further analyzed, as will be shown, into general equivalence 1) between the chains of operations describing the constructions and 2) between certain elements (e.g., lexical items) in the more basic underlying structures. It will be seen that superficial differences in the ultimate shape of certain translation pairs can be accounted for as the result of minor differences in the particular operations involved or in the basic underlying structure. We shall examine two languages (e.g., French and German) in which attributive phrase formation and relative clause formation on the whole correspond and in which, in a more or less abstract way, the rules of relative clause formation are included as intermediate links in the chain of operations describing attributive phrases. The fact that in particular cases a relative clause in the one language corresponds to an attributive phrase in the other will be found to result from, e.g., differences in the choice of perfect auxiliary in the two languages.</abstract>
<bibkey>klima-1963-interlingual</bibkey>
</paper>
<paper id="21">
<!-- https://aclanthology.org/1963.earlymt-1.21/ -->
<title>Sentence structure diagrams</title>
<author><first>Susumu</first><last>Kuno</last></author>
<abstract>A system for automatically producing a sentence structure diagram for each analysis of a given sentence has been added to the program of the multiple-path syntactic analyzer. A structure code, consisting of a series of structure symbols or phrase markers that identify the successive higher-order structures to which the word in question belongs, is assigned to each word of the sentence. The set of structure codes for the words of a given sentence is equivalent to an explicit tree diagram of the sentence structure, but more compact and easier to lay out on conventional printers. The diagramming system makes some experimental assumptions about the dependencies of certain structures upon higher-level structures. All the major syntactic components of a sentence (i.e., subject, verb, object, complement, period, or question mark) are represented in the current system as occurring on the same level, all being dependent on the topmost level, “sentence”. A floating structure such as a prepositional phrase or adverbial phrase or clause, whose dependency is not determined in the analyzer, is represented as depending upon the nearest preceding structure modifiable by such a floating structure. Different assumptions as to structural dependencies would yield different diagrams without requiring modification on the main flow of the diagramming program. The diagrams thus obtained contribute greatly to the rapid and accurate evaluation of the analysis results, and they are also useful for obtaining basic syntactic patterns of analyzed structures, and for detecting the head of each identified structure.</abstract>
<bibkey>kuno-1963-sentence</bibkey>
</paper>
<paper id="22">
<!-- https://aclanthology.org/1963.earlymt-1.22/ -->
<title>Linguistic structure and machine translation</title>
<author><first>Sydney M.</first><last>Lamb</last></author>
<abstract>If one understands the nature of linguistic structure, one will know what design features an adequate machine translation system must have. To put it the other way around, it is futile to attempt the construction of a machine translation system without a knowledge of what the structure of language is like. This principle means that if someone wants to construct a machine translation system, the most important thing he must do is to understand the structure of language. Any MT system, whether by conscious intention on the part of its creators or not, is based upon some view of the nature of linguistic structure. By making explicit the underlying theory for various MT systems which have been proposed we can determine whether or not they are adequate. Similarly, by observing linguistic phenomena we can determine what properties an adequate theory of language must have, and such determination will show what features an MT system must have in order to be adequate. It can be shown that some of the approaches to MT now being pursued must necessarily fail because their underlying linguistic theories are inadequate to account for various well-known linguistic phenomena.</abstract>
<bibkey>lamb-1963-linguistic</bibkey>
</paper>
<paper id="23">
<!-- https://aclanthology.org/1963.earlymt-1.23/ -->
<title>On redundancy in artificial languages</title>
<author><first>W. P.</first><last>Lehmann</last></author>
<abstract>Artificial languages are one concern of work in computational linguistics, if only as a mnemonic device for interlinguas which will be developed. Even if it does not gain wider use, the structure of an artificial language is of general interest. In contrast to the artificial languages which have been widely proposed, linguistic principles underlying a welldesigned artificial language and its usefulness are wellestablished, particularly through Trubetzkoy’s article, TCLP 8.5-21. which indicates phonological limitations for such a language. Since Trubetzkoy’s specifications yield a total of approximately 11,000 morphemes, if an artificial language incorporated the degree of redundancy found in natural languages it would be severely handicapped by the size of its lexicon. The paper discusses the problem particularly with regard to suprasegmentals, which Trubetzkoy almost entirely ignored.</abstract>
<bibkey>lehmann-1963-redundancy</bibkey>
</paper>
<paper id="24">
<!-- https://aclanthology.org/1963.earlymt-1.24/ -->
<title>A procedure for automatic sentence structure analysis</title>
<author><first>D.</first><last>Lieberman</last></author>
<abstract>The two main considerations in the design of this procedure were the economical recognition and representation of multiple readings of syntactically ambiguous sentences, and general applicability to “all” languages (English, Russian, Chinese). The following features will be discussed: types of structural descriptions, form of linguistic rules, use of linguistic heuristics to achieve economical multiple analyses, application to linguistic research and application to production MT systems. Also, the relation between this procedure and other existing sentence analysis procedures will be discussed.</abstract>
<bibkey>lieberman-1963-procedure</bibkey>
</paper>
<paper id="25">
<!-- https://aclanthology.org/1963.earlymt-1.25/ -->
<title>An algorithm for the translation of <fixed-case>R</fixed-case>ussian inorganic-chemistry terms</title>
<author><first>L. R.</first><last>Micklesen</last></author>
<author><first>P. H.</first><last>Smith</last></author>
<abstract>An algorithm has been devised, and a computer program written, to translate certain recurring types of inorganic-chemistry terms from Russian to English. The terms arc all noun-phrases, and several different types of such phrases have been included in the program. Examples are: AZOTNONATRIEVA4 SOL6 sodium nitrate SOL6 ZAKISI/OKISI JELEZA ferrous/ferric salt ZAKISNA4 OKISNA4 SOL6 JELEZA GIDRAT ZAKISI/OKISI JELEZA ferrous/ferric salt etc., where the stems underlined may be replaced by any of a number of other stems (up to 65 in some positions) in the particular type. Translation of each type encounters problems common to almost all the types: (1) The Russian noun is translated as an English adjective, while the noun of the resulting English phrase is found among the modifiers of the Russian noun. (2) The Russian noun (English adjective) may be a metal with more than one valence state, the state indicated (if at all) by the modifiers. (3) The number of the resulting English noun-phrase is determined by some member of the Russian phrase other than the noun. (4) The phrase elements may occur compounded in the chemical phrase but free in other contexts, and dictionary storage must provide for this. The program permits translation of conjoined phrase elements as well. The paper also includes an investigation into the deeper grammatical implications of this type of chemical nomenclature, and some excursions into the semantic correlations involved.</abstract>
<bibkey>micklesen-smith-1963-algorithm</bibkey>
</paper>
<paper id="26">
<!-- https://aclanthology.org/1963.earlymt-1.26/ -->
<title>The application of table processing concepts to the Sakai translation technique</title>
<author><first>A.</first><last>Opler</last></author>
<author><first>R.</first><last>Silverstone</last></author>
<author><first>Y.</first><last>Saleh</last></author>
<author><first>M.</first><last>Hildebran</last></author>
<author><first>I.</first><last>Slutzky</last></author>
<abstract>In 1961, I. Sakai described a new technique for the mechanical translation of languages. The method utilizes large tables which contain the syntactic rules of the source and target languages. As part of a study of the AN/GSQ-16 Lexical Processing Machine, a modification of the Sakai method was developed. Five of six planned table scanning phases were implemented and tested. Our translation system (1) converts input text to syntactic and semantic codes with a dictionary scan, (2) clears syntactic ambiguities where resolution by adjacent words is effective, (3) resolves residual syntactic ambiguities by determining the longest meaningful semantic unit, (4) reorders word sequence according to the rules of the target language and (5) produces the final target language translation. French to English was the source-target pair selected for the study. An Input Dictionary of 3,000 French stems was prepared and 17,000 entries comprised the Input Product Table (allowable syntactic combinations ). Since Sakai was working with highly dissimilar languages, he found it necessary to use an intermediate language. Because of the structural similarity between French and English, we found an intermediate language was unnecessary. The method proved straightforward to implement using the table lookup logic of the Lexical Processor. The translation was actually performed on an IBM 1401 which we programmed to simulate the concept of the AN/GSQ-16 Lexical Processor. In our implementation magnetic tapes replaced the photoscopic storage disk.</abstract>
<bibkey>opler-etal-1963-application</bibkey>
</paper>
<paper id="27">
<!-- https://aclanthology.org/1963.earlymt-1.27/ -->
<title><fixed-case>S</fixed-case>lavic languages—comparative morphosyntactic research</title>
<author><first>Milos</first><last>Pacak</last></author>
<abstract>An appropriate goal for present-day linguistics is the development of a general theory of relations between languages. One necessary requirement in the development of such a theory is the identification and classification of inflected forms in terms of their morphosyntactic properties in a set of presumably related languages. According to Sapir, “all languages differ from one another, but certain ones differ far more than others”. As for the Slavic languages he might well have said that they are all alike, but some are more alike than others. The similarities stemming from their common origin and from subsequent parallel development enable us to group them into a number of more or less homogeneous types. The experimental comparative research at The Georgetown University was focused on a group of four Slavic languages, namely, Russian, Czech, Polish and Serbocroatian. The first step in the comparative procedure here described is the morphosyntactic analysis of each of the four languages individually. The analysis should be based on the complementary distribution of inflectional morphemes. The properties whose distribution must be determined are: 1) the graphemic shape of the inflectional morphemes, 2) the establishment of distributional classes and subclasses of stem morphemes and (on the basis of 1 and 2), 3) the morphosyntactic function of inflectional morphemes which is determined by the distributional subclass of the stem morpheme. f(x,y)-l, where x is the distributional subclass of the stem morpheme (which is a constant) and y is the given inflectional morpheme (which is a free variable). On the basis of this preliminary analysis the patterns of absolute equivalence, partial equivalence, and absolute difference can be established for each class of inflected forms in each language under study. Once this has been accomplished, the results can be used in order to determine the extent of distributional equivalences among the individual languages. The applicability of this procedure was tested on the class of adjectivals. Within the frame of adjectivals the following morphosyntactic properties were analyzed within each language languages: 1) the category of gender, 2) the category of animateness, 3) the category of case and murder. The product of this comparative analysis is a set of formation rules which embody a system for the identification of the inflected forms. The detailed result will be presented in an additional report.</abstract>
<bibkey>pacak-1963-slavic</bibkey>
</paper>
<paper id="28">
<!-- https://aclanthology.org/1963.earlymt-1.28/ -->
<title>Types of language hierarchy</title>
<author><first>E. D.</first><last>Pendergraft</last></author>
<abstract>Various relations lead to hierarchical systems of linguistic description. This paper considers briefly a typology of descriptive metalanguages based on such relations and sketches possible consequences for computational linguistics. Its scope is accordingly limited to metalanguages having operational interpretations which specify individual linguistic processes and structural interpretations which specify language data of individual languages. Immediate-constituent, context-free metalanguages are used to illustrate hierarchical types.</abstract>
<bibkey>pendergraft-1963-types</bibkey>
</paper>
<paper id="29">
<!-- https://aclanthology.org/1963.earlymt-1.29/ -->
<title>Path economization in exhaustive left-to-right syntactic analysis</title>
<author><first>Warren J.</first><last>Plath</last></author>
<abstract>In exhaustive left-to-right syntactic analysis using the predictive approach, each path of syntactic connection which originates at the beginning of a sentence must be followed until it is clear whether or not it will lead to the production of a well-formed analysis. The original scheme of following each path until it terminates either in an analysis or in a grammatical inconsistency has been considerably improved through the. incorporation of two path-testing techniques. Using the first technique, the program abandons a path as unproductive whenever a situation is detected where the prediction pool contains more predictions of a given type than can possibly be fulfilled by the remaining words in the sentence. Employment of the second technique, which is based on periodic comparison of the current prediction pool with pools formed on earlier productive paths, eliminates repeated analysis of identical right-hand segments which belong to distinct paths. Taken together, the two path-testing procedures frequently enable the program to terminate the processing of a path well before its end has been reached. For most sentences, this means a considerable reduction in the total path length traversed, accompanied by a corresponding increase in the speed of analysis. Comparison of runs performed using both versions of the program indicates that employment of the new techniques reduces the average running time per sentence to less than one-fifth of its former value.</abstract>
<bibkey>plath-1963-path</bibkey>
</paper>
<paper id="30">
<!-- https://aclanthology.org/1963.earlymt-1.30/ -->
<title>A computer representation for semantic information</title>
<author><first>Bertram</first><last>Raphael</last></author>
<abstract>This paper deals with the problem of representing in a useful form, within a digital computer, the information content of statements in natural language. The model proposed consists of words and list-structure associations between words. Statements in simple English are thought of as describing relations between objects in the real world. Sentences are analyzed by matching them against members of a list of formats, each of which determines a unique relation. These relations are stored on description-lists associated with those words which denote objects (or sets of objects). A LISP computer program uses this model in the context of a simple question-answering system. Functions are provided which may grow, search, and modify this model. Formats and functions dealing with set-relations, part-whole and numeric relations, and left-toright spatial relations have been included in the system, which is being expanded to handle other types of relations. All functions which operate on the model report information concerning their actions to the programmer, so that the applicability and limitations of this kind of model may more easily be evaluated.</abstract>
<bibkey>raphael-1963-computer</bibkey>
</paper>
<paper id="31">
<!-- https://aclanthology.org/1963.earlymt-1.31/ -->
<title>Specifications for generative grammars used in language data processing</title>
<author><first>Robert</first><last>Tabory</last></author>
<abstract>It becomes more and more evident that successful pragmatics (i.e. automatic recognition and production procedures for sentences) cannot be performed without previously written generative grammars for the languages involved, using an underlying meta-theoretical framework proposed by the present school of mathematical linguistics. Two aspects of grammar writing are examined: 1. A taxonomy over the non-terminal vocabulary, using a subscripting system for signs and fitting into the more general string taxonomy of phrase structure components. The resulting more complex lexical organization is studied. 2. A command syntax for phrase structure components limiting the full, not necessarily needed generative power of these grammars. The proposed restrictions correspond to a priori linguistic intuition. Applicational order and location of the rules is studied. Finally, the recognitional power and generative capacity of a computer are examined, the machine being structured according to a Newell-Shaw-Simon list system. It is well known that pushdown stores are particular cases of list structures, that context-free grammars are particular cases of phrase structure grammars and that pushdown stores are the generative devices for context-free grammars.</abstract>
<bibkey>tabory-1963-specifications</bibkey>
</paper>
<paper id="32">
<!-- https://aclanthology.org/1963.earlymt-1.32/ -->
<title>Collecting linguistic data for the grammar of a language</title>
<author><first>Wayne</first><last>Tosh</last></author>
<abstract>Establishing the grammatical description of a language is one of the major tasks facing the technician in machine translation. Another is that of creating the system of programs with which to carry out the translation process. The Linguistics Research Center of The University of Texas recognizes the advantages in maintaining the specialties of linguistic research and computer programming as two separate areas of endeavor. We regard the linguistic task as a problem in convergence. We do not expect ever to have a final description of a language (except theoretically for a given point in the history of that language). We do expect, however, to begin with almost immediate application of the very first grammatical description. We shall make repeated revisions of the grammar as we learn how to make it approximate better the language text fed into the computer. The grammatical description of any one language is based primarily on specific text evidence. We are not attempting to describe “the language”. We are, however, attempting to make descriptive decisions sufficiently general that new text evidence does not require extensive revision of earlier descriptions. Corpora selected for description are chosen so as to have similar texts within the same scientific discipline for the several languages. Tree diagrams are drawn for each sentence in detail. The diagrams are inspected for consistency before corresponding phrase-structure rules are compiled in the computer. The grammar is then verified in the computer system and revised as necessary.</abstract>
<bibkey>tosh-1963-collecting</bibkey>
</paper>
<paper id="33">
<!-- https://aclanthology.org/1963.earlymt-1.33/ -->
<title>Derivational suffixes in <fixed-case>R</fixed-case>ussian general vocabulary and in chemical nomenclature</title>
<author><first>John H.</first><last>Wahlgren</last></author>
<abstract>A grammar based upon a conventional morphemic analysis of Russian will have a rather large inventory of derivational suffixes. A relatively small number of these recur with sufficient generality to acquire lexemic status (i.e., to be what is usually termed “productive”). Names of chemical substances in Russian may likewise be analyzed as combinations of roots or stems with derivational affixes, in particular, suffixes. The number of productive suffixes in the chemical nomenclature is considerably larger than in the general vocabulary. These suffixes derive from adoption into Russian of an international system of chemical nomenclature. A grammar of this system is basically independent of any grammar of Russian. It must, however, be consistently incorporated into the grammar and dictionary which are to serve in a machine translation system for texts in the source language containing chemical names. Grammatical analysis of chemical suffixes and connected study of general Russian derivational suffixes has raised certain practical problems and theoretical questions concerning the nature of derivation. On the practical side, where a complex and highly productive system is involved, effective means of detecting and dealing with homography have required development. Theoretical consideration has been given to the question of grammaticality in chemical names and to problems of sememic analysis and classification of root and stem lexemes into tactic classes on the basis of cooccurrence with derivational suffixes.</abstract>
<bibkey>wahlgren-1963-derivational</bibkey>
</paper>
<paper id="34">
<!-- https://aclanthology.org/1963.earlymt-1.34/ -->
<title>On the order of clauses</title>
<author><first>Victor H.</first><last>Yngve</last></author>
<abstract>We used to think that the output of a translation machine would be stylistically inelegant, but this would be tolerable if only the message got across. We now find that getting the message across accurately is difficult, but we may be able to have stylistic elegance in the output since much of style reflects depth phenomena and thus is systematic. As an example, the order of the clauses in many twoclause sentences can be reversed without a change of meaning, but the same is not normally true of sentences with more than two clauses. The meaning usually changes when the clause order is changed. Equivalently, there appear to be severe restrictions on clause order for any given meaning. These restrictions appear to follow from depth considerations. The idea is being investigated that there is a normal depth-related clause order and any deviations from this order must be signalled by special syntactic or semantic devices. The nature of these devices is being explored. When translating multi-clause sentences, there may be trouble due to the fact that the clause types of the two languages are not exactly parallel. Therefore the list of allowed and preferred clause orders in the two languages will not be equivalent and the special syntactic and semantic devices available to signal deviations from the normal order will be different. Thus one would predict that multi-clause sentences in language A often have to be split into two or more sentences when translated into language B, while at the same time multi-clause sentences in language B will often have to be broken into two or more sentences when translating into language A.</abstract>
<bibkey>yngve-1963-order</bibkey>
</paper>
</volume>
</collection>
+273
View File
@@ -0,0 +1,273 @@
<?xml version='1.0' encoding='UTF-8'?>
<collection id="T78">
<volume id="1" type="proceedings">
<!-- https://aclanthology.org/T78-1/ -->
<meta>
<booktitle>Theoretical Issues in Natural Language <fixed-case>P</fixed-case>rocessing-2</booktitle>
<editor id="david-l-waltz"><first>David L.</first><last>Waltz</last></editor>
<venue>tinlap</venue>
<year>1978</year>
</meta>
<frontmatter>
<!-- https://aclanthology.org/T78-1000/ -->
<pdf hash="fb6164ad"/>
<bibkey>tinlap-1978-theoretical</bibkey>
</frontmatter>
<paper id="1">
<!-- https://aclanthology.org/T78-1001/ -->
<title>Testing The Psychological Reality of a Representational Model</title>
<author><first>Dedre</first><last>Gentner</last></author>
<pdf hash="f723a956"/>
<bibkey>gentner-1978-testing-psychological</bibkey>
</paper>
<paper id="2">
<!-- https://aclanthology.org/T78-1002/ -->
<title>What Makes Something “Ad Hoc”</title>
<author id="roger-c-schank"><first>Roger C.</first><last>Schank</last></author>
<pdf hash="636853f9"/>
<bibkey>schank-1978-makes-something</bibkey>
</paper>
<paper id="3">
<!-- https://aclanthology.org/T78-1003/ -->
<title>The Relation of Grammar to Cognition–a Synopsis</title>
<author><first>Leonard</first><last>Talmy</last></author>
<pdf hash="101c67dc"/>
<bibkey>talmy-1978-relation-grammar</bibkey>
</paper>
<paper id="4">
<!-- https://aclanthology.org/T78-1004/ -->
<title>On primitives, prototypes, and other semantic anomalies</title>
<author><first>Terry</first><last>Winograd</last></author>
<pdf hash="f8e47023"/>
<bibkey>winograd-1978-primitives-prototypes</bibkey>
</paper>
<paper id="5">
<!-- https://aclanthology.org/T78-1005/ -->
<title>Taxonomic Lattice Structures for Situation Recognition</title>
<author id="william-a-woods"><first>William A.</first><last>Woods</last></author>
<pdf hash="92957ddf"/>
<bibkey>woods-1978-taxonomic-lattice</bibkey>
</paper>
<paper id="6">
<!-- https://aclanthology.org/T78-1006/ -->
<title>Description Formation and Discourse Model Synthesis</title>
<author id="bonnie-webber"><first>Bonnie Lynn</first><last>Webber</last></author>
<pdf hash="ffded1d2"/>
<bibkey>webber-1978-description-formation</bibkey>
</paper>
<paper id="7">
<!-- https://aclanthology.org/T78-1007/ -->
<title>The Processing of Referring Expressions within a Semantic Network</title>
<author><first>John R.</first><last>Anderson</last></author>
<pdf hash="8bcc083a"/>
<bibkey>anderson-1978-processing-referring</bibkey>
</paper>
<paper id="8">
<!-- https://aclanthology.org/T78-1008/ -->
<title>Reference Diaries</title>
<author><first>Herbert H.</first><last>Clark</last></author>
<author><first>Catherine</first><last>Marshall</last></author>
<pdf hash="d06b7ea2"/>
<bibkey>clark-marshall-1978-reference-diaries</bibkey>
</paper>
<paper id="9">
<!-- https://aclanthology.org/T78-1009/ -->
<title>Subsequent Reference: Syntactic and Rhetorical Constraints</title>
<author id="david-d-mcdonald"><first>David D.</first><last>McDonald</last></author>
<pdf hash="2d424559"/>
<bibkey>mcdonald-1978-subsequent-reference</bibkey>
</paper>
<paper id="10">
<!-- https://aclanthology.org/T78-1010/ -->
<title>Some Psycholinguistic Constraints on the Construction and Interpretation of Definite Descriptions</title>
<author><first>Andrew</first><last>Ortony</last></author>
<pdf hash="69807727"/>
<bibkey>ortony-1978-psycholinguistic-constraints</bibkey>
</paper>
<paper id="11">
<!-- https://aclanthology.org/T78-1011/ -->
<title>Bound Variables and Other Anaphors</title>
<author id="barbara-h-partee"><first>Barbara H.</first><last>Partee</last></author>
<pdf hash="e8e6a510"/>
<bibkey>partee-1978-bound-variables</bibkey>
</paper>
<paper id="12">
<!-- https://aclanthology.org/T78-1012/ -->
<title>The Use of Focus as a Tool for Disambiguation of Definite Noun Phrases</title>
<author id="candace-l-sidner"><first>Candace L.</first><last>Sidner</last></author>
<pdf hash="7b96d2b7"/>
<bibkey>sidner-1978-use-focus</bibkey>
</paper>
<paper id="13">
<!-- https://aclanthology.org/T78-1013/ -->
<title>Focusing in Dialog</title>
<author id="barbara-j-grosz"><first>Barbara J.</first><last>Grosz</last></author>
<pdf hash="8c8d4c2e"/>
<bibkey>grosz-1978-focusing-dialog</bibkey>
</paper>
<paper id="14">
<!-- https://aclanthology.org/T78-1014/ -->
<title>Topic Levels</title>
<author><first>Joseph E.</first><last>Grimes</last></author>
<pdf hash="f485b2ba"/>
<bibkey>grimes-1978-topic-levels</bibkey>
</paper>
<paper id="15">
<!-- https://aclanthology.org/T78-1015/ -->
<title>Toward a Rational Model of Discourse Comprehension</title>
<author><first>Jerry L.</first><last>Morgan</last></author>
<pdf hash="5e12a597"/>
<bibkey>morgan-1978-toward-rational</bibkey>
</paper>
<paper id="16">
<!-- https://aclanthology.org/T78-1016/ -->
<title>Assent and Compliance in Children’s Language Comprehension</title>
<author><first>David R.</first><last>Olson</last></author>
<pdf hash="708bc4a0"/>
<bibkey>olson-1978-assent-compliance</bibkey>
</paper>
<paper id="17">
<!-- https://aclanthology.org/T78-1017/ -->
<title>Speech Acts as a Basis for Understanding Dialogue Coherence</title>
<author id="c-raymond-perrault"><first>C. Raymond</first><last>Perrault</last></author>
<author id="james-allen"><first>James F.</first><last>Allen</last></author>
<pdf hash="d111402b"/>
<bibkey>perrault-allen-1978-speech</bibkey>
</paper>
<paper id="18">
<!-- https://aclanthology.org/T78-1018/ -->
<title>A Framework for Comparing Language Experiences (with particular emphasis on: The Effect of Audience on Discourse Models)</title>
<author><first>Andee</first><last>Rubin</last></author>
<pdf hash="5d9a6150"/>
<bibkey>rubin-1978-framework-comparing</bibkey>
</paper>
<paper id="19">
<!-- https://aclanthology.org/T78-1019/ -->
<title>Intentlonallty and Human Conversations</title>
<author id="jaime-g-carbonell"><first>Jaime G.</first><last>Carbonell Jr</last></author>
<pdf hash="c50fc619"/>
<bibkey>carbonell-jr-1978-intentlonallty-human</bibkey>
</paper>
<paper id="20">
<!-- https://aclanthology.org/T78-1020/ -->
<title>On the Interdependence of Language and Perception</title>
<author id="david-l-waltz"><first>David L.</first><last>Waltz</last></author>
<pdf hash="3c708191"/>
<bibkey>waltz-1978-interdependence-language</bibkey>
</paper>
<paper id="21">
<!-- https://aclanthology.org/T78-1021/ -->
<title>The Problem of Naming Shapes: Vision-Language Interface</title>
<author id="ruzena-bajcsy"><first>R.</first><last>Bajcsy</last></author>
<author id="aravind-joshi"><first>A. K.</first><last>Joshi</last></author>
<pdf hash="50331b4e"/>
<bibkey>bajcsy-joshi-1978-problem-naming</bibkey>
</paper>
<paper id="22">
<!-- https://aclanthology.org/T78-1022/ -->
<title>An Argument about the Composition of Conceptual Structure</title>
<author><first>Ray</first><last>Jackendoff</last></author>
<pdf hash="57d001f8"/>
<bibkey>jackendoff-1978-argument-composition</bibkey>
</paper>
<paper id="23">
<!-- https://aclanthology.org/T78-1023/ -->
<title>On the Ontological Status of Visual Mental Images</title>
<author><first>Stephen Michael</first><last>Kosslyn</last></author>
<pdf hash="fd06e265"/>
<bibkey>kosslyn-1978-ontological-status</bibkey>
</paper>
<paper id="24">
<!-- https://aclanthology.org/T78-1024/ -->
<title>What has language to do with perception? Some speculations on the <i>Lingua Mentis</i></title>
<author><first>Zenon W.</first><last>Pylyshyn</last></author>
<pdf hash="a2f3a044"/>
<bibkey>pylyshyn-1978-language-perception</bibkey>
</paper>
<paper id="25">
<!-- https://aclanthology.org/T78-1025/ -->
<title>Semantic Primitives in Language and Vision</title>
<author id="yorick-wilks"><first>Yorick</first><last>Wilks</last></author>
<pdf hash="b4e13896"/>
<bibkey>wilks-1978-semantic-primitives</bibkey>
</paper>
<paper id="26">
<!-- https://aclanthology.org/T78-1026/ -->
<title>A Note on Partial Match of Descriptions. Can One Simultaneously Question (Retrieve) and Inform (Update)?</title>
<author id="aravind-joshi"><first>Aravind K.</first><last>Joshi</last></author>
<pdf hash="74969243"/>
<bibkey>joshi-1978-note-partial</bibkey>
</paper>
<paper id="27">
<!-- https://aclanthology.org/T78-1027/ -->
<title>With a Spoon in Hand This Must Be the Eating Frame</title>
<author><first>Eugene</first><last>Charniak</last></author>
<pdf hash="f1be68bb"/>
<bibkey>charniak-1978-spoon-hand</bibkey>
</paper>
<paper id="28">
<!-- https://aclanthology.org/T78-1028/ -->
<title>Fragments of a Theory of Human Plausible Reasoning</title>
<author><first>Allan</first><last>Collins</last></author>
<pdf hash="67b4bca2"/>
<bibkey>collins-1978-fragments-theory</bibkey>
</paper>
<paper id="29">
<!-- https://aclanthology.org/T78-1029/ -->
<title>Indirect Responses to Loaded Questions</title>
<author><first>S. Jerrold</first><last>Kaplan</last></author>
<pdf hash="135037f9"/>
<bibkey>kaplan-1978-indirect-responses</bibkey>
</paper>
<paper id="30">
<!-- https://aclanthology.org/T78-1030/ -->
<title>On Reasoning by Default</title>
<author><first>Raymond</first><last>Reiter</last></author>
<pdf hash="0de53d33"/>
<bibkey>reiter-1978-reasoning-default</bibkey>
</paper>
<paper id="31">
<!-- https://aclanthology.org/T78-1031/ -->
<title>Path-Based and Node-Based Inference in Semantic Networks</title>
<author id="stuart-c-shapiro"><first>Stuart C.</first><last>Shapiro</last></author>
<pdf hash="2ddc5535"/>
<bibkey>shapiro-1978-path-based</bibkey>
</paper>
<paper id="32">
<!-- https://aclanthology.org/T78-1032/ -->
<title>The Representation of Derivable Information in Memory: When What Might Have Been Left Unsaid Is Said</title>
<author><first>Rand J.</first><last>Spiro</last></author>
<author><first>Joseph</first><last>Esposito</last></author>
<author><first>Richard J.</first><last>Vondruska</last></author>
<pdf hash="7758fb45"/>
<bibkey>spiro-etal-1978-representation-derivable</bibkey>
</paper>
<paper id="33">
<!-- https://aclanthology.org/T78-1033/ -->
<title>A Heuristic for Paradigms</title>
<author><first>Joseph E.</first><last>Grimes</last></author>
<pdf hash="53b42c16"/>
<bibkey>grimes-1978-heuristic-paradigms</bibkey>
</paper>
<paper id="34">
<!-- https://aclanthology.org/T78-1034/ -->
<title>A Computational Account of Some Constraints on Language</title>
<author id="mitch-marcus"><first>Mitchell</first><last>Marcus</last></author>
<pdf hash="c9724c43"/>
<bibkey>marcus-1978-computational-account</bibkey>
</paper>
<paper id="35">
<!-- https://aclanthology.org/T78-1035/ -->
<title>Remarks on Processing, Constraints, and the Lexicon</title>
<author id="thomas-wasow"><first>Thomas</first><last>Wasow</last></author>
<pdf hash="8beed275"/>
<bibkey>wasow-1978-remarks-processing</bibkey>
</paper>
<paper id="36">
<!-- https://aclanthology.org/T78-1036/ -->
<title>List of questions suggested for consideration in each session</title>
<pdf hash="1950b908"/>
<bibkey>nn-1978-list-questions</bibkey>
</paper>
</volume>
</collection>
Binary file not shown.
+15
View File
@@ -0,0 +1,15 @@
{
"receipts": [
{
"url": "https://neurips.cc/static/virtual/data/neurips-2026-orals-posters.json",
"final_url": "https://neurips.cc/static/virtual/data/neurips-2026-orals-posters.json",
"status": 200,
"content_type": "application/json",
"file": "program-json.response",
"bytes": 20447021,
"sha256": "879293a863057fa05370443b3b5ddee12a10eeb37c810abc6c1d53f71f545997",
"started_at": "2026-11-09T17:57:23.825509+00:00",
"finished_at": "2026-11-09T17:57:40.125580+00:00"
}
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1646
View File
File diff suppressed because it is too large Load Diff
-1585
View File
File diff suppressed because it is too large Load Diff
-148
View File
@@ -1,148 +0,0 @@
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<meta name="citation_title" content="Learned Perceptive Forward Dynamics Model for Safe and Platform-aware Robotic Navigation" />
<meta name="citation_author" content="Pascal Roth" />
<meta name="citation_author" content="Jonas Frey" />
<meta name="citation_author" content="Cesar Cadena" />
<meta name="citation_author" content="Marco Hutter" />
<meta name="citation_publication_date" content="2025/06/21" />
<meta name="citation_conference_title" content="Robotics: Science and Systems XXI" />
<meta name="citation_isbn" content="979-8-9902848-1-4" />
<meta name="citation_volume" content="21" />
<meta name="citation_pdf_url" content="http://www.roboticsproceedings.org/rss21/p001.pdf" />
<title>Robotics: Science and Systems XXI - Online Proceedings</title>
<link type="text/css" rel="stylesheet" href="../style.css">
<!--[if IE 5]>
<link type="text/css" rel="stylesheet" href="../ie5.css">
<![endif]-->
<!--[if IE 6]>
<link type="text/css" rel="stylesheet" href="../ie6.css">
<![endif]-->
</head>
<body>
<div class="menu">
<ul>
<li class="here"><a class="menu" href="../index.html">Home</a></li>
<li class="label">RSS XXII</span></li>
<li class="menu"><a class="menu" href="../rss22/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss22/authors.html">Authors</a></li>
<li class="label">RSS XXI</span></li>
<li class="menu"><a class="menu" href="../rss21/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss21/authors.html">Authors</a></li>
<li class="label">RSS XX</span></li>
<li class="menu"><a class="menu" href="../rss20/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss20/authors.html">Authors</a></li>
<li class="label">RSS XIX</span></li>
<li class="menu"><a class="menu" href="../rss19/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss19/authors.html">Authors</a></li>
<li class="label">RSS XVIII</span></li>
<li class="menu"><a class="menu" href="../rss18/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss18/authors.html">Authors</a></li>
<li class="label">RSS XVII</span></li>
<li class="menu"><a class="menu" href="../rss17/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss17/authors.html">Authors</a></li>
<li class="label">RSS XVI</span></li>
<li class="menu"><a class="menu" href="../rss16/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss16/authors.html">Authors</a></li>
<li class="label">RSS XV</span></li>
<li class="menu"><a class="menu" href="../rss15/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss15/authors.html">Authors</a></li>
<li class="label">RSS XIV</span></li>
<li class="menu"><a class="menu" href="../rss14/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss14/authors.html">Authors</a></li>
<li class="label">RSS XIII</span></li>
<li class="menu"><a class="menu" href="../rss13/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss13/authors.html">Authors</a></li>
<li class="label">RSS XII</span></li>
<li class="menu"><a class="menu" href="../rss12/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss12/authors.html">Authors</a></li>
<li class="label">RSS XI</span></li>
<li class="menu"><a class="menu" href="../rss11/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss11/authors.html">Authors</a></li>
<li class="label">RSS X</span></li>
<li class="menu"><a class="menu" href="../rss10/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss10/authors.html">Authors</a></li>
<li class="label">RSS IX</span></li>
<li class="menu"><a class="menu" href="../rss09/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss09/authors.html">Authors</a></li>
<li class="label">RSS VIII</span></li>
<li class="menu"><a class="menu" href="../rss08/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss08/authors.html">Authors</a></li>
<li class="label">RSS VII</span></li>
<li class="menu"><a class="menu" href="../rss07/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss07/authors.html">Authors</a></li>
<li class="label">RSS VI</span></li>
<li class="menu"><a class="menu" href="../rss06/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss06/authors.html">Authors</a></li>
<li class="label">RSS V</span></li>
<li class="menu"><a class="menu" href="../rss05/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss05/authors.html">Authors</a></li>
<li class="label">RSS IV</span></li>
<li class="menu"><a class="menu" href="../rss04/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss04/authors.html">Authors</a></li>
<li class="label">RSS III</span></li>
<li class="menu"><a class="menu" href="../rss03/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss03/authors.html">Authors</a></li>
<li class="label">RSS II</span></li>
<li class="menu"><a class="menu" href="../rss02/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss02/authors.html">Authors</a></li>
<li class="label">RSS I</span></li>
<li class="menu"><a class="menu" href="../rss01/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss01/authors.html">Authors</a></li>
<li class="label">Ethics and Malpractice</span></li>
<li class="menu"><a class="menu" href="../statement.html">Statement</a></li>
<li class="label">Contact</span></li>
<li class="last"><a class="menu" href="../contact.html">Webmaster</a></li>
<li class="no-style"><center><img border="0" src="../robot-small.jpg"></center>
<br><br><br>
<center>We are using google analytics for statistics!</center>
</ul>
</div>
<div class="content">
<h1>Robotics: Science and Systems XXI</h1>
<h3>Learned Perceptive Forward Dynamics Model for Safe and Platform-aware Robotic Navigation</h3>
<i>Pascal Roth, Jonas Frey, Cesar Cadena, Marco Hutter</i><br>
<p>
<b>Abstract:</b>
</p><p style="text-align: justify;">
Ensuring safe navigation in complex environments requires accurate real-time traversability assessment and understanding of environmental interactions relative to the robot's capabilities. Traditional methods, which assume simplified dynamics, often require designing and tuning cost functions to safely guide paths or actions toward the goal. This process is tedious, environment-dependent, and not generalizable. To overcome these issues, we propose a novel learned perceptive Forward Dynamics Model (FDM) that predicts the robot's future state conditioned on the surrounding geometry and history of proprioceptive measurements, proposing a more scalable, safer, and heuristic-free solution. The FDM is trained on multiple years of simulated navigation experience, including high-risk maneuvers, and real-world interactions to incorporate the full system dynamics beyond rigid body simulation. We integrate our perceptive FDM into a zero-shot Model Predictive Path Integral (MPPI) planning framework, leveraging the learned mapping between actions, future states, and failure probability. This allows for optimizing a simplified cost function, eliminating the need for extensive cost-tuning to ensure safety. On the legged robot ANYmal, the proposed perceptive FDM improves the position estimation by on average 41% over competitive baselines, which translates into a 27% higher navigation success rate in rough simulation environments. Moreover, we demonstrate effective sim-to-real transfer and showcase the benefit of training on synthetic and real data. Code and models are made publicly available under https://github.com/leggedrobotics/fdm.
</p>
<p>
<b>Download:</b> <a href="p001.pdf" target="_blank" onclick="_gaq.push(['_trackEvent','rss10/p001.pdf','PDF',this.href]);"><img src="../icon-pdf.png" border=0></a>
</p>
<p>
<b>Bibtex:</b>
<pre>
@INPROCEEDINGS{RothP-RSS-25,
AUTHOR = {Pascal Roth AND Jonas Frey AND Cesar Cadena AND Marco Hutter},
TITLE = {{Learned Perceptive Forward Dynamics Model for Safe and Platform-aware Robotic Navigation}},
BOOKTITLE = {Proceedings of Robotics: Science and Systems},
YEAR = {2025},
ADDRESS = {LosAngeles, CA, USA},
MONTH = {June},
DOI = {10.15607/RSS.2025.XXI.001}
}
</pre>
</p>
</div>
<div class="analyt">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-24631810-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</div>
</body>
</html>
-155
View File
@@ -1,155 +0,0 @@
<html>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<head>
<meta name="citation_title" content="Demonstrating ViSafe: Vision-enabled Safety for High-speed Detect and Avoid" />
<meta name="citation_author" content="Parv Kapoor" />
<meta name="citation_author" content="Ian Higgins" />
<meta name="citation_author" content="Nikhil Varma Keetha" />
<meta name="citation_author" content="Jay Patrikar" />
<meta name="citation_author" content="Brady Moon" />
<meta name="citation_author" content="Zelin Ye" />
<meta name="citation_author" content="Yao He" />
<meta name="citation_author" content="Ivan Cisneros" />
<meta name="citation_author" content="Changliu Liu" />
<meta name="citation_author" content="Eunsuk Kang" />
<meta name="citation_author" content="Sebastian Scherer" />
<meta name="citation_publication_date" content="2025/06/21" />
<meta name="citation_conference_title" content="Robotics: Science and Systems XXI" />
<meta name="citation_isbn" content="979-8-9902848-1-4" />
<meta name="citation_volume" content="21" />
<meta name="citation_pdf_url" content="http://www.roboticsproceedings.org/rss21/p002.pdf" />
<title>Robotics: Science and Systems XXI - Online Proceedings</title>
<link type="text/css" rel="stylesheet" href="../style.css">
<!--[if IE 5]>
<link type="text/css" rel="stylesheet" href="../ie5.css">
<![endif]-->
<!--[if IE 6]>
<link type="text/css" rel="stylesheet" href="../ie6.css">
<![endif]-->
</head>
<body>
<div class="menu">
<ul>
<li class="here"><a class="menu" href="../index.html">Home</a></li>
<li class="label">RSS XXII</span></li>
<li class="menu"><a class="menu" href="../rss22/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss22/authors.html">Authors</a></li>
<li class="label">RSS XXI</span></li>
<li class="menu"><a class="menu" href="../rss21/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss21/authors.html">Authors</a></li>
<li class="label">RSS XX</span></li>
<li class="menu"><a class="menu" href="../rss20/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss20/authors.html">Authors</a></li>
<li class="label">RSS XIX</span></li>
<li class="menu"><a class="menu" href="../rss19/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss19/authors.html">Authors</a></li>
<li class="label">RSS XVIII</span></li>
<li class="menu"><a class="menu" href="../rss18/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss18/authors.html">Authors</a></li>
<li class="label">RSS XVII</span></li>
<li class="menu"><a class="menu" href="../rss17/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss17/authors.html">Authors</a></li>
<li class="label">RSS XVI</span></li>
<li class="menu"><a class="menu" href="../rss16/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss16/authors.html">Authors</a></li>
<li class="label">RSS XV</span></li>
<li class="menu"><a class="menu" href="../rss15/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss15/authors.html">Authors</a></li>
<li class="label">RSS XIV</span></li>
<li class="menu"><a class="menu" href="../rss14/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss14/authors.html">Authors</a></li>
<li class="label">RSS XIII</span></li>
<li class="menu"><a class="menu" href="../rss13/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss13/authors.html">Authors</a></li>
<li class="label">RSS XII</span></li>
<li class="menu"><a class="menu" href="../rss12/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss12/authors.html">Authors</a></li>
<li class="label">RSS XI</span></li>
<li class="menu"><a class="menu" href="../rss11/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss11/authors.html">Authors</a></li>
<li class="label">RSS X</span></li>
<li class="menu"><a class="menu" href="../rss10/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss10/authors.html">Authors</a></li>
<li class="label">RSS IX</span></li>
<li class="menu"><a class="menu" href="../rss09/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss09/authors.html">Authors</a></li>
<li class="label">RSS VIII</span></li>
<li class="menu"><a class="menu" href="../rss08/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss08/authors.html">Authors</a></li>
<li class="label">RSS VII</span></li>
<li class="menu"><a class="menu" href="../rss07/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss07/authors.html">Authors</a></li>
<li class="label">RSS VI</span></li>
<li class="menu"><a class="menu" href="../rss06/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss06/authors.html">Authors</a></li>
<li class="label">RSS V</span></li>
<li class="menu"><a class="menu" href="../rss05/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss05/authors.html">Authors</a></li>
<li class="label">RSS IV</span></li>
<li class="menu"><a class="menu" href="../rss04/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss04/authors.html">Authors</a></li>
<li class="label">RSS III</span></li>
<li class="menu"><a class="menu" href="../rss03/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss03/authors.html">Authors</a></li>
<li class="label">RSS II</span></li>
<li class="menu"><a class="menu" href="../rss02/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss02/authors.html">Authors</a></li>
<li class="label">RSS I</span></li>
<li class="menu"><a class="menu" href="../rss01/index.html">Content</a></li>
<li class="menu"><a class="menu" href="../rss01/authors.html">Authors</a></li>
<li class="label">Ethics and Malpractice</span></li>
<li class="menu"><a class="menu" href="../statement.html">Statement</a></li>
<li class="label">Contact</span></li>
<li class="last"><a class="menu" href="../contact.html">Webmaster</a></li>
<li class="no-style"><center><img border="0" src="../robot-small.jpg"></center>
<br><br><br>
<center>We are using google analytics for statistics!</center>
</ul>
</div>
<div class="content">
<h1>Robotics: Science and Systems XXI</h1>
<h3>Demonstrating ViSafe: Vision-enabled Safety for High-speed Detect and Avoid</h3>
<i>Parv Kapoor, Ian Higgins, Nikhil Varma Keetha, Jay Patrikar, Brady Moon, Zelin Ye, Yao He, Ivan Cisneros, Changliu Liu, Eunsuk Kang, Sebastian Scherer</i><br>
<p>
<b>Abstract:</b>
</p><p style="text-align: justify;">
Assured safe-separation is essential for achieving seamless high-density operation of airborne vehicles in a shared airspace. To equip resource-constrained aerial systems with this safety-critical capability, we present ViSafe, a high-speed vision-only airborne collision avoidance system. ViSafe offers a full-stack solution to the Detect and Avoid (DAA) problem by tightly integrating a learning-based edge-AI framework with a custom multi-camera hardware prototype designed under SWaP-C constraints. By leveraging perceptual input-focused control barrier functions (CBF) to design, encode, and enforce safety thresholds, ViSafe can provide provably safe runtime guarantees for self-separation in high-speed aerial operations. We evaluate ViSafe’s performance through an extensive test campaign involving both simulated digital twins and real-world flight scenarios. By independently varying agent types, closure rates, interaction geometries, and environmental conditions (e.g., weather and lighting), we demonstrate that ViSafe consistently ensures self-separation across diverse scenarios. In first-of-its-kind real-world high-speed collision avoidance tests with closure rates reaching 144 km/h, ViSafe sets a new benchmark for vision-only autonomous collision avoidance, establishing a new standard for safety in high-speed aerial navigation.
</p>
<p>
<b>Download:</b> <a href="p002.pdf" target="_blank" onclick="_gaq.push(['_trackEvent','rss10/p002.pdf','PDF',this.href]);"><img src="../icon-pdf.png" border=0></a>
</p>
<p>
<b>Bibtex:</b>
<pre>
@INPROCEEDINGS{KapoorP-RSS-25,
AUTHOR = {Parv Kapoor AND Ian Higgins AND Nikhil Varma Keetha AND Jay Patrikar AND Brady Moon AND Zelin Ye AND Yao He AND Ivan Cisneros AND Changliu Liu AND Eunsuk Kang AND Sebastian Scherer},
TITLE = {{Demonstrating ViSafe: Vision-enabled Safety for High-speed Detect and Avoid}},
BOOKTITLE = {Proceedings of Robotics: Science and Systems},
YEAR = {2025},
ADDRESS = {LosAngeles, CA, USA},
MONTH = {June},
DOI = {10.15607/RSS.2025.XXI.002}
}
</pre>
</p>
</div>
<div class="analyt">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-24631810-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</div>
</body>
</html>
-65
View File
@@ -1,65 +0,0 @@
{
"scope": "Six unmodified official HTML fixtures; exact observations from full167 server freeze. No research PDFs.",
"receipts": [
{
"file": "index.html",
"url": "https://www.roboticsproceedings.org/rss21/",
"final_url": "https://www.roboticsproceedings.org/rss21/",
"http_status": 200,
"content_type": "text/html",
"bytes": 77753,
"sha256": "5d8e530b5ce7184795c14a7dca615f9c1a8b0b189661e0e6e795167b68b8516d",
"at": "2026-11-09T09:53:09.121733+00:00"
},
{
"file": "accepted.html",
"url": "https://roboticsconference.org/2025/program/papers/",
"final_url": "https://roboticsconference.org/2025/program/papers/",
"http_status": 200,
"content_type": "text/html",
"bytes": 197708,
"sha256": "fa1a5bce496a85eca8cec79aed8dbc148cda791fe6859375ba0c294983a18683",
"at": "2026-11-09T09:53:10.171450+00:00"
},
{
"file": "demos.html",
"url": "https://roboticsconference.org/2025/program/demos/",
"final_url": "https://roboticsconference.org/2025/program/demos/",
"http_status": 200,
"content_type": "text/html",
"bytes": 43542,
"sha256": "0b154ed110f6d49ebe2ba87f2c9c498df5fc1ea00bfe508b548522dfe4779698",
"at": "2026-11-09T09:53:11.145288+00:00"
},
{
"file": "authorinfo.html",
"url": "https://roboticsconference.org/2025/information/authorinfo/",
"final_url": "https://roboticsconference.org/2025/information/authorinfo/",
"http_status": 200,
"content_type": "text/html",
"bytes": 54508,
"sha256": "24d023daf9a074434c6e004001b863dfd260330e523671d614fa120b052927ab",
"at": "2026-11-09T09:53:12.170246+00:00"
},
{
"file": "p001.html",
"url": "https://www.roboticsproceedings.org/rss21/p001.html",
"final_url": "https://www.roboticsproceedings.org/rss21/p001.html",
"http_status": 200,
"content_type": "text/html",
"bytes": 9048,
"sha256": "973172348096a5cbf3e311bdc1ce33b27447761fa1058a050e611aa17547d821",
"at": "2026-11-09T09:53:13.437943+00:00"
},
{
"file": "p002.html",
"url": "https://www.roboticsproceedings.org/rss21/p002.html",
"final_url": "https://www.roboticsproceedings.org/rss21/p002.html",
"http_status": 200,
"content_type": "text/html",
"bytes": 9335,
"sha256": "30b5b68e7ed6a809ca51282c5efd1416513417ba91eba2845f2e73ed18b90ba4",
"at": "2026-11-09T09:53:14.697857+00:00"
}
]
}
Binary file not shown.
+85
View File
@@ -0,0 +1,85 @@
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from paper_atlas.collectors import aamas, aamas2025
FIXTURE = Path(__file__).parent/'fixtures/aamas2025'
class Aamas2025Tests(unittest.TestCase):
def parse(self, html=None):
return aamas2025.parse_contents(html or (FIXTURE/'contents.html').read_text(),
{'observed_at': '2026-11-09T12:31:50+00:00', 'url': aamas2025.URLS['contents']})
def test_real_toc_preserves_every_title_author_and_affiliation(self):
expected = json.loads((FIXTURE/'expected-authors.json').read_text())
self.assertEqual(hashlib.sha256((FIXTURE/'contents.html').read_bytes()).hexdigest(), expected['source_sha256'])
rows, excluded = self.parse()
self.assertEqual((len(rows), len(excluded)), (476, 3))
by_url = {r['paper_url']: r for r in expected['entries']}
for row in rows:
original = by_url[row['metadata']['paper_url']]
self.assertEqual(row['title'], original['title'])
self.assertEqual(row['authors'], [a['name'] for a in original['authors']])
self.assertEqual(row['metadata']['author_affiliations'], [a['affiliation'] for a in original['authors']])
self.assertIsNone(row['abstract'])
self.assertEqual(row['identifiers'], {})
self.assertEqual(row['status'], 'accepted')
self.assertNotIn('decision_date', row)
self.assertTrue(all(t['namespace'] == 'aamas_proceedings:publication_format' for t in row['tags']))
def test_publication_format_and_track_are_separate(self):
rows, excluded = self.parse()
groups = {k: sum(r['track'] == k for r in rows) for k in {r['track'] for r in rows}}
self.assertEqual(groups, {'proceedings_unspecified': 409, 'blue_sky': 12, 'doctoral': 25, 'demo': 21, 'jaamas': 9})
competition = next(r for r in rows if r['external_id'] == '2025/p3000')
self.assertEqual(competition['year'], 2025)
self.assertEqual(competition['metadata']['publication_format'], 'competition_report')
self.assertEqual(competition['metadata']['section_heading'], 'Demo Track')
self.assertEqual(competition['track'], 'proceedings_unspecified')
self.assertEqual(len({r['external_id'] for r in rows}), 476)
self.assertEqual([r['publisher_page'] for r in excluded], [1, 3, 4])
def test_refuses_changed_edition_inventory_or_author_markup(self):
original = (FIXTURE/'contents.html').read_text()
for html in [original.replace('AAMAS 2025:', 'AAMAS 2026:', 1),
original.replace('../pdfs/p5.pdf', '../pdfs/p14.pdf', 1),
original.replace('Paper Track</font>', 'Workshop Track</font>'),
original.replace('Amir Abolfazli', '', 1),
original.replace('[COMP24]', '[CHANGED]', 1)]:
with self.subTest(change=html[:60]):
with self.assertRaises(ValueError): self.parse(html)
def test_frozen_collection_is_partial_and_never_fetches_pdfs(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(aamas2025, 'fetch', side_effect=AssertionError('No HTTP in frozen mode')):
out = Path(tmp)/'out'
report = aamas.collect(2025, out, snapshots=FIXTURE)
self.assertEqual(report['records'], 476)
self.assertFalse(report['database_written'])
self.assertFalse(report['pdfs_downloaded'])
self.assertEqual(len(report['files']), 5)
for m in report['files']:
self.assertEqual(m['status'], 'partial')
self.assertEqual(m['missing_abstract_count'], m['count'])
self.assertEqual(hashlib.sha256(Path(m['file']).read_bytes()).hexdigest(), m['file_sha256'])
receipt = report['source_receipts'][0]['response_receipt']
self.assertEqual(receipt['at_precision'], 'filesystem_write_time_recovered')
self.assertTrue((out/'aamas-proceedings-2025.json').is_file())
def test_rejects_tampered_or_unsafe_frozen_response(self):
import shutil
for mode in ['tamper', 'path', 'status']:
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); source = root/'source'; shutil.copytree(FIXTURE, source)
p = source/'receipts.json'; r = json.loads(p.read_text())
if mode == 'tamper': (source/'contents.html').write_text('bad')
elif mode == 'path': r['receipts'][0]['file'] = '../index.html'
else: r['receipts'][0]['http_status'] = 403
p.write_text(json.dumps(r))
with self.assertRaises(ValueError): aamas.collect(2025, root/'out', snapshots=source)
self.assertFalse((root/'out/aamas-proceedings-2025.json').exists())
+167
View File
@@ -0,0 +1,167 @@
import gzip
import hashlib
import json
from pathlib import Path
import tempfile
import sqlite3
import unittest
from unittest.mock import patch
from paper_atlas.collectors import aamas, aamas2026
from paper_atlas.ingestion.records import ingest_file
FIXTURE = json.loads(gzip.decompress((Path(__file__).parent/'fixtures/aamas2026-fixture.json.gz').read_bytes()))
EVIDENCE = {'url': aamas2026.URL, 'observed_at': FIXTURE['receipts']['receipts'][0]['finished_at']}
class Aamas2026Tests(unittest.TestCase):
def parse(self, html=None):
return aamas2026.parse_contents(FIXTURE['contents'] if html is None else html, EVIDENCE)
def snapshots(self, folder):
folder.mkdir()
(folder/'contents.html').write_text(FIXTURE['contents'])
(folder/'receipts.json').write_text(json.dumps(FIXTURE['receipts']))
return folder
def test_every_publisher_title_author_affiliation_and_page_preserved(self):
rows, excluded, held = self.parse()
self.assertEqual((len(rows), len(excluded), len(held)), (639, 3, 1))
expected = {r['publisher_page']: r for r in FIXTURE['expected_entries']}
self.assertEqual(len(expected), 643)
all_rows = rows+excluded+held
self.assertEqual({r['metadata']['publisher_page'] for r in all_rows}, set(expected))
for r in all_rows:
e = expected[r['metadata']['publisher_page']]
self.assertEqual(r['title'], e['title'])
self.assertEqual(r['authors'], e['authors'])
self.assertEqual(r['metadata']['author_affiliations'], e['affiliations'])
self.assertEqual(r['metadata']['section_heading'], e['section'])
for r in rows:
self.assertIsNone(r['abstract'])
self.assertEqual(r['identifiers'], {})
if r['external_id'] != '2026/p3178':
self.assertEqual(r['status'], 'accepted')
self.assertNotIn('decision_date', r)
def test_explicit_withdrawal_preserves_status_date_and_no_pdf(self):
rows, _, _ = self.parse()
row, = [r for r in rows if r['external_id'] == '2026/p3178']
self.assertEqual(row['status'], 'withdrawn')
self.assertEqual(row['metadata']['withdrawal_date'], '2026-06-08')
self.assertNotIn('decision_date', row)
self.assertEqual(row['raw_decision'], 'Withdrawn June 8, 2026')
self.assertNotIn('paper_url', row['metadata'])
self.assertEqual(row['metadata']['publication_state'], 'withdrawn')
self.assertEqual(sum(r['status'] == 'accepted' for r in rows), 638)
def test_withdrawal_ingestion_retains_date_precision_without_invented_acceptance(self):
rows, _, _ = self.parse()
row, = [r for r in rows if r['external_id'] == '2026/p3178']
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); path = root/'withdrawn.jsonl'; db = root/'sample.sqlite'
path.write_text(json.dumps(row, ensure_ascii=False)+'\n')
path.with_suffix('.manifest.json').write_text(json.dumps({
'source': 'aamas_proceedings', 'list_url': aamas2026.URL,
'venue': 'AAMAS', 'year': 2026, 'track': row['track'],
'count': 1, 'status': 'partial', 'missing_abstract_count': 1,
'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest()}))
ingest_file(db, path)
with sqlite3.connect(db) as c:
self.assertEqual(c.execute('SELECT status,raw_value,decision_date FROM decisions').fetchall(),
[('withdrawn', 'Withdrawn June 8, 2026', None)])
metadata = json.loads(c.execute('SELECT metadata_json FROM source_records').fetchone()[0])
self.assertEqual(metadata['withdrawal_date'], '2026-06-08')
self.assertEqual(metadata['decision_date_precision'], 'day')
self.assertEqual(c.execute('PRAGMA foreign_key_check').fetchall(), [])
def test_retraction_is_not_accepted_or_withdrawn_and_retains_date(self):
rows, _, held = self.parse()
self.assertNotIn('2026/p238', {r['external_id'] for r in rows})
h, = held
self.assertEqual(h['publication_state'], 'retracted')
self.assertEqual(h['retraction_date'], '2026-06-08')
self.assertEqual(h['raw_publication_state'], 'Retracted June 8, 2026')
self.assertNotIn('status', h)
self.assertNotIn('paper_url', h['metadata'])
self.assertEqual(h['metadata']['source_evidence'], EVIDENCE)
def test_tracks_formats_and_prior_decisions_are_not_conflated(self):
rows, _, _ = self.parse()
counts = {t: sum(r['track'] == t for r in rows) for t in {r['track'] for r in rows}}
self.assertEqual(counts, {'proceedings_unspecified': 522, 'aaai_transfer': 19,
'blue_sky': 17, 'doctoral': 34, 'demo': 44, 'jaamas': 3})
for row in rows:
self.assertEqual(row['tags'][0]['namespace'], 'aamas_proceedings:publication_format')
if row['track'] == 'aaai_transfer':
self.assertIn('none inferred', row['metadata']['prior_submission_note'])
self.assertNotIn('decision_events', row)
if row['track'] == 'jaamas':
self.assertEqual(row['metadata']['publication_format'], 'journal_presentation')
self.assertIn('not another independent', row['metadata']['track_scope_note'])
def test_refuses_changed_year_section_page_link_author_and_retraction(self):
html = FIXTURE['contents']
replacements = [('AAMAS 2026:', 'AAMAS 2027:'),
('name="R" id="R"></a><font color="#010101">Research Paper Track',
'name="R" id="R"></a><font color="#010101">Workshop Track'),
('(Page 4)', '(Page 1)'),
('../pdfs/HQQZ1937.pdf', 'https://other.example/paper.pdf'),
('Michael Wooldridge <i>', '<i>'),
('Retracted June 8, 2026', 'Withdrawn June 8, 2026')]
for old, new in replacements:
self.assertIn(old, html)
with self.subTest(change=old), self.assertRaises(ValueError):
self.parse(html.replace(old, new, 1))
def test_frozen_collection_is_partial_with_full_held_evidence(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(aamas2026, 'fetch', side_effect=AssertionError('No HTTP')):
root = Path(tmp)
report = aamas.collect(2026, root/'out', snapshots=self.snapshots(root/'source'))
self.assertEqual(report['records'], 639)
self.assertEqual(report['status_counts'], {'accepted': 638, 'withdrawn': 1})
self.assertEqual(len(report['files']), 6)
self.assertEqual(len(report['held_publications']), 1)
self.assertFalse(report['database_written'])
self.assertFalse(report['pdfs_downloaded'])
for m in report['files']:
self.assertEqual(m['status'], 'partial')
self.assertEqual(m['missing_abstract_count'], m['count'])
self.assertEqual(hashlib.sha256(Path(m['file']).read_bytes()).hexdigest(), m['file_sha256'])
self.assertTrue((root/'out/aamas-proceedings-2026.json').is_file())
def test_frozen_response_rejects_tamper_unsafe_path_status_and_symlink(self):
for mode in ('tamper', 'path', 'status', 'symlink'):
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); source = self.snapshots(root/'source')
r = json.loads((source/'receipts.json').read_text())
if mode == 'tamper':
(source/'contents.html').write_text('bad')
elif mode == 'path':
r['receipts'][0]['file'] = '../outside.html'
elif mode == 'status':
r['receipts'][0]['status'] = 403
else:
(root/'outside.html').write_text(FIXTURE['contents'])
(source/'contents.html').unlink()
(source/'contents.html').symlink_to(root/'outside.html')
(source/'receipts.json').write_text(json.dumps(r))
with self.assertRaises(ValueError):
aamas.collect(2026, root/'out', snapshots=source)
self.assertFalse((root/'out/aamas-proceedings-2026.json').exists())
def test_live_mode_fetches_only_html_and_preserves_receipt(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); cache = root/'cache'; cache.mkdir()
receipt = {'retrieved_at': EVIDENCE['observed_at'], 'url': aamas2026.URL}
(cache/hashlib.sha256(aamas2026.URL.encode()).hexdigest()).with_suffix('.json').write_text(json.dumps(receipt))
with patch.object(aamas2026, 'fetch', return_value=FIXTURE['contents'].encode()) as fetch:
report = aamas.collect(2026, root/'out', cache=cache)
fetch.assert_called_once_with(aamas2026.URL, cache, 8*1024**2, delay=1)
self.assertEqual(report['source_receipts'][0]['response_receipt'], receipt)
def test_frozen_fixture_is_original_hashed_response(self):
r, = FIXTURE['receipts']['receipts']
self.assertEqual(hashlib.sha256(FIXTURE['contents'].encode()).hexdigest(), r['sha256'])
self.assertEqual(r['bytes'], len(FIXTURE['contents'].encode()))
+1 -1
View File
@@ -99,7 +99,7 @@ class AamasProceedingsTests(unittest.TestCase):
aamas.collect(2012, root/'out', snapshots=source)
self.assertFalse((root/'out/aamas-proceedings-2012.json').exists())
with self.assertRaises(ValueError):
aamas.collect(2025, root/'future', snapshots=source)
aamas.collect(2027, root/'future', snapshots=source)
self.assertFalse((root/'future').exists())
def test_ids_colliding_across_sections_do_not_produce_a_completed_collection(self):
+81
View File
@@ -0,0 +1,81 @@
"""Exact AMTCL volume provenance and native metadata/history preservation."""
import copy
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
import xml.etree.ElementTree as ET
from paper_atlas.collectors.acl import collect_xml, volume_scope, text
from paper_atlas.collectors.acl_historical import event_scope
from paper_atlas.ingestion.records import ingest_file
FIXTURE = Path(__file__).parent/'fixtures/acl-1963-earlymt.xml'
COMMIT = 'a7be623d01c48c61405dec31c871121cdbed5a01'
class Acl1963Tests(unittest.TestCase):
def setUp(self):
self.volume = ET.parse(FIXTURE).getroot().find('volume')
self.meta = self.volume.find('meta')
def test_official_snapshot_and_exact_scope(self):
self.assertEqual(hashlib.sha256(FIXTURE.read_bytes()).hexdigest(), '4a4626d792ac507de0281cb358769f7246f4a0d6b6a9176ac02e61545c1635fc')
self.assertEqual(len(self.volume.findall('paper')), 34)
self.assertEqual(volume_scope(self.meta, '1', '1963.earlymt'), ('ACL', ['ACL', 'AMTCL'], 'proceedings_unspecified'))
scope = event_scope(self.meta, '1', '1963.earlymt')
self.assertEqual(scope['original_venue'], 'EarlyMT')
self.assertEqual(scope['publication_format'], 'published_abstract')
self.assertEqual(scope['event_dates'], ['1963-08-25', '1963-08-26'])
def test_other_earlymt_collections_and_volumes_stay_excluded(self):
for collection, volume in [('1962.earlymt', '1'), ('1971.earlymt', '1'), ('1963.earlymt', '2'), ('1963.other', '1')]:
with self.subTest(collection=collection, volume=volume):
self.assertIsNone(volume_scope(self.meta, volume, collection))
self.assertIsNone(volume_scope(self.meta, '1'))
def test_scope_metadata_drift_and_duplicates_are_rejected(self):
for field in ['venue', 'year', 'booktitle', 'address', 'month']:
for duplicate in [False, True]:
meta = copy.deepcopy(self.meta)
if duplicate:
ET.SubElement(meta, field).text = text(meta.find(field))
else:
meta.find(field).text = 'changed'
with self.subTest(field=field, duplicate=duplicate), self.assertRaises(ValueError):
volume_scope(meta, '1', '1963.earlymt')
def test_every_original_contribution_survives_collection(self):
with tempfile.TemporaryDirectory() as tmp:
manifest, = collect_xml(FIXTURE, tmp, COMMIT)
rows = [json.loads(raw) for raw in Path(manifest['file']).read_text().splitlines()]
self.assertEqual(manifest['count'], 34)
self.assertEqual(manifest['missing_abstract_count'], 0)
self.assertEqual(manifest['excluded_materials'], [])
for paper, row in zip(self.volume.findall('paper'), rows):
self.assertEqual(row['external_id'], '1963.earlymt-1.'+paper.attrib['id'])
self.assertEqual(row['title'], text(paper.find('title')))
self.assertEqual(row['abstract'], text(paper.find('abstract')))
self.assertEqual(row['authors'], [{'given':text(a.find('first')), 'family':text(a.find('last'))} for a in paper.findall('author')])
self.assertEqual(row['metadata']['venue_codes'], ['earlymt'])
self.assertEqual(row['metadata']['publication_format'], 'published_abstract')
self.assertEqual(row['tags'], [{'namespace':'acl_anthology:publication_format','value':'published_abstract'}])
self.assertEqual((row['venue'], row['track'], row['status']), ('ACL', 'proceedings_unspecified', 'accepted'))
def test_native_ingestion_is_idempotent_and_preserves_abstracts_and_history(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
manifest, = collect_xml(FIXTURE, root/'source', COMMIT)
db = root/'sample.sqlite'
ingest_file(db, manifest['file'])
ingest_file(db, manifest['file'])
with sqlite3.connect(db) as c:
for table in ['papers', 'source_records', 'submissions', 'decisions']:
self.assertEqual(c.execute('SELECT count(*) FROM '+table).fetchone()[0], 34)
self.assertEqual(c.execute("SELECT count(*) FROM papers WHERE abstract IS NOT NULL AND abstract!=''").fetchone()[0], 34)
self.assertEqual(c.execute("SELECT DISTINCT status FROM decisions").fetchall(), [('accepted',)])
self.assertEqual(c.execute("SELECT v.acronym,e.year,e.track FROM editions e JOIN venues v ON v.id=e.venue_id").fetchall(), [('ACL',1963,'proceedings_unspecified')])
self.assertIsNone(c.execute("SELECT 1 FROM venue_aliases WHERE alias='EarlyMT'").fetchone())
self.assertEqual(c.execute("SELECT count(*) FROM paper_tags pt JOIN tags t ON t.id=pt.tag_id WHERE t.namespace='acl_anthology:publication_format' AND t.value='published_abstract'").fetchone()[0], 34)
+114
View File
@@ -0,0 +1,114 @@
"""Pinned T78 evidence, edition-local aliases and an explicitly identified material."""
import copy
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
import xml.etree.ElementTree as ET
from paper_atlas.collectors.acl import collect_xml, volume_scope
from paper_atlas.collectors.acl_historical import event_scope, material_reason
from paper_atlas.db.store import Store
from paper_atlas.ingestion.records import ingest_file
FIXTURE = Path(__file__).parent/'fixtures/acl-t78.xml'
COMMIT = 'a7be623d01c48c61405dec31c871121cdbed5a01'
class AclHistoricalTests(unittest.TestCase):
def setUp(self):
self.root = ET.parse(FIXTURE).getroot()
self.volume = self.root.find('volume')
self.meta = self.volume.find('meta')
def test_frozen_volume_matches_official_snapshot(self):
self.assertEqual(hashlib.sha256(FIXTURE.read_bytes()).hexdigest(), 'f712f94cff0cba05f786b7ac31dad2ea9408fde2a17a04531130df623f57dd84')
self.assertEqual(len(self.volume.findall('paper')), 36)
self.assertEqual(volume_scope(self.meta, '1', 'T78'), ('ACL+TINLAP', ['ACL', 'TINLAP'], 'proceedings_unspecified'))
def test_unknown_year_collection_or_volume_is_not_globally_aliased(self):
for collection, volume in [('T75', '1'), ('T87', '1'), ('T78', '2'), ('C65', '1')]:
with self.subTest(collection=collection, volume=volume):
self.assertIsNone(event_scope(self.meta, volume, collection))
self.assertIsNone(volume_scope(self.meta, volume, collection))
self.assertIsNone(volume_scope(self.meta, '1'))
def test_known_volume_scope_rejects_critical_metadata_drift(self):
for field, replacement in [('venue', 'acl'), ('year', '1979'), ('booktitle', 'Another TINLAP'), ('editor/first', 'Another')]:
changed = copy.deepcopy(self.meta)
changed.find(field).text = replacement
with self.subTest(field=field), self.assertRaises(ValueError):
event_scope(changed, '1', 'T78')
changed = copy.deepcopy(self.meta)
ET.SubElement(changed, 'year').text = '1978'
with self.assertRaises(ValueError):
event_scope(changed, '1', 'T78')
def test_evidence_retains_original_series_dates_and_role_boundary(self):
scope = event_scope(self.meta, '1', 'T78')
self.assertEqual(scope['original_venue'], 'TINLAP')
self.assertEqual(scope['event_dates'], ['1978-07-25', '1978-07-27'])
self.assertEqual([e['pdf_pages'] for e in scope['evidence']], [[9, 10], [3]])
self.assertIn('not identified', scope['track_scope_note'])
self.assertIn('pending', scope['evidence'][0]['scope'])
def test_only_exact_session_questions_material_is_excluded(self):
paper = copy.deepcopy(self.volume.findall('paper')[-1])
self.assertIn('Session question list', material_reason('T78', '1', paper))
self.assertIsNone(material_reason('C65', '1', paper))
self.assertIsNone(material_reason('T78', '2', paper))
paper.set('id', '37')
self.assertIsNone(material_reason('T78', '1', paper))
paper.set('id', '36')
ET.SubElement(paper, 'author')
with self.assertRaises(ValueError):
material_reason('T78', '1', paper)
def test_all_authored_source_fields_and_material_receipt_survive_collection(self):
with tempfile.TemporaryDirectory() as temporary:
manifest, = collect_xml(FIXTURE, temporary, COMMIT)
rows = [json.loads(raw) for raw in Path(manifest['file']).read_text().splitlines()]
self.assertEqual(manifest['count'], 35)
self.assertEqual(manifest['xml_paper_elements'], 36)
self.assertEqual(manifest['missing_abstract_count'], 35)
self.assertEqual(manifest['excluded_materials'][0]['external_id'], 'T78-1036')
for number, row in enumerate(rows, 1):
paper = self.volume.findall('paper')[number-1]
self.assertEqual(row['external_id'], f'T78-1{number:03d}')
self.assertEqual(row['title'], ' '.join(''.join(paper.find('title').itertext()).split()))
self.assertEqual(row['metadata']['venue_codes'], ['tinlap'])
self.assertEqual(row['metadata']['historical_event_scope']['original_venue'], 'TINLAP')
self.assertEqual(row['metadata']['metadata_commit'], COMMIT)
self.assertEqual((row['status'], row['track'], row['abstract']), ('accepted', 'proceedings_unspecified', None))
def test_old_coling_scope_is_unchanged_by_calendar_replacement_evidence(self):
meta = ET.fromstring('<meta><venue>coling</venue><year>1965</year><booktitle>COLING 1965</booktitle></meta>')
self.assertEqual(volume_scope(meta, '1', 'C65'), ('COLING', ['COLING'], 'main'))
self.assertIsNone(event_scope(meta, '1', 'C65'))
def test_native_ingestion_does_not_alias_other_tinlap_years_or_infer_negative_status(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
manifest, = collect_xml(FIXTURE, root/'source', COMMIT)
db = root/'sample.sqlite'
store = Store(db)
pid, rid = store.record('test_control', 'tinlap1975', 'https://example.org/tinlap1975',
'An earlier TINLAP contribution', None, ['Jane Doe'])
store.decision(pid, rid, 'TINLAP', 1975, 'main', 'tinlap1975', 'https://example.org/tinlap1975', 'accepted', 'test control')
store.conn.commit()
store.conn.close()
ingest_file(db, manifest['file'])
ingest_file(db, manifest['file'])
with sqlite3.connect(db) as c:
self.assertEqual(c.execute('SELECT count(*) FROM papers').fetchone()[0], 36)
self.assertEqual(c.execute('SELECT count(*) FROM submissions').fetchone()[0], 36)
self.assertEqual(c.execute('SELECT count(*) FROM decisions').fetchone()[0], 36)
self.assertEqual(c.execute("SELECT count(*) FROM decisions WHERE status!='accepted'").fetchone()[0], 0)
self.assertIsNone(c.execute("SELECT 1 FROM source_records WHERE external_id='T78-1036'").fetchone())
aliases = c.execute('SELECT v.acronym,a.alias FROM venue_aliases a JOIN venues v ON v.id=a.venue_id ORDER BY a.alias').fetchall()
self.assertEqual(aliases, [('ACL+TINLAP', 'ACL'), ('ACL+TINLAP', 'TINLAP')])
metadata = json.loads(c.execute("SELECT metadata_json FROM source_records WHERE external_id='T78-1001'").fetchone()[0])
self.assertEqual(metadata['historical_event_scope']['event_year'], 1978)
self.assertEqual(c.execute('PRAGMA foreign_key_check').fetchall(), [])
+141
View File
@@ -0,0 +1,141 @@
import gzip
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from unittest.mock import patch
from paper_atlas.collectors.dblp import catalogue
from paper_atlas.collectors.dblp_recovery import recover
from paper_atlas.collectors.dblp_scope import scope
from paper_atlas.ingestion.records import ingest_file
class DblpRecoveryTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.addCleanup(self.tmp.cleanup)
self.root = Path(self.tmp.name)
self.dtd = self.root / 'dblp.dtd'
self.dtd.write_bytes(b'<!ENTITY eacute "&#233;">')
self.archive = self.root / 'dblp.xml.gz'
# These include a keynote and a workshop, neither a main-paper inference.
body = '''<proceedings key="conf/ifaamas/2025"><title>AAMAS 2025</title><year>2025</year></proceedings>
<inproceedings key="conf/ifaamas/keynote"><author>Ren&eacute;</author><title>Keynote: Agents</title><year>2025</year><booktitle>AAMAS</booktitle><crossref>conf/ifaamas/2025</crossref></inproceedings>
<inproceedings key="conf/ifaamas/paper"><author>Alice Doe</author><title>Research <i>agents</i></title><year>2025</year><booktitle>AAMAS</booktitle><crossref>conf/ifaamas/2025</crossref><ee>https://dl.acm.org/doi/10.5555/3709347.123</ee></inproceedings>
<proceedings key="conf/ifaamas/2025w"><title>AAMAS Workshop</title><year>2025</year></proceedings>
<inproceedings key="conf/ifaamas/workshop"><title>Workshop paper</title><year>2025</year><booktitle>AAMAS Workshop</booktitle><crossref>conf/ifaamas/2025w</crossref></inproceedings>
<inproceedings key="conf/unrelated/other"><title>Other family</title><year>2025</year></inproceedings>'''
with gzip.open(self.archive, 'wb') as stream:
stream.write(('<?xml version="1.0"?><!DOCTYPE dblp SYSTEM "dblp.dtd"><dblp>'
+ body + '</dblp>').encode())
self.sha = hashlib.sha256(self.archive.read_bytes()).hexdigest()
self.md5 = hashlib.md5(self.dtd.read_bytes()).hexdigest()
self.manifest = catalogue(self.archive, self.dtd, self.root / 'catalogue',
self.sha, self.md5)
self.catalogue = Path(self.manifest['catalogue'])
self.manifest_path = self.catalogue.with_name('dblp-ai-candidates.manifest.json')
def run_recovery(self, **kwargs):
options = dict(expected_sha=self.sha, expected_dtd_md5=self.md5)
options.update(kwargs)
return recover(self.archive, self.dtd, self.catalogue, self.root / 'recovered',
['ifaamas'], **options)
def test_fresh_discovery_and_recovery_from_old_candidate_omission(self):
self.assertEqual(self.manifest['candidate_family_counts']['ifaamas'], 3)
# Simulate an older filter without altering its complete catalogue.
manifest = dict(self.manifest)
manifest['candidate_family_counts'] = {}
self.manifest_path.write_text(json.dumps(manifest))
candidate = Path(manifest['file'])
candidate.write_text('old incomplete candidate file\n')
before = {p: p.read_bytes() for p in [self.archive, self.dtd, self.catalogue,
self.manifest_path, candidate]}
with patch('urllib.request.urlopen', side_effect=AssertionError('No network')):
report = self.run_recovery()
rows = [json.loads(line) for line in Path(report['file']).read_text().splitlines()]
self.assertEqual(report['recovered_publications'], 3)
self.assertEqual(report['recovered_volumes'], 2)
self.assertEqual(report['recovered_records'], 5)
self.assertEqual(report['records_scanned'], 6)
self.assertEqual(report['old_candidate_family_counts'], {'ifaamas': 0})
self.assertFalse(report['database_written'])
self.assertEqual(next(r for r in rows if r['key'].endswith('/keynote'))['fields']['author'], ['René'])
self.assertTrue(any(r['key'].endswith('/workshop') for r in rows))
self.assertTrue(all(not ({'source', 'status', 'venue', 'track'} & r.keys()) for r in rows))
publication = next(r for r in rows if r['key'].endswith('/paper'))
volume = next(r for r in rows if r['key'] == 'conf/ifaamas/2025')
self.assertEqual(scope(publication, volume), (None, 'outside_target_family'))
self.assertEqual({p: p.read_bytes() for p in before}, before)
self.assertEqual(hashlib.sha256(Path(report['file']).read_bytes()).hexdigest(),
report['file_sha256'])
# Raw recovery files cannot accidentally enter the normalized writer.
with patch('paper_atlas.ingestion.records.open_store') as writer:
with self.assertRaisesRegex(ValueError, 'source manifest'):
ingest_file(self.root / 'never.sqlite', report['file'])
writer.assert_not_called()
self.assertFalse((self.root / 'never.sqlite').exists())
def test_checksum_mismatch_does_not_publish(self):
for options in [{'expected_sha': '0' * 64}, {'expected_dtd_md5': '0' * 32}]:
with self.subTest(options=options), self.assertRaisesRegex(ValueError, 'checksum'):
self.run_recovery(**options)
self.assertFalse((self.root / 'recovered').exists())
def test_wrong_catalogue_snapshot_and_incomplete_totals_are_rejected(self):
for changes in [{'snapshot_sha256': '0' * 64}, {'dtd_md5': '0' * 32},
{'records_scanned': 99}, {'record_types': {'proceedings': 2}}]:
manifest = {**self.manifest, **changes}
self.manifest_path.write_text(json.dumps(manifest))
with self.subTest(changes=changes), self.assertRaises(ValueError):
self.run_recovery()
self.assertFalse((self.root / 'recovered').exists())
def test_changed_family_count_does_not_publish_partial_result(self):
with sqlite3.connect(self.catalogue) as c:
c.execute("UPDATE family_counts SET count=count+1 WHERE family='ifaamas' AND record_type='inproceedings'")
with self.assertRaisesRegex(ValueError, 'family totals'):
self.run_recovery()
self.assertFalse((self.root / 'recovered').exists())
def test_changed_volume_metadata_is_detected(self):
with sqlite3.connect(self.catalogue) as c:
row = json.loads(c.execute("SELECT metadata_json FROM proceedings WHERE key='conf/ifaamas/2025'").fetchone()[0])
row['fields']['title'] = ['Wrong volume']
c.execute("UPDATE proceedings SET metadata_json=? WHERE key='conf/ifaamas/2025'", (json.dumps(row),))
with self.assertRaisesRegex(ValueError, 'proceedings differs'):
self.run_recovery()
self.assertFalse((self.root / 'recovered').exists())
def test_truncated_gzip_never_publishes(self):
self.archive.write_bytes(self.archive.read_bytes()[:-8])
revised_sha = hashlib.sha256(self.archive.read_bytes()).hexdigest()
self.manifest_path.write_text(json.dumps({**self.manifest, 'snapshot_sha256': revised_sha}))
with self.assertRaises((EOFError, OSError)):
self.run_recovery(expected_sha=revised_sha)
self.assertFalse((self.root / 'recovered').exists())
def test_existing_output_is_preserved_and_unknown_family_is_rejected(self):
out = self.root / 'recovered'
out.mkdir()
sentinel = out / 'keep'
sentinel.write_text('existing')
with self.assertRaisesRegex(ValueError, 'already exist'):
self.run_recovery()
self.assertEqual(sentinel.read_text(), 'existing')
for names in [[], ['../ifaamas'], ['not-a-supported-family'], [None]]:
with self.subTest(names=names), self.assertRaises(ValueError):
recover(self.archive, self.dtd, self.catalogue, self.root / 'other', names)
def test_requested_family_missing_from_catalogue_is_explicit(self):
with self.assertRaisesRegex(ValueError, 'absent from complete catalogue'):
recover(self.archive, self.dtd, self.catalogue, self.root / 'recovered',
['ifaamas', 'icml'], expected_sha=self.sha, expected_dtd_md5=self.md5)
self.assertFalse((self.root / 'recovered').exists())
if __name__ == '__main__':
unittest.main()
+174
View File
@@ -0,0 +1,174 @@
import copy
from collections import Counter
import gzip
import hashlib
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from urllib.parse import parse_qs, urlsplit
from paper_atlas.collectors import neurips_program as program
FIXTURES = Path(__file__).parent/'fixtures'
RAW = gzip.decompress((FIXTURES/'neurips2026-program.json.gz').read_bytes())
RECEIPTS = json.loads((FIXTURES/'neurips2026-receipts.json').read_text())
EVIDENCE = {'url': program.URL, 'observed_at': RECEIPTS['receipts'][0]['finished_at']}
class NeuripsProgramTests(unittest.TestCase):
def sample(self):
rows = json.loads(RAW)['results']
oral = next(r for r in rows if r['id'] == 154866)
poster = next(r for r in rows if r['paper_url'] == oral['paper_url'] and r['event_type'] == 'Poster')
return copy.deepcopy(poster), copy.deepcopy(oral)
def payload(self, rows):
return json.dumps({'count': len(rows), 'next': None, 'previous': None, 'results': rows})
def snapshots(self, folder):
folder.mkdir()
(folder/'program-json.response').write_bytes(RAW)
(folder/'receipts.json').write_text(json.dumps(RECEIPTS))
return folder
def test_full_snapshot_tracks_and_all_titles_authors_affiliations(self):
rows, held, duplicates, folded = program.parse_program(RAW, EVIDENCE)
self.assertEqual((len(rows), len(held), len(duplicates), folded), (9004, 139, 1, 132))
self.assertEqual(Counter(r['track'] for r in rows),
{'main': 7896, 'evaluations_datasets': 971, 'position': 137})
expected = {}
for e in json.loads(RAW)['results']:
if e['sourceurl'] in program.GROUPS and e['event_type'] == 'Poster':
forum = parse_qs(urlsplit(e['paper_url']).query)['id'][0]
expected[forum] = e
self.assertEqual(len(expected), 9004)
for r in rows:
e = expected[r['identifiers']['openreview']]
self.assertEqual(r['title'], e['name'])
self.assertEqual(r['authors'], [a['fullname'] for a in e['authors']])
self.assertEqual(r['metadata']['author_affiliations'], [a.get('institution') for a in e['authors']])
self.assertEqual(r['raw_decision'], e['decision'])
self.assertEqual(r['status'], 'accepted')
self.assertNotIn('decision_date', r)
self.assertIsNone(r['abstract'])
self.assertEqual(r['year'], 2026)
self.assertEqual(duplicates[0]['event_id'], 154866)
def test_same_forum_folds_presentations_and_preserves_signed_source_ids(self):
poster, oral = self.sample()
rows, held, duplicates, folded = program.parse_program(self.payload([oral, poster, oral]), EVIDENCE)
row, = rows
self.assertEqual((len(held), len(duplicates), folded), (0, 1, 1))
self.assertEqual({p['sourceid'] for p in row['metadata']['presentations']},
{poster['sourceid'], oral['sourceid']})
self.assertLess(oral['sourceid'], 0)
self.assertEqual({t['value'] for t in row['tags']}, {'Poster', 'Oral'})
self.assertTrue(all(t['namespace'] == 'neurips_program:presentation_format' for t in row['tags']))
def test_identical_title_and_authors_in_distinct_forums_remain_distinct(self):
poster, _ = self.sample(); other = copy.deepcopy(poster)
other.update(id=999999, uid='0'*32, sourceid=999999, paper_url='https://openreview.net/forum?id=otherForum7',
virtualsite_url='/virtual/2026/poster/999999')
rows, _, _, _ = program.parse_program(self.payload([poster, other]), EVIDENCE)
self.assertEqual(len(rows), 2)
self.assertEqual(len({r['external_id'] for r in rows}), 2)
def test_conflicting_event_ids_or_same_forum_bibliography_are_rejected(self):
poster, oral = self.sample()
for field in ('name', 'authors', 'sourceurl', 'decision'):
changed = copy.deepcopy(oral)
if field == 'authors':
changed['authors'][0]['fullname'] = 'Different Author'
elif field == 'sourceurl':
changed[field] = 'https://openreview.net/group?id=NeurIPS.cc/2026/Position_Paper_Track'
elif field == 'decision':
changed[field] = 'Accept (poster)'
else:
changed[field] += ' changed'
with self.subTest(field=field), self.assertRaises(ValueError):
program.parse_program(self.payload([poster, changed]), EVIDENCE)
changed = copy.deepcopy(poster); changed['name'] += ' changed'
with self.assertRaises(ValueError):
program.parse_program(self.payload([poster, changed]), EVIDENCE)
def test_unreviewed_origin_decision_visibility_and_link_are_held(self):
poster, _ = self.sample()
mutations = [{'sourceurl': 'journal-track/NeurIPS_2026-Posters_TMLR.xlsx'},
{'sourceurl': 'neurips2026-bridging-the-future'},
{'sourceurl': None}, {'sourceurl': 'https://openreview.net/group?id=NeurIPS.cc/2027/Conference'},
{'decision': 'Reject'}, {'visible': False},
{'paper_url': 'https://other.example/forum?id=anotherID7'}]
for mutation in mutations:
changed = copy.deepcopy(poster); changed.update(id=999999, virtualsite_url='/virtual/2026/poster/999999')
changed.update(mutation)
rows, held, _, _ = program.parse_program(self.payload([poster, changed]), EVIDENCE)
self.assertEqual((len(rows), len(held)), (1, 1))
self.assertEqual(held[0]['raw_event'], changed)
def test_rejects_truncation_pagination_invalid_url_and_missing_authors(self):
poster, _ = self.sample()
raw = json.loads(self.payload([poster]))
bad = [dict(raw, count=2), dict(raw, next='https://example.com/next'),
{k: v for k, v in raw.items() if k != 'previous'}]
for payload in bad:
with self.assertRaises(ValueError):
program.parse_program(json.dumps(payload), EVIDENCE)
for mutation in [{'authors': []}, {'virtualsite_url': '/virtual/2025/poster/154865'}, {'id': True}]:
changed = copy.deepcopy(poster); changed.update(mutation)
with self.assertRaises(ValueError):
program.parse_program(self.payload([changed]), EVIDENCE)
def test_frozen_collection_is_partial_and_no_http_or_database_write(self):
with tempfile.TemporaryDirectory() as tmp, patch.object(program, 'fetch', side_effect=AssertionError('No HTTP')):
root = Path(tmp); source = self.snapshots(root/'source')
report = program.collect(root/'out', snapshots=source)
self.assertEqual(report['records'], 9004)
self.assertEqual(len(report['files']), 3)
self.assertFalse(report['database_written'])
self.assertFalse(report['pdfs_downloaded'])
for m in report['files']:
self.assertEqual(m['status'], 'partial')
self.assertEqual(m['missing_abstract_count'], m['count'])
self.assertEqual(hashlib.sha256(Path(m['file']).read_bytes()).hexdigest(), m['file_sha256'])
with self.assertRaises(ValueError):
program.collect(root/'out', snapshots=source)
def test_frozen_receipt_tamper_path_symlink_status_and_duplicate(self):
for mode in ('tamper', 'path', 'symlink', 'status', 'duplicate'):
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); source = self.snapshots(root/'source')
receipts = copy.deepcopy(RECEIPTS)
if mode == 'tamper':
(source/'program-json.response').write_text('bad')
elif mode == 'path':
receipts['receipts'][0]['file'] = '../outside.json'
elif mode == 'symlink':
(root/'outside.json').write_bytes(RAW)
(source/'program-json.response').unlink()
(source/'program-json.response').symlink_to(root/'outside.json')
elif mode == 'status':
receipts['receipts'][0]['status'] = 403
else:
receipts['receipts'].append(copy.deepcopy(receipts['receipts'][0]))
(source/'receipts.json').write_text(json.dumps(receipts))
with self.assertRaises(ValueError):
program.collect(root/'out', snapshots=source)
self.assertFalse((root/'out/neurips-program-2026.json').exists())
def test_online_mode_only_requests_fixed_program_metadata(self):
poster, _ = self.sample(); raw = self.payload([poster]).encode()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp); cache = root/'cache'; cache.mkdir()
receipt = {'retrieved_at': EVIDENCE['observed_at'], 'url': program.URL}
(cache/hashlib.sha256(program.URL.encode()).hexdigest()).with_suffix('.json').write_text(json.dumps(receipt))
with patch.object(program, 'fetch', return_value=raw) as fetch:
report = program.collect(root/'out', cache=cache)
fetch.assert_called_once_with(program.URL, cache, program.LIMIT, delay=1)
self.assertEqual(report['records'], 1)
def test_frozen_fixture_preserves_actual_response_bytes(self):
receipt, = RECEIPTS['receipts']
self.assertEqual(hashlib.sha256(RAW).hexdigest(), receipt['sha256'])
self.assertEqual(len(RAW), receipt['bytes'])
-186
View File
@@ -1,186 +0,0 @@
"""Frozen publisher examples, conflicting evidence and preserved attempt identity."""
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from unittest.mock import patch
from paper_atlas.collectors import rss
from paper_atlas.ingestion.records import ingest_file
FIXTURES = Path(__file__).parent/'fixtures/rss2025'
OBSERVED = '2026-11-09T10:00:00Z'
def fixture(name):
return (FIXTURES/name).read_text()
class RssProceedingsTests(unittest.TestCase):
def setUp(self):
self.index = rss.parse_index(fixture('index.html'))
self.accepted = rss.parse_program(fixture('accepted.html'), 'Accepted Papers', ('ID', 'Session', 'Title', 'Authors'))
self.demos = rss.parse_program(fixture('demos.html'), 'Demos', ('ID', 'Location', 'Title', 'Day', 'Time'))
def paper(self, pid=1, html=None, entry=None):
return rss.parse_paper(html or fixture(f'p{pid:03d}.html'), pid, entry or self.index[pid],
self.accepted[pid], self.demos.get(pid), {'paper': {'observed_at': OBSERVED}})
def test_frozen_official_census_and_all_fixture_hashes(self):
receipts = json.loads((FIXTURES/'receipts.json').read_bytes())['receipts']
self.assertEqual(len(receipts), 6)
for r in receipts:
data = (FIXTURES/r['file']).read_bytes()
self.assertEqual((len(data), hashlib.sha256(data).hexdigest()), (r['bytes'], r['sha256']))
self.assertEqual(len(self.index), 163)
self.assertEqual(set(self.index), set(self.accepted))
self.assertEqual(len(self.demos), 15)
rss.validate_program(self.index, self.accepted, self.demos, fixture('authorinfo.html'))
self.assertFalse(self.demos[93]['Title'].startswith('Demonstrating'))
def test_complete_authors_abstract_and_source_metadata_are_preserved(self):
row = self.paper()
self.assertEqual(row['authors'], ['Pascal Roth', 'Jonas Frey', 'Cesar Cadena', 'Marco Hutter'])
self.assertTrue(row['abstract'].startswith('Ensuring safe navigation'))
self.assertTrue(row['abstract'].endswith('https://github.com/leggedrobotics/fdm.'))
self.assertNotIn('Download:', row['abstract'])
self.assertEqual(row['identifiers'], {'doi': '10.15607/rss.2025.xxi.001'})
self.assertEqual(row['metadata']['publication_date'], '2025/06/21')
self.assertIn('AUTHOR', row['metadata']['original_bibtex'])
self.assertEqual(row['status'], 'accepted')
self.assertNotIn('decision_date', row)
self.assertEqual(row['metadata']['decision_date_precision'], 'not_supplied')
def test_demo_evidence_is_independent_of_title_and_session_topic(self):
row = self.paper(2)
self.assertEqual(row['track'], 'demo')
self.assertEqual(row['metadata']['demo_program_entry']['ID'], '2')
self.assertEqual(self.paper()['track'], 'proceedings_unspecified')
self.assertEqual({t['namespace'] for t in row['tags']}, {'rss_proceedings:session', 'rss_proceedings:program_role'})
self.assertFalse(any('research_type' in t['namespace'] for t in row['tags']))
html = fixture('p001.html').replace('Learned Perceptive Forward Dynamics Model', 'Demonstrating a Learned Model')
entry = {**self.index[1], 'title': self.index[1]['title'].replace('Learned Perceptive Forward Dynamics Model', 'Demonstrating a Learned Model')}
self.assertEqual(self.paper(html=html, entry=entry)['track'], 'proceedings_unspecified')
def test_missing_abstract_is_explicit_and_multiple_paragraphs_are_complete(self):
html = fixture('p001.html')
beginning = html.index('<p style="text-align: justify;">')
end = html.index('</p>', beginning)
empty = html[:beginning]+'<p style="text-align: justify;">'+html[end:]
row = self.paper(html=empty)
self.assertIsNone(row['abstract'])
self.assertEqual(row['metadata']['missing_abstract_reason'], 'empty_official_abstract_section')
multi = html.replace('</p> \n<p> \n<b>Download:', '</p><p>Second abstract paragraph.</p><p><b>Download:', 1)
self.assertTrue(self.paper(html=multi)['abstract'].endswith('Second abstract paragraph.'))
def test_metadata_drift_author_truncation_and_wrong_paper_doi_fail(self):
original = fixture('p001.html')
for altered in [original.replace('XXI.001}', 'XXI.002}'),
original.replace('content="2025/06/21"', 'content="2026/06/21"'),
original.replace('<meta name="citation_author" content="Marco Hutter" />', ''),
original.replace('<b>Abstract:</b>', '<b>Summary:</b>'),
original.replace('rss21/p001.pdf', 'rss21/p002.pdf'),
original.replace('citation_volume" content="21', 'citation_volume" content="22')]:
with self.subTest(altered=altered[:60]), self.assertRaises(ValueError):
self.paper(html=altered)
def test_duplicate_and_unparsed_index_entries_fail(self):
for html in [fixture('index.html').replace('p002.html', 'p001.html'),
fixture('index.html')+'<a href="p999.html">Stray paper</a>',
fixture('index.html').replace( rss.HEADING, 'A different volume')]:
with self.subTest(html=html[:40]), self.assertRaises(ValueError):
rss.parse_index(html)
def test_program_title_id_category_and_header_changes_require_review(self):
for change in ['title', 'id', 'category']:
index = {k: dict(v) for k, v in self.index.items()}
demos = {k: dict(v) for k, v in self.demos.items()}
if change == 'title':
demos[2]['Title'] = 'Another paper'
elif change == 'id':
del index[1]
else:
del demos[93]
with self.subTest(change=change), self.assertRaises(ValueError):
rss.validate_program(index, self.accepted, demos, fixture('authorinfo.html'))
with self.assertRaises(ValueError):
rss.parse_program(fixture('demos.html').replace('>Location<', '>Other<'), 'Demos', ('ID', 'Location', 'Title', 'Day', 'Time'))
def snapshot(self, root):
root.mkdir()
data = (FIXTURES/'p001.html').read_bytes()
(root/'paper.html').write_bytes(data)
receipt = {'file': 'paper.html', 'url': self.index[1]['url'], 'final_url': self.index[1]['url'],
'http_status': 200, 'content_type': 'text/html', 'bytes': len(data),
'sha256': hashlib.sha256(data).hexdigest(), 'at': OBSERVED}
(root/'receipts.json').write_text(json.dumps({'receipts': [receipt]}))
return receipt
def test_frozen_receipt_hash_status_redirect_and_path_guards(self):
for failure in ['hash', 'status', 'redirect', 'path', 'duplicate', 'symlink']:
with self.subTest(failure=failure), tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)/'source'
r = self.snapshot(root)
receipts = [r]
if failure == 'hash': r['sha256'] = '0'*64
elif failure == 'status': r['http_status'] = 404
elif failure == 'redirect': r['final_url'] = 'https://example.org/p001.html'
elif failure == 'path': r['file'] = '../outside.html'
elif failure == 'duplicate': receipts.append(dict(r))
else:
(root.parent/'outside.html').write_bytes((root/'paper.html').read_bytes())
(root/'paper.html').unlink()
(root/'paper.html').symlink_to(root.parent/'outside.html')
(root/'receipts.json').write_text(json.dumps({'receipts': receipts}))
with self.assertRaises(ValueError): rss.Pages(snapshots=root).read(self.index[1]['url'])
def test_unreviewed_edition_and_existing_output_refuse_work(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(ValueError): rss.collect(2026, tmp, snapshots=FIXTURES)
(Path(tmp)/'rss-2025.json').write_text('{}')
with self.assertRaises(FileExistsError): rss.collect(2025, tmp, snapshots=FIXTURES)
def test_native_roundtrip_is_idempotent_with_same_doi_and_distinct_tracks(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
original = self.paper()
dblp = {**original, 'source': 'dblp', 'external_id': 'conf/rss/example', 'metadata': {}, 'tags': []}
variant = {**original, 'external_id': '2025/other-track', 'track': 'workshop'}
for n, record in enumerate([dblp, original, variant, original]):
path = root/f'{n}.jsonl'
path.write_text(json.dumps(record)+'\n')
path.with_suffix('.manifest.json').write_text(json.dumps({'source': record['source'], 'list_url': rss.ROOT, 'count': 1}))
ingest_file(root/'sample.sqlite', path)
with sqlite3.connect(root/'sample.sqlite') as c:
self.assertEqual(c.execute('SELECT count(*) FROM papers').fetchone()[0], 1)
self.assertEqual(c.execute('SELECT count(*) FROM source_records').fetchone()[0], 3)
self.assertEqual(c.execute('SELECT count(*) FROM submissions').fetchone()[0], 2)
self.assertEqual(c.execute('SELECT count(*) FROM decisions').fetchone()[0], 2)
self.assertEqual(c.execute('SELECT count(*) FROM submission_sources').fetchone()[0], 3)
self.assertEqual(c.execute('PRAGMA foreign_key_check').fetchall(), [])
def test_two_real_papers_collection_commits_evidence_only_after_success(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
index = {pid: self.index[pid] for pid in [1, 2]}
accepted = {pid: self.accepted[pid] for pid in [1, 2]}
demos = {2: self.demos[2]}
with (patch.object(rss, 'EXPECTED', 2), patch.object(rss, 'DEMO_IDS', {2}),
patch.object(rss, 'parse_index', return_value=index),
patch.object(rss, 'parse_program', side_effect=[accepted, demos])):
result = rss.collect(2025, root/'out', snapshots=FIXTURES)
self.assertEqual((result['records'], len(result['files'])), (2, 2))
self.assertFalse(result['database_written'])
self.assertFalse(result['pdfs_downloaded'])
self.assertEqual(len(result['source_evidence']), 6)
for item in result['source_evidence']:
self.assertEqual(hashlib.sha256((root/'out'/item['file']).read_bytes()).hexdigest(), item['sha256'])
for item in result['files']:
self.assertEqual(item['missing_abstract_count'], 0)
self.assertEqual(hashlib.sha256(Path(item['file']).read_bytes()).hexdigest(), item['file_sha256'])
if __name__ == '__main__':
unittest.main()
+148
View File
@@ -0,0 +1,148 @@
import gzip
import hashlib
import json
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
from paper_atlas.audits.source_identity import audit, classify, load_inputs
from paper_atlas.postgres.client import ReadConnection
def record(**updates):
row = dict(source='reviewed_program', external_id='2026/forum-a',
title='A sufficiently specific scientific paper title', authors=['Jane Doe', 'John Smith'],
venue='NeurIPS', year=2026, track='main', status='accepted', identifiers={},
source_record_hits=[], identifier_hits=[], title_candidate_ids=[])
row.update(updates)
return row
def write_input(root, rows):
path = root / 'source.jsonl'
raw = b''.join((json.dumps(row) + '\n').encode() for row in rows)
path.write_bytes(raw)
manifest = {k: rows[0][k] for k in ('source', 'venue', 'year', 'track')}
manifest.update(count=len(rows), file_sha256=hashlib.sha256(raw).hexdigest(),
file='/ignored/untrusted/location.jsonl', status='partial')
path.with_suffix('.manifest.json').write_text(json.dumps(manifest))
return path
class CandidateTests(unittest.TestCase):
def test_duplicate_names_do_not_equal_distinct_author_multiset(self):
row = record(authors=['Jane Doe', 'Jane Doe'], title_candidate_ids=[7])
result = classify(row, {7: {'title': row['title'], 'authors': ['Jane Doe']}})
self.assertEqual(result['kind'], 'partial_author_overlap_requires_review')
self.assertIn('duplicate_normalized_author_names', result['flags'])
self.assertFalse(result['comparisons'][0]['nonempty_author_multiset_equal'])
def test_arxiv_author_representation_and_order_match_but_never_approve(self):
row = record(title_candidate_ids=[7])
result = classify(row, {7: {'title': row['title'], 'authors': [['Smith', 'John', ''], ['Doe', 'Jane', '']]}})
self.assertEqual(result['kind'], 'one_exact_title_author_multiset_requires_review')
self.assertEqual(result['decision'], 'not_approved_for_import')
def test_binding_conflict_overrides_existing_source(self):
row = record(source_record_hits=[{'paper_id': 7}], identifier_hits=[{'paper_id': 8}])
entities = {pid: {'title': row['title'], 'authors': row['authors']} for pid in (7, 8)}
self.assertEqual(classify(row, entities)['kind'], 'conflicting_existing_bindings_hold')
def test_wrong_doi_binding_retains_author_title_disagreement(self):
row = record(identifier_hits=[{'paper_id': 7}])
result = classify(row, {7: {'title': 'Completely different work', 'authors': ['Somebody Else']}})
self.assertEqual(result['kind'], 'existing_identifier_binding_requires_review')
self.assertFalse(result['comparisons'][0]['normalized_title_equal'])
self.assertEqual(result['comparisons'][0]['author_name_overlap'], [])
def test_no_hit_is_not_novelty_and_missing_authors_never_agree(self):
self.assertEqual(classify(record(), {})['kind'], 'no_exact_match_not_proof_of_novelty')
row = record(authors=[], title_candidate_ids=[7])
result = classify(row, {7: {'title': row['title'], 'authors': []}})
self.assertEqual(result['kind'], 'no_author_agreement_hold')
def test_title_candidate_cap_is_explicit(self):
row = record(title_candidate_ids=list(range(21)))
entities = {pid: {'title': row['title'], 'authors': row['authors']} for pid in range(21)}
self.assertTrue(classify(row, entities)['title_candidates_truncated'])
self.assertEqual(classify(row, entities)['kind'], 'title_candidate_limit_reached_hold')
class InputTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
def test_raw_line_hash_and_manifest_path_not_followed(self):
path = write_input(self.root, [record()])
rows, inputs = load_inputs([path])
self.assertEqual(rows[0]['raw_line_sha256'], hashlib.sha256(path.read_bytes()).hexdigest())
self.assertEqual(inputs[0]['count'], 1)
def test_hash_tamper_rejected_before_database_connection(self):
path = write_input(self.root, [record()])
path.write_bytes(path.read_bytes().replace(b'Jane Doe', b'Jane Roe'))
with patch('paper_atlas.audits.source_identity.ReadConnection') as connection:
with self.assertRaisesRegex(ValueError, 'hash mismatch'):
audit([path], self.root / 'out', pg_service='test', pg_schema='research')
connection.assert_not_called()
def test_manifest_scope_and_count_tamper_and_duplicate_sources(self):
path = write_input(self.root, [record()])
manifest_path = path.with_suffix('.manifest.json')
manifest = json.loads(manifest_path.read_text())
for key, value in [('year', 2025), ('count', 2)]:
altered = dict(manifest, **{key: value})
manifest_path.write_text(json.dumps(altered))
with self.assertRaises(ValueError):
load_inputs([path])
manifest_path.write_text(json.dumps(manifest))
with self.assertRaisesRegex(ValueError, 'Duplicate source'):
load_inputs([path, path])
def test_existing_output_never_replayed_and_failure_not_completion(self):
path = write_input(self.root, [record()])
out = self.root / 'out'
with patch('paper_atlas.audits.source_identity.ReadConnection', side_effect=RuntimeError('unavailable')):
with self.assertRaises(RuntimeError):
audit([path], out, pg_service='test', pg_schema='research')
self.assertTrue((out / 'failed.json').exists())
self.assertFalse((out / 'postgresql-preflight.json').exists())
with self.assertRaises(FileExistsError):
audit([path], out, pg_service='test', pg_schema='research')
@unittest.skipUnless(os.environ.get('PAPER_ATLAS_TEST_PG_SERVICE'), 'Requires the authorized remote PostgreSQL service')
class ReadOnlyIntegrationTests(unittest.TestCase):
def test_existing_binding_full_evidence_and_no_import_approval(self):
service = os.environ['PAPER_ATLAS_TEST_PG_SERVICE']
connection = ReadConnection(service, 'research')
try:
self.assertEqual(connection.execute('SHOW transaction_read_only').fetchone()[0], 'on')
hit = connection.execute("SELECT value,paper_id FROM identifiers WHERE scheme='arxiv' ORDER BY value LIMIT 1").fetchone()
self.assertIsNotNone(hit)
paper = connection.execute('SELECT title,authors_json FROM papers WHERE id=?', (hit['paper_id'],)).fetchone()
finally:
connection.close()
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
path = write_input(root, [record(title=paper['title'], authors=json.loads(paper['authors_json']),
identifiers={'arxiv': hit['value']})])
report = audit([path], root / 'out', pg_service=service, pg_schema='research')
self.assertTrue(report['transaction_read_only'])
self.assertFalse(report['database_written'])
self.assertEqual(report['approved_for_import'], 0)
row, = report['entries']
self.assertIn(hit['paper_id'], [r['paper_id'] for r in row['identifier_hits']])
shard = report['entities'][str(hit['paper_id'])]
raw = (root / 'out' / shard['file']).read_bytes()
self.assertEqual(hashlib.sha256(raw).hexdigest(), shard['sha256'])
packet = json.loads(gzip.decompress(raw))
self.assertEqual(packet['paper']['id'], hit['paper_id'])
self.assertEqual(packet['paper']['title'], paper['title'])
self.assertEqual(packet['paper']['authors_json'], paper['authors_json'])
self.assertIn('attempts', packet)
self.assertTrue((root / 'out/postgresql-preflight.json').is_file())
+160
View File
@@ -0,0 +1,160 @@
"""Real 49-chapter snapshots compared with a separately frozen metadata audit."""
import copy
import gzip
import hashlib
import json
from pathlib import Path
import sqlite3
import tempfile
import unittest
from paper_atlas.collectors.springer_selected import (
BOOK, SOURCE, collect, parse_chapter, parse_toc, read_page, snapshot_index,
)
from paper_atlas.db.store import Store
from paper_atlas.ingestion.records import ingest_file
FIXTURE = Path(__file__).parent/'fixtures/springer-aistats1993-fixture.json.gz'
class SpringerSelectedTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
raw = FIXTURE.read_bytes()
if hashlib.sha256(raw).hexdigest() != '94028fd37080f9d8027e5790e379cc151ee6eab17ed0d1dcc0f59e6e75f9d4d0':
raise AssertionError('Fixed source fixture differs')
cls.fixture = json.loads(gzip.decompress(raw))
def snapshots(self, root):
for group in ['toc','chapters']:
folder = root/group
folder.mkdir()
for name, text in self.fixture[group].items():
(folder/name).write_text(text)
return {'toc_snapshots':root/'toc','chapter_snapshots':root/'chapters'}
def test_all49_original_fields_against_independent_audit(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
report = collect(root/'out', **self.snapshots(root))
manifest, = report['files']
rows = [json.loads(s) for s in Path(manifest['file']).read_text().splitlines()]
self.assertEqual(len(rows), 49)
self.assertEqual(manifest['status'], 'partial')
self.assertEqual(manifest['missing_abstract_count'], 0)
self.assertTrue(manifest['excluded_materials'])
self.assertTrue(all(x['title'] == 'Front Matter' for x in manifest['excluded_materials']))
for row, expected in zip(rows, self.fixture['expected']):
for field in ['title','abstract']:
self.assertEqual(row[field], expected[field])
self.assertEqual(row['identifiers'], {'doi':expected['doi']})
self.assertEqual(row['submission_identifiers'], [{'scheme':'publication_doi','value':expected['doi']}])
self.assertEqual(row['external_id'], expected['doi'])
self.assertEqual(row['metadata']['authors_citation_raw'], expected['authors_citation_raw'])
self.assertEqual(row['authors'], [{'family':s.split(',',1)[0].strip(), 'given':s.split(',',1)[1].strip()} for s in expected['authors_citation_raw']])
self.assertEqual(row['metadata']['first_page'], expected['first_page'])
self.assertEqual(row['metadata']['last_page'], expected['last_page'])
self.assertEqual((row['year'],row['metadata']['bibliographic_year']), (1993,1994))
self.assertEqual(row['track'], 'selected_volume')
self.assertEqual(row['event_key'], 'aistats-1993-selected-volume-1994')
self.assertNotIn('decision_date', row)
self.assertIn('original 1993 review decision not supplied', row['raw_decision'])
for e in [row['metadata']['source_evidence'], *row['metadata']['scope_evidence']]:
self.assertEqual(hashlib.sha256((root/'out'/e['file']).read_bytes()).hexdigest(), e['sha256'])
self.assertFalse(report['database_written'])
with self.assertRaises(FileExistsError):
collect(root/'out', toc_snapshots=root/'toc', chapter_snapshots=root/'chapters')
def test_native_history_namespaces_and_idempotent_import(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
report = collect(root/'out', **self.snapshots(root))
manifest, = report['files']
first = json.loads(Path(manifest['file']).read_text().splitlines()[0])
db = root/'sample.sqlite'
store = Store(db)
pid, rid = store.record('prior_public','earlier','https://example.org/earlier',first['title'],first['abstract'],first['authors'])
store.decision(pid,rid,'AISTATS',1993,'main','earlier','https://example.org/earlier','rejected','original public rejection')
# Explicitly verified work identity; the short title alone must not
# force a merge or attach this publication DOI to the old attempt.
store.conn.execute('INSERT INTO identifiers VALUES(?,?,?)', ('doi',first['identifiers']['doi'],pid))
store.conn.commit()
old = store.conn.execute('SELECT * FROM decisions').fetchall()
store.conn.close()
ingest_file(db, manifest['file'])
ingest_file(db, manifest['file'])
with sqlite3.connect(db) as c:
self.assertEqual(c.execute('SELECT * FROM decisions WHERE status=\'rejected\'').fetchall(), [tuple(r) for r in old])
self.assertEqual(c.execute('SELECT count(*) FROM papers').fetchone()[0], 49)
self.assertEqual(c.execute('SELECT count(*) FROM submissions').fetchone()[0], 50)
self.assertEqual(c.execute('SELECT count(*) FROM decisions').fetchone()[0], 50)
self.assertEqual(c.execute('SELECT e.year,e.track,e.event_key FROM editions e ORDER BY e.id').fetchall(), [(1993,'main',''),(1993,'selected_volume','aistats-1993-selected-volume-1994')])
self.assertEqual(c.execute('SELECT count(*) FROM paper_tags pt JOIN tags t ON t.id=pt.tag_id WHERE t.namespace=?', (SOURCE+':publication_format',)).fetchone()[0], 49)
self.assertEqual(c.execute('SELECT count(*) FROM submission_identifiers WHERE scheme=\'publication_doi\'').fetchone()[0], 49)
self.assertIsNone(c.execute("SELECT 1 FROM submission_identifiers si JOIN submissions s ON s.id=si.submission_id JOIN editions e ON e.id=s.edition_id WHERE e.track='main'").fetchone())
def test_short_unanchored_title_keeps_identity_unconfirmed(self):
with tempfile.TemporaryDirectory() as tmp:
store = Store(Path(tmp)/'identity.sqlite')
title = 'Statistical strategy: step 1'
authors = [{'given':'D. J.','family':'Hand'}]
old, _ = store.record('prior_public','earlier','https://example.org/old',title,'Same abstract',authors)
new, _ = store.record(SOURCE,BOOK+'_1','https://link.springer.com/chapter/'+BOOK+'_1',title,'Same abstract',authors,{'doi':BOOK+'_1'})
self.assertNotEqual(old, new)
self.assertEqual(store.conn.execute('SELECT count(*) FROM match_candidates WHERE method=\'exact_title_unconfirmed\'').fetchone()[0], 1)
store.conn.close()
def test_receipt_hash_path_redirect_and_content_type_guards(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
self.snapshots(root)
index = snapshot_index(root/'chapters')
url = 'https://link.springer.com/chapter/'+BOOK+'_1'
self.assertTrue(read_page(index,url)[0])
for key, value in [('sha256','0'*64),('file','../escape.html'),('final_url','https://example.org/chapter'),('http_status',403),('headers',{'Content-Type':'application/pdf'})]:
changed = copy.deepcopy(index)
changed[url][1][key] = value
with self.subTest(key=key), self.assertRaises((ValueError,KeyError)):
read_page(changed,url)
changed = copy.deepcopy(index)
changed[url][1]['final_url'] = url+'?page=2'
with self.assertRaises(ValueError):
read_page(changed,url)
def test_missing_or_duplicate_snapshot_stops_collection(self):
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
args = self.snapshots(root)
path = root/'chapters/receipts.json'
d = json.loads(path.read_bytes())
original = copy.deepcopy(d)
d['responses'].pop()
path.write_text(json.dumps(d))
with self.assertRaises(ValueError):
collect(root/'out', **args)
self.assertFalse((root/'out').exists())
original['responses'].append(original['responses'][0])
path.write_text(json.dumps(original))
with self.assertRaises(ValueError):
snapshot_index(root/'chapters')
def test_book_doi_year_and_duplicate_citation_fields_reject_drift(self):
html = self.fixture['chapters']['chapter-01.html']
for key, value in [('citation_doi',BOOK+'_2'),('citation_inbook_title','Another Book'),('citation_publication_date','1993')]:
import re
changed, count = re.subn(r'(<meta[^>]*name="'+key+r'"[^>]*content=")[^"]*', lambda m:m[1]+value, html)
self.assertEqual(count, 1)
with self.subTest(key=key), self.assertRaises(ValueError):
parse_chapter(changed, BOOK+'_1')
with self.assertRaises(ValueError):
parse_chapter(html+'<meta name="citation_doi" content="'+BOOK+'_1">', BOOK+'_1')
def test_missing_abstract_is_explicit_and_does_not_use_other_page_text(self):
html = self.fixture['chapters']['chapter-01.html'].replace('id="Abs1-content"','id="not-an-abstract"')
self.assertIsNone(parse_chapter(html, BOOK+'_1')['abstract'])
def test_toc_ignores_frontmatter_but_rejects_duplicate_publications(self):
html = self.fixture['toc']['springer-toc1.html']
self.assertEqual(len(parse_toc(html)), 17)
with self.assertRaises(ValueError):
parse_toc(html+html)