Files
mdpolish/research-wiki/design/0004-arxiv-submission-stamp-component.md
T
Bepr4 8c23ac5521 实现 arXiv 提交边栏戳清洗组件
冻结 0004,新增严格整行删除组件和测试,并记录 5 份论文的只读验证结果。同步 ClinDB 清洗范围,并忽略本地 reference 调研副本。
2026-08-22 15:02:21 +08:00

11 KiB
Raw Blame History

0004:arXiv 提交边栏戳自动清洗组件

状态

已批准并冻结(2026-08-22)。

本设计使用 0003 已实现的组件、精确修改和流水线契约,不改变核心接口。它只决定第一个真实清洗组件的 识别边界、删除语义、代码位置和验收方式。

1. 问题与可观察现象

ClinDB-ReviewBench 的论文转换结果中,有两份 Markdown 保留了 arXiv 提交页的独立边栏戳:

  • Statistics in Medicine/arXiv 文档第 1 行;
  • Springer/arXiv 文档第 18 行。

这类行只包含 arXiv 编号、分类和提交日期,不是论文正文。现有只读审计同时确认,Springer 文档后部还有两处 合法参考文献包含 arXiv preprint arXiv:…。如果只搜索 arXiv: 子串并删除整行,会误删参考文献。

第一版内存核心已经能够安全应用精确删除,但测试中只有假组件。现在需要一个范围足够小的真实组件,验证业务规则 能否遵守快照绑定、原子应用、审计记录和幂等约束,而不立即引入 HTML parser、文件适配器或项目 profile。

本规则在 ClinDB 范围中的权威编号为 H1,见 CLINDB_REVIEWBENCH_CLEANING_SCOPE.md

2. 目标与非目标

目标:

  • 只删除完整一行的 arXiv 提交边栏戳;
  • 保留所有未完整满足目标行模式的 arXiv 参考文献、正文和链接;
  • 保留原文已有的换行风格,不顺带整理空行;
  • 每个删除位置产生可追踪的候选修改和实际改动记录;
  • 使用合成样例验证确定性、反向用例和幂等性;
  • 在本地 5 份论文 Markdown 上做只读、纯内存复核。

非目标:

  • 不删除一般的 arXiv 引用、论文元数据、封面页或作者信息;
  • 不识别旧式 arXiv 编号,也不把本组件扩展为通用参考文献清洗器;
  • 不识别或保护围栏代码、行内代码及其他 Markdown 块结构;
  • 不提供可配置正则表达式或“删除任意匹配行”的通用组件;
  • 不实现独立检查、待审标记、人工建议或问题报告;
  • 不读取文件、目录、PDF、图片、环境变量或网络;
  • 不建立 profile 格式、CLI、文件输出或审计文件;
  • 不实现 HTML 实体、Word 批注、行号、表格等其他 ClinDB 规则。

3. 组件身份与代码边界

组件元数据固定为:

项目 决定
Python 类名 ArxivSubmissionStampComponent
组件标识 paper.arxiv_submission_stamp
初始版本 1.0.0
参数 空;第一版不允许调用方替换模式或放宽边界
适用范围 PDF/arXiv 论文转换产生的独立提交戳行;严格整行匹配;排除所有相似文本

新增文件范围:

src/mdpolish/components/
├── __init__.py
└── arxiv_submission_stamp.py
tests/
└── test_arxiv_submission_stamp.py

arxiv_submission_stamp.py 只依赖 component.pymodels.py,不导入 edits.pypipeline.py。组件只提出 ProposedChange,仍由公共流水线应用。components/__init__.py 导出该组件,但顶层 mdpolish/__init__.py 不新增快捷导出,避免把业务组件和核心契约混在同一命名空间。

本轮不新增运行依赖,也不创建共享 Markdown parser 或通用行匹配框架。以后第二个组件出现重复定位需求时, 再用实际重复代码判断是否需要抽取辅助模块。

4. 什么算目标行

组件按 Markdown 的物理行扫描。去掉行尾的 \n\r\n 或单独 \r 后,整行必须匹配以下语义:

arXiv:<四位年份>.<数字编号>v<数字版本> [<ASCII 分类>] <1 至 31 的日> <英文月份缩写> <四位年份>

对应第一版正则表达式:

^arXiv:[0-9]{4}\.[0-9]+v[0-9]+ \[[A-Za-z0-9_.-]+\] (?:[1-9]|[12][0-9]|3[01]) (?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) [0-9]{4}$

这里有意使用 ASCII 字符范围和月份白名单,不使用 Python 默认的 Unicode \w。目标是识别已知提交戳格式, 不是验证 arXiv 元数据的真实性,也不校验日期是否真实存在。例如 31 Feb 仍满足形状规则;组件不访问外部日历或 arXiv 服务。

以下差异均不匹配:

  • 行首或行尾存在空格;
  • 前面有列表、引用或标题标记;
  • 缺少版本、分类或日期;
  • 使用旧式编号;
  • arXiv: 出现在句子、链接或参考文献中;
  • 月份不是表中 12 个英文缩写之一。

严格拒绝相似文本的代价是可能漏掉格式稍有变化的边栏戳。第一版接受漏删,不通过自动 strip()、大小写忽略或 宽松日期模式提高命中率。以后真实样本出现新格式时,应补证据、更新组件版本和测试,而不是悄悄放宽模式。

第一版不解析围栏代码、HTML 注释、YAML front matter 或其他 Markdown 块。只要某个物理行完整满足上述模式, 无论它处于什么 Markdown 结构中都会命中。当前范围不要求保护这些结构;以后出现需要保留的真实反例时,应重新 评审识别边界,不能在实现中临时增加例外。

5. 精确删除与换行语义

