Preserve new workshop labels and historical edition scope from main
Metadata integrity checks / metadata (push) Successful in 1m45s
Metadata integrity checks / metadata (push) Successful in 1m45s
# Conflicts: # paper_atlas/collectors/dblp_scope.py
This commit is contained in:
@@ -95,6 +95,10 @@ paper-atlas audit public-openreview --db /data/paper-index/papers.sqlite \
|
||||
|
||||
`parser_version=2` 还会将明确负面标签但标题缺失、空白或 null 的记录放入 `held-records.jsonl`,保留原始状态、forum ID、固定来源版本和行号;不制造标题或导入不可识别的论文,也不让这一条缺失阻断其他可用记录。held 文件哈希与各原因数量保存在 inventory;非文本标题、错误身份、冲突届次和哈希不符仍使整批失败。旧年 ICLR 镜像的明确 `Withdraw` 标签归为 `withdrawn`,`Workshop`、`Poster Workshop`、`Oral Workshop` 均不作为拒稿。采集文件与导入主库仍是两个独立步骤。
|
||||
|
||||
`parser_version=3` 增加五组已核实身份的 ICLR workshop 标签:2024 ME-FoMo / GenAI4DM 的 Withdrawn Submission、2025 AgenticAI 的 Reject / LMRL 的 Withdrawn Submission,以及 2026 LLM Reasoning 的 Withdrawn Submission。仅在 `--include-workshops` 下输出,并分别保存 `workshop_me_fomo`、`workshop_genai4dm`、`workshop_agenticai`、`workshop_lmrl`、`workshop_llm_reasoning`;默认仍暂存。标签必须与原文件会议、年份和原始 workshop track 一致,错年/错会议使采集失败,错轨道单独暂存,未知标签继续待核查,不用宽泛的 Reject/Withdraw 正则猜测。
|
||||
|
||||
[OpenReview 官方 ICLR 活动目录](https://openreview.net/venue?id=ICLR.cc) 确认上述独立 workshop 身份;[ME-FoMo 2024](https://sites.google.com/view/me-fomo2024/call-for-papers)、[GenAI4DM 2024](https://sites.google.com/view/genai4dm-iclr2024)、[AgenticAI 2025](https://iclragenticai.github.io/) 和 [LLM Reasoning 2026](https://sites.google.com/view/iclr-2026-llmreasoning) 的活动网站也提供对应投稿入口。输出在 metadata 保存 `workshop_venueid` 和身份目录证据,决定仍只标为固定镜像的公开分类断言,保留原始标签、forum、来源行号与哈希,不声称核验原始决定签名。2026-11-08 核查时部分原站记录页要求浏览器验证;身份目录和主办方入口可读,不代表逐篇原始决定已可访问,也不外推其他 workshop 的公开政策。采集后仍须经过 PostgreSQL 身份/决定冲突预检、串行导入和独立验收。
|
||||
|
||||
该来源明确标记为 `curated_public_status_assertion`:保留源 commit、文件哈希、行号、原标签和 forum ID,不声称已核验原始 PC 签名。只保存标题、摘要、公开作者名、关键词、领域、索引与决定来源,不缓存镜像中的个人资料、邮箱、性别、评审内容或 PDF。所有输出清单均为 partial;盘点中的零条记录仅描述这个固定镜像。
|
||||
|
||||
入库前审计核验文件哈希、数量、轨道和来源声明,并用真实 forum ID 对比同届同轨道已有投稿。存在不同决定时阻止整批导入,交由来源和时序核查;另一会议的后续录用属于独立投稿历史,不构成该次拒稿的冲突。同一 forum 的镜像不新增投稿事件,但论文实体的跨平台去重仍须在全量结束后核验。审计通过的报告包含带 SHA-256 的导入队列;应在单写入维护锁下及时串行导入,主库变化后重新审计。
|
||||
@@ -180,6 +184,10 @@ paper-atlas collect dblp \
|
||||
|
||||
输出按会议/年份/track 分文件,附 manifest、`dblp-normalization.json` 范围审查统计和带哈希的 `dblp-import-queue.json`。不能识别的书目、年份或归属会逐类登记,不默认算作主会。DBLP 不含摘要,因此输出摘要为空、清单为 partial;需要通过有证据的跨来源关联补齐论文实体摘要。书目发表证据与原始审稿决定分别保留,不依据书目缺席推断拒稿。已存在其他出版来源的届次,还须先回填其权威身份并核验镜像记录,不能直接累加投稿数。
|
||||
|
||||
早期 AISTATS 1995/1997/1999/2001/2003/2005 和 COLT 1988–1991 的主系列名称本身含 Workshop,不能当作附属 workshop 漏掉。识别要求已核对的完整 proceedings key、会议书目名、原始标题前缀、届数和事件年一致;轨道名为 `main` 表示主系列,保留原始 Workshop 名称及官方目录证据,不改造其历史名称或出版形式。参见 [AISTATS 官方历史目录](https://aistats.org/other.html)、[COLT 协会目录](https://www.learningtheory.org/)。1995 AISTATS 的会议年与1996图书出版年分开;[PMLR R0](https://proceedings.mlr.press/r0/) 是后来重发的 pre-proceedings,不与 DBLP 图书条目盲目逐篇等同。
|
||||
|
||||
真正附属的 AISTATS 2010 Active Learning and Experimental Design workshop 仍单列。COLT 2003 与 Kernel Workshop 的 [混合论文集](https://doi.org/10.1007/b12006) 缺少逐篇章节信息,使用 `proceedings_unspecified`,不能整卷改成 main 或附属 workshop。输出保存 `track_scope_note` 与 `track_scope_evidence_url`,导入后仍可追查分类理由;这些出版证据不是真实拒稿或原始决定签名,也不自动修正既有主库记录。
|
||||
|
||||
部分早期 SIGIR、CIKM、IJCAI 出版卷使用两位年份键;只有论文集标题明确给出唯一完整年份且与短键相符时才采用,不推测世纪。WWW 特殊兴趣、Alternate track 单列;同卷混合 Applied Data Science、Demo、Industrial 且不能逐篇区分时使用 `proceedings_unspecified`,不把整卷归为某一个具体 track。
|
||||
|
||||
书目中的前后页、目录组合和明确会务标题不产生论文录用记录;缺作者的赞助、委员会、活动汇总等标题单独排除。WWW 2019 companion 的匿名合成活动条目归入待核查材料,原始快照仍保留。范围报告分别统计这些原因;单纯缺作者并不排除真实研究论文,也不补造作者。
|
||||
|
||||
@@ -92,6 +92,8 @@ def normalize_row(row,volumes,targets,include_workshops=False,through_year=2026)
|
||||
metadata['joint_edition_evidence'] = edition['joint_edition_evidence']
|
||||
if edition.get('track_scope_note'):
|
||||
metadata['track_scope_note']=edition['track_scope_note']
|
||||
if edition.get('track_scope_evidence_url'):
|
||||
metadata['track_scope_evidence_url']=edition['track_scope_evidence_url']
|
||||
flagged=bool(re.search(r'\b(?:withdrawn|retracted)\b',row.get('publtype') or '',re.I))
|
||||
result={'source':'dblp','external_id':row['key'],'url':'https://dblp.org/rec/'+row['key']+'.html',
|
||||
'title':title,'abstract':None,'authors':authors,'identifiers':own,
|
||||
|
||||
@@ -38,6 +38,39 @@ BOOKS = {
|
||||
}
|
||||
|
||||
|
||||
# These are the historical primary series themselves, not co-located workshops.
|
||||
# Exact proceedings keys, original names, ordinals and event years must agree.
|
||||
HISTORICAL_WORKSHOP_TITLES = {
|
||||
**{('aistats', year): prefix + ordinal + ' International Workshop on Artificial Intelligence and Statistics'
|
||||
for year, ordinal, prefix in [
|
||||
(1995, 'Fifth', 'Learning from Data - '),
|
||||
(1997, 'Sixth', 'Proceedings of the '),
|
||||
(1999, 'Seventh', 'Proceedings of the '),
|
||||
(2001, 'Eighth', 'Proceedings of the '),
|
||||
(2003, 'Ninth', 'Proceedings of the '),
|
||||
(2005, 'Tenth', 'Proceedings of the ')]},
|
||||
**{('colt', year): 'Proceedings of the ' + ordinal + ' Annual Workshop on Computational Learning Theory'
|
||||
for year, ordinal in [(1988, 'First'), (1989, 'Second'), (1990, 'Third'), (1991, 'Fourth')]},
|
||||
}
|
||||
|
||||
|
||||
def historical_track(family, base, suffix, volume_key, year, title):
|
||||
if suffix or base.casefold() != family or volume_key != f'conf/{family}/{year}':
|
||||
return None
|
||||
prefix = HISTORICAL_WORKSHOP_TITLES.get((family, year))
|
||||
if (prefix and title.casefold().startswith(prefix.casefold() + ',')
|
||||
and re.search(r'\b' + str(year) + r'\b', title)):
|
||||
url = 'https://aistats.org/other.html' if family == 'aistats' else 'https://www.learningtheory.org/'
|
||||
return ('main', 'Historical primary series named Workshop; exact proceedings key, ordinal, title and event year verified.', url)
|
||||
if (family == 'colt' and year == 2003
|
||||
and title.startswith('Computational Learning Theory and Kernel Machines, 16th Annual Conference on Computational Learning Theory and 7th Kernel Workshop, COLT/Kernel 2003,')
|
||||
and title.endswith('Proceedings')):
|
||||
return ('proceedings_unspecified',
|
||||
'Combined COLT 2003 and Kernel Workshop volume; per-paper section absent from DBLP XML. Do not infer a main or workshop track.',
|
||||
'https://doi.org/10.1007/b12006')
|
||||
return None
|
||||
|
||||
|
||||
def scope(row,volume,through_year=2026):
|
||||
fields=row['fields'];books=fields.get('booktitle',[])
|
||||
if len(books)!=1:return None,'ambiguous_booktitle'
|
||||
@@ -75,7 +108,9 @@ def scope(row,volume,through_year=2026):
|
||||
if len(bibliographic)!=1 or not re.fullmatch(r'\d{4}',bibliographic[0]):return None,'ambiguous_bibliographic_year'
|
||||
if int(bibliographic[0])!=year and not re.search(r'\b'+str(year)+r'\b',title):return None,'unverified_year_difference'
|
||||
text=(suffix+' '+title).casefold();track='main'
|
||||
if 'findings' in text:track='findings'
|
||||
historical = historical_track(family, base, suffix, volume['key'], year, title)
|
||||
if historical:track=historical[0]
|
||||
elif 'findings' in text:track='findings'
|
||||
elif 'shared task' in text:track='shared_task'
|
||||
elif 'student research' in text or 'workshop' in text or 'wokshop' in text or base in ('ICCVW','EWSL'):track='workshop'
|
||||
elif ('demo track' in text and ('industrial track' in text or 'applied data science track' in text)):track='proceedings_unspecified'
|
||||
@@ -105,6 +140,9 @@ def scope(row,volume,through_year=2026):
|
||||
'rule':family+':'+base,'proceedings_title':title,'edition_year_basis':year_basis}
|
||||
if joint_evidence:
|
||||
result['joint_edition_evidence'] = joint_evidence
|
||||
if historical:
|
||||
result['track_scope_note'] = historical[1]
|
||||
result['track_scope_evidence_url'] = historical[2]
|
||||
if venue=='MM' and track=='main':
|
||||
# A main MM volume also includes doctoral, tutorials, companion papers
|
||||
# and other sections. DBLP XML omits the per-paper TOC section.
|
||||
|
||||
@@ -32,6 +32,20 @@ TRACKS = {'main': 'main', 'Position': 'position',
|
||||
'Datasets & Benchmarks': 'datasets_benchmarks'}
|
||||
WORKSHOP_REJECT_LABELS = {"aim-fm workshop @ neurips'24 reject",
|
||||
'coginterp @ neurips 2025 reject'}
|
||||
# Exact curated labels, tied to the year and raw workshop track. The official
|
||||
# venue directory establishes workshop identity, not an original decision signature.
|
||||
ICLR_WORKSHOP_LABELS = {
|
||||
'iclr 2024 workshop mefomo withdrawn submission':
|
||||
(2024, 'workshop_me_fomo', 'withdrawn', 'ME-FoMo'),
|
||||
'iclr 2024 workshop genai4dm withdrawn submission':
|
||||
(2024, 'workshop_genai4dm', 'withdrawn', 'GenAI4DM'),
|
||||
'iclr 2025 workshop agenticai reject':
|
||||
(2025, 'workshop_agenticai', 'rejected', 'AgenticAI'),
|
||||
'iclr 2025 workshop lmrl withdrawn submission':
|
||||
(2025, 'workshop_lmrl', 'withdrawn', 'LMRL'),
|
||||
'iclr 2026 workshop llm reasoning withdrawn submission':
|
||||
(2026, 'workshop_llm_reasoning', 'withdrawn', 'LLM_Reasoning'),
|
||||
}
|
||||
|
||||
|
||||
def download(url, limit):
|
||||
@@ -63,6 +77,20 @@ def normalize(row, family, year, provenance, include_workshops=False):
|
||||
"""Return a normalized assertion or a reason requiring separate review."""
|
||||
raw_status = row.get('status', '')
|
||||
status = negative_status(raw_status)
|
||||
workshop = ICLR_WORKSHOP_LABELS.get(' '.join(raw_status.casefold().split()))
|
||||
workshop_metadata = {}
|
||||
if workshop:
|
||||
expected_year, expected_track, status, group_name = workshop
|
||||
if family != 'iclr' or year != expected_year:
|
||||
raise ValueError('Workshop status label conflicts with source conference year')
|
||||
if row.get('track') != expected_track:
|
||||
return None, 'conflicting_workshop_track'
|
||||
if not include_workshops:
|
||||
return None, 'separate_workshop'
|
||||
workshop_metadata = {
|
||||
'workshop_venueid': f'ICLR.cc/{year}/Workshop/{group_name}',
|
||||
'workshop_identity_evidence_url': 'https://openreview.net/venue?id=ICLR.cc',
|
||||
}
|
||||
if status is None:
|
||||
if re.search(r'\bretract\w*\b', raw_status, re.I):
|
||||
return None, 'retracted'
|
||||
@@ -118,7 +146,8 @@ def normalize(row, family, year, provenance, include_workshops=False):
|
||||
'venue': venue, 'year': year, 'track': normalized_track,
|
||||
'status': status, 'raw_decision': raw_status, 'evidence_url': evidence,
|
||||
'observed_at': AS_OF, 'tags': tags,
|
||||
'metadata': {**provenance, 'raw_track': track, 'raw_status': raw_status,
|
||||
'metadata': {**provenance, **workshop_metadata,
|
||||
'raw_track': track, 'raw_status': raw_status,
|
||||
'evidence_kind': 'curated_public_status_assertion',
|
||||
'original_decision_signature_verified': False,
|
||||
'source_snapshot_upper_bound': AS_OF}}, None
|
||||
@@ -217,7 +246,7 @@ def _collect_files(out, selected, include_workshops, fetcher):
|
||||
name = f'public-openreview-{family}-{year}-{track}.jsonl'
|
||||
path = stage / name
|
||||
path.write_text(''.join(json.dumps(row, ensure_ascii=False) + '\n' for row in records))
|
||||
manifest = {'source': SOURCE, 'parser_version': 2, 'venue': records[0]['venue'],
|
||||
manifest = {'source': SOURCE, 'parser_version': 3, 'venue': records[0]['venue'],
|
||||
'year': year, 'track': track, 'count': len(records), 'status': 'partial',
|
||||
'file': str(out / name), 'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
|
||||
'list_url': REPOSITORY + '/tree/' + REVISION, 'source_revision': REVISION,
|
||||
|
||||
@@ -10,7 +10,7 @@ from unittest.mock import patch
|
||||
|
||||
from paper_atlas.collectors.dblp import XML_SHA
|
||||
from paper_atlas.collectors.dblp_records import collect,identifiers,normalize_row,material_reason
|
||||
from paper_atlas.collectors.dblp_scope import scope
|
||||
from paper_atlas.collectors.dblp_scope import scope, HISTORICAL_WORKSHOP_TITLES
|
||||
from paper_atlas.db.store import Store
|
||||
from paper_atlas.ingestion.records import ingest_file
|
||||
|
||||
@@ -70,6 +70,69 @@ class DblpRecordTests(unittest.TestCase):
|
||||
self.assertEqual(scope(self.row(),self.volume('kdd',2024))[1],'crossref_venue_conflict')
|
||||
self.assertEqual(scope(self.row(year=2027),self.volume('aaai',2027))[1],'future_edition')
|
||||
|
||||
def test_historical_primary_series_workshop_names_require_exact_editions(self):
|
||||
self.assertEqual(set(HISTORICAL_WORKSHOP_TITLES),
|
||||
{('aistats',year) for year in [1995,1997,1999,2001,2003,2005]} |
|
||||
{('colt',year) for year in [1988,1989,1990,1991]})
|
||||
for (family,year),prefix in HISTORICAL_WORKSHOP_TITLES.items():
|
||||
with self.subTest(family=family,year=year):
|
||||
title=prefix+f', {family.upper()} {year}, Proceedings.'
|
||||
volume=self.volume(family,year,title)
|
||||
row=self.row(family,family.upper(),year)
|
||||
if year==1995:
|
||||
row['fields']['year']=['1996'];volume['fields']['year']=['1996']
|
||||
edition,reason=scope(row,volume)
|
||||
self.assertIsNone(reason);self.assertEqual(edition['track'],'main')
|
||||
self.assertEqual(edition['year'],year)
|
||||
self.assertIn('Historical primary series',edition['track_scope_note'])
|
||||
record,reason=normalize_row(row,{volume['key']:volume},{family.upper()})
|
||||
self.assertIsNone(reason);self.assertEqual(record['metadata']['track_scope_evidence_url'],edition['track_scope_evidence_url'])
|
||||
self.assertEqual(record['metadata']['proceedings_title'],title)
|
||||
if year==1995:self.assertEqual(record['metadata']['bibliographic_year'],1996)
|
||||
|
||||
def test_historical_exception_does_not_promote_affiliated_or_wrong_scope_workshops(self):
|
||||
title=HISTORICAL_WORKSHOP_TITLES[('aistats',1995)]+', AISTATS 1995, Proceedings.'
|
||||
for book,key,changed in [
|
||||
('AISTATS','conf/aistats/1995w',title),
|
||||
('AISTATS (Workshops)','conf/aistats/1995',title),
|
||||
('AISTATS','conf/aistats/1995',title.replace('Fifth','Sixth')),
|
||||
('AISTATS','conf/aistats/1995',title.replace('1995','1997'))]:
|
||||
volume=self.volume('aistats',1995,changed);volume['key']=key
|
||||
row=self.row('aistats',book,1995);row['fields']['crossref']=[key]
|
||||
self.assertEqual(scope(row,volume)[0]['track'],'workshop')
|
||||
volume=self.volume('aistats',2010,'Active Learning and Experimental Design workshop, In conjunction with AISTATS 2010')
|
||||
volume['key']='conf/aistats/2010w'
|
||||
row=self.row('aistats','AISTATS',2011);row['fields']['crossref']=[volume['key']]
|
||||
self.assertEqual(scope(row,volume)[0]['track'],'workshop')
|
||||
|
||||
def test_combined_colt_kernel_volume_keeps_unknown_per_paper_track(self):
|
||||
title='Computational Learning Theory and Kernel Machines, 16th Annual Conference on Computational Learning Theory and 7th Kernel Workshop, COLT/Kernel 2003, Washington, DC, USA, August 24-27, 2003, Proceedings'
|
||||
volume=self.volume('colt',2003,title);row=self.row('colt','COLT',2003)
|
||||
record,reason=normalize_row(row,{volume['key']:volume},{'COLT'})
|
||||
self.assertIsNone(reason);self.assertEqual(record['track'],'proceedings_unspecified')
|
||||
self.assertEqual(record['status'],'accepted')
|
||||
self.assertIn('per-paper section absent',record['metadata']['track_scope_note'])
|
||||
self.assertEqual(record['metadata']['track_scope_evidence_url'],'https://doi.org/10.1007/b12006')
|
||||
volume['key']='conf/colt/2003w';row['fields']['crossref']=[volume['key']]
|
||||
self.assertEqual(scope(row,volume)[0]['track'],'workshop')
|
||||
|
||||
def test_historical_scope_provenance_survives_file_ingestion(self):
|
||||
title=HISTORICAL_WORKSHOP_TITLES[('colt',1988)]+', COLT 1988, Proceedings.'
|
||||
row=self.row('colt','COLT',1988);volume=self.volume('colt',1988,title)
|
||||
record,reason=normalize_row(row,{volume['key']:volume},{'COLT'})
|
||||
self.assertIsNone(reason)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root=Path(tmp);path=root/'history.jsonl'
|
||||
path.write_text(json.dumps(record)+'\n')
|
||||
path.with_suffix('.manifest.json').write_text(json.dumps({'source':'dblp','count':1,'list_url':'https://doi.org/10.4230/dblp.xml.2026-11-01','file_sha256':hashlib.sha256(path.read_bytes()).hexdigest()}))
|
||||
ingest_file(root/'sample.sqlite',path)
|
||||
with closing(sqlite3.connect(root/'sample.sqlite')) as connection:
|
||||
track=connection.execute('SELECT track FROM editions').fetchone()[0]
|
||||
metadata=json.loads(connection.execute('SELECT metadata_json FROM source_records').fetchone()[0])
|
||||
self.assertEqual(track,'main')
|
||||
self.assertEqual(metadata['track_scope_evidence_url'],'https://www.learningtheory.org/')
|
||||
self.assertEqual(metadata['proceedings_title'],title)
|
||||
|
||||
def test_only_own_identifier_links_are_used_and_ambiguity_is_preserved(self):
|
||||
own,ambiguous=identifiers({'ee':['https://doi.org/10.1234/published','https://doi.org/10.48550/arXiv.2401.00001','https://openreview.net/forum?id=realForum'],
|
||||
'cite':['https://doi.org/10.9999/reference']})
|
||||
|
||||
@@ -125,7 +125,7 @@ class PublicOpenReviewTests(unittest.TestCase):
|
||||
self.assertNotIn('private@example.org', held_path.read_text())
|
||||
self.assertNotIn('private review', held_path.read_text())
|
||||
manifest = result['files'][0]
|
||||
self.assertEqual((manifest['parser_version'], manifest['status']), (2, 'partial'))
|
||||
self.assertEqual((manifest['parser_version'], manifest['status']), (3, 'partial'))
|
||||
rows = [json.loads(line) for line in Path(manifest['file']).read_text().splitlines()]
|
||||
self.assertEqual([(row['identifiers']['openreview'], row['status']) for row in rows],
|
||||
[('ValidReject', 'rejected'), ('Withdraw01', 'withdrawn')])
|
||||
@@ -140,6 +140,81 @@ class PublicOpenReviewTests(unittest.TestCase):
|
||||
self.assertFalse((Path(tmp) / 'public-openreview-inventory.json').exists())
|
||||
self.assertEqual(list(Path(tmp).glob('*.jsonl')), [])
|
||||
|
||||
def workshop_cases(self):
|
||||
return [
|
||||
(2024, 'workshop_me_fomo', 'ICLR 2024 Workshop MEFoMo Withdrawn Submission', 'withdrawn', 'ME-FoMo'),
|
||||
(2024, 'workshop_genai4dm', 'ICLR 2024 Workshop GenAI4DM Withdrawn Submission', 'withdrawn', 'GenAI4DM'),
|
||||
(2025, 'workshop_agenticai', 'ICLR 2025 Workshop AgenticAI Reject', 'rejected', 'AgenticAI'),
|
||||
(2025, 'workshop_lmrl', 'ICLR 2025 Workshop LMRL Withdrawn Submission', 'withdrawn', 'LMRL'),
|
||||
(2026, 'workshop_llm_reasoning', 'ICLR 2026 Workshop LLM Reasoning Withdrawn Submission', 'withdrawn', 'LLM_Reasoning'),
|
||||
]
|
||||
|
||||
def test_iclr_workshop_labels_require_opt_in_and_retain_identity_evidence(self):
|
||||
for year, track, label, status, group in self.workshop_cases():
|
||||
with self.subTest(label=label):
|
||||
raw = self.row(track=track, status=label)
|
||||
self.assertEqual(normalize(raw, 'iclr', year, self.provenance()),
|
||||
(None, 'separate_workshop'))
|
||||
row, reason = normalize(raw, 'iclr', year, self.provenance(), True)
|
||||
self.assertIsNone(reason)
|
||||
self.assertEqual((row['venue'], row['year'], row['track'], row['status']),
|
||||
('ICLR', year, track, status))
|
||||
self.assertEqual(row['raw_decision'], label)
|
||||
self.assertEqual(row['metadata']['workshop_venueid'],
|
||||
f'ICLR.cc/{year}/Workshop/{group}')
|
||||
self.assertFalse(row['metadata']['original_decision_signature_verified'])
|
||||
self.assertEqual(row['evidence_url'], 'https://example.org/pinned.jsonl#L7')
|
||||
self.assertNotIn('private review', json.dumps(row))
|
||||
|
||||
def test_workshop_label_scope_conflicts_never_become_main_events(self):
|
||||
for year, track, label, _, _ in self.workshop_cases():
|
||||
for enabled in (False, True):
|
||||
for wrong_track in ('main', 'workshop_other'):
|
||||
self.assertEqual(normalize(self.row(track=wrong_track, status=label),
|
||||
'iclr', year, self.provenance(), enabled),
|
||||
(None, 'conflicting_workshop_track'))
|
||||
for family, wrong_year in [('icml', year), ('iclr', year + 1)]:
|
||||
with self.assertRaisesRegex(ValueError, 'conference year'):
|
||||
normalize(self.row(track=track, status=label), family,
|
||||
wrong_year, self.provenance(), enabled)
|
||||
row, reason = normalize(self.row(track='workshop_unknown',
|
||||
status='ICLR 2026 Workshop Unknown Reject'),
|
||||
'iclr', 2026, self.provenance(), True)
|
||||
self.assertIsNone(row)
|
||||
self.assertEqual(reason, 'unmapped_negative_label')
|
||||
|
||||
def test_workshop_collection_and_import_keep_five_tracks_separate_from_main(self):
|
||||
files = {}
|
||||
for index, (year, track, label, _, _) in enumerate(self.workshop_cases()):
|
||||
files.setdefault(f'venues/iclr/iclr{year}.jsonl', []).append(
|
||||
self.row(id=f'Workshop{index:02}', track=track, status=label))
|
||||
files['venues/iclr/iclr2024.jsonl'].append(self.row())
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
default = self.collect(root / 'default', files, families=['iclr'])
|
||||
self.assertEqual(default['normalized_records'], 1)
|
||||
held = [json.loads(line) for line in Path(default['held_file']).read_bytes().split(b'\n') if line]
|
||||
self.assertEqual(len(held), 5)
|
||||
self.assertTrue(all(row['reason'] == 'separate_workshop' for row in held))
|
||||
selected = self.collect(root / 'selected', files, families=['iclr'], include_workshops=True)
|
||||
self.assertEqual(selected['normalized_records'], 6)
|
||||
self.assertEqual(len(selected['files']), 6)
|
||||
self.assertEqual(Path(selected['held_file']).read_bytes(), b'')
|
||||
db = root / 'db.sqlite'
|
||||
for manifest in selected['files']:
|
||||
self.assertEqual(manifest['parser_version'], 3)
|
||||
ingest_file(db, manifest['file'])
|
||||
store = Store(db)
|
||||
try:
|
||||
scopes = set(store.conn.execute('SELECT e.year,e.track,d.status FROM submissions s '
|
||||
'JOIN editions e ON e.id=s.edition_id '
|
||||
'JOIN decisions d ON d.submission_id=s.id'))
|
||||
expected = {(year, track, status) for year, track, _, status, _ in self.workshop_cases()}
|
||||
self.assertEqual(scopes, expected | {(2024, 'main', 'rejected')})
|
||||
self.assertEqual(store.conn.execute('PRAGMA foreign_key_check').fetchall(), [])
|
||||
finally:
|
||||
store.conn.close()
|
||||
|
||||
def test_mirror_reimport_preserves_one_attempt_and_later_acceptance_elsewhere(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user