diff --git a/docs/data-sources.md b/docs/data-sources.md index f26758d..0bb6733 100644 --- a/docs/data-sources.md +++ b/docs/data-sources.md @@ -342,3 +342,9 @@ ICML 2024 起的一个 PMLR 卷可以同时包含研究主轨道与 Position Pap 2024 年的官方节目单把两轨都放在 `Conference`,但提供 PMLR 论文自链接。该年仅采用组织方在 [2025 Position CFP](https://icml.cc/Conferences/2025/CallForPositionPapers) 明确链接的 [2024 Position 论文标题筛选](https://icml.cc/virtual/2024/papers.html?filter=titles&search=position%3A) 规则;同时保存规则页面的哈希。这个规则不用于其他会议或年份。官方节目单将 `guyard24a` 链接误写为 `theo24a`:只对这条已核查的映射额外要求标题精确一致、至少两名公开作者重合,不进行全库模糊匹配。 导入与身份回填拒绝 ICML 2024 起 parser_version<3 或没有官方轨道证据的旧文件。已有主库中的错误轨道需要在独占维护锁内迁移原投稿事件、保留决定与来源关联;仅重新导入拆分文件会留下旧的主轨道事件,不能作为修复方式。完成迁移前,不把新文件直接导入现有库。 + +### RSS 发表身份 + +已核验的 `rss_proceedings` 录用记录与其他出版社来源一样,将 DOI 作为届次范围内的 `publication_doi` 投稿身份。这样同一 RSS 出版物先由官方论文集、后由 DBLP 导入(或反向)时可以共享一次投稿,并保留各自来源和决定观察。此规则不把节目显示编号当原始 OpenReview 投稿号,不把拒稿/撤稿升级为录用,也不跨届次合并历史。 + +这不证明任意 DOI 的科学作品归属:导入前仍需核查原始标题、完整作者、版本和已有标识冲突。变更只作用于后续导入;不会自动回填、合并或改写旧 RSS 记录,旧记录需要独立审查计划。 diff --git a/paper_atlas/db/submissions.py b/paper_atlas/db/submissions.py index 8f2fc84..6982e10 100644 --- a/paper_atlas/db/submissions.py +++ b/paper_atlas/db/submissions.py @@ -4,7 +4,7 @@ import re from paper_atlas.io.publication_ids import mlsys_id -PUBLICATIONS={'pmlr','acl_anthology','colm','neurips','mlsys','cvf','ecva','dblp','aamas_proceedings'} +PUBLICATIONS={'pmlr','acl_anthology','colm','neurips','mlsys','cvf','ecva','dblp','aamas_proceedings','rss_proceedings'} def timestamp(value=None): diff --git a/tests/test_rss_publication_identity.py b/tests/test_rss_publication_identity.py new file mode 100644 index 0000000..f475407 --- /dev/null +++ b/tests/test_rss_publication_identity.py @@ -0,0 +1,59 @@ +"""RSS publisher/DBLP observations share one event without losing old decisions.""" +from pathlib import Path +import json +import sqlite3 +import tempfile +import unittest +from paper_atlas.db.submissions import identities_for_row +from tests.test_aamas_publication_identity import row, ingest + +DOI='10.15607/rss.2026.xxii.041' + +def rss(source,external,**kwargs): + return row(source,external,venue='RSS',year=2026,track='main', + identifiers=kwargs.pop('identifiers',{'doi':DOI}),**kwargs) + +class RssPublicationIdentityTests(unittest.TestCase): + def test_only_verified_accepted_source_adds_publication_identity(self): + r=rss('rss_proceedings','2026/p041',identifiers={'doi':'https://doi.org/'+DOI.upper()}) + self.assertEqual(identities_for_row(r),[('publication_doi',DOI)]) + for status in ('rejected','desk_rejected','withdrawn','under_review','unknown'): + self.assertEqual(identities_for_row({**r,'status':status}),[]) + self.assertEqual(identities_for_row({**r,'source':'unverified_rss_copy'}),[]) + self.assertEqual(identities_for_row({**r,'identifiers':{'doi':'not-a-doi'}}),[]) + + def test_publisher_and_bibliography_both_orders_preserve_two_observations(self): + a=rss('rss_proceedings','2026/p041');b=rss('dblp','conf/rss/Example26') + for pair in ((a,b),(b,a)): + with self.subTest(first=pair[0]['source']),tempfile.TemporaryDirectory() as tmp: + root=Path(tmp);db=root/'papers.sqlite' + for r in pair:ingest(db,root,r) + ingest(db,root,a) + with sqlite3.connect(db) as c: + self.assertEqual(c.execute('SELECT count(*) FROM papers').fetchone()[0],1) + self.assertEqual(c.execute('SELECT count(*) FROM submissions').fetchone()[0],1) + self.assertEqual(c.execute('SELECT count(*) FROM source_records').fetchone()[0],2) + self.assertEqual(c.execute('SELECT count(*) FROM submission_sources').fetchone()[0],2) + self.assertEqual(c.execute('SELECT count(*) FROM decisions').fetchone()[0],2) + self.assertEqual(c.execute('SELECT count(*) FROM decision_sources').fetchone()[0],2) + self.assertEqual(c.execute('SELECT scheme,value FROM submission_identifiers').fetchall(),[('publication_doi',DOI)]) + metadata=[json.loads(r[0]) for r in c.execute('SELECT metadata_json FROM source_records')] + self.assertEqual({x['original_source'] for x in metadata},{'rss_proceedings','dblp'}) + self.assertEqual(c.execute('PRAGMA foreign_key_check').fetchall(),[]) + + def test_prior_rejection_remains_independent_and_raw_evidence_unchanged(self): + with tempfile.TemporaryDirectory() as tmp: + root=Path(tmp);db=root/'papers.sqlite' + old=row('openreview_snapshot','old-forum',venue='ICLR',year=2025,track='main', + status='rejected',identifiers={'arxiv':'2501.00001','openreview':'old-forum'}) + ingest(db,root,old) + with sqlite3.connect(db) as c: + before=c.execute('SELECT * FROM decisions').fetchall() + new=rss('rss_proceedings','2026/p041',identifiers={'arxiv':'2501.00001','doi':DOI}) + ingest(db,root,new) + with sqlite3.connect(db) as c: + self.assertEqual(c.execute('SELECT * FROM decisions WHERE status=\'rejected\'').fetchall(),before) + self.assertEqual(c.execute('SELECT count(*) FROM submissions').fetchone()[0],2) + self.assertEqual(c.execute('SELECT count(*) FROM papers').fetchone()[0],1) + self.assertEqual(c.execute('SELECT count(*) FROM decisions WHERE status=\'accepted\'').fetchone()[0],1) + self.assertEqual(c.execute('PRAGMA foreign_key_check').fetchall(),[])