每个命中行产生一个 ProposedChange,其中只有一个删除型 TextEdit

  • 范围从该行第一个字符开始;
  • 如果该行带 \n\r\n\r,范围同时包含它自己的行尾;
  • 如果末行没有行尾,只删除该行文字;
  • expected_text 是范围内的完整原文;
  • replacement 是空字符串;
  • 修改理由固定为“删除完整匹配的 arXiv 提交边栏戳”。

该规则带来以下可预测结果:

输入形态 删除后的边界
stamp\n正文 正文
正文\nstamp\n后文 正文\n后文
正文\r\nstamp\r\n后文 正文\r\n后文
正文\nstamp 正文\n
只有 stamp 空字符串

末行没有行尾时保留前一行已有的行尾。它是原文的一部分,不是多余空白;这样也能保证相邻多个命中行的删除范围 互不重叠。组件不合并前后空行,不统一换行符,也不改变未命中的任何字符。

多个候选按原文位置从前到后返回。公共执行器仍把当前组件的全部候选作为一个原子批次:任何候选过期、冲突或 原文不符时,本次组件修改全部不应用。

6. 审计、确定性与幂等性

组件不生成随机 ID,不读取外部状态。相同 Markdown、组件版本和空参数必须产生相同顺序、相同范围、相同理由的 候选修改。

每个命中行单独成为一个候选修改,便于审计记录把一条删除对应到一个原始物理行。流水线分配确定性候选引用, 实际 Change 继续记录组件身份、理由、原文、空替换和批次前后哈希。

成功删除后,目标行已经不存在;同一组件在最终复查和再次运行时均不得产生新修改。这里的幂等性同时通过组件 测试和单组件 Pipeline 测试验证,不增加组件自己的 transform() 快捷入口。

7. 方案比较

7.1 全局删除含 arXiv: 的行

实现最短,但会删除合法参考文献和正文,已有反向样本已经证明不可接受。不采用。

7.2 允许项目传入正则表达式

看似通用,实际把误删边界交给每个调用方,并使相同组件版本可以表现出完全不同的语义。第一版也没有配置或 profile 契约。不采用。

7.3 严格整行匹配,不识别 Markdown 块结构

修法唯一、定位精确,只使用标准库即可实现,已知参考文献不会命中。代价是围栏代码或其他 Markdown 块内如果恰好 出现完整目标行也会被删除;当前范围没有保护这些结构的需求,因此不为假设场景增加扫描逻辑。采用。

7.4 先引入 Markdown parser

完整 parser 可以更准确识别代码、HTML 和其他块,但为删除两个格式固定的物理行引入运行依赖和方言选择,代价 明显超过收益。不采用;复杂表格组件另行设计 parser。

8. 测试与验收

合成测试至少覆盖:

  • 空 Markdown 和完全不含目标的 Markdown 返回零修改 success
  • 目标行位于首行、中间、带行尾的末行和不带行尾的末行;
  • \n\r\n 和单独 \r 三种行尾保持原有风格;
  • 同一文档含多个目标行,候选和 Change 按原文顺序记录且批次原子应用;
  • 合法参考文献 arXiv preprint arXiv:… 保留;
  • 行首/行尾空格、缺字段、旧式编号、错误月份和其他相似行保留;
  • 审计记录包含固定组件标识、版本、理由、删除原文和批次哈希;
  • 单组件 Pipeline 成功后最终复查无残留候选;
  • 对成功输出再次运行,内容不变且没有实际 Change
  • 相同输入重复运行得到相同有序结果。

基础检查继续使用根目录 README 的唯一命令,并要求 Ruff、mypy、pytest 全部通过。

实现完成后,对本地 5 份论文 Markdown 做一次只读、纯内存验证:

  1. 不复制、不改名、不写回任何真实文档;
  2. 不把原文片段加入测试、日志或提交;
  3. 预期只命中 sim 第 1 行和 springer 第 18 行,共两处;
  4. 复核 springer 两处合法 arXiv 参考文献保持原样;
  5. 复核除两条完整目标行外没有其他 diff;
  6. 只记录输入文件范围、组件版本、命中数量、未命中反例数量和验证结论。

如果真实验证结果与上述计数不一致,停止实施收尾,回到 design 或 reference 核对原因,不能放宽测试来适配结果。

9. 风险与代价

  • 严格模式会漏删变体: 这是有意选择;没有证据的新格式保持原样。
  • 整行相同的正文仍可能误删: 严格的完整格式降低风险,但无法证明未来正文不会独立引用同一字符串; 因此组件定位为论文转换规则,不进入尚不存在的全局默认 profile。
  • 不保护围栏代码或其他 Markdown 块: 其中如果出现完整目标行也会命中;当前范围接受这一代价,出现真实反例后 再重新评审,不在本组件内预建通用保护机制。
  • 真实数据验证不进入自动测试: 避免提交客户或项目材料;合成测试负责稳定契约,真实材料只做本地只读复核。
  • 第一条真实规则覆盖面很小: 它优先验证扩展和审计闭环,不追求清洗率。下一候选是 HTML 实体双重转义, 仍需单独 design 确定 HTML 范围和实体替换边界。

10. 批准后的实施边界

批准本设计只授权:

  1. 创建第 3 节列出的组件和测试文件;
  2. 按第 4 至 6 节实现无外部依赖的精确删除组件;
  3. 运行第 8 节合成测试和本地 5 份论文的只读、纯内存验证;
  4. 根据真实实现更新 README 当前阶段和 explanation/ 当前机制。

批准不授权实现其他清洗规则、共享 parser、profile、文件读写、CLI、独立检查或人工建议;不授权保存真实清洗 输出、修改真实数据、提交、推送或发布。