Compare commits

...

6 Commits

Author SHA1 Message Date
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
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
8 changed files with 431 additions and 5 deletions
+10
View File
@@ -52,6 +52,16 @@ jobs:
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','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','acl','--help'],cwd=outside,check=True)
subprocess.run([python,'-I','-B','-m','paper_atlas','collect','springer-selected','--help'],cwd=outside,check=True)
+2
View File
@@ -8,6 +8,8 @@ Paper Atlas 将 arXiv、AI 会议论文集及公开投稿决定整理到可追
当前主动补漏从 **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)。
`collect springer-selected` 核验限定的会后选集,分开保存会议年份、出版年份及选集事件身份。目前支持 AISTATS 1993 对应的 1994 年出版选集,原始会议决定仍须单独核查;配置、证据和恢复见 [会后选集说明](docs/springer-selected-volumes.md)。
+26 -1
View File
@@ -1,6 +1,31 @@
# AAMAS 官方论文集元数据
`paper_atlas.collectors.aamas` 是按需运行的采集适配器,通过统一入口 `collect aamas-proceedings` 使用。支持已独立核验的 **AAMAS 2012、2025** HTML 版式;其他年份需先核对官方目录和逐篇结构,再增加适配,不能仅替换年份。2025 解析单独封装在 `paper_atlas.collectors.aamas2025`。
`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。
+6 -3
View File
@@ -136,6 +136,9 @@ 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)
@@ -193,8 +196,8 @@ def _collect(year, out, *, cache=None, snapshots=None):
def collect(year, out, *, cache=None, snapshots=None):
if year not in (2012, 2025):
raise ValueError('Only independently reviewed AAMAS 2012/2025 layouts are 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:
@@ -204,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, 2025], 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')
+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
Binary file not shown.
+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(2026, 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):