Files
mdpolish/research-wiki/design/0002-composable-cleaning-pipeline.md
T
Bepr4 8ac4f1dd12 更名 mdpolish 并补充清洗流水线设计与调研记录
- 仓库由 govdoc-md-cleaner 更名为 mdpolish,更新 README、AGENTS、CLAUDE
  及 reference 中的仓库名;冻结的 design 与带日期 scratch 保留旧名
- 冻结 0002:可组合清洗组件与流水线(check/transform、单轮修改加最终复查)
- 新增 0003 草稿:第一版可执行核心架构,待评审
- 新增 HTML 表格清洗专题调研(2026-08-21)
2026-08-21 22:49:03 +08:00

254 lines
16 KiB
Markdown
Raw 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.
# 0002:可组合的清洗组件与流水线
## 状态
已批准并冻结(2026-08-21)。本设计只确定总体组织方式,不授权创建源码、测试、依赖或公共接口。
后续如果改变本设计的选择,应新增 design 并以 `supersedes: 0002` 指向本记录,不回写本文件。
## 1. 问题
实验室不同项目面对的 Markdown 问题并不相同。论文项目需要处理手稿行号、批注和 arXiv 边栏戳,
GovDoc 则更关注幻觉、复杂表格、页眉页脚和隐私。如果把这些规则都塞进一个清洗器,默认行为会越来越难懂,
项目之间也容易互相影响。
师兄希望采用类似 PyTorch 的组合方式:库提供多个功能独立的组件,各项目按需导入,并自行搭建清洗流程。
## 2. 目标与非目标
目标:
- 每个组件只处理一类清楚、可单独测试的问题;
- 项目可以直接导入组件,并明确决定组件的组合与顺序;
- Markdown 是流水线唯一的文档输入,组件不接收图片、PDF、转换器 JSON 或其他项目材料;
- 每个组件都能先检查,能够安全修复的组件再提供修改行为;
- 所有组件使用同一套调用方式和结果记录;
- 每次修改可追踪,输入不被原地覆盖;
- 每个项目在自己的仓库中保存实际使用的组件、参数和顺序。
非目标:
- 本设计不确定 Python 包名、目录结构、函数签名和第三方依赖;
- 本设计不批准任何具体清洗规则的实现;
- 本设计不建设 PDF、图片或转换器 JSON 的读取、适配与回源能力;
- 本设计不承诺自动修复缺失正文、OCR 语义错误或其他无法确认正确内容的问题。
## 3. 方案比较
- **单体清洗器:** 使用简单,但规则增多后难以复用,也难以解释某个项目实际启用了什么。
- **单体清洗器加配置:** 可以开关规则,但全部规则仍由同一个入口和执行过程控制,项目之间容易耦合。
- **独立组件加统一流水线:** 组件可以单独导入和测试,项目显式组合,公共能力仍由统一底座提供。
采用第三种方案。
### 3.1 外部架构参照
本设计借鉴以下项目的职责划分,但这些参照不表示已经选择对应语言、依赖或接口:
| 项目 | 借鉴内容 | 不直接照搬的部分 |
| --- | --- | --- |
| [`remark` / `unified`](https://github.com/remarkjs/remark) | 处理器统一组织有序插件,项目显式启用插件,命令行只是处理器之外的入口 | 解析后全量重新输出 MarkdownNode.js 运行时 |
| [`ESLint`](https://eslint.org/docs/latest/extend/custom-rules) 与 [`rumdl`](https://github.com/rvben/rumdl) | 问题可以携带精确修复,修改后重新检查,并集中处理修改冲突 | 面向开发者文档的默认规则、自动多轮修改、宽松冲突处理和原地覆盖 |
| [`OpenRewrite`](https://docs.openrewrite.org/concepts-and-explanations/recipes) | 单项变换可以组成有序 Recipe,重视非目标内容的保留 | 第一版即建设庞大的无损语法树和跨语言运行平台 |
| [`mdformat`](https://mdformat.readthedocs.io/en/stable/users/plugins.html) | 插件安装不等于启用,调用方必须明确选择 | 把全文格式化作为清洗的必经步骤 |
其中 `remark` 最适合作为产品分层参照,ESLint 和 `rumdl` 更适合作为检查、精确修改与复查机制参照。
本项目保真要求更高,因此结构解析主要用于识别边界和生成证据;能够用精确文本范围表达的修改,不通过全篇
解析后重新渲染来实现。
## 4. 产品分层与职责
整体产品采用“项目组装、统一执行、边界适配”的分层方式:
```text
使用项目保存的组件、参数和顺序
Pipeline
┌──────┴──────┐
│ │
check transform
│ │
▼ ▼
Issue 汇总 公共修改执行器
当前 Markdown 快照
Result
```
- **文档上下文:** 保存当前 Markdown 快照、内容哈希和从当前快照派生的可复用只读分析结果;
- **组件:** 只负责一种问题的定位、证据和候选处理方式,不读写项目文件;
- **流水线:** 按使用项目给出的顺序调用组件,管理当前快照、错误、冲突、最终复查和结果汇总;
- **公共修改执行器:** 验证修改范围与当前快照是否匹配,统一应用非重叠修改并生成实际改动记录;
- **输入输出适配层:** 负责读取 Markdown,以及把结果输出为文件、终端文本或机器可读报告;
它不实现第二套清洗逻辑。
Python API、命令行、批处理和未来服务如果存在,都应调用同一个流水线核心。第一版不建设自动插件发现、
任务调度、数据库或 Web 服务。安装了某个组件包也不代表它会自动执行;只有使用项目显式导入并加入流水线的
组件才会生效。
本节只确定职责边界,不确定源码目录、类名、函数签名和序列化格式。
## 5. 输入边界:只接收 Markdown
流水线只接收 Markdown 文本作为文档输入。组件可以使用项目明确给出的参数,但不能要求或读取图片、PDF、
转换器 JSON、项目目录或其他外部文档材料。文档上下文中的解析结果和其他缓存也必须从当前 Markdown 快照派生。
图片、原始 PDF 和转换器 JSON 的目录、格式、可信度与对应关系都由使用项目或上游转换流程决定,不进入共用库
的输入契约。项目需要断链核验、版面坐标、原文比对或回源重提取时,应在自己的边界内处理,不能让通用组件
依赖某个项目的文件布局或转换器 schema。
因此,共用库可以根据 Markdown 本身检查图片引用语法、固定幻觉特征、截断迹象和表格结构异常,但不能据此
声称图片文件存在、原文已经缺失或回源修复已经完成。以后如果多个项目证明存在相同的外部材料接入需求,
再通过新的 design 决定是否增加独立适配能力;本设计不提前预留该接口。
## 6. 一个组件,两种行为
`Component` 对应一种清洗问题,例如 HTML 实体双重转义、arXiv 边栏戳、手稿行号或空图片引用。组件不是按
`Check``Transform` 分成两类,而是可以提供两种行为。
### 6.1 `check`:所有组件必须提供
`check` 找出当前组件负责的问题,返回位置、证据和处理能力,但不修改 Markdown。问题分为三种处理状态:
- **仅检查:** 能确认异常,但不知道唯一正确的改法;
- **建议修改:** 能给出候选改法,但需要项目或人工明确选择,清洗流程不会自动应用;
- **可自动修复:** 前置条件严格、改法唯一,并且组件明确声明支持自动修改。
没有明确声明“可自动修复”的组件一律按仅检查处理,不能因为问题中包含候选文本就自动修改。
检查通常依靠确定规则实现,例如正则表达式、Markdown/HTML 解析、连续编号判断和重复率统计。
大语言模型以后可以作为某个组件的可选检查方式,但不进入默认流程,也不能根据模型判断自动改写正文。
### 6.2 `transform`:能够安全修复的组件才提供
`transform` 修改 `check` 已经能够准确定位、且正确处理方式已经明确的问题。例如,HTML 实体组件可以还原一层
重复转义,arXiv 边栏戳组件可以删除严格匹配的整行。
组件的 `check``transform` 必须使用同一套定位逻辑,不能出现检查报告了一批位置、清洗时却另行扫描并
修改另一批内容。推荐由检查结果携带绑定当前输入快照的候选修改,`transform` 只确认并提交这些修改;具体接口
留到后续设计确定。
检查阶段产生的位置和候选修改不能直接延后应用。只要前一个组件改变了 Markdown,旧结果就只保留为历史证据;
后续修改必须在当前 Markdown 快照上重新定位。疑似幻觉、内容截断迹象和无法仅凭 Markdown 确认正确结构的
损坏表格通常只能检查;这类组件不提供 `transform`
组件至少需要说明自己的标识和版本、参数、能否修改、适用边界,以及修改是否幂等。具体字段和函数签名留到
接口设计时确定。
## 7. 流水线如何使用组件
组件可以被项目单独调用,也可以按顺序放入 `Pipeline`。流水线提供两种运行方式。
### 7.1 检查流程
项目可以把所有相关组件放进一条流水线并执行 `check`。流水线逐个调用组件的检查行为,汇总发现的问题和
执行错误,全程不修改 Markdown。
### 7.2 清洗流程
项目阅读检查结果后,再选择真正需要的组件和顺序,执行 `transform`。流水线只调用这些组件的修改行为,
处理修改范围冲突,并汇总清洗后的 Markdown 和每一处改动。这里选择的是组件及其参数,不是保存第一次检查时
得到的一批旧位置后直接套用。
下面只说明使用方式,不是已经批准的 Python 接口:
```python
inspection = Pipeline(all_relevant_components)
check_result = inspection.check(markdown)
cleaning = Pipeline(selected_components)
transform_result = cleaning.transform(markdown)
```
组件的顺序由使用项目决定。共用库不能假设所有组件可以任意交换,也不能根据 Markdown 内容自动选择项目流程。
### 7.3 单轮清洗与最终复查
清洗流程按以下语义执行:
1. 记录输入 Markdown 的内容哈希,建立当前快照;
2. 按项目给出的顺序,让组件针对当前快照重新检查并产生候选修改;
3. 公共修改执行器验证候选修改引用的原文、范围和快照,重叠且处理方式不同的修改视为冲突;
4. 成功应用一个组件的修改后,生成新快照,使旧位置和旧解析缓存失效;下一个组件读取这个新快照;
5. 每个选中组件只执行一次修改。全部组件结束后,流水线对最终快照再执行一次只读检查,不再应用任何修改;
6. 如果最终复查发现某个已选组件仍存在可自动修复的问题,说明组件之间产生了连锁影响,本次流水线标记为
“未稳定”,由项目调整组件顺序或组成后重新运行。
流水线不能为了消除冲突而暗中调整项目给出的组件顺序,也不能静默选择某一项重叠修改。组件异常、修改冲突
或最终复查未稳定时,本次清洗整体不算成功;结果仍保留已经发生的内存中修改和失败证据,但输入文件不会因此
被写入或覆盖。一次组件可提交多少项修改以及失败结果的具体字段留到后续接口设计。
最终复查中仍然存在仅检查或建议修改的问题,不会触发第二轮自动修改;它们继续记录为未解决问题。是否阻止
结果用于后续流程,由使用项目根据用途和风险另行决定。
## 8. 结果与修改记录
`Result` 不是一个独立业务组件,只是让组件和流水线使用相同的返回形式。第一版需要表达两种结果:
- 检查结果:发现的问题、建议修改和执行错误;
- 清洗结果:运行状态、清洗后的 Markdown、实际改动、仍未解决的问题、执行错误和最终复查结果。
结果中的三个概念不能混用:
- **问题:** 组件在某个 Markdown 快照中发现的异常,包含位置、证据和处理能力;
- **候选修改:** 组件针对某条问题提出、但尚未实际执行的精确修改,必须绑定产生它的输入快照;
- **实际改动:** 公共修改执行器已经应用的修改,记录修改前后内容及对应快照。
每条问题至少应能说明组件标识和版本、问题位置、判断依据以及属于仅检查、建议修改还是可自动修复。每条实际
改动至少应能说明由哪个组件执行、执行顺序、修改范围、修改前后内容、修改理由以及修改前后的快照标识。
流水线结果还应记录实际组件顺序和参数、输入输出内容哈希、冲突、最终复查发现的问题和组件错误。
“完成了部分修改”不等于清洗成功;只有所有选中组件完成执行、没有执行错误,而且最终复查没有发现仍可由
已选组件自动修复的问题,结果才能标记为成功。具体字段和保存格式留到接口设计时确定。
第一版不单独建设 Audit 子系统。清洗结果中的改动记录就是审计依据;以后确实需要保存时,再把这些记录导出
为机器可读文件。终端文本、JSON 或其他报告只是同一结果的不同表示,不能各自维护不同事实。
## 9. 项目如何组装
共用库提供组件和流水线能力,不根据内容猜测当前属于哪个项目,也不自动选择清洗流程。ClinDB、GovDoc 和
其他使用方应在各自项目中直接导入所需组件,并保存组件的参数和执行顺序。
项目可以把这种有明确用途的组合称为 profile,但 profile 的权威仍在使用项目中。它只是组件、参数、顺序和
用途的显式组合,不是共用库根据内容自动推断的标签。安装、注册或能够导入一个组件,都不会使它自动加入 profile。
共用库可以在文档和测试中提供组合示例,但示例不是默认流程,也不代替使用方对清洗范围的决定。某个项目新写
的组件只有被证明可以复用后,才考虑放回共用库。
组件按用途区分通用能力和项目能力。图片引用语法、表格校验、异常字符等可以作为通用组件;arXiv 边栏戳、
手稿行号等属于论文组件;GovDoc 的幻觉模板、投标文档页眉页脚和隐私检查属于 GovDoc 组件。
只审计、忠实修复、RAG、文档对比和公开脱敏属于不同使用目的,应由流程明确选择,不写死在组件内部。
## 10. 第一阶段边界
获得批准后,第一阶段先验证以下最小闭环:
1. 组件能够独立执行 `check`
2. 检查结果能够区分仅检查、建议修改和可自动修复,建议修改不会被自动应用;
3. 支持安全修复的组件能够执行 `transform`
4. 流水线能够汇总检查结果、让每个选中组件按顺序修改一次、刷新当前快照并记录改动;
5. 流水线能够识别修改冲突,并通过最终只读复查发现组件之间的连锁影响,失败时不把部分结果报告为成功;
6. 流水线和所有组件的文档输入都只有 Markdown,不读取外部材料;
7. 原输入不被覆盖,单个清洗组件和选定流水线都满足幂等要求。
ClinDB 已明确的九类规则中,只依赖 Markdown 的规则可作为首批候选组件,具体流程保存在 ClinDB 项目中。
GovDoc 可以选择所需组件;疑似幻觉、缺失内容和损坏表格在共用库中只检查,不自动猜测、改写或回源修复。
第一阶段的具体输入输出、验收方法、源码结构和依赖仍需后续设计批准后才能实施。
## 11. 风险与边界
- 组件过细会变成难以理解的正则表达式集合;公共组件应表达完整行为,而不是简单包装一次替换;
- 文本修改存在先后顺序和范围重叠,不能假设组件可以任意交换;
- 单轮执行不会自动处理后一个组件新产生的前置问题,必须由最终只读复查明确报告未稳定;
- 项目流程不能藏进共用库的默认行为,否则使用方无法确认实际启用了哪些规则;
- 自动发现或仅因安装而启用第三方组件会使结果随环境变化,第一版只允许显式导入和组装;
- 发现异常不等于知道正确修法,检查结果不能自动变成删除或内容补写;
- 同一组件的检查和修改逻辑如果发生漂移,会使检查报告失去可信度,必须共用定位逻辑;
- Markdown AST 适合识别结构,但全篇重新输出可能改变未被组件选中的内容,不作为忠实清洗的默认方式;
- 位置必须绑定具体输入快照,否则一次修改后继续使用旧范围会改错内容;
- 修改记录用于解释实际变化,不能代替针对组件和项目流程的测试。