feat: 增加评审文档机器投影与 JSON 报告
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
# 0012:ReviewDocument 的正式机器投影
|
||||
|
||||
## 状态
|
||||
|
||||
已于 2026-08-28 获用户明确批准,按本文第 16 节实施。本文自批准起冻结;后续改变决策需新增 design 并使用
|
||||
`supersedes` 指向本文。
|
||||
|
||||
`supersedes: 0011`(范围有限):本文只替代 `0011` 第 8 节“第一版不提供 JSON reporter”和其中把稳定序列化继续留给
|
||||
调用项目的决定。`0011` 已批准并实现的可信重放、`ReviewDocument`、阶段坐标、Markdown reporter、无文件 I/O 和项目拥有
|
||||
持久化决定权继续有效。
|
||||
|
||||
本文不改变 `Pipeline`、`TransformResult`、清洗语义或评审重放结果。它只定义怎样把已经建立并验证的
|
||||
`ReviewDocument` 转成稳定的普通 Python 数据,再按同一结构编码为 JSON。
|
||||
|
||||
## 1. 问题与可观察现象
|
||||
|
||||
`v0.4.0` 已经可以在同一个 Python 进程中这样消费评审结果:
|
||||
|
||||
```python
|
||||
review = build_review_document(input_markdown, result)
|
||||
|
||||
if review.status is RunStatus.SUCCESS:
|
||||
print(review.current_sha256)
|
||||
```
|
||||
|
||||
这时调用方直接使用不可变的 `ReviewDocument`、`ReviewStage` 和 `ReviewChange`,不需要序列化。
|
||||
|
||||
一旦评审结果需要经过 Web、数据库、消息队列、JSON 文件或其他语言,内部 Python 对象就不能直接作为契约。当前项目端只能
|
||||
自己决定怎样处理 dataclass、`StrEnum`、tuple、半开范围、阶段哈希和部分输出。最短的做法看似是:
|
||||
|
||||
```python
|
||||
payload = dataclasses.asdict(review)
|
||||
```
|
||||
|
||||
但这会产生四类问题:
|
||||
|
||||
1. 内部 dataclass 字段会在未经评审的情况下变成外部协议;以后正常的 Python 重构也会破坏消费者;
|
||||
2. enum、tuple、递归参数值和位置单位没有正式 JSON 表达,消费者容易形成不同解释;
|
||||
3. `ReviewDocument` 包含完整输入、当前全文、每阶段全文和修改片段,机械展开会默认暴露全部正文;
|
||||
4. 当前 `RunError.error_type` 是诊断用 Python 异常类名,不能被项目端误当成稳定错误代码。
|
||||
|
||||
因此,需要由 `mdpolish` 自己提供一个经过选择和转换的对外视图,而不是让每个项目根据内部字段猜一个版本。
|
||||
|
||||
本文把这个视图称为“机器投影”:它是 `ReviewDocument` 的有损、单向、稳定表示,不是内部对象的镜像,也不是可用于重新
|
||||
应用修改的序列化快照。
|
||||
|
||||
## 2. 外部规范带来的约束
|
||||
|
||||
2026-08-28 查阅的官方规范给出以下直接约束:
|
||||
|
||||
- [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259.html) 把 JSON object 定义为无序名称/值集合,把 array 定义为有序序列;
|
||||
对外语义不能依赖 object 成员顺序,但修改器、阶段、修改和残留候选的顺序必须用 array 保存;
|
||||
- RFC 8259 要求开放系统中的 JSON 文本使用 UTF-8,成员名应唯一,并指出超出 IEEE 754 binary64 精确整数范围的数字会降低
|
||||
互操作性;本投影只发出唯一键、可 UTF-8 编码的字符串和安全范围整数;
|
||||
- [Python `json` 文档](https://docs.python.org/3/library/json.html) 显示 `allow_nan` 默认允许非标准的 `NaN` / `Infinity`,
|
||||
`ensure_ascii` 默认转义非 ASCII;官方 reporter 必须显式使用 `allow_nan=False` 和 `ensure_ascii=False`;
|
||||
- [JSON Schema 2020-12](https://json-schema.org/draft/2020-12/json-schema-core.html) 区分 schema 版本和实例内容,并允许通过
|
||||
schema 约束对象、数组和 enum;本轮先固定实例 schema 和兼容策略,不引入运行时 validator 或第三方 schema 依赖;
|
||||
- [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html) 区分稳定标识符与给人看的消息,并明确
|
||||
不应从没有稳定标识的来源中猜造精细规则 ID;本投影只为现有 `ErrorStage` 提供粗粒度稳定代码,不从异常类名或文案
|
||||
推断更细错误原因。
|
||||
|
||||
这些参考不表示 `mdpolish` 要实现 SARIF 或 JSON Schema validator。它们只帮助确定 JSON 互操作、顺序、版本和错误身份的
|
||||
边界。
|
||||
|
||||
## 3. 目标与非目标
|
||||
|
||||
### 3.1 目标
|
||||
|
||||
- 提供官方 `ReviewDocument -> dict` 投影,返回只含 JSON 基本值的全新普通数据结构;
|
||||
- 提供使用同一投影的确定性 JSON reporter,不建立第二套字段;
|
||||
- 每个结果都携带独立于包版本的 `schema_version`;
|
||||
- 固定 enum、array、哈希和位置的外部表达,避免调用方解释 Python 内部对象;
|
||||
- 提供 `summary`、`changes`、`full` 三个单调增加的正文暴露等级,默认不暴露正文;
|
||||
- 给当前三个错误阶段提供粗粒度、稳定、可供程序判断的代码,同时保留诊断字段的非稳定身份;
|
||||
- 明确 schema 的兼容升级规则,并让旧消费者可以安全忽略同一主版本新增的可选字段;
|
||||
- 保持纯函数、无文件 I/O、无网络、零第三方核心运行依赖;
|
||||
- 只用合成内容验证 Unicode、换行、部分输出、残留候选和内容泄漏边界。
|
||||
|
||||
### 3.2 非目标
|
||||
|
||||
- 不提供 `dict` / JSON 到 `ReviewDocument` 的反序列化或 round-trip;
|
||||
- 不把 JSON 投影作为重新应用 `Change`、恢复 Pipeline 或验证原始结果的权威输入;
|
||||
- 不生成或保存 `.json` 文件,不接收路径,不决定目录、权限、覆盖或保留周期;
|
||||
- 不提供 CLI、Web API、数据库模型、消息队列协议、OpenAPI、HTML 或项目页面;
|
||||
- 不发布 SARIF、JSON Lines、JSON Patch、JSON-LD 或项目 artifact 格式;
|
||||
- 不改变 `ReviewDocument`、`ReviewStage`、`Change`、`RunError` 或 `TransformResult` 的字段;
|
||||
- 不增加细粒度的 Pipeline 错误原因。当前结果没有保存足够的稳定原因身份,本轮不解析异常消息来猜;
|
||||
- 不把 Python 码点坐标转换成 UTF-16、UTF-8 byte offset、LSP 位置或终端显示宽度;
|
||||
- 不承诺 `summary` 是可以公开传播的“安全日志”。它不含正文,但 modifier id、版本、计数和哈希仍可能属于项目元数据;
|
||||
- 不读取、复制或修改项目端测试仓、外部真实文档、历史报告或数据库。
|
||||
|
||||
## 4. 方案比较
|
||||
|
||||
| 方案 | 优点 | 问题 | 选择 |
|
||||
| --- | --- | --- | --- |
|
||||
| 各项目继续 `dataclasses.asdict()` | 上游零工作 | 内部结构意外变成协议;enum、tuple、正文和兼容策略失控 | 不采用 |
|
||||
| 使用 Pydantic / Marshmallow 建模 | schema 和校验工具成熟 | 给零依赖核心增加运行依赖,并形成第二套评审模型 | 不采用 |
|
||||
| 只提供 JSON reporter | 调用入口短 | Web 或数据库仍要解析 JSON 才能得到 Python 基本值 | 不采用 |
|
||||
| 只提供 dict,不提供 reporter | 表面积最小 | 各项目会重复 JSON 编码选项,可能生成 NaN、ASCII 转义或不同格式 | 不采用 |
|
||||
| 手写窄投影,JSON reporter 复用它 | 字段和暴露级别可审计;零依赖;dict 与 JSON 只有一个语义来源 | 需要长期维护 schema 兼容 | 采用 |
|
||||
| 第一版同时发布 JSON Schema 文件 | 其他语言可直接验证 | 增加一份必须与代码同步的公共文件和打包契约;当前尚无独立 validator 需求 | 本轮不采用 |
|
||||
|
||||
如果真实消费者以后需要脱离 Python wheel 独立验证 payload,再新增 design 决定是否把 JSON Schema 文件作为 Release 资产或
|
||||
包资源发布,不能根据本文自动补一个未维护的 schema 文件。
|
||||
|
||||
## 5. 职责与数据流
|
||||
|
||||
```text
|
||||
Pipeline.transform()
|
||||
│
|
||||
▼
|
||||
TransformResult + 原始 Markdown
|
||||
│
|
||||
▼
|
||||
build_review_document() 0011:验证和可信重放
|
||||
│
|
||||
▼
|
||||
ReviewDocument
|
||||
│
|
||||
├── render_markdown_report() 人工完整评审
|
||||
│
|
||||
└── review_document_to_dict(detail=...)
|
||||
│
|
||||
├── 项目 Web / 数据库 / 其他语言
|
||||
│
|
||||
└── render_json_report() 内存 JSON 字符串
|
||||
```
|
||||
|
||||
`build_review_document()` 仍是建立可信评审对象的唯一入口。机器投影不重新运行 modifier,也不重新实现阶段重放、冲突检查或
|
||||
哈希证明。
|
||||
|
||||
投影函数负责:
|
||||
|
||||
- 检查输入确实是 `ReviewDocument`;
|
||||
- 把已知 enum 转成规定字符串;
|
||||
- 把 tuple 和内部值转成规定 array/object;
|
||||
- 选择当前 detail 允许的字段;
|
||||
- 拒绝不能安全进入标准 JSON 的值;
|
||||
- 返回一个与原对象不共享 dict/list 容器的全新结果。
|
||||
|
||||
投影函数不负责:
|
||||
|
||||
- 修复手工伪造或语义矛盾的 `ReviewDocument`;
|
||||
- 再次应用修改或重新计算阶段;
|
||||
- 对正文脱敏、截断或摘要生成;
|
||||
- 保存或发送结果。
|
||||
|
||||
调用方应把 `build_review_document()` 返回的对象传给投影。手工构造的对象即使恰好通过结构检查,也不获得可信重放保证。
|
||||
|
||||
## 6. 第一版公共接口
|
||||
|
||||
公共名称继续位于 `mdpolish.review`,不在包根 `mdpolish.__init__` 重新导出:
|
||||
|
||||
```python
|
||||
from enum import StrEnum
|
||||
from typing import TypeAlias
|
||||
|
||||
|
||||
JsonScalar: TypeAlias = str | int | float | bool | None
|
||||
JsonValue: TypeAlias = JsonScalar | list["JsonValue"] | dict[str, "JsonValue"]
|
||||
ReviewProjection: TypeAlias = dict[str, JsonValue]
|
||||
|
||||
|
||||
class ReviewDetail(StrEnum):
|
||||
SUMMARY = "summary"
|
||||
CHANGES = "changes"
|
||||
FULL = "full"
|
||||
|
||||
|
||||
class ReviewProjectionError(ValueError):
|
||||
"""ReviewDocument 不能按正式机器契约投影。"""
|
||||
|
||||
|
||||
def review_document_to_dict(
|
||||
review: ReviewDocument,
|
||||
*,
|
||||
detail: ReviewDetail | str = ReviewDetail.SUMMARY,
|
||||
) -> ReviewProjection:
|
||||
...
|
||||
|
||||
|
||||
def render_json_report(
|
||||
review: ReviewDocument,
|
||||
*,
|
||||
detail: ReviewDetail | str = ReviewDetail.SUMMARY,
|
||||
) -> str:
|
||||
...
|
||||
```
|
||||
|
||||
允许字符串形式是为了让项目配置和 Web 层直接传入 `"summary"`、`"changes"` 或 `"full"`。其他字符串、非字符串且非
|
||||
`ReviewDetail` 的值直接抛出 `ReviewProjectionError`,不能回退到默认值。
|
||||
|
||||
默认 `summary` 是有意的隐私边界:调用方必须显式选择 `changes` 或 `full` 才能得到 Markdown 正文或修改片段。
|
||||
|
||||
返回类型是普通可变 dict/list,因为目标就是 Python 和 JSON 生态通用的数据载体;权威 `ReviewDocument` 仍然不可变。
|
||||
每次调用都返回新的递归容器,修改返回值不能反向改变 review,也不能影响下一次投影。
|
||||
|
||||
## 7. Schema 身份与顶层结构
|
||||
|
||||
第一版 schema 名称和版本固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_name": "mdpolish.review",
|
||||
"schema_version": "1.0"
|
||||
}
|
||||
```
|
||||
|
||||
`schema_version` 是数据格式版本,不是 `mdpolish` 包版本,也不是 modifier 版本。第一版完整顶层结构为:
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_name": "mdpolish.review",
|
||||
"schema_version": "1.0",
|
||||
"detail": "summary",
|
||||
"status": "success",
|
||||
"current_kind": "success_output",
|
||||
"stages_complete": true,
|
||||
"hash_contract": {
|
||||
"algorithm": "sha256",
|
||||
"encoding": "utf-8",
|
||||
"normalization": "none"
|
||||
},
|
||||
"coordinate_contract": {
|
||||
"offset_unit": "unicode_code_point",
|
||||
"span_index_base": 0,
|
||||
"span_end": "exclusive",
|
||||
"location_index_base": 1,
|
||||
"physical_line_endings": ["lf", "crlf", "cr"]
|
||||
},
|
||||
"input": {},
|
||||
"current": {},
|
||||
"counts": {},
|
||||
"modifiers": [],
|
||||
"stages": [],
|
||||
"errors": []
|
||||
}
|
||||
```
|
||||
|
||||
`residual_proposals` 只在 `changes` 和 `full` 中出现;`summary` 通过 `counts.residual_proposal_count` 报告总数。
|
||||
|
||||
所有 object 键必须唯一。上面的成员排列是官方 renderer 的可读输出顺序,但 JSON object 本身无序,消费者不得根据键顺序
|
||||
解释语义。所有 array 顺序都有意义,必须保持 `ReviewDocument` 的权威顺序,不按 ID、哈希或文本重新排序。
|
||||
|
||||
### 7.1 enum 表达
|
||||
|
||||
enum 一律投影为已批准的 `.value` 小写字符串,不输出 Python 类名、`repr()` 或整数序号:
|
||||
|
||||
| Python enum | 第一版允许值 |
|
||||
| --- | --- |
|
||||
| `RunStatus` | `success`、`failed`、`unstable` |
|
||||
| `ReviewCurrentKind` | `success_output`、`partial_output` |
|
||||
| `ErrorStage` | `preflight`、`transform`、`final_review` |
|
||||
| `ReviewDetail` | `summary`、`changes`、`full` |
|
||||
|
||||
投影遇到未知 enum 实例或未知值时失败,不把 `str(value)` 当作向前兼容。
|
||||
|
||||
### 7.2 哈希口径
|
||||
|
||||
所有 `sha256` 字段都是对对应精确 Markdown 字符串执行:
|
||||
|
||||
```python
|
||||
sha256(markdown.encode("utf-8")).hexdigest()
|
||||
```
|
||||
|
||||
结果为 64 位小写十六进制字符串。不规范化 Unicode,不统一 CR/LF/CRLF,不添加或删除 BOM、空白或末尾换行。
|
||||
`Change.before_sha256` / `after_sha256` 和阶段哈希指向完整阶段快照,不是片段哈希。
|
||||
|
||||
### 7.3 位置口径
|
||||
|
||||
- `span.start` / `span.end` 是所属 `ReviewStage.before_markdown` 中的 0-based Unicode code point 半开范围;
|
||||
- `location.line` / `location.column` 指向 `span.start`,是同一阶段修改前全文中的 1-based 人类位置;
|
||||
- LF、CRLF、CR 都形成一个物理换行;CRLF 在 offset 中仍占两个 code point;
|
||||
- 不提供最终全文坐标、UTF-16、byte offset 或显示列宽。
|
||||
|
||||
所有计数、位置和长度必须是 `0..2**53-1` 范围内的 JSON integer;超出时投影失败,避免其他语言使用 binary64 数字时静默
|
||||
丢失整数精度。现实内存文档远小于该上限,因此这不是实际文档规模承诺。
|
||||
|
||||
## 8. 三个正文暴露等级
|
||||
|
||||
三个等级必须满足单调关系:
|
||||
|
||||
```text
|
||||
summary 的字段 ⊂ changes 的字段 ⊂ full 的字段
|
||||
```
|
||||
|
||||
高等级只能增加正文相关字段,不能改变低等级已有字段的值、顺序或语义。
|
||||
|
||||
| 内容 | `summary` | `changes` | `full` |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 状态、哈希、坐标契约、计数 | 是 | 是 | 是 |
|
||||
| modifier id / version / position | 是 | 是 | 是 |
|
||||
| modifier parameters / applicability | 否 | 是 | 是 |
|
||||
| 阶段前后哈希和修改数 | 是 | 是 | 是 |
|
||||
| 实际修改位置、理由、`before` / `after` | 否 | 是 | 是 |
|
||||
| 稳定错误代码和错误位置 | 是 | 是 | 是 |
|
||||
| Python 诊断类型和错误消息 | 否 | 是 | 是 |
|
||||
| 残留候选理由、范围、`expected_text` / `replacement` | 否 | 是 | 是 |
|
||||
| 完整输入和当前 Markdown | 否 | 否 | 是 |
|
||||
| 每个阶段的完整 before / after Markdown | 否 | 否 | 是 |
|
||||
|
||||
低等级不允许用 `null` 或空字符串代替被隐藏的正文,而是完全省略对应键。这样消费者能够区分“字段因 detail 未暴露”和“原文
|
||||
本来就是空字符串”。`detail` 顶层字段说明当前投影使用的等级。
|
||||
|
||||
`summary` 只承诺不包含以下正文承载字段:`markdown`、`before`、`after`、`expected_text`、`replacement`、`reason`、
|
||||
`message`、`parameters`、`applicability`。它仍含 modifier identity、哈希、位置和计数,不能在不了解项目数据政策的情况下
|
||||
称为匿名、脱敏或可公开日志。
|
||||
|
||||
`changes` 会暴露实际修改和未应用残留候选中的片段,也会暴露项目 modifier 配置和诊断消息。它可能足以还原敏感局部内容。
|
||||
|
||||
`full` 还会重复保存输入、当前全文及每个完整阶段的前后全文,内存和 JSON 大小可能随修改器数量线性增长。调用方必须显式
|
||||
选择,库不截断、不脱敏,也不自动落盘。
|
||||
|
||||
## 9. 各对象的正式投影
|
||||
|
||||
以下字段名称、类型和层级属于 schema `1.0`。示例中的省略号只为文档可读,正式输出不得包含省略号。
|
||||
|
||||
### 9.1 输入和当前文本
|
||||
|
||||
三个 detail 都输出:
|
||||
|
||||
```json
|
||||
"input": {
|
||||
"sha256": "...",
|
||||
"code_point_length": 123
|
||||
},
|
||||
"current": {
|
||||
"sha256": "...",
|
||||
"code_point_length": 120
|
||||
}
|
||||
```
|
||||
|
||||
`full` 分别增加:
|
||||
|
||||
```json
|
||||
"markdown": "完整文本"
|
||||
```
|
||||
|
||||
`current.markdown` 的性质必须结合根字段 `current_kind` 判断。`partial_output` 永远不能因为进入 JSON 而改名为 cleaned、final
|
||||
或 successful。
|
||||
|
||||
### 9.2 计数
|
||||
|
||||
```json
|
||||
"counts": {
|
||||
"modifier_count": 3,
|
||||
"completed_stage_count": 2,
|
||||
"change_count": 5,
|
||||
"error_count": 1,
|
||||
"residual_proposal_count": 0
|
||||
}
|
||||
```
|
||||
|
||||
这些值是投影时从权威 array 计算的派生摘要,必须与 `ReviewDocument` 一致。`change_count` 是所有完整阶段实际
|
||||
`ReviewChange` 的总数,不包含 residual proposal edit;零修改阶段仍计入 `completed_stage_count`。
|
||||
|
||||
### 9.3 Modifier
|
||||
|
||||
所有 detail 都输出所有 modifier 的稳定身份:
|
||||
|
||||
```json
|
||||
"modifiers": [
|
||||
{
|
||||
"position": 0,
|
||||
"modifier_id": "example.normalize",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`changes` 和 `full` 增加:
|
||||
|
||||
```json
|
||||
"parameters": [
|
||||
["pattern", " {2,}"],
|
||||
["replacement", " "]
|
||||
],
|
||||
"applicability": "调用方声明的适用范围"
|
||||
```
|
||||
|
||||
`parameters` 不投影成 JSON object。当前内部 `ParameterValue` 会把 mapping 和 sequence 都冻结成 tuple;某些嵌套值在运行时
|
||||
无法可靠区分原来是 mapping 还是二元组 sequence。第一版忠实投影冻结后的结构:顶层及所有 tuple 都变成有序 array,
|
||||
标量保持 `str`、`int`、有限 `float`、`bool` 或 `null`。投影不得根据“看起来像键值对”猜成 object。
|
||||
|
||||
### 9.4 完整阶段
|
||||
|
||||
所有 detail 都输出:
|
||||
|
||||
```json
|
||||
"stages": [
|
||||
{
|
||||
"modifier_position": 0,
|
||||
"before": {
|
||||
"sha256": "...",
|
||||
"code_point_length": 123
|
||||
},
|
||||
"after": {
|
||||
"sha256": "...",
|
||||
"code_point_length": 120
|
||||
},
|
||||
"change_count": 2
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`changes` 和 `full` 增加 `changes` array;`full` 再给 `before`、`after` 增加 `markdown`。阶段通过
|
||||
`modifier_position` 引用根 `modifiers`,不复制第二份 modifier 身份。
|
||||
|
||||
阶段顺序与 `ReviewDocument.stages` 相同。不得从 `change_count` 猜测阶段是否完整;完整性继续由根字段
|
||||
`stages_complete` 和已有阶段边界表达。
|
||||
|
||||
### 9.5 已应用修改
|
||||
|
||||
只在 `changes` 和 `full` 中出现:
|
||||
|
||||
```json
|
||||
"changes": [
|
||||
{
|
||||
"proposal_index": 0,
|
||||
"edit_index": 0,
|
||||
"reason": "应用调用方声明的替换",
|
||||
"location": {
|
||||
"line": 3,
|
||||
"column": 7
|
||||
},
|
||||
"span": {
|
||||
"start": 24,
|
||||
"end": 31
|
||||
},
|
||||
"before": "exam-\nple",
|
||||
"after": "example",
|
||||
"before_sha256": "...",
|
||||
"after_sha256": "..."
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
修改所在的 modifier 由外层 stage 唯一确定,因此不重复输出 `modifier_id`、版本和位置。`proposal_index` / `edit_index` 保留
|
||||
原权威引用顺序;`before_sha256` / `after_sha256` 是完整阶段快照哈希。
|
||||
|
||||
### 9.6 错误
|
||||
|
||||
所有 detail 都输出稳定身份和位置:
|
||||
|
||||
```json
|
||||
"errors": [
|
||||
{
|
||||
"code": "run.transform_failed",
|
||||
"stage": "transform",
|
||||
"modifier_position": 1,
|
||||
"modifier_id": "example.normalize",
|
||||
"modifier_version": "1.0.0"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
`changes` 和 `full` 增加:
|
||||
|
||||
```json
|
||||
"diagnostic_type": "ModifierContractError",
|
||||
"message": "modifier proposal failed"
|
||||
```
|
||||
|
||||
稳定代码只按已经存在的 `ErrorStage` 映射:
|
||||
|
||||
| `ErrorStage` | `code` |
|
||||
| --- | --- |
|
||||
| `preflight` | `run.preflight_failed` |
|
||||
| `transform` | `run.transform_failed` |
|
||||
| `final_review` | `run.final_review_failed` |
|
||||
|
||||
`diagnostic_type` 是当前 Python 异常类名,`message` 是给人排障的消息;两者都不属于稳定程序分支条件。消费者只能使用
|
||||
`code` 和 `stage` 做稳定判断。
|
||||
|
||||
这些代码有意保持粗粒度。当前 `RunError` 没有保存“元数据变化”“propose 失败”“批次验证失败”等稳定原因,投影不得解析
|
||||
`error_type` 或 `message` 猜出更细代码。未来要增加精细代码,必须先用另一份 design 改变 Pipeline 的错误事实来源。
|
||||
|
||||
### 9.7 残留候选
|
||||
|
||||
`summary` 只输出总数。`changes` 和 `full` 输出完整 residual proposal:
|
||||
|
||||
```json
|
||||
"residual_proposals": [
|
||||
{
|
||||
"modifier_position": 0,
|
||||
"proposal_index": 0,
|
||||
"snapshot_sha256": "...",
|
||||
"reason": "仍可应用的候选",
|
||||
"edits": [
|
||||
{
|
||||
"edit_index": 0,
|
||||
"span": {
|
||||
"start": 10,
|
||||
"end": 15
|
||||
},
|
||||
"expected_text": "exam-",
|
||||
"replacement": "example"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
modifier id 和版本通过 `modifier_position` 引用根 `modifiers`。array 顺序严格保持 `ReviewDocument.residual_proposals` 和
|
||||
`ProposedChange.edits` 的顺序。残留候选仍未应用;投影不能把它放进 stages 或 change count。
|
||||
|
||||
## 10. JSON 基本值和失败关闭
|
||||
|
||||
`review_document_to_dict()` 只能发出:
|
||||
|
||||
```text
|
||||
object / array / string / integer / finite number / boolean / null
|
||||
```
|
||||
|
||||
它不能依赖 `json.dumps(default=...)` 临时处理未知对象。每种公共模型和 enum 都要显式转换;遇到未知类型立即抛出
|
||||
`ReviewProjectionError`。
|
||||
|
||||
投影时至少检查:
|
||||
|
||||
- `review` 是 `ReviewDocument`;
|
||||
- detail 类型和值有效;
|
||||
- enum 是 schema `1.0` 明确支持的成员;
|
||||
- array 中的公共模型类型符合预期;
|
||||
- 字符串可以无损 UTF-8 编码,不含孤立 UTF-16 surrogate;
|
||||
- 整数不是 `bool` 且在安全范围内;
|
||||
- float 有限,不含 NaN 或正负 Infinity;
|
||||
- `modifier_position` 能引用根 `modifiers`;
|
||||
- summary 中没有任何正文承载键;
|
||||
- changes/full 的附加字段符合第 8、9 节。
|
||||
|
||||
这组检查保证序列化结构,不复制 `build_review_document()` 的哈希重放和修改契约。如果调用方绕过 builder 手工构造了
|
||||
语义矛盾但结构合法的 review,投影不声称能恢复可信性。
|
||||
|
||||
`ReviewProjectionError` 的消息只说明字段路径和契约类别,不拼入具体正文、modifier 参数、reason、error message 或周边
|
||||
文本。底层异常可以作为 `__cause__` 保留,但顶层消息不能泄漏被拒绝值。
|
||||
|
||||
## 11. JSON reporter
|
||||
|
||||
`render_json_report()` 必须只做两步:
|
||||
|
||||
1. 调用 `review_document_to_dict(review, detail=detail)`;
|
||||
2. 使用标准库 `json.dumps()` 编码这个返回值。
|
||||
|
||||
固定编码行为:
|
||||
|
||||
```python
|
||||
dumps(
|
||||
projection,
|
||||
ensure_ascii=False,
|
||||
allow_nan=False,
|
||||
indent=2,
|
||||
)
|
||||
```
|
||||
|
||||
第一版不开放 `indent`、`sort_keys`、encoder、`default` 或文件对象参数,避免把 JSON 编码器的全部表面积变成库契约。需要紧凑
|
||||
JSON 的项目可以对官方 dict 投影自行调用 `json.dumps()`,但不能改变字段语义。
|
||||
|
||||
reporter 返回 Python `str`,不写文件、不添加 UTF-8 BOM,也不在末尾额外添加换行。调用方通过文件、HTTP 或数据库发送时
|
||||
负责按 UTF-8 编码并设置正确媒体类型。
|
||||
|
||||
相同值的 `ReviewDocument` 和相同 detail 必须得到相等 dict 和完全相同的 JSON 字符串。官方实现会使用固定插入顺序方便
|
||||
diff 和测试,但消费者仍不得把 object 键顺序当成语义。
|
||||
|
||||
## 12. Schema 兼容策略
|
||||
|
||||
`schema_version` 使用 `MAJOR.MINOR`:
|
||||
|
||||
- `MAJOR` 改变表示现有消费者可能误读或无法读取;
|
||||
- `MINOR` 只允许旧消费者在忽略未知字段时仍能正确理解的加法变化;
|
||||
- 文案修正、实现重构和使输出重新符合既有契约的 bug fix 不改变 schema 版本;
|
||||
- schema 版本与 Python 包版本分别管理。`mdpolish 0.6.0` 可以继续输出 schema `1.0`。
|
||||
|
||||
下列变化必须提升 schema major:
|
||||
|
||||
- 删除或改名已有字段;
|
||||
- 改变字段类型、坐标、哈希或 array 顺序语义;
|
||||
- 改变已有 enum 或稳定错误代码的含义;
|
||||
- 删除 enum 值,或让生产者在既有字段中自动输出消费者不认识的新 enum 值;
|
||||
- 在相同 detail 下新增正文承载字段,导致原暴露等级泄漏更多内容;
|
||||
- 把可选字段改为必需,或改变字段缺失与空值的区别。
|
||||
|
||||
下列变化可以提升 schema minor:
|
||||
|
||||
- 增加不改变现有字段含义的非正文可选字段;
|
||||
- 增加只有调用方显式请求才会返回的新 detail;
|
||||
- 增加一个新的可选顶层摘要对象,同时保留既有对象。
|
||||
|
||||
同一 major 的消费者必须忽略未知 object 字段,但必须保留 array 顺序;不得接受未知 major。消费者应对自己依赖的 enum 值
|
||||
显式处理未知情况,不能把未知状态当成 `success`。
|
||||
|
||||
正文暴露是安全边界:即使新增字段通常属于 minor,在 `summary` 或 `changes` 中新增更高等级正文也必须升 major,或新增一个
|
||||
需要调用方显式选择的 detail。
|
||||
|
||||
第一版只提供生产,不提供兼容读取器。历史 JSON 的迁移、数据库 schema 和多版本读取由实际跨进程需求触发下一份 design。
|
||||
|
||||
## 13. 源码、文档与版本边界
|
||||
|
||||
批准后计划修改:
|
||||
|
||||
```text
|
||||
src/mdpolish/review.py
|
||||
tests/test_review_projection.py
|
||||
README.md
|
||||
research-wiki/explanation/review-projection.md
|
||||
research-wiki/reference/review-projection-schema-v1.md
|
||||
pyproject.toml
|
||||
```
|
||||
|
||||
- `review.py` 增加第 6 节的公共类型、投影和 JSON reporter,并更新模块 `__all__`;
|
||||
- 独立测试文件固定 schema 和内容暴露边界,避免继续扩大现有 700 行的 `test_review.py`;
|
||||
- explanation 只在实现完成后更新当前机制;
|
||||
- reference 记录代码难以完整表达的 schema `1.0`、兼容和正文暴露契约,不复制 README 的当前进度;
|
||||
- README 在实现完成并验证后才增加能力、示例和真实检查结果;
|
||||
- 不把新接口导出到包根,不新增依赖或源码包目录。
|
||||
|
||||
这是新的公共接口和跨进程数据契约,计划包版本为 `0.5.0`。批准 design 不自动改变当前 `v0.4.0` 事实,也不授权创建 tag、
|
||||
GitHub Release 或发布 wheel。
|
||||
|
||||
当前工作区已有不属于本文的 `AGENTS.md`、`CLAUDE.md` 和 `src/mdpolish/regex.py` 修改。后续实施必须继续保留并隔离这些改动,
|
||||
不能把它们混入本功能的 diff 或提交。
|
||||
|
||||
## 14. 测试与验收
|
||||
|
||||
### 14.1 Schema 和 detail
|
||||
|
||||
合成测试至少覆盖:
|
||||
|
||||
- 空文档、空流水线和零修改结果;
|
||||
- `success`、`failed`、`unstable`,以及完整和不完整 stages;
|
||||
- 三个 detail 的精确顶层键、嵌套键、enum 字符串和 array 顺序;
|
||||
- `summary` 的递归结果中不存在第 8 节列出的任何正文承载键,也找不到专门放入原文、reason、message 和参数的哨兵字符串;
|
||||
- `changes` 包含实际及残留修改片段,但不包含输入、当前和阶段完整 `markdown`;
|
||||
- `full` 包含完整输入、当前文本、阶段全文、修改片段和残留候选;
|
||||
- 空字符串正文通过 `markdown: ""` 与字段未暴露清楚区分;
|
||||
- detail 之间共同字段的值和 array 顺序完全一致;
|
||||
- 返回 dict/list 是新容器,修改一次投影不影响 review 或下一次投影。
|
||||
|
||||
### 14.2 坐标、哈希和参数
|
||||
|
||||
- 中文、补充平面字符、组合字符、BOM、LF、CRLF、CR 和无末尾换行;
|
||||
- span 的 0-based 半开码点范围和 location 的 1-based 码点行列保持现有口径;
|
||||
- 输入、当前、阶段和 change 哈希字段指向正确的精确全文;
|
||||
- tuple、顶层参数对、嵌套二元组、空 tuple、bool、null、int 和有限 float 都按第 9.3 节投影;
|
||||
- mapping 形状的 tuple 不被启发式改成 JSON object;
|
||||
- 计数与权威 array 一致,残留 edit 不计入实际 change count。
|
||||
|
||||
### 14.3 错误与失败关闭
|
||||
|
||||
- 三种 `ErrorStage` 分别得到固定稳定代码;
|
||||
- summary 不含 `diagnostic_type` 和 `message`,changes/full 原样包含;
|
||||
- 未知 detail、错误 review 类型、未知 enum、错误嵌套模型、孤立 surrogate、非有限 float、越界整数和无效引用都失败;
|
||||
- 失败异常为 `ReviewProjectionError`,消息不包含测试正文、参数、reason 或 error message 哨兵;
|
||||
- 投影不调用 modifier、不读文件、不访问网络、不修复非法值、不静默省略错误字段。
|
||||
|
||||
### 14.4 JSON reporter
|
||||
|
||||
- `json.loads(render_json_report(...))` 与同 detail 的官方 dict 投影值相等;
|
||||
- 相同输入重复调用得到逐字符相同的 JSON;
|
||||
- Unicode 正文不被强制写成 `\uXXXX`,控制字符仍由标准 JSON 正确转义;
|
||||
- 输出没有 BOM、没有尾随换行、没有 NaN / Infinity,也不依赖 object 键顺序解释;
|
||||
- summary JSON 中不存在正文哨兵,changes/full 的暴露边界与 dict 完全一致;
|
||||
- reporter 不接受自定义 encoder 或文件对象,不写入磁盘。
|
||||
|
||||
### 14.5 回归和交付检查
|
||||
|
||||
实施完成后实际运行根 README 当时列出的全部检查,并确认:
|
||||
|
||||
- 现有 `Pipeline`、编辑执行器、modifier、`ReviewDocument` 和 Markdown reporter 行为不变;
|
||||
- mypy strict、Ruff 和全部 pytest 通过;
|
||||
- 核心安装仍然没有第三方运行依赖;
|
||||
- wheel 包含更新后的 `review.py` 和 `py.typed`,不包含 tests、Wiki、JSON 报告、真实数据或项目文件;
|
||||
- README 示例只处理内存对象和字符串,不暗示 JSON 已经保存;
|
||||
- `AGENTS.md` 与 `CLAUDE.md` 除标题外正文一致;
|
||||
- Git diff 不混入用户现有改动、真实文本、大文件或生成产物。
|
||||
|
||||
## 15. 风险与代价
|
||||
|
||||
- **公共 schema 需要长期维护:** 内部模型以后可以重构,但 schema `1.x` 不能跟着任意改变;这是正式跨进程接口的必要成本。
|
||||
- **三个 detail 增加测试矩阵:** 每个字段都要证明在哪些等级出现;换来的是正文暴露由调用方显式决定。
|
||||
- **`changes` 仍可能泄漏大量内容:** 多条修改和 residual proposal 能覆盖文档大部分区域;它不是脱敏模式。
|
||||
- **`full` 重复全文:** `ReviewDocument` 已持有阶段快照,投影和 JSON 会再次分配;本轮不做流式或惰性序列化。
|
||||
- **错误代码较粗:** 它只能稳定表达失败阶段,不能区分具体原因;精细化必须先改善 `RunError` 的事实来源。
|
||||
- **参数 tuple 表达不够自然:** array-of-pairs 比 JSON object 更啰嗦,但不会猜错已经丢失的 mapping/sequence 身份。
|
||||
- **没有正式 JSON Schema 文件:** 第一版依靠代码、严格测试和 reference 契约;真正出现独立 validator 需求后再增加发布资产。
|
||||
- **summary 可能被误称为安全日志:** 它只排除正文承载字段,不替代项目的数据分类、访问控制和哈希治理。
|
||||
|
||||
## 16. 批准后的实施边界
|
||||
|
||||
用户明确批准本文后,只授权:
|
||||
|
||||
1. 在 `mdpolish.review` 实现第 6 至 12 节的公共类型、dict 投影和 JSON reporter;
|
||||
2. 新增合成测试并按第 14 节验证,不接触真实文档;
|
||||
3. 更新第 13 节列出的 README、explanation、reference 和包版本;
|
||||
4. 在功能 diff 中隔离并保留工作区已有的其他修改;
|
||||
5. 报告实际测试、wheel 内容和 Git diff,不把设计批准描述成已经发布。
|
||||
|
||||
批准本文不授权:
|
||||
|
||||
- 提交、push、创建 PR、tag、GitHub Release 或上传 wheel;
|
||||
- 修改 `Pipeline`、清洗规则、错误捕获语义、`ReviewDocument` 字段或其他仓库;
|
||||
- 创建 CLI、文件适配器、Web 服务、数据库表、JSON Schema 发布资产或反序列化器;
|
||||
- 读取、复制、修改或公开真实文档和外部数据。
|
||||
Reference in New Issue
Block a user