diff --git a/paper_atlas/db/submissions.py b/paper_atlas/db/submissions.py index 2c36ed4..8f2fc84 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'} +PUBLICATIONS={'pmlr','acl_anthology','colm','neurips','mlsys','cvf','ecva','dblp','aamas_proceedings'} def timestamp(value=None): diff --git a/tests/test_aamas_publication_identity.py b/tests/test_aamas_publication_identity.py new file mode 100644 index 0000000..14f2472 --- /dev/null +++ b/tests/test_aamas_publication_identity.py @@ -0,0 +1,89 @@ +"""Cross-source publisher identities must preserve attempts and decision history.""" +import hashlib +import json +from pathlib import Path +import sqlite3 +import tempfile +import unittest + +from paper_atlas.db.submissions import identities_for_row +from paper_atlas.ingestion.records import ingest_file + + +DOI = '10.1234/aamas.identity.example' + + +def row(source, external_id, *, venue='AAMAS', year=2012, track='demo', status='accepted', identifiers=None): + return {'source': source, 'external_id': external_id, + 'url': 'https://example.org/'+source+'/'+external_id, + 'title': 'A shared publication with independently recorded evidence', + 'abstract': 'An independently recorded source abstract.', + 'authors': ['Jane Doe', 'Bob Smith'], + 'identifiers': identifiers if identifiers is not None else {'doi': DOI}, + 'venue': venue, 'year': year, 'track': track, 'status': status, + 'observed_at': '2026-11-09T00:00:00Z', + 'raw_decision': source+' observed '+status, + 'metadata': {'original_source': source}, 'tags': []} + + +def ingest(db, root, record): + path = root/(record['source']+'-'+record['external_id'].replace('/', '-')+'.jsonl') + path.write_text(json.dumps(record)+'\n') + path.with_suffix('.manifest.json').write_text(json.dumps({ + 'source': record['source'], 'parser_version': 1, 'venue': record['venue'], + 'year': record['year'], 'track': record['track'], 'count': 1, + 'list_url': 'https://example.org/'+record['source'], 'status': 'partial', + 'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest(), + 'missing_abstract_count': 0})) + ingest_file(db, path) + + +class AamasPublicationIdentityTests(unittest.TestCase): + def test_only_accepted_known_publisher_rows_supply_publication_doi_identity(self): + record = row('aamas_proceedings', '2012/D1_1', identifiers={'doi': 'https://doi.org/'+DOI.upper()}) + self.assertEqual(identities_for_row(record), [('publication_doi', DOI)]) + for status in ['rejected', 'desk_rejected', 'withdrawn', 'under_review', 'unknown']: + with self.subTest(status=status): + self.assertEqual(identities_for_row({**record, 'status': status}), []) + self.assertEqual(identities_for_row({**record, 'source': 'unverified_source'}), []) + + def test_same_edition_doi_joins_sources_in_both_orders_without_duplicate_attempts(self): + publisher = row('aamas_proceedings', '2012/D1_1') + bibliography = row('dblp', 'conf/atal/Example12') + for order in [(publisher, bibliography), (bibliography, publisher)]: + with self.subTest(first=order[0]['source']), tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + db = root/'sample.sqlite' + for record in order: + ingest(db, root, record) + ingest(db, root, publisher) + with sqlite3.connect(db) as connection: + self.assertEqual(connection.execute('SELECT count(*) FROM papers').fetchone()[0], 1) + self.assertEqual(connection.execute('SELECT count(*) FROM source_records').fetchone()[0], 2) + self.assertEqual(connection.execute('SELECT count(*) FROM submissions').fetchone()[0], 1) + self.assertEqual(connection.execute('SELECT count(*) FROM submission_sources').fetchone()[0], 2) + self.assertEqual(connection.execute('SELECT count(*) FROM decision_sources').fetchone()[0], 2) + self.assertEqual(connection.execute('SELECT scheme,value FROM submission_identifiers').fetchall(), [('publication_doi', DOI)]) + metadata = [json.loads(r[0]) for r in connection.execute('SELECT metadata_json FROM source_records')] + self.assertEqual({r['original_source'] for r in metadata}, {'aamas_proceedings', 'dblp'}) + self.assertEqual(connection.execute('PRAGMA foreign_key_check').fetchall(), []) + + def test_cross_venue_rejection_and_distinct_tracks_remain_separate_attempts(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + db = root/'sample.sqlite' + rejected = row('openreview_snapshot', 'original-forum', venue='AAAI', year=2011, + track='main', status='rejected', identifiers={'arxiv': '1101.00001', 'openreview': 'original-forum'}) + accepted = row('aamas_proceedings', '2012/D1_1', identifiers={'arxiv': '1101.00001', 'doi': DOI}) + another_track = row('aamas_proceedings', '2012/F1_1', track='proceedings_unspecified', identifiers={'doi': DOI}) + for record in [rejected, accepted, another_track]: + ingest(db, root, record) + with sqlite3.connect(db) as connection: + self.assertEqual(connection.execute('SELECT count(*) FROM papers').fetchone()[0], 1) + self.assertEqual(connection.execute('SELECT count(*) FROM submissions').fetchone()[0], 3) + history = connection.execute('''SELECT v.acronym,e.year,e.track,d.status FROM decisions d + JOIN submissions s ON s.id=d.submission_id JOIN editions e ON e.id=s.edition_id + JOIN venues v ON v.id=e.venue_id ORDER BY v.acronym,e.track''').fetchall() + self.assertEqual(set(history), {('AAAI',2011,'main','rejected'),('AAMAS',2012,'demo','accepted'),('AAMAS',2012,'proceedings_unspecified','accepted')}) + self.assertEqual(connection.execute('SELECT count(*) FROM decision_sources').fetchone()[0], 3) + self.assertEqual(connection.execute('PRAGMA foreign_key_check').fetchall(), [])