feat: prepare v0.3.0 library delivery
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
> 1. 本仓库是实验室共用的 Markdown 清洗研究与基础工具库;已有实现不等于完整清洗工具或生产能力。
|
> 1. 本仓库是实验室共用的 Markdown 清洗研究与基础工具库;已有实现不等于完整清洗工具或生产能力。
|
||||||
> 2. 真实文档和外部数据默认只读,不修改、不复制、不提交。
|
> 2. 真实文档和外部数据默认只读,不修改、不复制、不提交。
|
||||||
> 3. 面向用户的说明使用简体中文;代码、命令、路径和标识符使用英文。
|
> 3. 面向用户的说明使用简体中文,代码注释使用中文;代码、命令、路径和标识符使用英文。
|
||||||
> 4. `AGENTS.md` 与 `CLAUDE.md` 是同步镜像,除第一行标题外正文必须一致。
|
> 4. `AGENTS.md` 与 `CLAUDE.md` 是同步镜像,除第一行标题外正文必须一致。
|
||||||
|
|
||||||
## 0. 事实权威
|
## 0. 事实权威
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
>
|
>
|
||||||
> 1. 本仓库是实验室共用的 Markdown 清洗研究与基础工具库;已有实现不等于完整清洗工具或生产能力。
|
> 1. 本仓库是实验室共用的 Markdown 清洗研究与基础工具库;已有实现不等于完整清洗工具或生产能力。
|
||||||
> 2. 真实文档和外部数据默认只读,不修改、不复制、不提交。
|
> 2. 真实文档和外部数据默认只读,不修改、不复制、不提交。
|
||||||
> 3. 面向用户的说明使用简体中文;代码、命令、路径和标识符使用英文。
|
> 3. 面向用户的说明使用简体中文,代码注释使用中文;代码、命令、路径和标识符使用英文。
|
||||||
> 4. `AGENTS.md` 与 `CLAUDE.md` 是同步镜像,除第一行标题外正文必须一致。
|
> 4. `AGENTS.md` 与 `CLAUDE.md` 是同步镜像,除第一行标题外正文必须一致。
|
||||||
|
|
||||||
## 0. 事实权威
|
## 0. 事实权威
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
`mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、
|
`mdpolish` 是实验室共用的、项目无关的 Python Markdown 修改库。它提供函数式 `Modifier`、精确文本编辑执行器、
|
||||||
有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
|
有序 `Pipeline`、正则修改器工厂,以及少量可以用合成样例完整说明的通用修改器。
|
||||||
|
|
||||||
当前版本是 `0.2.0`。库只处理内存中的 Markdown 字符串,不读取或写入文件,不提供默认流水线,也不包含任何项目的
|
当前源码包版本是 `0.3.0`。库只处理内存中的 Markdown 字符串,不读取或写入文件,不提供默认流水线,也不包含任何
|
||||||
规则集合、数据清单、实验脚本或评审工具。
|
项目的规则集合、数据清单、实验脚本或评审工具。`v0.3.0` tag 和 GitHub Release 尚未创建,因此当前源码是交付候选,
|
||||||
|
不是已经发布的版本。
|
||||||
|
|
||||||
## 当前能力
|
## 当前能力
|
||||||
|
|
||||||
@@ -27,13 +28,23 @@
|
|||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
项目仍在仓库内开发,使用项目可以从本地路径安装:
|
正式版本只计划通过 GitHub tag 和 GitHub Release 交付,不发布到 PyPI 或其他 Python 包索引。`v0.3.0` 发布后,
|
||||||
|
使用项目可以固定不可移动的 tag:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m pip install /path/to/mdpolish
|
python -m pip install 'mdpolish @ git+https://github.com/Bepr4/mdpolish.git@v0.3.0'
|
||||||
|
python -m pip install 'mdpolish[lexical] @ git+https://github.com/Bepr4/mdpolish.git@v0.3.0'
|
||||||
|
python -m pip install 'mdpolish[frequency] @ git+https://github.com/Bepr4/mdpolish.git@v0.3.0'
|
||||||
```
|
```
|
||||||
|
|
||||||
开发环境:
|
也可以安装同一 GitHub Release 附带的 wheel;下面的 URL 只有在 Release 实际创建后才存在:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install 'mdpolish[lexical] @ https://github.com/Bepr4/mdpolish/releases/download/v0.3.0/mdpolish-0.3.0-py3-none-any.whl'
|
||||||
|
```
|
||||||
|
|
||||||
|
Release 页面同时提供 wheel 的 SHA-256 校验值。仓库或 Release 如果是私有的,调用方需要自行配置 GitHub 访问权限;
|
||||||
|
库不会保存凭据。开发环境仍从本地工作树安装:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
@@ -87,6 +98,52 @@ if result.status is not RunStatus.SUCCESS:
|
|||||||
assert result.output_markdown == "an example text"
|
assert result.output_markdown == "an example text"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 自动英文断词
|
||||||
|
|
||||||
|
普通英文断词不需要逐词维护映射。调用方安装 `lexical` extra 后,可以明确选择 `pyspellchecker` 的本地词典,
|
||||||
|
并用 Pyphen 排除不合法的断词位置:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from mdpolish import Pipeline, RunStatus
|
||||||
|
from mdpolish.modifiers import mapped_line_join
|
||||||
|
from mdpolish.modifiers.mapped_line_join import (
|
||||||
|
LexicalCandidateForm,
|
||||||
|
LexicalLineJoinRule,
|
||||||
|
LexiconBackend,
|
||||||
|
)
|
||||||
|
|
||||||
|
join_english_wraps = mapped_line_join(
|
||||||
|
(
|
||||||
|
LexicalLineJoinRule(
|
||||||
|
rule_id="english.dehyphenate",
|
||||||
|
left_pattern=r"[A-Za-z]+",
|
||||||
|
right_pattern=r"[a-z]+",
|
||||||
|
separator="-",
|
||||||
|
backend=LexiconBackend.SPELLCHECKER,
|
||||||
|
language="en",
|
||||||
|
candidate_forms=(
|
||||||
|
LexicalCandidateForm.JOINED,
|
||||||
|
LexicalCandidateForm.HYPHENATED,
|
||||||
|
),
|
||||||
|
minimum_score=1.0,
|
||||||
|
minimum_score_margin=0.5,
|
||||||
|
hyphenation_language="en_US",
|
||||||
|
case_sensitive=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result = Pipeline((join_english_wraps,)).transform("an exam-\nple")
|
||||||
|
if result.status is not RunStatus.SUCCESS:
|
||||||
|
raise RuntimeError(f"cleaning did not succeed: {result.status}")
|
||||||
|
|
||||||
|
assert result.output_markdown == "an example"
|
||||||
|
```
|
||||||
|
|
||||||
|
`pyspellchecker` 的内置语言词典不支持大小写敏感查询,因此这个 backend 要求调用方明确写出
|
||||||
|
`case_sensitive=False`;库不会替调用方改变该开关或切换 backend。上面的分数和 margin 只用于演示完整配置,
|
||||||
|
不是适合所有项目的生产阈值。`JOINED` 与源 `HYPHENATED` 会共同参与候选选择;词典不能确认唯一结果时保持原文。
|
||||||
|
|
||||||
库不会保存 `output_markdown`。调用项目应先检查状态,再自行决定写入位置:
|
库不会保存 `output_markdown`。调用项目应先检查状态,再自行决定写入位置:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -158,7 +215,9 @@ remove_marker = Modifier(
|
|||||||
中间最多一个同风格空行,并可在一次提议内完成多行链式合并。保守词法扫描只正向识别段落、ATX 标题 continuation、
|
中间最多一个同风格空行,并可在一次提议内完成多行链式合并。保守词法扫描只正向识别段落、ATX 标题 continuation、
|
||||||
列表 continuation 和同深度引用;代码块、GFM pipe table、raw HTML table、混合候选行尾及无法确认的容器保持原文。
|
列表 continuation 和同深度引用;代码块、GFM pipe table、raw HTML table、混合候选行尾及无法确认的容器保持原文。
|
||||||
完整公共模型、选择流程和限制见
|
完整公共模型、选择流程和限制见
|
||||||
[`0009-generalized-mapped-line-join.md`](research-wiki/design/0009-generalized-mapped-line-join.md)。
|
[`0009-generalized-mapped-line-join.md`](research-wiki/design/0009-generalized-mapped-line-join.md),真实 backend 验收、
|
||||||
|
版本身份和 GitHub 发布边界见
|
||||||
|
[`0010-first-cross-project-library-delivery.md`](research-wiki/design/0010-first-cross-project-library-delivery.md)。
|
||||||
|
|
||||||
`html_table_entity_unescape()` 只在严格完整的 `<td>` / `<th>` 文本中处理 `&lt;`、`&gt;` 和
|
`html_table_entity_unescape()` 只在严格完整的 `<td>` / `<th>` 文本中处理 `&lt;`、`&gt;` 和
|
||||||
`&amp;`。`html_table_layout()` 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。
|
`&amp;`。`html_table_layout()` 只调整严格单行表格的外层行布局,并保留标签、属性和单元格内容。
|
||||||
@@ -216,8 +275,13 @@ git diff --check
|
|||||||
git status --short
|
git status --short
|
||||||
```
|
```
|
||||||
|
|
||||||
上述检查已于 2026-08-26 在 Python 3.13.11 环境实际运行:Ruff 通过,mypy 检查 21 个源码和测试文件无问题,
|
上述检查已于 2026-08-27 实际运行。Python 3.13.11 核心开发环境中 Ruff 和 mypy 通过,pytest 为
|
||||||
pytest 共 168 项通过、2 项因本环境未安装可选词典 backend 而跳过,`mdpolish-0.2.0-py3-none-any.whl` 构建成功。
|
`170 passed, 3 skipped`;三个 skip 是该环境没有安装的真实 optional backend 路径,不计入发布验收。
|
||||||
wheel 内容已单独检查,只包含通用 Python 包、类型标记和包元数据,不包含项目规则、实验脚本、评审器或 Node.js 文件。
|
|
||||||
|
|
||||||
`pyproject.toml` 声明的 Python 3.11 及以上为支持范围;本次结果不表示已经在每个受支持版本上完成兼容性验证。
|
从 `mdpolish-0.3.0-py3-none-any.whl` 安装全部 extras 后,Python 3.11.16 和 Python 3.13.11 环境分别得到
|
||||||
|
`173 passed`,没有 skip。仓库外消费者 smoke test 已覆盖核心精确/正则规则、`pyspellchecker + Pyphen` 和
|
||||||
|
`wordfreq` 的最终输出与版本审计。wheel 共 16 个文件,只包含通用 Python 包、类型标记和包元数据;不包含 tests、
|
||||||
|
Wiki、真实数据或项目规则。当前候选 wheel 的 SHA-256 是
|
||||||
|
`a510ae1755c281f7b40262e242441882f3ccbc3fbc2174c6f5db3e8a8ae85060`。
|
||||||
|
|
||||||
|
上述结果证明当前候选可安装并按合成契约运行,不代表任意词典阈值已经在真实业务语料上达到生产准确率。
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "mdpolish"
|
name = "mdpolish"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
description = "Deterministic functional core for composing exact Markdown modifiers"
|
description = "Deterministic functional core for composing exact Markdown modifiers"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|||||||
@@ -0,0 +1,359 @@
|
|||||||
|
# 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 结果。
|
||||||
@@ -14,6 +14,11 @@ word-splitting problem, while Enchant/Hunspell depend on system dictionaries,
|
|||||||
so those libraries are not runtime backends here. No backend is treated as
|
so those libraries are not runtime backends here. No backend is treated as
|
||||||
ground truth or used for edit-distance correction.
|
ground truth or used for edit-distance correction.
|
||||||
|
|
||||||
|
The bundled ``pyspellchecker`` language dictionaries do not support
|
||||||
|
case-sensitive lookup. A lexical rule selecting that backend must explicitly
|
||||||
|
set ``case_sensitive=False``; the unsupported combination is rejected while
|
||||||
|
the modifier is built, without switching backend or inspecting source text.
|
||||||
|
|
||||||
Public rules support exact fragments, named regular expressions, a right-side
|
Public rules support exact fragments, named regular expressions, a right-side
|
||||||
condition at a logical line end, explicit keep/veto rules, and local dictionary
|
condition at a logical line end, explicit keep/veto rules, and local dictionary
|
||||||
candidate selection. A join may preserve the physical boundary, delete it,
|
candidate selection. A join may preserve the physical boundary, delete it,
|
||||||
@@ -785,6 +790,11 @@ def _validate_rule(rule: LineJoinRule) -> tuple[re.Pattern[str] | None, re.Patte
|
|||||||
raise ModifierContractError("line join backend must be a LexiconBackend value")
|
raise ModifierContractError("line join backend must be a LexiconBackend value")
|
||||||
if not isinstance(rule.language, str) or not rule.language.strip():
|
if not isinstance(rule.language, str) or not rule.language.strip():
|
||||||
raise ModifierContractError("line join lexical language must be a non-empty string")
|
raise ModifierContractError("line join lexical language must be a non-empty string")
|
||||||
|
if rule.backend is LexiconBackend.SPELLCHECKER and rule.case_sensitive:
|
||||||
|
raise ModifierContractError(
|
||||||
|
f"line join rule {rule.rule_id!r}: pyspellchecker bundled language dictionaries do not support "
|
||||||
|
"case_sensitive=True; set case_sensitive=False explicitly to use this backend"
|
||||||
|
)
|
||||||
if rule.separator not in {"", "-"}:
|
if rule.separator not in {"", "-"}:
|
||||||
raise ModifierContractError("line join lexical separator must be '-' or empty")
|
raise ModifierContractError("line join lexical separator must be '-' or empty")
|
||||||
if not isinstance(rule.candidate_forms, tuple) or any(
|
if not isinstance(rule.candidate_forms, tuple) or any(
|
||||||
@@ -1363,7 +1373,7 @@ def mapped_line_join(
|
|||||||
|
|
||||||
return Modifier(
|
return Modifier(
|
||||||
modifier_id="markdown.mapped_line_join",
|
modifier_id="markdown.mapped_line_join",
|
||||||
version="2.0.0",
|
version="2.0.1",
|
||||||
parameters={
|
parameters={
|
||||||
"conflict_policy": conflict_policy.value,
|
"conflict_policy": conflict_policy.value,
|
||||||
"max_intervening_blank_lines": max_intervening_blank_lines,
|
"max_intervening_blank_lines": max_intervening_blank_lines,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
from importlib.metadata import version as distribution_version
|
||||||
from typing import TYPE_CHECKING, cast
|
from typing import TYPE_CHECKING, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -356,10 +357,14 @@ def test_empty_document_and_empty_rule_set_are_no_ops() -> None:
|
|||||||
|
|
||||||
assert transform("", ()) == ""
|
assert transform("", ()) == ""
|
||||||
assert transform("exam-\nple", ()) == "exam-\nple"
|
assert transform("exam-\nple", ()) == "exam-\nple"
|
||||||
assert modifier.version == "2.0.0"
|
assert modifier.version == "2.0.1"
|
||||||
assert dict(modifier.parameters)["rules"] == ()
|
assert dict(modifier.parameters)["rules"] == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_installed_package_version_matches_delivery_candidate() -> None:
|
||||||
|
assert distribution_version("mdpolish") == "0.3.0"
|
||||||
|
|
||||||
|
|
||||||
def test_parameters_preserve_rule_order_and_record_all_options() -> None:
|
def test_parameters_preserve_rule_order_and_record_all_options() -> None:
|
||||||
first = mapped_line_join((exact_rule(rule_id="first"), exact_rule(rule_id="second")))
|
first = mapped_line_join((exact_rule(rule_id="first"), exact_rule(rule_id="second")))
|
||||||
second = mapped_line_join((exact_rule(rule_id="second"), exact_rule(rule_id="first")))
|
second = mapped_line_join((exact_rule(rule_id="second"), exact_rule(rule_id="first")))
|
||||||
@@ -530,20 +535,65 @@ def test_missing_requested_optional_backend_raises_without_fallback(monkeypatch:
|
|||||||
mapped_line_join((lexical_rule(),))
|
mapped_line_join((lexical_rule(),))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
def test_spellchecker_case_sensitive_dictionary_is_rejected_before_backend_loading(
|
||||||
("backend", "available"),
|
monkeypatch: MonkeyPatch,
|
||||||
[
|
|
||||||
(LexiconBackend.SPELLCHECKER, mapped_module._SpellChecker is not None),
|
|
||||||
(LexiconBackend.WORDFREQ, mapped_module._zipf_frequency is not None),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_installed_optional_backend_constructs_or_is_reported_as_skipped(
|
|
||||||
backend: LexiconBackend,
|
|
||||||
available: bool,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
if not available:
|
monkeypatch.setattr(mapped_module, "_SpellChecker", None)
|
||||||
pytest.skip(f"optional backend is not installed: {backend.value}")
|
rule = lexical_rule(backend=LexiconBackend.SPELLCHECKER)
|
||||||
mapped_line_join((lexical_rule(backend=backend),))
|
|
||||||
|
with pytest.raises(ModifierContractError, match=r"english\.lexical") as raised:
|
||||||
|
mapped_line_join((rule,))
|
||||||
|
|
||||||
|
message = str(raised.value)
|
||||||
|
assert "pyspellchecker" in message
|
||||||
|
assert "case_sensitive=False" in message
|
||||||
|
assert "lexical' extra" not in message
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_spellchecker_and_pyphen_transform_and_record_versions() -> None:
|
||||||
|
if mapped_module._SpellChecker is None or mapped_module._Pyphen is None:
|
||||||
|
pytest.skip("optional lexical backend is not installed")
|
||||||
|
rule = lexical_rule(
|
||||||
|
backend=LexiconBackend.SPELLCHECKER,
|
||||||
|
case_sensitive=False,
|
||||||
|
hyphenation_language="en_US",
|
||||||
|
)
|
||||||
|
modifier = mapped_line_join((rule,))
|
||||||
|
result = Pipeline((modifier,)).transform("an exam-\nple")
|
||||||
|
|
||||||
|
assert result.status is RunStatus.SUCCESS
|
||||||
|
assert result.output_markdown == "an example"
|
||||||
|
records = cast(tuple[tuple[tuple[str, object], ...], ...], dict(modifier.parameters)["rules"])
|
||||||
|
package_versions = dict(cast(tuple[tuple[str, str], ...], dict(records[0])["package_versions"]))
|
||||||
|
assert package_versions == {
|
||||||
|
"pyphen": distribution_version("pyphen"),
|
||||||
|
"pyspellchecker": distribution_version("pyspellchecker"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_pyphen_invalid_breakpoint_keeps_joined_candidate_out() -> None:
|
||||||
|
if mapped_module._SpellChecker is None or mapped_module._Pyphen is None:
|
||||||
|
pytest.skip("optional lexical backend is not installed")
|
||||||
|
rule = lexical_rule(
|
||||||
|
backend=LexiconBackend.SPELLCHECKER,
|
||||||
|
case_sensitive=False,
|
||||||
|
hyphenation_language="en_US",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert transform("exa-\nmple", (rule,)) == "exa-\nmple"
|
||||||
|
|
||||||
|
|
||||||
|
def test_real_wordfreq_transforms_and_records_version() -> None:
|
||||||
|
if mapped_module._zipf_frequency is None:
|
||||||
|
pytest.skip("optional frequency backend is not installed")
|
||||||
|
modifier = mapped_line_join((lexical_rule(),))
|
||||||
|
result = Pipeline((modifier,)).transform("an exam-\nple")
|
||||||
|
|
||||||
|
assert result.status is RunStatus.SUCCESS
|
||||||
|
assert result.output_markdown == "an example"
|
||||||
|
records = cast(tuple[tuple[tuple[str, object], ...], ...], dict(modifier.parameters)["rules"])
|
||||||
|
package_versions = dict(cast(tuple[tuple[str, str], ...], dict(records[0])["package_versions"]))
|
||||||
|
assert package_versions == {"wordfreq": distribution_version("wordfreq")}
|
||||||
|
|
||||||
|
|
||||||
def test_successful_output_is_idempotent() -> None:
|
def test_successful_output_is_idempotent() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user