# ReviewDocument 机器投影 schema 1.0 本文记录 `mdpolish.review.review_document_to_dict()`、`render_json_report()` 和 `parse_json_report()` 当前稳定的跨进程查询 口径。公共 Python 类型和运行校验以代码与测试为准;生产契约的设计理由见 [`0012-review-document-machine-projection.md`](../design/0012-review-document-machine-projection.md),只读解析与本地页面边界见 [`0014-generic-local-reviewer.md`](../design/0014-generic-local-reviewer.md)。 ## 1. Schema 身份与入口 每个投影都包含: ```json { "schema_name": "mdpolish.review", "schema_version": "1.0" } ``` schema 版本独立于 `mdpolish` 包版本和 modifier 版本。公共入口是: ```python from mdpolish.review import parse_json_report, render_json_report, review_document_to_dict payload = review_document_to_dict(review, detail="summary") json_text = render_json_report(review, detail="full") parsed = parse_json_report(json_text, expected_detail="full") ``` 前两个生产入口只接受内存中的 `ReviewDocument`。JSON reporter 编码同 detail 的正式 dict,不定义另一套字段,也不读写文件。 reader 只接受内存字符串,返回新的普通 JSON 基本值容器;它不接收路径,也不恢复 `ReviewDocument`。 ## 2. 顶层字段 | 字段 | JSON 类型 | 口径 | | --- | --- | --- | | `schema_name` | string | 固定为 `mdpolish.review` | | `schema_version` | string | 当前固定为 `1.0` | | `detail` | string | `summary`、`changes`、`full` | | `status` | string | `success`、`failed`、`unstable` | | `current_kind` | string | `success_output`、`partial_output` | | `stages_complete` | boolean | 是否能证明全部 modifier transform 阶段完成 | | `hash_contract` | object | 所有 Markdown 哈希的算法、编码和规范化口径 | | `coordinate_contract` | object | span、行列和物理换行口径 | | `input` | object | 输入文本摘要;`full` 增加正文 | | `current` | object | 当前文本摘要;`full` 增加正文 | | `counts` | object | modifier、完成阶段、实际修改、错误和残留候选数量 | | `modifiers` | array | 全部 modifier 的权威顺序 | | `stages` | array | 已完成 transform 阶段的权威顺序 | | `errors` | array | Pipeline 记录的错误顺序 | | `residual_proposals` | array | 只在 `changes` / `full` 出现;候选未应用 | JSON object 成员顺序不构成语义。所有 array 顺序构成语义,消费者不得重新按 ID、哈希或文本排序后再解释位置。 ## 3. 内容暴露等级 | 内容 | `summary` | `changes` | `full` | | --- | ---: | ---: | ---: | | 状态、哈希、位置口径和计数 | 是 | 是 | 是 | | modifier id、版本和位置 | 是 | 是 | 是 | | modifier 参数和 applicability | 否 | 是 | 是 | | 阶段前后哈希、长度和修改数 | 是 | 是 | 是 | | 实际修改的 reason、位置、`before` / `after` | 否 | 是 | 是 | | 稳定错误代码和错误位置 | 是 | 是 | 是 | | Python 诊断类型和错误消息 | 否 | 是 | 是 | | 残留候选的 reason、`expected_text` / `replacement` | 否 | 是 | 是 | | 输入、当前和阶段完整 Markdown | 否 | 否 | 是 | 默认是 `summary`。被隐藏的内容键不存在;`null` 和空字符串都不是“已隐藏”的替代值。`summary` 不含直接正文,但仍含 modifier identity、哈希和计数,不自动等于匿名、脱敏或适合公开传播。 ## 4. 固定口径 ### 4.1 哈希 ```json "hash_contract": { "algorithm": "sha256", "encoding": "utf-8", "normalization": "none" } ``` 哈希是精确 Markdown 的 `sha256(markdown.encode("utf-8")).hexdigest()`。不规范化 Unicode、BOM、空白、末尾换行或 CR/LF/CRLF。`Change.before_sha256` / `after_sha256` 是完整阶段快照哈希,不是修改片段哈希。 ### 4.2 坐标 ```json "coordinate_contract": { "offset_unit": "unicode_code_point", "span_index_base": 0, "span_end": "exclusive", "location_index_base": 1, "physical_line_endings": ["lf", "crlf", "cr"] } ``` - `span` 相对于所属 `ReviewStage.before_markdown`,使用 0-based Unicode code point 半开范围; - `location.line` / `column` 指向 `span.start`,使用同一阶段修改前文本中的 1-based Unicode code point 位置; - CRLF 形成一个物理换行,但在 offset 中占两个 code point; - 不提供最终全文坐标、UTF-16、byte offset 或终端显示宽度。 所有整数都在 `0..2**53-1`;参数中的有符号整数在 `-(2**53-1)..2**53-1`。所有 float 必须有限。 ## 5. 嵌套对象 ### 5.1 文本摘要 `input`、`current` 以及 stage 的 `before` / `after` 都至少包含: ```json { "sha256": "64 位小写十六进制", "code_point_length": 123 } ``` `full` 增加 `markdown`。`current.markdown` 是否为正式输出必须看根字段 `current_kind`;`partial_output` 不能当作成功结果。 ### 5.2 Counts ```json "counts": { "modifier_count": 3, "completed_stage_count": 2, "change_count": 5, "error_count": 1, "residual_proposal_count": 0 } ``` `change_count` 只统计已完成 stage 中的实际 `ReviewChange`,不含 residual proposal edit。零修改的完整 stage 仍计入 `completed_stage_count`。 ### 5.3 Modifier 所有 detail: ```json { "position": 0, "modifier_id": "example.normalize", "version": "1.0.0" } ``` `changes` / `full` 增加 `parameters` 和 `applicability`。`parameters` 是有序 array-of-pairs;所有内部 tuple 继续投影成 array,不根据二元组外形猜成 JSON object: ```json "parameters": [ ["pattern", " {2,}"], ["replacement", " "] ] ``` ### 5.4 Stage 所有 detail: ```json { "modifier_position": 0, "before": {"sha256": "...", "code_point_length": 20}, "after": {"sha256": "...", "code_point_length": 18}, "change_count": 1 } ``` `changes` / `full` 增加 `changes` array;`full` 还给两个文本摘要增加 `markdown`。`modifier_position` 引用根 `modifiers[position]`,不复制 modifier 元数据。 ### 5.5 实际 Change 只在 `changes` / `full` 出现: ```json { "proposal_index": 0, "edit_index": 0, "reason": "修改原因", "location": {"line": 3, "column": 7}, "span": {"start": 24, "end": 31}, "before": "原片段", "after": "新片段", "before_sha256": "修改前完整阶段哈希", "after_sha256": "修改后完整阶段哈希" } ``` ### 5.6 Error 所有 detail 都含 `code`、`stage`、modifier 位置与身份。`changes` / `full` 再增加诊断用的 `diagnostic_type` 和 `message`。 | `stage` | 稳定 `code` | | --- | --- | | `preflight` | `run.preflight_failed` | | `transform` | `run.transform_failed` | | `final_review` | `run.final_review_failed` | 代码只稳定表达失败阶段。`diagnostic_type` 是 Python 异常类名,`message` 是人类消息,不能作为稳定程序分支条件。当前 `RunError` 没有保存更细的稳定原因;不能从类名或消息猜造更细代码。 ### 5.7 Residual proposal 只在 `changes` / `full` 出现: ```json { "modifier_position": 0, "proposal_index": 0, "snapshot_sha256": "当前完整快照哈希", "reason": "候选原因", "edits": [ { "edit_index": 0, "span": {"start": 10, "end": 15}, "expected_text": "原片段", "replacement": "候选片段" } ] } ``` 它是 final review 证据,没有应用,不进入 stage 或实际 change count。 ## 6. JSON 编码 `render_json_report()` 固定使用标准库 JSON 的以下语义: - `ensure_ascii=False`; - `allow_nan=False`; - `indent=2`; - 无 UTF-8 BOM; - 返回字符串末尾不额外添加换行。 返回值是 Python `str`。调用方保存或发送时负责 UTF-8 编码、媒体类型、权限和保留周期。库不接收路径或文件对象。 ## 7. 只读解析保证 ```python from mdpolish.review import ReviewParseError, parse_json_report payload = parse_json_report(json_text) full_payload = parse_json_report(json_text, expected_detail="full") ``` `expected_detail` 可以省略,也可以显式指定 `summary`、`changes` 或 `full`。报告自己的 detail 不匹配时失败。返回结果是本次 解析新建的 dict/list;修改它不会恢复或改变 producer 侧的 `ReviewDocument`。 所有 detail 都检查: - JSON object key 唯一,字符串可以严格编码为 UTF-8,整数和浮点数满足第 4 节范围; - schema、detail、enum、稳定错误代码、数组顺序、引用位置和计数; - `summary` / `changes` 没有越过各自的正文暴露边界; - 状态、`current_kind`、错误阶段、完成阶段和残留候选相容。 `summary` 和 `changes` 没有完整阶段正文,reader 不会声称能验证不存在的文本。`full` 另外检查: - input、current、所有 stage before/after 的码点长度和 SHA-256; - 第一个阶段从 input 开始,相邻阶段首尾相接,最后一个完成阶段等于 current; - Change span 的原文、位置、哈希、proposal/edit index、冲突和正式报告顺序; - 用核心共用的精确编辑原语重放每个 Change 批次,结果与 stage after 完全相同; - 零修改阶段的 before 和 after 完全相同; - residual edit 的原文、范围、顺序和冲突。 任一步失败都抛出 `ReviewParseError`。顶层错误消息只包含字段路径和契约类别,不包含正文、modifier 参数、reason 或诊断消息。 reader 不调用 Modifier,不修复、截断或猜测损坏结果。 这是只读解析,不是反序列化。返回 dict 不能重新运行 Pipeline、恢复 Python 模型或获得原始 `TransformResult` 的权威身份。 ## 8. 兼容策略 `schema_version` 使用 `MAJOR.MINOR`: - 删除、改名、改类型、改变位置/哈希/顺序语义、改变已有 enum 或错误代码含义,需要提升 major; - 在相同 detail 中新增更高敏感度的正文承载字段,需要提升 major,或增加必须显式请求的新 detail; - 不改变旧字段解释的非正文可选字段,可以提升 minor; - 实现修正为重新符合已有契约,不改变 schema 版本; - schema 版本不跟随 Python 包版本自动变化。 同一 major 的消费者必须忽略未知 object 字段,但必须保持 array 顺序;不得把未知状态当成 `success`。消费者应拒绝自己不 支持的 schema major。 schema `1.0` 仍是单向生产契约。官方 reader 接受同一 major 的已知字段语义,并忽略未知 object 字段的语义;未知状态、 detail、enum 或坐标契约仍会失败。当前没有 `ReviewDocument` 反序列化器、JSON Schema 文件、历史迁移器或数据库 schema。