30 KiB
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 进程中这样消费评审结果:
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、半开范围、阶段哈希和部分输出。最短的做法看似是:
payload = dataclasses.asdict(review)
但这会产生四类问题:
- 内部 dataclass 字段会在未经评审的情况下变成外部协议;以后正常的 Python 重构也会破坏消费者;
- enum、tuple、递归参数值和位置单位没有正式 JSON 表达,消费者容易形成不同解释;
ReviewDocument包含完整输入、当前全文、每阶段全文和修改片段,机械展开会默认暴露全部正文;- 当前
RunError.error_type是诊断用 Python 异常类名,不能被项目端误当成稳定错误代码。
因此,需要由 mdpolish 自己提供一个经过选择和转换的对外视图,而不是让每个项目根据内部字段猜一个版本。
本文把这个视图称为“机器投影”:它是 ReviewDocument 的有损、单向、稳定表示,不是内部对象的镜像,也不是可用于重新
应用修改的序列化快照。
2. 外部规范带来的约束
2026-08-28 查阅的官方规范给出以下直接约束:
- RFC 8259 把 JSON object 定义为无序名称/值集合,把 array 定义为有序序列; 对外语义不能依赖 object 成员顺序,但修改器、阶段、修改和残留候选的顺序必须用 array 保存;
- RFC 8259 要求开放系统中的 JSON 文本使用 UTF-8,成员名应唯一,并指出超出 IEEE 754 binary64 精确整数范围的数字会降低 互操作性;本投影只发出唯一键、可 UTF-8 编码的字符串和安全范围整数;
- Python
json文档 显示allow_nan默认允许非标准的NaN/Infinity,ensure_ascii默认转义非 ASCII;官方 reporter 必须显式使用allow_nan=False和ensure_ascii=False; - JSON Schema 2020-12 区分 schema 版本和实例内容,并允许通过 schema 约束对象、数组和 enum;本轮先固定实例 schema 和兼容策略,不引入运行时 validator 或第三方 schema 依赖;
- SARIF 2.1.0 区分稳定标识符与给人看的消息,并明确
不应从没有稳定标识的来源中猜造精细规则 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. 职责与数据流
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__ 重新导出:
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 名称和版本固定为:
{
"schema_name": "mdpolish.review",
"schema_version": "1.0"
}
schema_version 是数据格式版本,不是 mdpolish 包版本,也不是 modifier 版本。第一版完整顶层结构为:
{
"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 字符串执行:
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. 三个正文暴露等级
三个等级必须满足单调关系:
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 都输出:
"input": {
"sha256": "...",
"code_point_length": 123
},
"current": {
"sha256": "...",
"code_point_length": 120
}
full 分别增加:
"markdown": "完整文本"
current.markdown 的性质必须结合根字段 current_kind 判断。partial_output 永远不能因为进入 JSON 而改名为 cleaned、final
或 successful。
9.2 计数
"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 的稳定身份:
"modifiers": [
{
"position": 0,
"modifier_id": "example.normalize",
"version": "1.0.0"
}
]
changes 和 full 增加:
"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 都输出:
"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 中出现:
"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 都输出稳定身份和位置:
"errors": [
{
"code": "run.transform_failed",
"stage": "transform",
"modifier_position": 1,
"modifier_id": "example.normalize",
"modifier_version": "1.0.0"
}
]
changes 和 full 增加:
"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:
"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() 只能发出:
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() 必须只做两步:
- 调用
review_document_to_dict(review, detail=detail); - 使用标准库
json.dumps()编码这个返回值。
固定编码行为:
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可以继续输出 schema1.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. 源码、文档与版本边界
批准后计划修改:
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. 批准后的实施边界
用户明确批准本文后,只授权:
- 在
mdpolish.review实现第 6 至 12 节的公共类型、dict 投影和 JSON reporter; - 新增合成测试并按第 14 节验证,不接触真实文档;
- 更新第 13 节列出的 README、explanation、reference 和包版本;
- 在功能 diff 中隔离并保留工作区已有的其他修改;
- 报告实际测试、wheel 内容和 Git diff,不把设计批准描述成已经发布。
批准本文不授权:
- 提交、push、创建 PR、tag、GitHub Release 或上传 wheel;
- 修改
Pipeline、清洗规则、错误捕获语义、ReviewDocument字段或其他仓库; - 创建 CLI、文件适配器、Web 服务、数据库表、JSON Schema 发布资产或反序列化器;
- 读取、复制、修改或公开真实文档和外部数据。