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

220 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](../reference/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 论文转换产生的独立提交戳行;严格整行匹配;排除所有相似文本 |
新增文件范围:
```text
src/mdpolish/components/
├── __init__.py
└── arxiv_submission_stamp.py
tests/
└── test_arxiv_submission_stamp.py
```
`arxiv_submission_stamp.py` 只依赖 `component.py``models.py`,不导入 `edits.py``pipeline.py`。组件只提出
`ProposedChange`,仍由公共流水线应用。`components/__init__.py` 导出该组件,但顶层 `mdpolish/__init__.py`
不新增快捷导出,避免把业务组件和核心契约混在同一命名空间。
本轮不新增运行依赖,也不创建共享 Markdown parser 或通用行匹配框架。以后第二个组件出现重复定位需求时,
再用实际重复代码判断是否需要抽取辅助模块。
## 4. 什么算目标行
组件按 Markdown 的物理行扫描。去掉行尾的 `\n``\r\n` 或单独 `\r` 后,整行必须匹配以下语义:
```text
arXiv:<四位年份>.<数字编号>v<数字版本> [<ASCII 分类>] <1 至 31 的日> <英文月份缩写> <四位年份>
```
对应第一版正则表达式:
```regex
^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、独立检查或人工建议;不授权保存真实清洗
输出、修改真实数据、提交、推送或发布。