# 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 表格实体解除一层转义 - 只处理严格识别的 `