From 16f428cae9230729c05522bab07ba999ad6cf5db Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Fri, 6 Nov 2026 17:01:40 +0800 Subject: [PATCH] Report every target venue and expose missing historical coverage --- README.md | 11 ++++++++++- coverage_report.py | 41 +++++++++++++++++++++++++++++++++++++++++ test_coverage_report.py | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 coverage_report.py create mode 100644 test_coverage_report.py diff --git a/README.md b/README.md index 498ed4f..02d83d0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Paper Atlas 是一个**长期维护、按需或按批次运行的研究数据工 目标包括:完整历史元数据导入、逐年会议目录、跨平台论文去重、公开拒稿/撤稿信息、平台标签与自定义标签隔离、可恢复更新和覆盖率报告。 -**目前仍在开发,不能视为完整数据集。** 已实现 SQLite 数据结构、arXiv 元数据 ZIP 流式导入、断点恢复、跨平台标识合并及审计、PMLR、COLM、NeurIPS、ACL Anthology、ICLR 官方公开程序、CVF/ECVA 视觉会议元数据采集、带清单核验的会议导入和只读查询。PeerRead 提供公开 ICLR 2017 明确决定的历史补充;近期 ICLR 投稿与决定快照解析器正在验证。arXiv 元数据 ZIP 已完整下载(1,878,831,586 字节),主批次已读至 EOF 并完成 3,233,620 条源记录的导入;这不是去重后的论文数,最终全库审计尚未完成。其他会议适配器、历史缺失届次、完整投稿历史、跨平台疑似重复复核和覆盖率核验仍待完成。回归检查在远端执行,已配置 ces-control:host 对应的 CI 工作流,实际 CI 结果以 Gitea Actions 为准。`venues.json` 的 41 个会议是候选覆盖范围,不代表已完成采集,也不是与 COLM 比较的客观排名。 +**目前仍在开发,不能视为完整数据集。** 已实现 SQLite 数据结构、arXiv 元数据 ZIP 流式导入、断点恢复、跨平台标识合并及审计、PMLR、COLM、NeurIPS、ACL Anthology、ICLR 官方公开程序、CVF/ECVA 视觉会议元数据采集、带清单核验的会议导入和只读查询。PeerRead 提供公开 ICLR 2017 明确决定的历史补充;近期 ICLR 公开决定快照已解析,实际主库进度以运行日志与统计为准。arXiv 元数据 ZIP 已完整下载(1,878,831,586 字节),主批次读取 3,233,620 行至 EOF;主库有 3,233,527 条 arXiv 来源记录,93 行差额仍需按原始 ID 独立核验。这些不是去重后的全库论文数,最终全库审计尚未完成。其他会议适配器、历史缺失届次、完整投稿历史、跨平台疑似重复复核和覆盖率核验仍待完成。回归检查在远端执行,已配置 ces-control:host 对应的 CI 工作流,实际 CI 结果以 Gitea Actions 为准。`venues.json` 的 41 个会议是候选覆盖范围,不代表已完成采集,也不是与 COLM 比较的客观排名。 ## 数据范围与来源 @@ -100,6 +100,15 @@ python3 -B query.py --db /data/paper-index/papers.sqlite --venue COLM --year 202 查询连接使用 SQLite 只读模式,结果输出 JSON。`--stats` 中 `papers` 是按当前规则去重后的论文实体数,`records_by_source` 是各平台已入库记录数,`decision_events_by_status` 是决定事件数。采集文件 manifest 的 `count` 是已采集数量,不能等同已入库数量,也不能把各平台记录数相加当成独立论文数。 +全部目标会议的覆盖报告会保留尚无入库记录的项目,避免只展示已经做完的会议: + +```sh +python3 -B coverage_report.py --db /data/paper-index/papers.sqlite \ + --out /data/paper-index/collected/venue-coverage-current.json +``` + +报告按会议、年份和 track 列出投稿数、独立论文数及来源清单状态,包含联合会议别名。没有数据的目标明确标为 no_imported_records;已有数据也保持“历史待独立核验”,不会因单个文件导入通过就宣称历年覆盖完成。同一论文可以出现于多个会议或年份,各会议论文数也不能直接相加作为全库数量。 + 用 `--paper 531782` 可查看单篇论文的所有已知来源、平台标签、投稿届次及带证据的决定历史。会议筛选不区分大小写,状态筛选使用该次投稿的最新已知决定。当前没有公共 Web 页面或网络查询服务。 ## 官方会议元数据采集 diff --git a/coverage_report.py b/coverage_report.py new file mode 100644 index 0000000..11cdbac --- /dev/null +++ b/coverage_report.py @@ -0,0 +1,41 @@ +"""Show every target venue, including those with no imported records.""" +import argparse +import json +from pathlib import Path +import sqlite3 + +from submission_evidence import timestamp + + +def report(db,registry): + registry=json.loads(Path(registry).read_text());targets=registry['venues'] + names=[row['acronym'].casefold() for row in targets] + if len(names)!=len(set(names)):raise ValueError('Duplicate target venue') + scope='(v.acronym=? COLLATE NOCASE OR EXISTS(SELECT 1 FROM venue_aliases a WHERE a.venue_id=v.id AND a.alias=? COLLATE NOCASE))' + c=sqlite3.connect(Path(db).resolve().as_uri()+'?mode=ro',uri=True);c.row_factory=sqlite3.Row + try: + c.execute('BEGIN');venues=[] + for target in targets: + acronym=target['acronym'];params=(acronym,acronym) + editions=[] + for row in c.execute('SELECT e.id,v.acronym AS canonical_venue,e.year,e.track,count(s.id) AS submissions,count(DISTINCT s.paper_id) AS papers FROM editions e JOIN venues v ON v.id=e.venue_id LEFT JOIN submissions s ON s.edition_id=e.id WHERE '+scope+' GROUP BY e.id ORDER BY e.year,e.track',params): + edition=dict(row) + edition['sources']=[dict(source) for source in c.execute('SELECT so.name,es.status,es.expected_count,es.imported_count,es.list_url,es.last_success_at,es.last_error FROM edition_sources es JOIN sources so ON so.id=es.source_id WHERE es.edition_id=? ORDER BY so.name',(row['id'],))] + editions.append(edition) + totals=c.execute('SELECT count(s.id),count(DISTINCT s.paper_id) FROM submissions s JOIN editions e ON e.id=s.edition_id JOIN venues v ON v.id=e.venue_id WHERE '+scope,params).fetchone() + venues.append({'acronym':acronym,'field':target['field'],'imported_submissions':totals[0],'distinct_papers':totals[1], + 'history_status':'partial_history_pending_independent_audit' if totals[0] else 'no_imported_records', + 'discovery_sources':target.get('discovery_sources',[]),'editions':editions}) + return {'checked_at':timestamp(),'target_venue_count':len(targets),'venues_with_imported_records':sum(bool(v['imported_submissions']) for v in venues), + 'scope_note':registry['scope_note'],'interpretation':'All registry targets are listed. Edition source complete means that source file passed import checks; it does not prove full historical or abstract coverage. Papers can appear in multiple venues/years, so do not sum venue paper counts as a global total.', + 'venues':venues} + finally:c.close() + + +if __name__=='__main__': + parser=argparse.ArgumentParser();parser.add_argument('--db',required=True) + parser.add_argument('--registry',default=str(Path(__file__).with_name('venues.json')));parser.add_argument('--out') + args=parser.parse_args();result=report(args.db,args.registry);text=json.dumps(result,ensure_ascii=False,indent=2) + if args.out: + path=Path(args.out);stage=path.with_suffix('.part');stage.write_text(text+'\n');stage.replace(path) + else:print(text) diff --git a/test_coverage_report.py b/test_coverage_report.py new file mode 100644 index 0000000..c2c4e94 --- /dev/null +++ b/test_coverage_report.py @@ -0,0 +1,35 @@ +import json +from pathlib import Path +import tempfile +import unittest + +from coverage_report import report +from index_core import Store + + +class CoverageTests(unittest.TestCase): + def setUp(self): + self.tmp=tempfile.TemporaryDirectory();self.addCleanup(self.tmp.cleanup) + self.root=Path(self.tmp.name);self.db=self.root/'papers.sqlite';self.store=Store(self.db);self.addCleanup(self.store.conn.close) + self.registry=self.root/'venues.json';self.registry.write_text(json.dumps({'scope_note':'Candidate scope','venues':[{'acronym':'COLING','field':'language'},{'acronym':'AAAI','field':'general_ai'}]})) + + def test_zero_coverage_targets_are_listed_and_joint_alias_is_counted_once(self): + paper,record=self.store.record('acl_anthology','joint-paper','https://example.org','Joint paper','Abstract',['Jane Doe']) + edition=self.store.decision(paper,record,'COLING+LREC',2024,'main','joint-paper','https://example.org','accepted','Accepted',aliases=['COLING','LREC']) + self.store.decision(paper,record,'COLING+LREC',2024,'main','joint-paper','https://example.org/another-observation','accepted','Accepted',aliases=['COLING']) + self.store.conn.commit();result=report(self.db,self.registry) + self.assertEqual(result['target_venue_count'],2);self.assertEqual(result['venues_with_imported_records'],1) + coling,aaai=result['venues'];self.assertEqual(coling['imported_submissions'],1) + self.assertEqual(coling['editions'][0]['canonical_venue'],'COLING+LREC') + self.assertEqual(aaai['history_status'],'no_imported_records');self.assertEqual(aaai['editions'],[]) + self.assertNotEqual(coling['history_status'],'complete') + + def test_same_paper_across_years_is_not_two_distinct_papers(self): + paper,record=self.store.record('source','one','https://example.org','A paper','Abstract',[]) + for year in (2024,2025): + self.store.decision(paper,record,'COLING',year,'main',str(year),'https://example.org','accepted','Accepted') + self.store.conn.commit();coling=report(self.db,self.registry)['venues'][0] + self.assertEqual(coling['imported_submissions'],2);self.assertEqual(coling['distinct_papers'],1) + + +if __name__=='__main__':unittest.main()