Files
mdpolish/research-wiki/design/0008-generic-functional-library-boundary.md

409 lines
19 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.
# 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、发布、修改其他仓库,或删除、移动、复制本地真实材料。