Files
mdpolish/research-wiki/design/0010-first-cross-project-library-delivery.md

360 lines
18 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.
# 0010:第一版跨项目库交付
## 状态
已于 2026-08-27 经用户明确批准。本文自批准起冻结;后续若改变这里的 backend 契约、版本身份或发布渠道,
应新增 design,不得回写本文。
`supersedes: 0009`(范围有限):本文只替代 `0009` 中没有被真实 optional dependency 验证覆盖的
`SPELLCHECKER` 大小写适配、发布前验收和版本身份部分。`0009` 已批准的规则模型、候选比较、失败关闭、
Markdown 块边界、无默认规则、无网络和函数式 `Modifier` 契约继续有效。
本文批准后只授权实现和验证,不自动授权提交、创建 tag、push、创建 GitHub Release、上传产物、修改其他仓库
或读取真实材料。
## 1. 问题与可观察现象
提交 `11349e0` 已经实现通用 `mapped_line_join()`,仓库基础检查在 Python 3.13.11 下得到:
```text
168 passed, 2 skipped
```
两个 skip 分别对应没有安装的 `pyspellchecker``wordfreq`。这意味着核心契约和合成 backend 通过了测试,
但最关键的真实词典适配器没有进入同一次验收。
2026-08-27 在临时干净 venv 中从 wheel 安装全部 extras 后,实际版本为:
| 包 | 实际版本 |
| --- | --- |
| `mdpolish` | `0.2.0` |
| `pyspellchecker` | `0.9.0` |
| `pyphen` | `0.18.1` |
| `wordfreq` | `3.1.1` |
`wordfreq``Pyphen` 和显式 `case_sensitive=False``pyspellchecker` 都能把合成输入
`an exam-\nple` 稳定处理成 `an example`。但是,当前 `LexicalLineJoinRule.case_sensitive` 默认是 `True`
实现会把它直接传给:
```python
SpellChecker(language="en", case_sensitive=True)
```
真实上游立即拒绝:
```text
ValueError: case_sensitive can only be True when not using a language dictionary.
```
`pyspellchecker` 的源码和官方 quickstart 都说明,大小写敏感模式只适用于不加载内置 language dictionary 的实例:
- <https://github.com/barrust/pyspellchecker/blob/master/spellchecker/spellchecker.py>
- <https://github.com/barrust/pyspellchecker/blob/master/docs/source/quickstart.rst>
这不是词典无命中,而是构造阶段的能力冲突。当前实现把它包装成泛化的“无法加载语言”错误,调用方无法知道
应该怎样改配置。
另一个交付问题是包版本仍为 `0.2.0`。这个版本已经用于 `0008` 后的函数式核心;如果新旧内容继续生成同名 wheel,
其他项目、pip 缓存和问题报告都无法可靠区分实际安装的是哪一份代码。
因此,当前提交适合受控试接,不适合直接作为一个带稳定版本身份的跨项目交付。
## 2. 目标与非目标
### 2.1 目标
-`SPELLCHECKER` 不支持的大小写组合给出构造期、可操作、不会泄露正文的错误;
- 保留 `case_sensitive=True` 的公共默认值,不静默开启忽略大小写;
- 用真实安装的 `pyspellchecker`、Pyphen 和 `wordfreq` 验证 wheel,而不是把 skip 当成通过;
- 为这次新增公共能力分配唯一包版本 `0.3.0`
- 将修复后的 `mapped_line_join` 修改器版本更新为 `2.0.1`,使审计记录可以区分修复前后;
- 在 README 提供其他项目可以直接复制的安装、导入和自动英文断词示例;
- 将 GitHub tag 和 GitHub Release 确定为计划内的正式发布渠道,明确不使用 PyPI;
- 明确第一版固定版本方式和消费者责任;
- 保持核心安装零第三方运行依赖,extras 仍然只由调用方显式安装和启用。
### 2.2 非目标
- 不为 `pyspellchecker` 的内置语言词典自行实现大小写敏感查询;
- 不读取、复制或改写 `pyspellchecker` 的内部压缩词典资源;
- 不把 `case_sensitive` 默认值改成 `False`
- 不引入 Hunspell、Enchant、wordninja、在线词典、模型或 OCR 版面接口;
- 不新增默认规则、默认流水线、项目 profile、配置文件或 CLI
- 不声称词典规则已经在真实业务语料上达到生产准确率;
- 不在本轮定义标注指标、接受阈值或真实材料实验;
- 不发布到 PyPI 或其他 Python 包索引,不提交 wheel 到 Git;
- 不因批准本文就自动创建 GitHub Release 或上传产物;
- 不修改其他 modifier、`_text_ranges.py`、真实数据或其他仓库。
## 3. `SPELLCHECKER` 大小写适配
### 3.1 候选方案
| 方案 | 优点 | 代价与问题 | 选择 |
| --- | --- | --- | --- |
| 把 `LexicalLineJoinRule.case_sensitive` 默认改成 `False` | 默认示例可以直接运行 | 违反“不默认忽略大小写”,并静默改变公共语义 | 否决 |
| 忽略调用方的 `True`,内部总用大小写不敏感词典 | 代码最少 | 参数记录与实际行为不一致,属于静默降级 | 否决 |
| 读取上游包内 JSON 频率表,自己实现大小写敏感词典 | 理论上可以支持 `True` | 绑定上游内部资源路径和格式,扩大维护面;第一版没有证据需要它 | 本轮否决 |
| `SPELLCHECKER` 明确要求 `case_sensitive=False` | 行为诚实、改动小、调用方必须主动选择 | 该 backend 暂不提供大小写敏感语言词典 | 采用 |
### 3.2 决定
`case_sensitive` 继续是所有规则共有的显式开关,默认仍为 `True`。新增 backend 组合验证:
```text
backend == SPELLCHECKER and case_sensitive is True
```
`mapped_line_join()` 构造阶段立即抛出 `ModifierContractError`。错误消息必须说明:
- `pyspellchecker` 的内置 language dictionary 不支持大小写敏感模式;
- 调用方若接受大小写不敏感匹配,需要显式设置 `case_sensitive=False`
- 不建议切换 backend,也不执行自动回退。
错误只包含 backend、`rule_id` 和配置字段,不包含输入正文。因为错误发生在构造修改器时,也不应等到
`Pipeline.transform()` 才暴露。
`WORDFREQ` 保留现有 `case_sensitive=True/False` 行为。不能因为一个 backend 的限制,把共同字段的默认值改成
另一个含义。
### 3.3 为什么第一版不复制词典
`pyspellchecker` 官方允许用 `language=None, case_sensitive=True` 加载调用方自己的词典,但当前规则只接受语言代码,
设计也禁止读取任意路径。为了绕过限制而解析依赖包内部的 `resources/<language>.json.gz`,会新增一套未获保证的
资源格式契约。
第一版更重要的是不误导调用方。明确拒绝不支持的组合,比复制上游实现、静默忽略参数或假装拥有大小写敏感语言词典
更符合失败关闭原则。如果未来项目确实需要该能力,应以真实样本和新 design 决定是增加版本化自带词典、扩展 backend,
还是接受自定义词典输入;不能回写本文。
## 4. 版本身份
### 4.1 包版本
实现本文时把 `pyproject.toml` 和 README 中的包版本从 `0.2.0` 更新为 `0.3.0`
选择 `0.3.0` 而不是 `1.0.0`,原因是:
- 新能力是向现有函数式核心增加公共规则和 optional backend,属于 `0.x` 阶段的次版本变化;
- 公共规则还没有经过多个调用项目验证;
- Markdown 块扫描仍是保守词法子集;
- 没有真实语料准确率、默认 profile、CLI 或生产写入协议。
`0.3.0` 表示“可以被其他项目固定版本试用”,不表示“自动词典规则已经适合任意文档生产启用”。
### 4.2 修改器版本
`mapped_line_join()` 生成的 `Modifier.version``2.0.0` 更新为 `2.0.1`
规则 dataclass 和序列化参数格式不改变,因此不升到 `3.0.0`。补丁版本用于表示:
- 真实 `SPELLCHECKER` backend 的能力验证变得准确;
- 不支持的组合从上游泛化异常变成稳定的构造期契约错误;
- 已支持组合的候选选择语义不改变。
### 4.3 版本记录
词典规则继续在 `Modifier.parameters` 中记录 backend 包版本、语言、候选形式和阈值。调用项目还必须固定
`mdpolish` 包版本;只记录 `Modifier.version` 不能替代安装依赖锁定。
## 5. 第一版交付渠道
### 5.1 采用范围
项目计划通过 GitHub 发布版本,不发布到 PyPI 或其他 Python 包索引。不可移动的 Git tag 是源码身份,GitHub Release
是面向使用方的正式发布记录。第一版候选 tag 和 Release 名称为:
```text
v0.3.0
```
tag 必须指向通过本文全部验收的唯一提交,创建后不得移动;GitHub Release 必须绑定这个 tag,不能指向分支头。
其他项目可以通过 GitHub Git 地址和 tag 固定依赖,也可以安装该 Release 附带的 wheel。Python Packaging 规范允许
集成方使用 direct reference,但它不是包索引发布物:
- <https://packaging.python.org/en/latest/specifications/version-specifiers/#direct-references>
- <https://packaging.python.org/en/latest/specifications/dependency-specifiers/>
README 应给出 GitHub tag direct reference 和 Release wheel 两种安装形式,但不把仓库凭据或某个调用项目配置写入库代码。
仓库及 Release 是公开还是私有,由 GitHub 仓库权限决定;本文不授权改变仓库可见性。
### 5.2 wheel 的角色
wheel 在发布前首先是验证产物,不提交到 Git,也不在仓库内建立 artifact 目录。验收必须从最终源码构建 wheel,并在
干净环境从这个 wheel 安装,而不是依赖当前仓库的 editable install。
实际创建 GitHub Release 时,只能上传通过第 7 节验收的同一个 wheel,并同时提供 SHA-256 校验值。若 tag 后重新构建,
必须重新执行 wheel 元数据和消费者 smoke test,不能把不同构建物当作已经验收的产物。GitHub 自动生成的源码归档与
Release wheel 共同保存在 GitHub;本仓库不另存一份二进制副本。
PyPI、GitHub Packages、内部 Python 包索引和其他 artifact 仓库均不在计划内。将来若要增加其他发布渠道,必须用新的
design 改变本文,而不能只改发布脚本或 README。
### 5.3 tag 和 push 的确认门
本文批准后可以完成版本修改、测试、构建和提交前候选检查,但不能自动提交、创建或推送 `v0.3.0`,也不能创建
GitHub Release 或上传 wheel。
只有在用户看到最终 diff、真实测试输出和 wheel 元数据后,才能明确授权:
1. 提交交付改动;
提交完成并向用户报告提交哈希和 tag 的准确目标后,才能再明确授权:
1. 创建 `v0.3.0` tag
2. push 提交和 tag
3. 创建绑定 `v0.3.0` 的 GitHub Release
4. 上传已验收的 wheel 和 SHA-256 校验值。
这些动作不因“设计已批准”而自动获得授权。
## 6. 公共调用契约与 README
### 6.1 支持的导入路径
保持 `0009` 的决定,不扩大聚合导出:
```python
from mdpolish.modifiers import mapped_line_join
from mdpolish.modifiers.mapped_line_join import (
LexicalCandidateForm,
LexicalLineJoinRule,
LexiconBackend,
)
```
本轮不把全部枚举和 dataclass 重新导出到 `mdpolish.modifiers` 或包根。减少顶层公共表面积比缩短一行导入更重要。
### 6.2 README 示例
README 保留旧三元组示例,并新增一个最小自动英文断词示例。示例必须明确写出:
- 通过 GitHub tag direct reference 或 GitHub Release wheel 安装 `lexical` extra
- `backend=LexiconBackend.SPELLCHECKER`
- `case_sensitive=False`
- `JOINED` 与源 `HYPHENATED` 共同参与候选;
- 分数阈值只是示例配置,不是库推荐的通用生产阈值;
- 检查 `RunStatus` 后才能消费输出。
README 还要明确区分:
| 说法 | 当前是否成立 |
| --- | --- |
| wheel 可以安装,公共 API 可以运行 | 是,验收通过后成立 |
| 自动词典不需要逐词维护映射 | 是 |
| 库自带默认英文清洗规则 | 否 |
| 某个阈值适合所有项目 | 否 |
| 已在真实业务文档证明生产准确率 | 否 |
### 6.3 调用项目责任
调用项目必须:
- 固定 `mdpolish` 版本或不可移动 tag
- 把规则、顺序、backend、语言、阈值和例外作为项目配置评审;
- 检查 `success``failed``unstable`,不能只读取可能为空的输出;
- 自己负责文件读写、覆盖策略、批处理、日志和回滚;
- 在自己的语料上验证误合并,不能把库的合成测试当作领域准确率。
## 7. 验证矩阵
### 7.1 核心环境
不安装 `lexical``frequency` extras,运行根 README 的全部基础检查,确认:
- 精确、正则、链式和 Markdown 失败关闭测试通过;
- 导入模块不会加载词典;
- 请求缺失 backend 时给出稳定错误;
- optional backend 测试可以明确 skip,但 skip 不能计入真实 backend 验收。
### 7.2 extras 环境
另建干净环境,安装最终 wheel 的 `lexical``frequency` extras,并另行安装仓库测试工具,运行同一测试集。
该环境的发布门要求:
- `pyspellchecker`、Pyphen 和 `wordfreq` 真实 adapter 测试全部执行,不得 skip;
- `SPELLCHECKER + case_sensitive=True` 在构造期得到预期契约错误;
- `SPELLCHECKER + case_sensitive=False` 的唯一拼接候选可以合并;
- Pyphen 合法和非法断点分别影响 `JOINED`
- `WORDFREQ` 唯一胜者、margin 不足和歧义路径都符合 `0009`
- 包版本和 backend 版本进入审计参数;
- 全套测试没有因安装 extras 而改变精确/正则规则结果。
测试不能只断言“构造成功”。至少一个真实 backend 用例必须通过 `Pipeline.transform()` 检查最终状态和完整输出。
### 7.3 wheel 消费者 smoke test
从最终提交构建 wheel 后,在不位于仓库源码目录的临时环境验证:
1. 只安装核心 wheel,运行精确和正则示例;
2. 安装 `lexical` extra,运行 `SPELLCHECKER + Pyphen` 示例;
3. 安装 `frequency` extra,运行 `WORDFREQ` 示例;
4. 检查 `mdpolish`、修改器和 backend 版本记录;
5. 检查 wheel 不包含 tests、Wiki、真实数据、项目规则或临时产物;
6. 生成并记录待上传 wheel 的 SHA-256 校验值;
7. 确认执行期间不访问网络、调用模型或读取任意文档路径。
依赖安装本身可以访问配置的包索引;“运行期间无网络”指安装完成后的库行为,不把安装包与执行清洗混为一谈。
### 7.4 Python 支持范围
`pyproject.toml` 当前声明 Python 3.11 及以上。第一版交付前至少验证:
- 最低支持版本 Python 3.11
- 当前开发版本 Python 3.13。
如果本地缺少其中一个解释器,不能把单版本结果描述成完整支持矩阵;应在可复现 CI 或受控环境补齐后再创建 tag。
本文不新增 tox、nox、CI provider 或容器配置。若现有环境无法完成双版本验证,报告阻塞而不是降低声明。
## 8. 消费项目试用边界
通过第 7 节只证明“库可以被安装并按契约运行”,不证明“某组词典参数适合目标项目”。
第一个调用项目应先做只读或影子试用:保存提议和审计信息,由人复核后再决定是否应用。至少观察:
- 正确合并与错误合并;
- 本应合并但被保留的候选;
- `JOINED``HYPHENATED``SPACED` 的选择分布;
- 按段落、标题、列表和引用拆分的行为;
- 词典歧义、结构 `UNKNOWN`、代码和表格排除;
- 项目专名和自然连字符是否需要 `KeepLineJoinRule`
具体样本范围、标注方法、指标、接受阈值、输出目录和真实数据权限必须在调用项目或新的实验 design 中确认。
本文不授权读取 `/home/lihaoze/gov_test_data`,也不授权修改任何调用项目。
## 9. 风险与代价
- **`SPELLCHECKER` 能力不对称:** 使用内置语言词典时必须显式忽略大小写;需要精确大小写的项目应使用其他 backend
或等待新的词典设计。
- **真实 extras 增加验收成本:** `wordfreq` wheel 和传递依赖较大,但不能为了节省安装时间继续跳过发布关键路径。
- **GitHub 可用性与权限:** direct reference 需要 Git 和相应仓库权限;Release wheel 也受仓库可见性和 GitHub
可用性约束。
- **Release 产物一致性:** tag、Release 和 wheel 来自不同操作步骤,必须用提交哈希、版本元数据和 SHA-256 防止
上传错误构建物。
- **`0.3.0` 仍是预览契约:** 其他项目必须固定版本,不能跟随分支头自动升级。
- **双 Python 版本可能受环境限制:** 缺少最低版本验证时,tag 会被阻塞。
- **合成测试不代表领域准确率:** 即使全部 backend 测试通过,词典仍会漏掉专名、新词并误判自然连字符。
- **保守失败关闭降低覆盖率:** 这是第一版为了正文保真接受的代价,不用放宽块扫描来追求漂亮数字。
## 10. 实施与验收范围
本文获批后授权:
1. 修改 `src/mdpolish/modifiers/mapped_line_join.py`,增加 `SPELLCHECKER` 组合验证并把修改器版本更新到 `2.0.1`
2. 扩展 `tests/test_mapped_line_join.py`,覆盖真实 backend、明确错误和版本记录;
3.`pyproject.toml` 包版本更新到 `0.3.0`,不改变已批准 extras 的依赖集合;
4. 更新根 README 的当前版本、自动词典示例、交付边界和实际验证结果;
5. 在临时目录构建和安装 wheel,运行第 7 节验证;
6. 只读检查 `AGENTS.md``CLAUDE.md` 镜像、Git diff、工作区状态和最终提交候选范围。
本文获批后仍不授权:
- 修改已冻结的 `0009`
- 修改 `_text_ranges.py`、其他 modifier、其他 Wiki 文档或调用项目;
- 读取或复制真实材料;
- 新增默认规则、CLI、profile、artifact 目录、CI 配置或第三方 backend;
- 提交、创建 `v0.3.0` tag、push、创建 GitHub Release、上传 wheel 或创建 PR。
实施完成的最终报告必须分别给出核心环境、extras 环境、Python 版本和 wheel smoke test 的真实输出。任何必需环境未验证时,
明确写“未验证”或报告阻塞,不能用合成 adapter 测试替代真实 backend 结果。