更名 mdpolish 并补充清洗流水线设计与调研记录

- 仓库由 govdoc-md-cleaner 更名为 mdpolish,更新 README、AGENTS、CLAUDE
  及 reference 中的仓库名;冻结的 design 与带日期 scratch 保留旧名
- 冻结 0002:可组合清洗组件与流水线(check/transform、单轮修改加最终复查)
- 新增 0003 草稿:第一版可执行核心架构,待评审
- 新增 HTML 表格清洗专题调研(2026-08-21)
This commit is contained in:
2026-08-21 22:49:03 +08:00
parent b6e310f0a9
commit 8ac4f1dd12
8 changed files with 848 additions and 46 deletions
+14 -11
View File
@@ -12,21 +12,23 @@
发生冲突时,以对应的唯一权威为准,不把不同版本拼成新的说法。
| 事实类型 | 唯一权威 |
|---|---|
| 当前阶段与已经完成的工作 | 根目录 `README.md` |
| Wiki 分类、冻结规则与更新机制 | `research-wiki/README.md` |
| 已批准的选择、权衡与否决方案 | `research-wiki/design/` 中对应编号记录 |
| 当前有效的清洗机制与原因 | `research-wiki/explanation/`;没有文档时就是尚未确定 |
| 参数、输入输出和运行行为 | 未来的代码与测试;代码无法表达的事实才进入 `reference/` |
| 可复现的操作与排障步骤 | `research-wiki/guides/` |
| Agent 协作与执行规范 | 本文件及其同步镜像 |
| 事实类型 | 唯一权威 |
| ----------------- | ---------------------------------------- |
| 当前阶段与已经完成的工作 | 根目录 `README.md` |
| Wiki 分类、冻结规则与更新机制 | `research-wiki/README.md` |
| 已批准的选择、权衡与否决方案 | `research-wiki/design/` 中对应编号记录 |
| 当前有效的清洗机制与原因 | `research-wiki/explanation/`;没有文档时就是尚未确定 |
| 参数、输入输出和运行行为 | 未来的代码与测试;代码无法表达的事实才进入 `reference/` |
| 可复现的操作与排障步骤 | `research-wiki/guides/` |
| Agent 协作与执行规范 | 本文件及其同步镜像 |
路径、参数、命令、指标口径和当前进度不得维护多个权威版本。发现冲突时,先确认权威,再修复过期内容。
## 1. 项目定位与当前阶段
`govdoc-md-cleaner` 是独立的清洗算法研究与治理仓库。它用于理解 PDF→Markdown 噪音、定义清洗边界、
`mdpolish` 是独立的清洗算法研究与治理仓库。它用于理解 PDF→Markdown 噪音、定义清洗边界、
比较候选方案并积累可复核证据。
当前只建立文档治理基础。源码目录、测试目录、依赖配置、命令行接口、规则格式和评估体系均未获批准、
@@ -120,8 +122,9 @@ design 草稿可以在评审中修改;批准后冻结。决策发生变化时
## 9. 沟通与协作
- 像同事协作一样,用直接、可读的中文说明判断、变化和风险
- 像同事协作一样,用直接、可读的中文说明判断、变化和风险。要用人话讲解!别创造黑话,别堆积信息密度极大的长难句!
- 问什么答什么,一次聚焦当前问题,不把未经请求的后续工作一起推进;
- 能在授权范围内安全判断的事项直接完成;会改变范围或契约的选择交给用户;
- 进度和最终报告必须对应真实工具输出,不把计划描述成结果;
- 不输出内部思维链,只提供可复核的依据、实际变更和验证结果。
+14 -11
View File
@@ -12,21 +12,23 @@
发生冲突时,以对应的唯一权威为准,不把不同版本拼成新的说法。
| 事实类型 | 唯一权威 |
|---|---|
| 当前阶段与已经完成的工作 | 根目录 `README.md` |
| Wiki 分类、冻结规则与更新机制 | `research-wiki/README.md` |
| 已批准的选择、权衡与否决方案 | `research-wiki/design/` 中对应编号记录 |
| 当前有效的清洗机制与原因 | `research-wiki/explanation/`;没有文档时就是尚未确定 |
| 参数、输入输出和运行行为 | 未来的代码与测试;代码无法表达的事实才进入 `reference/` |
| 可复现的操作与排障步骤 | `research-wiki/guides/` |
| Agent 协作与执行规范 | 本文件及其同步镜像 |
| 事实类型 | 唯一权威 |
| ----------------- | ---------------------------------------- |
| 当前阶段与已经完成的工作 | 根目录 `README.md` |
| Wiki 分类、冻结规则与更新机制 | `research-wiki/README.md` |
| 已批准的选择、权衡与否决方案 | `research-wiki/design/` 中对应编号记录 |
| 当前有效的清洗机制与原因 | `research-wiki/explanation/`;没有文档时就是尚未确定 |
| 参数、输入输出和运行行为 | 未来的代码与测试;代码无法表达的事实才进入 `reference/` |
| 可复现的操作与排障步骤 | `research-wiki/guides/` |
| Agent 协作与执行规范 | 本文件及其同步镜像 |
路径、参数、命令、指标口径和当前进度不得维护多个权威版本。发现冲突时,先确认权威,再修复过期内容。
## 1. 项目定位与当前阶段
`govdoc-md-cleaner` 是独立的清洗算法研究与治理仓库。它用于理解 PDF→Markdown 噪音、定义清洗边界、
`mdpolish` 是独立的清洗算法研究与治理仓库。它用于理解 PDF→Markdown 噪音、定义清洗边界、
比较候选方案并积累可复核证据。
当前只建立文档治理基础。源码目录、测试目录、依赖配置、命令行接口、规则格式和评估体系均未获批准、
@@ -120,8 +122,9 @@ design 草稿可以在评审中修改;批准后冻结。决策发生变化时
## 9. 沟通与协作
- 像同事协作一样,用直接、可读的中文说明判断、变化和风险
- 像同事协作一样,用直接、可读的中文说明判断、变化和风险。要用人话讲解!别创造黑话,别堆积信息密度极大的长难句!
- 问什么答什么,一次聚焦当前问题,不把未经请求的后续工作一起推进;
- 能在授权范围内安全判断的事项直接完成;会改变范围或契约的选择交给用户;
- 进度和最终报告必须对应真实工具输出,不把计划描述成结果;
- 不输出内部思维链,只提供可复核的依据、实际变更和验证结果。
+21 -12
View File
@@ -1,10 +1,9 @@
# govdoc-md-cleaner
# mdpolish
实验室共用的 Markdown 清洗研究与基础工具库。仓库名沿用了最初的 GovDoc 场景,但项目不属于 GovDoc
专用组件,也不只服务政务文档。
实验室共用的 Markdown 清洗研究与基础工具库。项目不属于 GovDoc 专用组件,也不只服务政务文档。
本仓库面向实验室内不同项目复用,用于清洗 PDF、DOCX、OCR、网页等上游管线生成的 Markdown,统一解决
格式噪声、结构损坏、内容异常、来源追踪和多用途派生问题。各项目共享通用清洗能力,再通过独立配置或
格式噪声、结构损坏、内容异常、修改追踪和多用途派生问题。各项目共享通用清洗能力,再通过独立配置或
profile 表达论文、政务文档、RAG、文档对比等不同需求。
仓库当前仍处于研究和方案设计阶段:用于澄清问题、记录设计选择、积累可复核证据,并在方案获得确认后
@@ -24,9 +23,19 @@ profile 表达论文、政务文档、RAG、文档对比等不同需求。
`research-wiki/scratch/data-5papers-cleaning-audit-2026-08-21.md`),并确定其第一版清洗范围
`research-wiki/reference/CLINDB_REVIEWBENCH_CLEANING_SCOPE.md`9 类确定性规则)。
- 2026-08-21 明确本项目定位为实验室共用库;GovDoc 和论文清洗都是使用场景,不是核心边界。
- 2026-08-21 批准并冻结 `research-wiki/design/0002-composable-cleaning-pipeline.md`,确定只接收 Markdown、
项目显式组装组件、单轮修改加最终只读复查的总体组织方式。
- 2026-08-21 建立 `research-wiki/design/0003-first-executable-core-architecture.md` 草稿,等待评审第一版
Python 内存核心、精确修改协议、错误语义和测试边界。
- 2026-08-21 完成 HTML 表格清洗专题调研
`research-wiki/scratch/html-table-cleaning-ecosystem-research-2026-08-21.md`):核实 Pandoc 表格
方言能力边界、Turndown 不处理合并单元格、Docling Markdown 导出重复合并单元格内容、MinerU 全
HTML 输出,印证审计 T001 的分流方向。
- 2026-08-21 仓库由 `govdoc-md-cleaner` 更名为 `mdpolish`,GitHub 远程仓库与本地目录同步改名;
冻结的 design 记录和带日期的 scratch 笔记保留当时的旧名。
当前没有清洗算法、可执行命令、运行依赖、测试套件或已批准的输入输出契约。调研报告中的技术组合、
内部 IR、profiles 和实施路线是候选方案,尚未批准。
当前没有清洗算法、可执行命令、运行依赖、测试套件或函数级输入输出契约。`0002` 只批准了总体组织方式;
调研报告中的解析器、内部 IR、具体 profile 和实施路线是候选方案,尚未批准。
目录存在只代表文档落点已经建立,不代表相应能力已经完成。
## 服务对象与复用目标
@@ -42,12 +51,12 @@ GovDoc 目录、具体客户名称或某一转换器的固定输出路径。
## 面向复用的设计原则
- **通用核心**编码检查、Markdown/HTML 结构解析、异常检测可审计变换、资产校验和来源追踪
- **输入适配器**:不同 PDF/OCR/DOCX/HTML 转换器通过 adapter 接入,不把某个上游工具写死
- **通用核心**只接收 Markdown,提供结构检查、异常检测可审计变换,不读取 PDF、图片或转换器 JSON
- **输入边界**PDF/OCR/DOCX/HTML 转换和外部材料核验由使用项目或上游流程负责,不写入共用组件契约
- **项目 profile**:论文、GovDoc、对比、RAG、公开脱敏等规则独立组合,不互相污染默认行为;
- **保真优先**:不确定内容默认保留或进入人工确认,不能为了格式整齐改写业务或学术内容;
- **可复现**:规则、配置、输入哈希、输出和每次变更都可以追踪;
- **可扩展**:新增项目应主要增加 adapter、detector、transformer 或 profile,而不是复制一套清洗器。
- **可扩展**:新增项目在自身边界处理上游适配,并主要组合或补充组件和 profile,而不是复制一套清洗器。
## `data/` 的职责
@@ -69,7 +78,7 @@ GovDoc 目录、具体客户名称或某一转换器的固定输出路径。
## 目录结构
```text
govdoc-md-cleaner/
mdpolish/
├── AGENTS.md
├── CLAUDE.md
├── README.md
@@ -92,8 +101,8 @@ govdoc-md-cleaner/
3. `research-wiki/README.md`,确认文档应放在哪里;
4. 与任务直接相关的 `research-wiki/design/` 记录。
下一项实质工作开始前,应以现有论文数据、GovDoc 审计和生态调研为输入新增下一编号的 design,明确
通用核心与项目 profile 的边界、第一阶段范围、技术选型、输入输出、验证方法和非目标,并等待批准
下一项实质工作开始前,应评审并批准 `research-wiki/design/0003-first-executable-core-architecture.md`
草稿尚不授权创建源码、测试、依赖或公共接口
## 当前可用检查
+14 -11
View File
@@ -8,14 +8,16 @@
## 1. 目录与生命周期
| 内容 | 目录 | 维护方式 |
|---|---|---|
| 动工前的方案比较、选择和代价 | `design/` | 批准后冻结;改变时新增下一编号 |
| 当前有效的机制、数据流和原因 | `explanation/` | 事实变化时同步更新 |
| 代码无法完整表达的稳定查询事实 | `reference/` | 权威事实变化时更新 |
| 可复现运行、验证与排障步骤 | `guides/` | 操作变化时更新并重新验证 |
| 调研笔记、计划和未收敛草稿 | `scratch/` | 不作为当前事实;由项目负责人决定去留 |
| 当前阶段和已完成工作 | 根目录 `README.md` | 阶段变化时更新 |
| 内容 | 目录 | 维护方式 |
| --------------- | --------------- | ------------------ |
| 动工前的方案比较、选择和代价 | `design/` | 批准后冻结;改变时新增下一编号 |
| 当前有效的机制、数据流和原因 | `explanation/` | 事实变化时同步更新 |
| 代码无法完整表达的稳定查询事实 | `reference/` | 权威事实变化时更新 |
| 可复现运行、验证与排障步骤 | `guides/` | 操作变化时更新并重新验证 |
| 调研笔记、计划和未收敛草稿 | `scratch/` | 不作为当前事实;由项目负责人决定去留 |
| 当前阶段和已完成工作 | 根目录 `README.md` | 阶段变化时更新 |
根目录 `AGENTS.md``CLAUDE.md` 是协作者入口,不放入 Wiki。
空分类使用 `.gitkeep` 保留,不创建只写未来设想的占位文档。
@@ -83,11 +85,12 @@ guide 必须来自实际运行,至少包含前置条件、准确命令、预
- 小型汇总进入 Git 前必须确认无法还原客户内容;
- 每个实验应能追溯输入范围、版本、参数、环境、指标和输出位置。
## 9. 写作与审查
## 9. 写作与审查(非常重要!!!务必遵守!!!)
- 面向没有参加过讨论、但具备相关技术背景的读者
- 面向没有参加过讨论、但具备相关技术背景的读者,要用人话讲解!别创造黑话,别堆积信息密度极大的长难句!
- 尽量多使用表格和图,让读者更容易看懂。
- 从真实问题或读者可见现象开始,不先堆术语;
- 结论写清理由、代价、适用边界和验证状态;
- 参数、路径、命令、指标和当前阶段只维护一个权威版本;
- 不把目标写成已完成,不泄露真实文档内容,不用文档替代测试;
- 简单事实用短段落,只有比较关系确实更清楚时才使用表格或图。
@@ -0,0 +1,253 @@
# 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 适合识别结构,但全篇重新输出可能改变未被组件选中的内容,不作为忠实清洗的默认方式;
- 位置必须绑定具体输入快照,否则一次修改后继续使用旧范围会改错内容;
- 修改记录用于解释实际变化,不能代替针对组件和项目流程的测试。
@@ -0,0 +1,388 @@
# 0003Mdpolish 第一版可执行核心架构
## 状态
草稿,待批准。本设计细化已冻结的 `0002`,不替代或修改其中的选择。
本设计批准后,才授权创建这里列出的 Python 包、测试和工程配置,并实现不含真实清洗规则的最小核心。
在批准前,本仓库仍然没有可运行的清洗工具。
## 1. 问题
`0002` 已经确定:项目显式组合组件,流水线只接收 Markdown,组件先检查再提出精确修改,清洗只执行一轮,
最后进行只读复查。
这些总体原则还不足以开始实现。目前尚未确定:
- 组件通过什么 Python 接口报告问题;
- 中文文本的位置如何表示;
- 候选修改怎样绑定当前 Markdown,避免旧位置误改新文本;
- 多项修改如何保证全部成功或全部不执行;
- 检查错误、清洗错误和最终未稳定如何返回;
- 第一版源码、测试和依赖边界是什么。
如果这些问题留给实现时临时决定,组件很容易各自返回不同格式,或者直接生成整篇新 Markdown,最终无法统一
验证和追踪修改。
## 2. 目标与非目标
目标:
- 建立只处理内存字符串的 Python 核心;
- 确定快照、问题、候选修改、实际改动和运行结果的职责;
- 让具体组件只负责定位问题和提出精确修改,不直接改写整篇 Markdown;
- 让公共修改执行器统一验证范围、冲突、原子性和实际改动记录;
- 实现组件独立调用、流水线检查、单轮清洗和最终只读复查;
- 使用测试专用组件验证组合机制,不把真实清洗语义混入架构实现。
非目标:
- 不实现任何面向论文、GovDoc 或其他项目的真实清洗组件;
- 不引入 Markdown parser、AST、HTML parser 或其他运行依赖;
- 不读取或写入 Markdown 文件,不提供 CLI、批处理或服务接口;
- 不建设配置文件、profile 文件格式、插件自动发现或第三方插件市场;
- 不定义 JSON、数据库或长期审计文件格式;
- 不读取 PDF、图片、转换器 JSON、项目目录或外部真实材料;
- 不承诺第一版组件扩展接口已经长期稳定。
## 3. 名称与技术边界
- 项目展示名暂定为 `mdpolish`
- Python 分发名和导入名使用小写 `mdpolish`
- 源码包位于 `src/mdpolish/`
- 第一版支持 Python 3.11 及以上版本;
- 运行时只使用 Python 标准库;
- 使用 `pyproject.toml` 管理项目,构建后端采用 Hatchling;
- 开发检查使用 pytest、Ruff 和 mypy,具体依赖版本只在 `pyproject.toml` 中维护。
选择 Python 3.11 是为了使用现代类型能力,同时不把本地 Python 3.13 环境变成最低要求。第一版不引入解析器,
是为了先验证组件和修改协议;具体结构规则需要什么解析能力,由后续真实组件 design 决定。
## 4. 方案比较与决定
### 4.1 组件直接返回整篇新 Markdown
接口最简单,但流水线无法确认组件实际改了哪里,也无法统一检查过期位置、范围冲突和部分失败。组件的检查逻辑
还可能与修改逻辑逐渐分离。
不采用。
### 4.2 组件返回 AST,由流水线重新输出全文
适合格式化器和结构化编译,但会让没有被组件选中的 Markdown 也发生书写形式变化。第一版还需要先选择 parser、
扩展方言和 renderer,超出了当前最小闭环。
不采用。
### 4.3 组件报告问题及精确文本修改
组件只读取当前快照,返回问题和可选候选修改;公共执行器验证并应用修改。这样可以保留原文、统一审计,
也可以拒绝过期或重叠修改。
采用此方案。
## 5. 总体结构
```text
Markdown 字符串
DocumentSnapshot
Component._check_snapshot()
├── Issue
│ └── 可选 ProposedChange
│ └── 一个或多个 TextEdit
公共修改执行器
├── 验证快照、范围、原文和冲突
├── 原子应用当前组件的全部自动修改
└── 生成 Change 和新 DocumentSnapshot
下一个 Component
最终快照只读复查
TransformResult
```
核心分为四层:
- **数据模型:** 不可变地表达快照、范围、问题、修改、错误和结果;
- **组件基类:** 提供统一的 `check``transform` 行为,只把问题定位留给具体组件;
- **修改执行器:** 是唯一能够把 `TextEdit` 应用到 Markdown 的位置;
- **流水线:** 负责组件顺序、检查错误汇总、清洗失败停止和最终只读复查。
文件读写、终端输出和未来配置不进入这四层。
## 6. 快照与位置
### 6.1 `DocumentSnapshot`
快照至少包含:
- `markdown`:当前完整 Markdown 字符串;
- `sha256``markdown.encode("utf-8")` 的 SHA-256 十六进制摘要。
哈希由库根据 Markdown 计算,调用方不能传入一个自称匹配的哈希。空字符串是合法输入。库不自动改变编码、
换行符、Unicode 规范形式或文件末尾换行。
第一版快照不包含路径、文件名、PDF、图片、项目 ID、时间戳或任意外部元数据。
### 6.2 `TextSpan`
文本范围使用 Python 字符串下标:
- `start` 包含;
- `end` 不包含;
- 必须满足 `0 <= start <= end <= len(markdown)`
Python 字符串下标是第一版唯一权威位置。行号和列号由快照与下标计算,只用于显示,不作为修改依据。
这里的位置按 Unicode 码点工作,不按 UTF-8 字节或用户看到的字形数量工作。
## 7. 问题与候选修改
### 7.1 处理能力
问题的处理能力固定为三种:
- `detect_only`:只报告,不包含候选修改;
- `suggestion`:包含候选修改,但自动清洗不应用;
- `auto_fix`:包含能够自动应用的候选修改。
`detect_only` 如果携带候选修改,或者 `auto_fix` 没有候选修改,都属于组件契约错误。
### 7.2 `Issue`
一条问题至少包含:
- 产生问题的快照哈希;
- 组件标识和组件版本;
- 问题范围;文档级问题可以没有具体范围;
- 简明说明和可复核证据;
- 处理能力;
- 可选的候选修改。
问题只描述某个快照中的事实。快照变化后,它可以继续作为历史记录,但不能直接用于修改新快照。
### 7.3 `ProposedChange` 与 `TextEdit`
一个候选修改表示解决一条问题所需的完整动作,可以包含一个或多个 `TextEdit`。同一候选修改中的编辑必须
全部应用或全部不应用。
每个 `TextEdit` 至少包含:
- 目标快照哈希;
- `TextSpan`
- `expected_text`:修改前该范围必须准确等于的原文;
- `replacement`:替换内容。
插入使用 `start == end` 和空 `expected_text`;删除使用空 `replacement``replacement`
`expected_text` 完全相同的无效修改视为组件契约错误,不生成虚假的改动记录。
## 8. 组件接口
`Component` 使用抽象基类,而不是仅使用结构化 `Protocol`。公共基类负责保持独立调用与流水线调用的语义一致。
每个具体组件只实现以下扩展点:
- 组件标识、组件版本和当前参数;
- `_check_snapshot(snapshot)`:读取快照并返回问题,不产生副作用。
组件标识使用稳定的小写字符串;同一语义不能因为改了 Python 类名就更换标识。组件版本使用
`MAJOR.MINOR.PATCH` 形式。组件参数必须能表示为确定的只读基础数据,流水线将实际参数记录到结果中。
公共基类提供:
- `check(markdown)`:建立快照,执行该组件检查并返回检查结果;
- `transform(markdown)`:等价于只包含该组件的流水线清洗。
具体组件不得重写公共 `check``transform` 或修改执行器。类型声明使用 `final` 标记这些入口;代码评审和测试
同时检查组件只实现规定扩展点。
组件必须是确定性的:相同 Markdown、组件版本和参数必须产生相同问题及候选修改。组件不能读取文件、网络、
环境变量、当前时间或随机数,也不能修改传入对象和外部状态。
第一版流水线禁止出现两个相同组件标识的实例。需要用不同参数运行同一组件两次时,应由项目重新考虑组件边界,
不能依靠重复 ID 制造含义不清的执行记录。
## 9. 公共修改执行器
流水线不会把多个组件在旧快照上产生的修改集中到最后再应用。每个组件都针对当前快照检查;该组件结束后,
它的自动修改作为一个批次交给公共执行器。
执行器按以下顺序验证当前组件的整个批次:
1. 问题和编辑的快照哈希都等于当前快照哈希;
2. 所有范围合法;
3. `markdown[start:end]``expected_text` 完全一致;
4. 不存在重复编辑、范围重叠或同一位置的多个插入;
5. 所有编辑都会实际改变内容。
相邻但不重叠的范围可以同时修改。验证全部通过后,执行器按位置从后向前应用编辑,避免前面的修改使后面的
下标失效。任意一项验证失败,当前组件的整个批次都不应用。
这里的“当前组件整个批次”包括该组件本次检查产生的所有 `auto_fix` 候选修改。`suggestion``detect_only`
问题永远不进入自动修改批次。
### 9.1 `Change`
每个实际应用的 `TextEdit` 产生一条 `Change`,至少记录:
- 组件标识和版本;
- 组件在流水线中的执行位置;
- 修改理由;
- 修改前范围、`before``after`
- 修改前后的快照哈希;
- 所属候选修改,使一次多位置动作可以整体追踪。
修改记录描述实际发生的变化,不复制未执行的建议,也不把问题记录冒充改动记录。
## 10. 检查流程
`Pipeline.check(markdown)` 建立一个输入快照。所有组件按照项目给出的顺序检查同一个快照,Markdown 全程不变。
- 一个组件正常完成后,流水线按原顺序收集问题;
- 一个组件抛出异常或返回违反契约的数据时,流水线记录结构化组件错误,然后继续检查后续组件;
- 检查结果记录输入哈希、实际组件顺序、版本、参数、问题和错误;
- 只要存在组件错误,检查结果就不能表示为完整成功,但已经获得的问题仍然保留。
组件异常不能被静默忽略。错误至少记录组件身份、错误阶段、异常类型和安全的错误说明。是否保存 traceback
留在内存实现中决定,不向未来报告格式作承诺。
## 11. 清洗流程与运行状态
### 11.1 单轮清洗
`Pipeline.transform(markdown)` 按以下步骤运行:
1. 建立输入快照;
2. 按顺序让当前组件检查当前快照;
3. 收集该组件的 `auto_fix` 候选修改;
4. 原子验证并应用当前组件的整个修改批次;
5. 有修改时建立新快照,下一个组件只能读取新快照;
6. 所有组件各执行一次后,对最终快照运行最终只读复查。
清洗阶段如果组件异常、返回无效数据或修改批次冲突,流水线立即停止,不继续运行后面的组件,也不进行最终复查。
之前组件已经完成的内存修改和 `Change` 保留在失败结果中,但不能作为成功输出。
### 11.2 最终只读复查
最终复查让所有已选组件按照原顺序检查最终快照,不应用任何修改。
- 发现仍可由已选组件 `auto_fix` 的问题:状态为 `unstable`
- 只剩 `detect_only``suggestion` 问题:保留为未解决问题,不妨碍核心流水线成为 `success`
- 最终复查发生组件错误:状态为 `failed`;复查继续检查其余组件,以汇总只读阶段的错误和问题。
是否因为未解决的 `detect_only``suggestion` 问题阻止下游使用,由使用项目决定,不写死在共用核心中。
### 11.3 状态与文本字段
清洗状态至少包括:
- `success`:单轮清洗和最终复查完整完成,没有仍可自动修复的问题;
- `failed`:组件执行、数据契约或修改验证失败;
- `unstable`:修改阶段没有错误,但最终复查仍发现已选组件可自动修复的问题。
为避免调用方忽略状态并误用半成品:
- `success` 只提供 `output_markdown``partial_markdown` 为空;
- `failed``unstable` 不提供 `output_markdown`,只提供诊断用的 `partial_markdown`
- 三种状态都记录输入哈希、当前内容哈希、组件清单、实际改动、未解决问题和错误。
输入本身从不被原地覆盖。即使输出内容与输入完全相同,只要最终复查通过,也可以是零改动的 `success`
## 12. 源码与测试结构
批准后创建以下最小结构:
```text
pyproject.toml
src/
└── mdpolish/
├── __init__.py
├── py.typed
├── component.py
├── edits.py
├── models.py
└── pipeline.py
tests/
├── test_component.py
├── test_edits.py
├── test_models.py
└── test_pipeline.py
```
- `models.py` 只放不可变的数据模型和状态枚举;
- `component.py` 放组件基类和组件契约验证;
- `edits.py` 放纯文本修改验证与应用;
- `pipeline.py` 放检查、单轮清洗和最终复查编排;
- `__init__.py` 只导出第一版公共对象;
- `py.typed` 声明分发包提供类型信息;
- 测试辅助组件只存在于 `tests/`,不发布成示例清洗能力。
如果实现中发现这些边界导致循环依赖,可以在不改变公共职责的前提下机械拆分模块;新增新的业务层、运行依赖
或公共入口仍需要重新评审。
## 13. 测试专用组件与验收
第一版不借真实文档验证,也不把测试字符串包装成正式清洗规则。测试中建立最小假组件,分别产生固定问题、
建议修改、精确替换、组件异常和连锁影响。
至少覆盖:
- 空 Markdown、中文、换行和 Unicode 组合字符;
- 插入、删除、替换以及一次问题包含多个编辑;
- 相邻范围可以应用,重叠范围、重复编辑和同点插入明确失败;
- 哈希过期、范围越界、`expected_text` 不符和无效修改明确失败;
- 当前组件批次全部成功或全部不应用;
- 检查流程记录错误后继续其他组件;
- 清洗流程遇错立即停止,并区分成功输出与部分文本;
- 后一个组件制造前一个组件的新问题时,最终复查返回 `unstable`,且不自动开始第二轮;
- `suggestion``detect_only` 不被自动应用;
- 组件独立运行与单组件流水线结果一致;
- 成功流水线再次运行不产生实际改动;
- 输入字符串不被修改,相同输入、组件和参数产生相同结果。
批准并实现后,基础验证至少包括:
```bash
ruff check .
mypy src tests
pytest
```
README 届时记录实际可用命令。只有这些命令真实运行成功后,才能报告对应检查通过。
## 14. 风险与代价
- **没有公共 AST:** 结构复杂的组件以后可能需要重复解析;先保持解析细节为组件内部实现,等真实规则证明需要
共享分析后再设计。
- **Python 字符位置不是跨语言协议:** 第一版只承诺 Python API;以后输出机器可读跨语言格式时,需要单独定义
坐标语义,不能直接假设 JavaScript UTF-16 或 UTF-8 byte offset 与之相同。
- **最终复查增加检查成本:** 选中的组件最多检查两次,但换来对组合连锁影响的明确判断,第一版接受此代价。
- **组件级原子批次可能放弃部分正确修改:** 这是有意的保真选择;组件应修复自己的冲突,而不是让流水线猜测。
- **失败结果仍含部分 Markdown:** 使用独立字段并让成功输出为空,降低误用风险;未来文件适配层不得默认写出
`partial_markdown`
- **结果可能包含原文片段:** 第一版结果只存在内存,不建设日志和持久化;以后新增 reporter 时必须单独评审
脱敏和保存边界。
- **组件版本需要维护:** 组件语义、定位或修改行为变化时必须更新版本,不能只改代码而保留相同审计身份。
## 15. 批准后的实施边界
批准本设计只授权:
1. 创建第 12 节列出的工程与测试文件;
2. 实现第 5 至 11 节描述的内存核心;
3. 创建测试专用假组件并完成第 13 节验证;
4. 根据真实实现更新 README 当前阶段和基础检查;
5. 实现完成后新增 `research-wiki/explanation/` 文档,解释当前实际架构。
批准本设计不授权实现真实清洗规则,不授权读取真实数据,不授权文件覆盖、CLI、发布、提交或推送。
@@ -11,7 +11,7 @@ ClinDB-ReviewBench 是师姐的论文清洗项目。`data/` 下当前 5 份 DOI
JAMA、EJHF、Statistics in Medicine/arXiv、Springer/arXiv、Disaster Med Public Health Preparedness
是它的首批输入,未来会继续扩充同源转换产物。
本仓库(govdoc-md-cleaner)为该项目的数据提供清洗能力;ClinDB-ReviewBench 通过 profile
本仓库(mdpolish)为该项目的数据提供清洗能力;ClinDB-ReviewBench 通过 profile
表达论文场景的规则组合,不把论文专属规则写进通用核心。
## 2. 第一版清洗目标
@@ -0,0 +1,143 @@
# HTML 表格清洗专题调研:业界工具在 Markdown 清洗中如何处理 HTML 表格
> 状态:调研记录,尚未进入任何 design。
>
> 调研日期:2026-08-21。
>
> 定位:回答一个具体问题——Markdown 清洗中遇到 HTML 表格,业界工具实际怎么做。
> 结论用于印证或修正 [`../reference/GOVDOC_SAAS_CLEANING_SCOPE.md`](../reference/GOVDOC_SAAS_CLEANING_SCOPE.md)
> 中 T001/T002 的方向,不构成对任何方案的批准。
## 1. 结论先行
围绕“Markdown 里的 HTML 表格怎么办”,生态里的工具分成三种流派:
| 流派 | 做法 | 代表 |
|---|---|---|
| 生成端保真 | 复杂表格直接输出 HTML,不做管道表格 | MinerU、DoclingHTML/JSON 视图) |
| 强行归一 | 全部转成管道表格,合并单元格静默损坏或内容重复 | Turndown + gfm 插件、DoclingMarkdown 视图) |
| 保真派 | 容错解析 → 校验网格 → 简单表转 GFM、复杂表保留 HTML/JSON | 本项目审计 T001 方向、Pandocgrid tables / AST |
支撑这张表的共同事实是:**GFM 管道表格语法在原理上表达不了合并单元格**。三种流派只是对这条约束的
不同回答——绕开它、硬转它、或者按能力分流。
## 2. 语法能力边界:GFM 管道表格没有合并单元格写法
Pandoc 手册对各家 Markdown 表格方言的原文描述(2026-08-21 从官方 MANUAL 核实):
| 方言 | 合并单元格 | 单元格内块级元素 |
|---|---|---|
| pipe tables(≈GFM 表格) | 不支持(单元格不能跨多行) | 不能包含块级元素 |
| multiline tables | 明确不支持跨行/跨列单元格 | 可以 |
| grid tables | **支持**"Cells can span multiple columns or rows" | 可以 |
| HTML `<table>` | 原生 `rowspan`/`colspan` | 可以 |
grid tables 是唯一支持合并单元格的 Markdown 表格语法,但它是 Pandoc 扩展,GitHub 不渲染,
对“清洗后还要在 GFM 渲染器里查看”的场景不可用。所以在 GFM 方言内部,合并单元格没有任何无损写法;
想保真只能保留 HTML,或存结构化 JSON。
Pandoc 手册同时警告:从表达能力更强的格式转换时"some document elements, such as complex tables,
may not fit""can be expected to be lossy"。
来源:[Pandoc MANUAL - Tables](https://pandoc.org/MANUAL.html)
## 3. 生成端:PDF→Markdown 转换器为什么输出 HTML 表格
MD 文档里出现 HTML 表格,通常不是 bug,而是转换器面对合并单元格(`rowspan`/`colspan`)、
多级表头等管道语法表达不了的结构时的标准回退。
| 工具 | 表格输出策略 | 依据 |
|---|---|--- |
| [MinerU](https://github.com/opendatalab/MinerU) | 所有表格一律输出 HTML 嵌在 Markdown 中,不做管道表格;支持跨页表格拼接 | 官方 README 功能列表 |
| [Docling](https://docling.org/) | 内部 TableFormer 模型专门恢复合并单元格;同一文档可导出 HTML / Markdown / JSON 三种视图 | 官网能力页 + docling-core 2.92.0 源码 |
对本项目的含义:HTML 表格是合法的中间形态,不是待清除的垃圾。清洗目标不是“消灭 HTML 表格”,
而是“识别哪些表格结构正确、哪些在转换中损坏”。
## 4. 反面教材一:Turndown 静默产出错位表格
[Turndown](https://github.com/mixmark-io/turndown) 是最流行的 HTML→Markdown 转换库之一,
[turndown-plugin-gfm 的 tables.js](https://github.com/mixmark-io/turndown-plugin-gfm/blob/master/src/tables.js)
v1.0.2,2018 年发布后基本未改)只做两件事:
1. 首行不是全 `<th>`(无表头行)的表格:保留 HTML 不转;
2. 其余表格:按 DOM 位置逐格输出管道符。
它**完全没有 `colspan`/`rowspan` 的处理代码**。合并单元格不触发上面的回退,直接按 DOM 位置压扁,
转出列数不齐的坏表,且**不报任何错**。
对本项目的含义:
- “无表头就不转”是能力判断驱动的回退,这个思想是对的;但它的能力判断漏掉了合并单元格;
- 连最流行的转换库在这里都会静默弄坏表格——审计要求“先建 DOM、校验网格、禁止正则替换”有真实事故支撑;
- 选 HTML→Markdown 转换库时,“是否处理 span”必须列入验证项,不能信 README 宣称。
## 5. 反面教材二:Docling 的 Markdown 导出重复合并单元格内容
读了 docling-core 2.92.0 的源码(wheel 解包,2026-08-21):
- 内部 `TableCell``row_span`/`col_span` 和起止行列偏移;`TableData.grid` 属性把同一个 cell 对象
**铺满**它覆盖的每个 (行, 列) 位置;
- **HTML 序列化器**`transforms/serializer/html.py`):遍历网格时跳过被覆盖的续位
`rowstart != i``colstart != j``continue`),只在起始位置输出,并正确带上
`rowspan="N"`/`colspan="N"`——语义保真;
- **Markdown 序列化器**`transforms/serializer/markdown.py``MarkdownTableSerializer`):
直接遍历铺满后的 grid,每个位置都输出 `col.text`——一个 `row_span=3` 的单元格内容在 Markdown
输出里**重复出现 3 次**。转义只处理换行和管道符(`\n`→空格、`|``&#124;`),再用
tabulate `tablefmt="github"` 输出管道表格。
即 Docling 面对“Markdown 视图必须有合并单元格”的需求,选择了**内容重复**来保住矩形形状。
这是“强行转管道表格会丢语义”的又一个实例,和 Turndown 的压扁是同一根源的两种表现。
对本项目的含义:
- “转 GFM”不是免费的格式变换,每一家实现都发明了自己的有损映射;
- 如果未来用 Docling 做回源提取(调研报告第 6.1 节的候选方向),它的 Markdown 导出不能直接当作
保真输出使用,需要用它的 `DoclingDocument` JSON 或 HTML 视图;
- 审计 T001 说“强行转 GFM 会丢失语义”,这里的机制证据是:跨行列单元格要么被压扁(Turndown)、
要么被重复(Docling)、要么失去合并关系本身(都失去 `rowspan`/`colspan` 语义)。
## 6. 清洗与格式化工具:主流选择是“不动 HTML 块”
| 工具 | 对 Markdown 内 HTML 表格的行为 | 来源 |
|---|---|---|
| remark / mdformat | raw HTML 当不透明块原样传递,不重新格式化、不转换 | [mdformat](https://mdformat.readthedocs.io/) 官方文档(核心保证是格式化前后 AST 一致,HTML 块不在处理范围) |
| rumdl MD033no-inline-html | 报告 `<table>`(它有 Markdown 等价物),但 `fix` 只自动转 `em/strong/code/a/img/br/hr` 等行内简单标签,**不含表格**`allowed-inside = ["table"]` 可整块豁免 | 本地 `reference/rumdl/docs/md033.md` |
| rumdl MD056table-column-count | 校验每行列数与表头一致,自动修复方式是补/删空单元格 | 本地 `reference/rumdl/docs/md056.md` |
| rumdl MD058blanks-around-tables | GFM 表格前后补空行 | 本地 `reference/rumdl/docs/md058.md` |
两点值得注意:
1. **没有主流工具自动把 HTML 表格转成 GFM 表格。** 连以“消灭 HTML”为目标的 MD033 都把表格留在
“只报告、不修复”的范围里——因为工具作者知道这个转换会弄坏表格。
2. **MD056 的自动修复方向与本项目审计相反。** 审计 T002 反对“补空单元格凑齐列数通过语法检查”,
因为这可能掩盖静默丢列;MD056 恰恰把补空作为修复手段。借用这类规则时必须关掉它的自动修复,
只取检测部分。
## 7. 与既有材料的关系
- 审计 T001`../reference/GOVDOC_SAAS_CLEANING_SCOPE.md` 第 4.4 节)的五步法——容错解析建 DOM、
展开 rowspan/colspan 校验二维网格、简单矩形表转 GFM / 复杂表保留 HTML 或 JSON、拆多行、
回源确认幻觉——与本次调研的所有正面证据一致,未发现需要修正的点;
- [`markdown-cleaning-ecosystem-research-2026-08-20.md`](markdown-cleaning-ecosystem-research-2026-08-20.md)
第 5 节的推荐流程(html5lib 容错解析、禁正则、按合并单元格分流)同样得到印证;
- 新增的证据是反面案例的具体机制:Turndown 的压扁路径、Docling Markdown 视图的重复路径、
Pandoc 手册的方言能力原文、MD056 修复方向与审计相反。
## 8. 对本项目的待决问题(不是结论)
以下问题在对应 design 时需要回答,本调研只提供背景:
1. 简单/复杂表格的分界线,除了“有无合并单元格”,是否还要看单元格内块级元素、嵌套表格和表头层级;
2. 复杂表保留的“规范 HTML”具体规范到什么程度(属性白名单?标签重排?缩进策略?);
3. JSON grid 的格式是否对齐 Docling 的 `TableCell`row_span/col_span/offset 字段),
还是自定义 schema——涉及与未来回源 adapter 的成本权衡;
4. 无表头表格(Turndown 的回退条件)按哪种流派处理:补合成表头转 GFM,还是保留 HTML。
## 9. 验证状态
- Pandoc 手册、Turndown 源码、MinerU README、Docling 官网:2026-08-21 通过网络核实;
- docling-core 2.92.0:下载 wheel 解包读源码核实,涉及
`MarkdownTableSerializer.serialize``TableData.grid`、HTML 序列化器的 span 处理;
- rumdl 三条规则:读本地 `reference/rumdl/docs/`(该目录为镜像副本,以 rumdl 上游为准);
- 未验证:各工具在本项目真实数据上的实际表现——需要等对应组件 design 批准后用受控样本测试。