重构为函数式通用 Markdown 修改库
This commit is contained in:
@@ -1,203 +1,208 @@
|
||||
# mdpolish
|
||||
|
||||
实验室共用的 Markdown 清洗研究与基础工具库。项目不属于 GovDoc 专用组件,也不只服务政务文档。
|
||||
`mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、
|
||||
有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
|
||||
|
||||
本仓库面向实验室内不同项目复用,用于清洗 PDF、DOCX、OCR、网页等上游管线生成的 Markdown,统一解决
|
||||
格式噪声、结构损坏、内容异常、修改追踪和多用途派生问题。各项目共享通用清洗能力,再通过独立配置或
|
||||
profile 表达论文、政务文档、RAG、文档对比等不同需求。
|
||||
当前版本是 `0.2.0`。库只处理内存中的 Markdown 字符串,不读取或写入文件,不提供默认流水线,也不包含任何项目的
|
||||
规则集合、数据清单、实验脚本或评审工具。
|
||||
|
||||
仓库当前已从纯文档治理进入第一版核心和 ClinDB 第一批组件实现阶段:已经提供可安装的 Python 内存处理包、
|
||||
8 个论文清洗组件、本地实验入口,以及同仓但与清洗运行解耦的只读评审前端。实验可以保存指定论文的成功输出、
|
||||
审计和 diff;评审前端可以同时查看清洗前后全文,并逐组件复放修改。仓库仍不提供面向任意数据集的完整规则集、
|
||||
公共命令行工具、通用文件适配器或生产接口,因此目前还不是拿来即可完成任意 Markdown 清洗的成品工具。
|
||||
## 当前能力
|
||||
|
||||
## 当前阶段
|
||||
| 能力 | 作用 | 明确边界 |
|
||||
| --- | --- | --- |
|
||||
| `Modifier` | 把不可变元数据与普通提议函数组合起来 | 函数只提议修改,不直接改字符串或文件 |
|
||||
| 精确编辑执行器 | 校验快照、范围、原文、重复和冲突后原子应用一个批次 | 不判断项目业务语义 |
|
||||
| `Pipeline` | 按调用方顺序运行修改器,并对最终快照做只读稳定性复查 | 不自动选规则、不重排、不循环执行 |
|
||||
| `regex_replace()` | 把非空正则匹配转换为精确编辑 | 不提供规则注册表、配置加载或默认模式 |
|
||||
| `mapped_line_join()` | 按调用方提供的映射合并跨行片段 | 库内没有默认词表,不猜测未知词 |
|
||||
| HTML 表格修改器 | 处理严格表格子集的实体和单行布局 | 不是完整 HTML parser,也不是 HTML→GFM 转换器 |
|
||||
|
||||
项目当前已经进入第一版可执行核心和真实组件验证阶段:
|
||||
一次运行会返回 `success`、`failed` 或 `unstable`:
|
||||
|
||||
- 提供可安装的 Python 3.11+ 内存处理包,运行时只依赖标准库;
|
||||
- 已实现不可变数据契约、组件基类、原子修改执行器、顺序流水线、审计记录和最终稳定性复查;
|
||||
- 已实现 ClinDB 第一批 8 个正式组件,覆盖 Word 批注、手稿行号、arXiv 戳、重复页眉、批准映射断词、
|
||||
HTML 表格实体与布局、参考文献空行;
|
||||
- 已有仓库内实验运行层,能严格读取显式清单、保存成功 Markdown、JSON 审计、unified diff 和评审定位文件,
|
||||
并保持输入不变;
|
||||
- 已有独立的 `reviewer/` 本地只读前端,服务只消费一次已发布的产物和定位文件,并与报告层共用纯 Python 快照重放逻辑,
|
||||
不导入组件、不调用流水线,也不重新清洗;
|
||||
- 第一批流水线已对 5 份论文 Markdown 完成保存型实验,5/5 成功,共记录 155 条修改,第二次运行零修改;
|
||||
- 当前基础检查为 Ruff、mypy、229 项 pytest 测试,以及前端 ESLint、TypeScript、9 项 Vitest 测试和生产构建,
|
||||
实际命令见本文“当前可用检查”。
|
||||
- `success`:所选修改器完成运行,且对最终结果不再提出修改;
|
||||
- `failed`:修改器、契约或编辑验证发生错误;
|
||||
- `unstable`:运行没有错误,但最终复查仍发现有效候选修改。
|
||||
|
||||
项目还没有面向任意数据集的完整清洗规则集、Markdown/HTML 通用 parser、profile 格式、通用文件输入接口、公共 CLI、
|
||||
通用批处理或生产接口。评审前端也不是公共 Web 服务:它只绑定本机回环地址,只读展示 Markdown 源文和审计,不提供
|
||||
渲染预览、在线编辑或重新清洗。当前 first-batch 实验脚本只固定运行已批准的 5 份论文和 8 个组件;它只能证明当前
|
||||
8 类确定规则已经闭环,不能据此认为论文中的缺失内容、乱码、复杂表格、图片或 GovDoc 已具备完整清洗能力。
|
||||
`success` 只代表本次选择的修改器已经稳定,不代表文档不存在其他质量问题。
|
||||
|
||||
## 服务对象与复用目标
|
||||
## 安装
|
||||
|
||||
当前已经明确的使用场景包括:
|
||||
项目仍在仓库内开发,使用项目可以从本地路径安装:
|
||||
|
||||
- **论文清洗**:`data/` 中现有内容来自师姐的项目,包含论文 PDF 的 Markdown、JSON、图片等转换产物;
|
||||
- **GovDoc**:政务、招投标、采购、合同等文档的清洗、比对和 RAG 前处理;
|
||||
- **未来实验室项目**:后续可以继续接入其他需要 Markdown 质量检查、规范化或用途派生的项目数据。
|
||||
|
||||
当前用例只用于发现真实问题和验证通用能力,不能反过来限定库的设计。核心代码不得依赖论文 DOI、
|
||||
GovDoc 目录、具体客户名称或某一转换器的固定输出路径。
|
||||
|
||||
## 测试数据
|
||||
|
||||
- **论文 Markdown**:`data/md/`,共 5 份,按 ClinDB-ReviewBench 中使用的论文缩写命名,
|
||||
供本地查看和组件只读验证;
|
||||
- **GovDoc Markdown**:`/home/lihaoze/gov_test_data/compare`,共 7 组、45 份,输入位于各组 `uploads/` 下,
|
||||
保持仓库外只读,不复制到本项目。
|
||||
|
||||
两组数据都不是可提交的自动测试 fixture。`data/` 已被 Git 忽略,清洗实验不得覆盖这些输入。
|
||||
|
||||
本地清洗实验产物位于 `artifacts/<YYYY-MM-DD>/runs/<run_id>/`。产物可能包含完整原文;其中评审定位文件还包含
|
||||
输入源的绝对路径,因此整个目录均受 Git 忽略,默认保留 30 个日历日,不得提交或复制到外部系统。当前只批准为上述
|
||||
5 份论文副本保存产物,未批准保存 GovDoc 输出。
|
||||
|
||||
## 面向复用的设计原则
|
||||
|
||||
- **通用核心**:只接收 Markdown;第一版只执行确定、可审计的精确修改,不读取 PDF、图片或转换器 JSON;
|
||||
- **输入边界**:PDF/OCR/DOCX/HTML 转换和外部材料核验由使用项目或上游流程负责,不写入共用组件契约;
|
||||
- **项目 profile**:论文、GovDoc、对比、RAG、公开脱敏等规则独立组合,不互相污染默认行为;
|
||||
- **保真优先**:不确定内容默认保留;当前核心不猜测修改,也不承担人工确认流程;
|
||||
- **可复现**:规则、配置、输入哈希、输出和每次变更都可以追踪;
|
||||
- **可扩展**:新增项目在自身边界处理上游适配,并主要组合或补充组件和 profile,而不是复制一套清洗器。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
mdpolish/
|
||||
├── .gitignore
|
||||
├── AGENTS.md
|
||||
├── CLAUDE.md
|
||||
├── README.md
|
||||
├── pyproject.toml # Python 包、构建和开发检查配置
|
||||
├── src/
|
||||
│ └── mdpolish/
|
||||
│ ├── __init__.py # 第一版核心公共导出
|
||||
│ ├── _artifact_replay.py # 报告与评审器共用的纯快照重放
|
||||
│ ├── component.py # 组件基类和元数据契约
|
||||
│ ├── edits.py # 文本编辑验证与原子应用
|
||||
│ ├── experiment.py # 本地实验输入预检与批量编排
|
||||
│ ├── models.py # 不可变数据模型和运行状态
|
||||
│ ├── pipeline.py # 顺序执行和最终稳定性复查
|
||||
│ ├── reporting.py # JSON 审计、行列位置和 unified diff
|
||||
│ ├── artifact_store.py # 私有产物目录和原子发布
|
||||
│ ├── _html_table.py # 严格 HTML 表格词法范围
|
||||
│ ├── _text_ranges.py # 精确物理行与换行范围
|
||||
│ ├── py.typed # 类型信息声明
|
||||
│ └── components/
|
||||
│ ├── __init__.py
|
||||
│ ├── arxiv_submission_stamp.py
|
||||
│ ├── html_table_double_escape.py
|
||||
│ ├── html_table_layout.py
|
||||
│ ├── manuscript_line_number.py
|
||||
│ ├── page_break_word_join.py
|
||||
│ ├── reference_spacing.py
|
||||
│ ├── repeated_running_header.py
|
||||
│ └── word_review_comment.py
|
||||
├── scripts/
|
||||
│ ├── run_clindb_arxiv_experiment.py # 只含 arXiv 组件的历史实验入口
|
||||
│ └── run_clindb_first_batch_experiment.py # ClinDB 第一批 8 组件实验入口
|
||||
├── reviewer/ # 与组件和流水线解耦的本地只读评审器
|
||||
│ ├── .nvmrc # 前端开发使用 Node.js 24
|
||||
│ ├── package.json # React 依赖、检查和构建命令
|
||||
│ ├── server/ # Python 产物适配、快照重放接口和本地 HTTP 服务
|
||||
│ ├── src/
|
||||
│ │ ├── client/ # React 全文对比、组件时间线和审计界面
|
||||
│ │ └── shared/ # 浏览器使用的内部 API 类型
|
||||
│ └── tests/ # 前端响应、界面和源码安全测试
|
||||
├── tests/
|
||||
│ ├── test_arxiv_submission_stamp.py
|
||||
│ ├── test_artifact_store.py
|
||||
│ ├── test_clindb_first_batch_pipeline.py
|
||||
│ ├── test_component.py
|
||||
│ ├── test_edits.py
|
||||
│ ├── test_experiment.py
|
||||
│ ├── test_html_table_double_escape.py
|
||||
│ ├── test_html_table_layout.py
|
||||
│ ├── test_manuscript_line_number.py
|
||||
│ ├── test_models.py
|
||||
│ ├── test_page_break_word_join.py
|
||||
│ ├── test_pipeline.py
|
||||
│ ├── test_reference_spacing.py
|
||||
│ ├── test_repeated_running_header.py
|
||||
│ ├── test_reporting.py
|
||||
│ ├── test_word_review_comment.py
|
||||
│ ├── test_artifact_replay.py
|
||||
│ ├── test_reviewer_artifacts.py
|
||||
│ └── test_reviewer_server.py
|
||||
├── data/ # 本地测试数据;Git 忽略;此处只展开常用入口
|
||||
│ └── md/
|
||||
│ ├── dmp.md
|
||||
│ ├── ejhf.md
|
||||
│ ├── jama.md
|
||||
│ ├── sim.md
|
||||
│ └── springer.md
|
||||
├── artifacts/ # 本地敏感实验产物;Git 忽略
|
||||
│ └── <YYYY-MM-DD>/runs/<run_id>/
|
||||
│ ├── manifest.json
|
||||
│ ├── review-locator.json # 输入源定位和哈希;仅供本地评审
|
||||
│ └── documents/
|
||||
└── research-wiki/
|
||||
├── README.md # Wiki 分类与维护规则
|
||||
├── design/ # 批准前的选择;批准后冻结
|
||||
├── explanation/ # 当前有效机制及原因
|
||||
├── reference/ # 稳定查询事实
|
||||
├── guides/ # 已验证操作步骤
|
||||
└── scratch/ # 调研和未收敛材料
|
||||
```bash
|
||||
python -m pip install /path/to/mdpolish
|
||||
```
|
||||
|
||||
## 开始工作
|
||||
开发环境:
|
||||
|
||||
进入仓库后依次阅读:
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv/bin/python -m pip install -e '.[dev]'
|
||||
```
|
||||
|
||||
1. 本文件,确认当前阶段;
|
||||
2. `AGENTS.md` 或 `CLAUDE.md`,确认协作与安全边界;
|
||||
3. `research-wiki/README.md`,确认文档应放在哪里;
|
||||
4. 与任务直接相关的 `research-wiki/design/` 记录。
|
||||
运行时只依赖 Python 标准库,支持 Python 3.11 及以上版本。
|
||||
|
||||
第一版核心机制见 `research-wiki/explanation/first-executable-core.md`,ClinDB 第一批组件见
|
||||
`research-wiki/explanation/clindb-first-batch-components.md`,本地实验产物机制见
|
||||
`research-wiki/explanation/local-experiment-artifacts.md`,本地评审器机制见
|
||||
`research-wiki/explanation/local-markdown-reviewer.md`。实验和评审器的实际操作分别见
|
||||
`research-wiki/guides/run-local-clindb-first-batch-experiment.md` 和 `research-wiki/guides/review-local-cleaning-run.md`。
|
||||
解析器、CLI、文件适配器、profile 格式、图片资产打包和独立检查能力仍需分别设计;当前本地实验适配器和评审器不能
|
||||
被推导成这些公共接口已经获批。
|
||||
## 组装自己的流水线
|
||||
|
||||
项目拥有规则、参数和顺序。下面的正则规则与两个通用修改器最终都是 `Modifier`:
|
||||
|
||||
```python
|
||||
from mdpolish import Pipeline, RunStatus, regex_replace
|
||||
from mdpolish.modifiers import html_table_entity_unescape, mapped_line_join
|
||||
|
||||
normalize_spaces = regex_replace(
|
||||
modifier_id="my_project.normalize_spaces",
|
||||
version="1.0.0",
|
||||
pattern=r" {2,}",
|
||||
replacement=" ",
|
||||
applicability="把正文中连续两个及以上的 ASCII 空格收敛为一个;项目需自行排除不适用区域。",
|
||||
)
|
||||
|
||||
join_fragments = mapped_line_join(
|
||||
(
|
||||
("exam-", "ple", "example"),
|
||||
("rule-", "based", "rule-based"),
|
||||
)
|
||||
)
|
||||
|
||||
pipeline = Pipeline(
|
||||
(
|
||||
normalize_spaces,
|
||||
join_fragments,
|
||||
html_table_entity_unescape(),
|
||||
)
|
||||
)
|
||||
|
||||
result = pipeline.transform("an exam-\nple text")
|
||||
if result.status is not RunStatus.SUCCESS:
|
||||
raise RuntimeError(f"cleaning did not succeed: {result.status}")
|
||||
|
||||
assert result.output_markdown == "an example text"
|
||||
```
|
||||
|
||||
库不会保存 `output_markdown`。调用项目应先检查状态,再自行决定写入位置:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
source_path = Path("input.md")
|
||||
output_path = Path("output.md")
|
||||
|
||||
result = pipeline.transform(source_path.read_text(encoding="utf-8"))
|
||||
if result.status is RunStatus.SUCCESS and result.output_markdown is not None:
|
||||
output_path.write_text(result.output_markdown, encoding="utf-8")
|
||||
```
|
||||
|
||||
文件读取、输出命名、覆盖策略、批处理、CLI 和报告都属于调用项目,不属于 `mdpolish`。
|
||||
|
||||
## 编写项目自己的修改器
|
||||
|
||||
复杂规则使用普通函数返回精确候选修改,不需要继承库基类:
|
||||
|
||||
```python
|
||||
from mdpolish import DocumentSnapshot, Modifier, ProposedChange, TextEdit, TextSpan
|
||||
|
||||
|
||||
def propose_marker_removal(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
|
||||
marker = "[REMOVE-ME]"
|
||||
start = snapshot.markdown.find(marker)
|
||||
if start < 0:
|
||||
return ()
|
||||
end = start + len(marker)
|
||||
return (
|
||||
ProposedChange(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
reason="删除项目确认过的占位标记",
|
||||
edits=(
|
||||
TextEdit(
|
||||
snapshot_sha256=snapshot.sha256,
|
||||
span=TextSpan(start, end),
|
||||
expected_text=marker,
|
||||
replacement="",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
remove_marker = Modifier(
|
||||
modifier_id="my_project.remove_marker",
|
||||
version="1.0.0",
|
||||
parameters={"marker": "[REMOVE-ME]"},
|
||||
applicability="只删除项目声明的完整占位标记;不处理近似文本。",
|
||||
propose=propose_marker_removal,
|
||||
)
|
||||
```
|
||||
|
||||
修改器决定“建议改哪里、为什么改、改成什么”;公共执行器真正修改内存字符串。每项 `TextEdit` 都绑定当前快照哈希、
|
||||
半开字符串范围、预期原文和替换文本。任意候选无效时,该修改器当前批次不会产生部分修改。
|
||||
|
||||
项目函数应保持确定且无副作用,不读取文件、网络、环境变量、当前时间或随机数。Python 无法沙箱隔离任意函数;项目
|
||||
函数私下产生的外部副作用不属于本库的验证或审计范围。
|
||||
|
||||
## 通用修改器的严格边界
|
||||
|
||||
`mapped_line_join()` 只使用调用方显式传入的三元组:左片段、右片段和最终文本。它只处理相邻物理行或中间恰好一个
|
||||
同风格空行的情况,并检查 ASCII 词边界。
|
||||
|
||||
`html_table_entity_unescape()` 只在严格完整的 `<td>` / `<th>` 文本中处理 `&lt;`、`&gt;` 和
|
||||
`&amp;`。`html_table_layout()` 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。
|
||||
|
||||
这两个 HTML 修改器采用保守的词法子集,不识别 Markdown 围栏。混合换行、嵌套标签、`<tbody>`、嵌套表格或损坏
|
||||
结构会保持原样。
|
||||
|
||||
## 仓库结构
|
||||
|
||||
```text
|
||||
src/mdpolish/
|
||||
├── models.py # 不可变快照、精确编辑、审计和结果模型
|
||||
├── modifier.py # 函数式 Modifier 契约
|
||||
├── edits.py # 批次验证与原子应用
|
||||
├── pipeline.py # 有序执行与最终稳定性复查
|
||||
├── regex.py # 正则修改器工厂
|
||||
└── modifiers/ # 少量项目无关的通用修改器
|
||||
tests/ # 只使用虚构文本的核心与通用修改器测试
|
||||
research-wiki/
|
||||
├── design/ # 已批准决策及被冻结的历史记录
|
||||
├── explanation/ # 当前有效机制
|
||||
├── reference/ # 代码无法完整表达的稳定查询事实
|
||||
├── guides/ # 已实际验证的操作步骤
|
||||
└── scratch/ # 不作为当前事实的本地草稿
|
||||
```
|
||||
|
||||
## 当前不提供
|
||||
|
||||
- 文件适配器、公共 CLI、配置文件、profile 或批处理协议;
|
||||
- 自动规则发现、注册表或默认流水线;
|
||||
- Markdown AST、完整 HTML parser 或新运行依赖;
|
||||
- artifact、报告、Web/桌面评审器;
|
||||
- 任何业务项目的规则、固定参数、文档 ID、数据或验收统计。
|
||||
|
||||
公共边界与原因见
|
||||
[`0008-generic-functional-library-boundary.md`](research-wiki/design/0008-generic-functional-library-boundary.md),当前机制见
|
||||
[`functional-modifier-core.md`](research-wiki/explanation/functional-modifier-core.md)。旧 design 只保存历史决策,不代表当前
|
||||
交付能力。
|
||||
|
||||
## 当前可用检查
|
||||
|
||||
在已经安装开发依赖的仓库环境中运行:
|
||||
|
||||
```bash
|
||||
# 建立隔离环境并安装包与开发检查工具
|
||||
python -m venv .venv
|
||||
.venv/bin/python -m pip install -e '.[dev]'
|
||||
|
||||
# 第一版核心的基础验收
|
||||
.venv/bin/ruff check .
|
||||
.venv/bin/mypy src tests scripts/run_clindb_arxiv_experiment.py scripts/run_clindb_first_batch_experiment.py
|
||||
.venv/bin/mypy src tests
|
||||
.venv/bin/pytest
|
||||
.venv/bin/python -m pip wheel . --no-deps --wheel-dir /tmp/mdpolish-wheel-check
|
||||
```
|
||||
|
||||
# 本地评审器要求 Node.js 24 LTS;安装依赖后依次执行静态检查、测试和生产构建
|
||||
cd reviewer
|
||||
nvm use
|
||||
npm ci
|
||||
npm run check
|
||||
cd ..
|
||||
检查本地变更:
|
||||
|
||||
# 两份 Agent 入口除标题外必须一致;无输出且退出码为 0 表示通过
|
||||
diff -u <(tail -n +2 AGENTS.md) <(tail -n +2 CLAUDE.md)
|
||||
|
||||
# 查看当前 Wiki 中实际存在的文档
|
||||
find research-wiki -maxdepth 2 -type f | sort
|
||||
|
||||
# 检查本地变更
|
||||
```bash
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
上述 Python 验收已于 2026-08-24 在 Python 3.13.11 环境实际运行:Ruff 通过,mypy 检查 43 个源码、测试和实验脚本文件
|
||||
无问题,pytest 共 229 项测试通过。评审器验收也在当前用户 nvm 的 Node.js 24.19.0 环境实际运行:ESLint 和 TypeScript
|
||||
通过,Vitest 共 9 项测试通过,生产构建成功;并以一批真实的 5 文档、8 组件、155 条修改产物验证了接口读取和逐阶段哈希。
|
||||
当前环境没有可用的图形浏览器,因此页面视觉布局尚未进行真实浏览器人工验收。`requires-python` 仍以
|
||||
`pyproject.toml` 声明的 Python 3.11 及以上为准;本次结果不等于已经在每个受支持版本上完成兼容性验证。
|
||||
上述检查已于 2026-08-26 在 Python 3.13.11 环境实际运行:Ruff 通过,mypy 检查 21 个源码和测试文件无问题,
|
||||
pytest 共 104 项测试通过,`mdpolish-0.2.0-py3-none-any.whl` 构建成功。wheel 内容已单独检查,只包含通用 Python
|
||||
包、类型标记和包元数据,不包含项目规则、实验脚本、评审器或 Node.js 文件。
|
||||
|
||||
`pyproject.toml` 声明的 Python 3.11 及以上为支持范围;本次结果不表示已经在每个受支持版本上完成兼容性验证。
|
||||
|
||||
Reference in New Issue
Block a user