Compare commits

...

1 Commits

Author SHA1 Message Date
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
8 changed files with 476 additions and 0 deletions
+10
View File
@@ -50,6 +50,16 @@ 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)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','iclr-archive','--help'],cwd=outside,check=True)
+2
View File
@@ -68,6 +68,8 @@ 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 时会明确提示,避免静默遗漏。
+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 年起。期刊/展示关系、网页独有条目、摘要和公开负面决定仍有待办;冻结资料和代码适配不代表完整会议覆盖。
+1
View File
@@ -26,6 +26,7 @@ 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 的原始候选'),
+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))
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"
}
]
}
+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'])