重构为函数式通用 Markdown 修改库

This commit is contained in:
2026-08-26 15:33:37 +08:00
parent 1abf72ccb1
commit bb0507db30
89 changed files with 1589 additions and 14850 deletions
@@ -0,0 +1,408 @@
# 0008:函数式通用库边界与项目组件外置
## 状态
已于 2026-08-26 经用户明确批准。本文自批准起冻结;后续若改变这里的公共边界或修改语义,应新增 design,
不得回写本文掩盖决策变化。
`supersedes: 0003`(范围有限):本文拟替代基于 `Component` 抽象基类的扩展接口和当前公共导出形式;
快照绑定、精确修改、整批验证、原子应用、显式顺序、单轮执行和最终稳定性复查继续保留。
`supersedes: 0005, 0007`(仓库职责范围):本地实验产物和评审器不再属于通用库交付物。它们曾经完成的实验
和验证仍是历史事实,但不继续作为 `mdpolish` 的当前能力维护。
`supersedes: 0006`(仓库职责范围):ClinDB 第一批组件、固定参数、流水线和验收计数不再由通用库拥有。
本文不否定这些规则当时在 5 份论文上的验证结果,只改变它们今后的代码归属。
`0001``0002` 中的保真优先、项目显式组装、Markdown 单一输入、核心无文件 I/O 和外部真实材料只读边界继续有效。
历史 design 保持冻结,不回写成新的决定。
## 1. 问题与可观察现象
仓库的底层修改执行器和 `Pipeline` 不认识具体项目,但当前交付物已经与 ClinDB 深度绑定:
- 发布包内包含 `paper.*` 组件,其中多项识别条件来自 5 份论文的特定形态;
- first-batch 脚本固定了 5 个文档 ID、8 个组件、6 条断词映射和执行顺序;
- README、reference、guide 和 explanation 以 ClinDB 的 155 条修改作为主要成果;
- artifact、review locator 和 React 评审器围绕这套本地实验流程继续扩张;
- 外部使用者若只需要安全修改内核,仍会看到项目术语、实验入口和第二套 Node.js 工具链。
这与新的目标不一致。新的 `mdpolish` 应是一个真正可复用的 Python 库:它定义怎样描述、组合、校验和应用修改,
但不拥有任何项目的规则集合、数据清单、流水线或评审流程。
当前 `Component` 已经被约束为确定、无副作用的对象,但外部扩展仍需要继承抽象基类、实现四个属性和一个私有方法。
这里的继承没有提供运行时隔离,反而增加了编写简单修改器的仪式。通用库更适合把行为表达为纯函数,把身份和参数表达为
不可变数据,再由 `Pipeline` 组合。
## 2. 目标与非目标
### 2.1 目标
- 把仓库收敛为可安装、项目无关的 Python 修改库;
- 以纯函数加不可变元数据代替 `Component` 抽象基类;
- 保留当前快照、精确编辑、冲突检查、原子应用、审计和稳定性复查不变量;
- 提供通用的正则修改器工厂,使项目规则可以在项目仓库中声明;
- 提供少量经合成测试验证、无需项目数据的通用修改器;
- 让任何项目显式创建自己的修改器、参数和 `Pipeline`
- 从当前能力、Python 包、测试和用户文档中移除 ClinDB、论文缩写、固定映射和真实样本计数;
- 移除通用库不再拥有的本地实验层和评审器工具链;
- 保持运行时只依赖 Python 标准库。
### 2.2 非目标
- 不在本轮建设公共 CLI、配置文件、profile、插件发现、LSP、Web 服务或桌面应用;
- 不建立新的 artifact schema、文件适配器或批处理协议;
- 不把 ClinDB 组件迁移到另一个仓库;修改其他仓库需要另行授权;
- 不修改、移动、复制或删除本地 `data/``artifacts/` 和仓库外真实材料;
- 不把现有项目规则改名后冒充通用组件;
- 不承诺任意用户函数在运行时被沙箱隔离;纯函数和无副作用是修改器契约,由内置实现和测试保证;
- 不在本轮引入 Markdown parser、HTML parser 或新的运行依赖;
- 不提供默认流水线。安装库或导入修改器不会自动修改任何文本。
## 3. 新的依赖方向
```text
使用项目
├── 项目规则函数
├── 项目参数与顺序
└── 项目文件、CLI、profile、报告和评审
mdpolish
├── 不可变快照与修改模型
├── 函数式 Modifier 协议
├── 修改验证与原子执行器
├── Pipeline
├── 通用正则修改器工厂
└── 少量通用内置修改器
```
依赖只能从使用项目指向 `mdpolish``mdpolish` 不导入项目包,不读取项目目录,不根据文件名选择规则,
也不保存任何项目的默认顺序或参数。
## 4. 函数式修改器接口
### 4.1 行为与元数据分开
修改行为使用一个普通可调用对象:
```python
ModifierFunction = Callable[
[DocumentSnapshot],
tuple[ProposedChange, ...],
]
```
库使用不可变的 `Modifier` 保存审计所需元数据和函数:
```python
@dataclass(frozen=True, slots=True)
class Modifier:
modifier_id: str
version: str
parameters: Parameters
applicability: str
propose: ModifierFunction
```
精确字段名可以在实现中根据类型检查做机械调整,但必须满足以下决定:
- 不要求外部作者继承基类;
- 修改逻辑是接收一个快照、返回不可变候选修改的普通函数;
- `modifier_id`、版本、参数和适用边界不得从函数名、模块路径或闭包内容自动猜测;
- 元数据在流水线开始时冻结,运行期间变化视为契约错误;
- 函数只能提出修改,仍不能直接应用编辑或返回一整篇新 Markdown;
- 相同输入、身份、版本和参数必须产生相同顺序的候选修改;
- 修改器不得读取文件、网络、环境变量、当前时间或随机数。
保留显式元数据是为了让外包组件仍可被审计和复现。函数式编程不等于丢弃组件身份和版本。
### 4.2 修改权限与数据流
这里把“修改权”拆成三层,避免把业务判断、字符串执行和文件写回混为一件事:
1. **使用项目拥有规则决策权。** 项目决定启用哪些 `Modifier`、传入什么参数、按什么顺序运行。修改器的
`propose(snapshot)` 只判断“建议改哪里、为什么改、改成什么”,不能原地改变不可变快照,也不能用一整篇
新 Markdown 绕过精确编辑契约。
2. **`mdpolish` 核心拥有验证和内存执行权。** `Pipeline` 调用修改器,公共执行器检查快照、范围、原文、重复和
冲突,然后才通过字符串切片应用编辑。一个修改器本轮的候选修改必须整批通过;任意一项无效时,该批不产生
部分结果。
3. **使用项目拥有持久化决定权。** `Pipeline` 只返回状态、结果文本和审计记录,不读取或写入文件。调用方检查
`success``failed``unstable` 后,自行决定是否以及怎样保存;`mdpolish` 不会自动覆盖输入文件。
修改器至少通过 `ProposedChange``TextEdit` 向核心提交以下信息:
- 候选修改及每条精确编辑所绑定的 `snapshot_sha256`
- 人可读且非空的修改原因 `reason`
- 使用 Python 字符串索引表示的半开范围 `TextSpan(start, end)`
- 该范围当前应有的原文 `expected_text`
- 替换后的文本 `replacement`
因此,一次调用的数据流固定为:
```text
项目组装 Modifier 与顺序
Modifier.propose(snapshot) 提出精确修改
mdpolish 验证并在内存中原子应用
Pipeline 返回状态、结果和审计记录
项目决定是否写回文件
```
项目规则当然可以决定自己的业务语义,也可以提出删除、替换或插入;但只有满足上述契约的候选修改才由核心执行。
Python 无法阻止项目函数私下写文件或直接调用字符串替换,这类副作用属于调用方绕过库契约,不属于 `mdpolish`
保证、审计或回滚的修改。
### 4.3 `Pipeline`
`Pipeline` 改为接收 `Iterable[Modifier]`,不再要求 `Component` 子类。其行为继续保持:
1. 预检全部修改器身份、版本、参数和重复 ID;
2. 按调用方顺序对当前快照调用 `propose`
3. 由公共执行器验证和原子应用当前修改器批次;
4. 为下一个修改器建立新快照;
5. 全部执行一次后,对最终快照进行只读稳定性复查;
6. 返回 `success``failed``unstable`,不自动开始第二轮。
本轮不自动重排修改器,不自动选择修改器,也不提供全局默认组合。
### 4.4 外部项目的两种扩展方式
简单规则优先使用库提供的工厂:
```python
remove_stamp = regex_replace(
modifier_id="my_project.remove_stamp",
version="1.0.0",
pattern=r"...",
replacement="",
)
```
复杂规则直接提供纯函数,再显式构造 `Modifier`
```python
def propose_project_changes(snapshot: DocumentSnapshot) -> tuple[ProposedChange, ...]:
...
project_modifier = Modifier(
modifier_id="my_project.rule",
version="1.0.0",
parameters=(),
applicability="...",
propose=propose_project_changes,
)
```
库不会自动注册或发现这些对象。
`Modifier``Pipeline` 接收的统一值类型;`regex_replace()` 是创建这种值的便捷工厂,不是与 `Modifier` 并列的
另一套接口。它返回的也是一个完整 `Modifier`。复杂规则与正则规则进入流水线后,都遵守同一套提议、验证、执行
和审计流程。
## 5. 通用正则修改器
第一版提供一个安全、窄边界的 `regex_replace()` 工厂:
- 调用方必须显式给出稳定 `modifier_id` 和版本;
- 输入为正则 pattern、固定 replacement、flags 和适用边界说明;
- pattern、replacement 和 flags 完整进入修改器参数,运行结果可以复核;
- 按 Python `re.finditer()` 的确定顺序定位非重叠匹配;
- replacement 使用 Python 正则的固定替换模板展开;
- 每个匹配生成绑定当前快照、范围和原文的精确编辑;
- 第一版拒绝零长度匹配,避免隐式插入和边界顺序不明确;
- 工厂不读 YAML,不维护规则注册表,也不提供默认规则集;
- 需要动态 replacement、跨匹配聚合或结构判断时,项目应编写自己的纯函数。
这个工厂提供正则能力,但不会退回“全局替换后返回整篇文本”的旧实现。所有修改仍经过公共执行器。
## 6. 通用内置修改器
第一版只保留以下与具体项目无关、能够用合成样例完整说明的能力:
### 6.1 映射驱动的跨行片段合并
- 保留当前“左片段、右片段、结果文本”的算法;
- 修改器身份改为领域无关名称;
- 映射必须由调用方传入并写入参数;
- 库中不保留 ClinDB 的 6 条映射;
- 不使用词典、模型或项目默认值猜测结果。
### 6.2 严格 HTML 表格实体解除一层转义
- 只处理严格识别的 `<td>` / `<th>` 文本;
- 只处理明确支持的双重实体;
- 不处理属性、表格外文本或任意 HTML;
- 名称和说明不得宣称已经支持通用损坏 HTML 修复。
### 6.3 严格单行 HTML 表格布局
- 保留 HTML 标签、属性、行和单元格内容;
- 只调整严格单行表格的外层行布局;
- 混合换行和不支持的结构保持原样;
- 它是保守的通用修改器,不是 HTML→GFM 转换器。
当前 `_html_table.py` 可以作为这两个修改器的私有词法辅助实现。由于它不识别 Markdown 围栏和完整方言,
公开说明必须保留这一边界。以后扩大为通用 HTML 清洗前必须另建设计。
除上述三类外,当前所有正式业务组件都移出 Python 包。合成示例可以演示怎样用 `regex_replace()` 和自定义函数
建立修改器,但不得包含论文原文、ClinDB 名称、固定文档 ID 或项目参数。
## 7. 从通用库移出的内容
设计获批后,当前工作树中以下内容不再作为 `mdpolish` 当前实现保留:
- `paper.word_review_comment`
- `paper.manuscript_line_number`
- `paper.arxiv_submission_stamp`
- `paper.repeated_running_header`
- `paper.reference_spacing`
- ClinDB 固定断词映射与 first-batch 组合;
- `scripts/run_clindb_arxiv_experiment.py`
- `scripts/run_clindb_first_batch_experiment.py`
- `experiment.py``artifact_store.py``reporting.py` 和只服务 artifact 的重放代码;
- `reviewer/` Python 服务、React 前端、Node.js 依赖与构建配置;
- 对应项目测试、实验 guide、当前机制 explanation、ClinDB/GovDoc reference 和项目 scratch
- README 中的数据目录、实验运行、155 条修改和评审器说明。
`research-wiki/design/0001``0007` 继续作为冻结的历史决策保留。`0008` 负责明确它们不再描述当前交付范围,
避免通过删除历史记录让旧方向看起来从未发生。
本轮只从 Git 跟踪的通用库工作树移除上述实现和当前说明。Git 历史仍可恢复这些内容。本地被忽略的 `data/`
`artifacts/`、虚拟环境、构建缓存和真实材料一律不删除、不移动、不读取、不复制。
## 8. 拟采用的源码结构
```text
pyproject.toml
src/mdpolish/
├── __init__.py
├── models.py
├── modifier.py
├── edits.py
├── pipeline.py
├── regex.py
├── _text_ranges.py
├── _html_table.py
└── modifiers/
├── __init__.py
├── mapped_line_join.py
├── html_table_entities.py
└── html_table_layout.py
tests/
├── test_models.py
├── test_modifier.py
├── test_edits.py
├── test_pipeline.py
├── test_regex.py
├── test_mapped_line_join.py
├── test_html_table_entities.py
└── test_html_table_layout.py
```
`modifier.py` 只定义函数式修改器的身份和契约;`regex.py` 提供通用工厂;`modifiers/` 只放领域无关实现。
业务组件不以示例为由继续进入发布包。
是否把 `DocumentSnapshot``ProposedChange``TextEdit` 等低层对象继续放在顶层公共命名空间,由实现时按外部作者
能否编写自定义函数决定。本设计要求它们具有受支持的导入路径,但不要求所有对象都堆在 `mdpolish.__init__`
## 9. 兼容与版本
这是明确的破坏性重构:
- 删除 `Component` 抽象基类;
- `Pipeline` 的元素类型改变;
- 删除项目组件和实验接口;
- wheel 不再包含项目工作流;
- README 的使用方式改变。
当前仓库仍处于 `0.x` 研究阶段,没有已批准的外部兼容承诺。实现后包版本从 `0.1.0` 更新为 `0.2.0`,不建立
兼容旧 `Component` 子类的适配层。保留适配层会让新的公共边界继续携带旧项目架构,本轮不采用。
## 10. 测试与验收
### 10.1 核心不变量
现有以下测试语义必须迁移并继续通过:
- 空文本、中文、组合字符和不同换行;
- 插入、删除、替换、多编辑候选和组件批次原子性;
- 过期快照、范围越界、原文不符、重复与冲突编辑明确失败;
- 后一个修改器读取新快照;
- 最终复查发现连锁修改时返回 `unstable`
- 异常和元数据变化返回安全错误;
- 相同输入、修改器和参数得到相同结果;
- 成功结果再次运行零修改;
- 输入字符串不被原地改变。
### 10.2 函数式 API
至少覆盖:
- 普通函数不继承任何库基类即可成为 `Modifier`
- 无效 ID、版本、参数、适用边界和不可调用对象明确失败;
- 两个相同 ID 的修改器仍在预检失败;
- 函数异常不会被吞掉,也不会把部分文本当成功输出;
- 无效候选修改导致当前修改器整批失败,不应用其中任何一项;
- 流水线记录实际修改器身份、版本、参数和顺序;
- 流水线运行只产生内存结果,不读取或写回调用方文件。
### 10.3 正则工厂
至少覆盖:
- 普通替换、捕获组展开、多命中和零命中;
- flags 和参数记录;
- 零长度模式拒绝;
- 原文、范围、理由和哈希审计正确;
- 第二次运行稳定;
- 没有 YAML、全局注册表或默认启用行为。
### 10.4 通用修改器
- 只使用小型虚构文本;
- 映射合并没有内置项目词表;
- HTML 修改器保留标签、属性、表格外文本和不支持结构;
- 正向、反向、换行、无末尾换行、确定性和幂等性均有覆盖;
- 不读取 `data/``artifacts/` 或仓库外数据。
### 10.5 仓库边界
实现完成后必须确认:
- wheel 只包含通用 Python 库;
- Python 源码、测试、README、explanation 和 guide 不包含 ClinDB 文档 ID、固定映射和项目流水线;
- 除冻结的 `research-wiki/design/0001``0007` 外,当前文档不再把项目实验描述为库能力;
- `reviewer/`、项目脚本和 Node.js 配置不再属于当前工作树;
- `.gitignore` 继续保护本地 `data/``artifacts/`
- `git status` 不出现被忽略的真实材料;
- `AGENTS.md``CLAUDE.md` 除标题外正文一致。
基础检查仍使用 Ruff、mypy 和 pytest。实现后根据实际文件更新根 README 中的唯一检查命令,并实际运行后才报告通过。
## 11. 风险与代价
- **历史实验入口消失:** 当前分支不再直接运行 ClinDB;旧代码仍在 Git 历史中,未来迁移需在目标项目重新评审。
- **评审能力不再随库提供:** 这符合最小公共库目标;多个项目产生共同需求后再设计独立扩展。
- **函数不能被强制纯净:** Python 无法仅靠类型禁止文件和网络副作用;内置修改器通过实现与测试保证,外部修改器由调用方负责。
- **正则工厂可能被滥用:** 它只保证修改执行安全,不保证项目正则语义正确;项目仍需为自己的模式、反例和版本负责。
- **HTML 修改器的“通用”范围有限:** 严格失败关闭,宁可漏处理未知结构,也不扩大成未经验证的 HTML 修复器。
- **没有文件产品入口:** 第一版调用方需要自己读取 Markdown 和处理结果;这正是新的库边界,不是遗漏。
- **破坏现有导入:** `0.2.0` 明确表示新边界,不维护尚未承诺的旧扩展接口。
## 12. 批准后的实施顺序
用户明确批准本文后,按以下顺序实施:
1. 新增函数式 `Modifier`,迁移核心测试,使 `Pipeline` 不再依赖抽象基类;
2. 实现 `regex_replace()` 和通用修改器,全部使用合成测试;
3. 更新公共导出、包描述和版本;
4. 移除项目组件、脚本、实验层、评审器及其测试;
5. 清理当前 README、explanation、reference、guide 和 scratch,只保留通用库当前事实与冻结历史 design;
6. 核对 `.gitignore`,但不触碰任何被忽略的真实输入和产物;
7. 运行 README 中实际保留的全部检查,检查 wheel 内容、Git diff、文档镜像和工作区状态。
本文批准不授权提交、推送、创建 PR、发布、修改其他仓库,或删除、移动、复制本地真实材料。