156 lines
9.4 KiB
Markdown
156 lines
9.4 KiB
Markdown
# 通用内存评审视图怎样解释一次清洗结果
|
||
|
||
## 1. 为什么不能直接把所有修改画在最终全文上
|
||
|
||
`TransformResult.changes` 中的每条范围都属于对应修改器执行前的快照。前一个修改器插入或删除文本后,后一个修改器看到的
|
||
位置已经不同;后续修改器也可能再次改写前一阶段生成的内容。因此,把所有 `Change.span` 直接当成最终全文坐标,会产生
|
||
错误跳转和错误归属。
|
||
|
||
当前实现要求调用方同时提供原始 Markdown 和已有 `TransformResult`:
|
||
|
||
```text
|
||
原始 Markdown + TransformResult
|
||
│
|
||
▼
|
||
build_review_document()
|
||
│ 验证并重放,不运行 Modifier
|
||
▼
|
||
ReviewDocument
|
||
├── render_markdown_report() ─────────────► 内存 Markdown 字符串
|
||
│
|
||
└── review_document_to_dict(detail=...)
|
||
├───────────────────────────► 项目自己的界面或转换层
|
||
└── render_json_report() ──► 内存 JSON 字符串
|
||
│
|
||
▼
|
||
parse_json_report()
|
||
│
|
||
▼
|
||
本地只读 reviewer 页面
|
||
```
|
||
|
||
清洗输入读取、报告保存位置、权限和审核流程仍由调用项目决定。项目可以把正式 `full` JSON 保存到自己的目录,再显式启动
|
||
`mdpolish-reviewer`;通用页面不替项目生成、命名或清理这些文件。
|
||
|
||
## 2. 构建过程为什么可以失败关闭
|
||
|
||
`build_review_document()` 从调用方提供的原文开始,先验证输入和当前全文的 SHA-256,再按修改器位置重建批次。每个有实际
|
||
修改的阶段都会检查:
|
||
|
||
- `modifier_id`、版本和位置与记录的修改器元数据一致;
|
||
- `before_sha256`、`proposal_ref` 和每条编辑都指向当前阶段的修改前快照;
|
||
- proposal index、edit index 和报告顺序没有缺失、重复或被重排;
|
||
- 范围没有越界、重复或冲突,`before` 与原文精确一致;
|
||
- 应用后的全文和 `after_sha256` 一致。
|
||
|
||
核心执行与评审重放调用 `edits.py` 中同一个私有应用原语。排序键和字符串应用没有在评审模块复制一份;现有
|
||
`apply_modifier_batch()` 仍负责生成权威 `Change` 审计记录。
|
||
|
||
任一条件不成立都会抛出 `ReviewBuildError`。错误只说明契约类别和修改器位置,不包含输入、`before`、`after` 或上下文
|
||
片段。构建函数不会通过搜索或 diff 猜测缺失信息,也不会重新调用修改器。
|
||
|
||
## 3. `stages_complete` 表示什么
|
||
|
||
`ErrorStage.PREFLIGHT` 用来区分“修改器尚未开始执行”和“已经进入 transform 后失败”。阶段边界如下:
|
||
|
||
| 结果 | `ReviewStage` 范围 | `stages_complete` |
|
||
| --- | --- | --- |
|
||
| `success` | 全部修改器,包括零修改阶段 | `True` |
|
||
| `unstable` | 全部 transform 阶段;残留候选不应用 | `True` |
|
||
| 只有 final review 错误的 `failed` | 全部 transform 阶段 | `True` |
|
||
| preflight 失败 | 空 | `False` |
|
||
| 修改器位置 `p` 的 transform 失败 | 只包含 `0..p-1` | `False` |
|
||
|
||
已经返回 proposals 但元数据复核、批次验证或应用失败的修改器,不会被伪装成零修改阶段。它的修改前全文就是
|
||
`ReviewDocument.current_markdown`,失败原因保存在 `errors`;失败位置之后的修改器没有运行,也没有阶段。
|
||
|
||
`stages_complete=True` 只证明 transform 阶段完整,不表示 final review 成功,更不把 `failed` 或 `unstable` 提升成成功
|
||
输出。
|
||
|
||
## 4. 位置口径
|
||
|
||
`ReviewLocation` 指向 `Change.span.start`,并相对于所属 `ReviewStage.before_markdown` 计算。行和列都是 1-based,列宽使用
|
||
Python Unicode 码点:
|
||
|
||
- BOM 和组合字符各占一个码点;
|
||
- 补充平面字符占一个码点,不按 UTF-16 的两个 code unit 计算;
|
||
- LF、CRLF 和 CR 都是物理换行,CRLF 是一个换行边界但占两个原始码点;
|
||
- 计算前不做 Unicode 或换行规范化。
|
||
|
||
权威范围仍然是 Python 半开区间 `Change.span`。`ReviewLocation` 只供人阅读,不用于重新应用修改,也不是 JavaScript、LSP
|
||
或终端显示列坐标。
|
||
|
||
## 5. Markdown reporter 的边界
|
||
|
||
`render_markdown_report()` 是纯函数,只返回 Markdown 源码字符串。报告包含状态、哈希、修改器顺序、完整输入、完整当前
|
||
全文、带物理换行标记的统一 diff,以及按 `ReviewStage` 排列的实际修改。`failed` 和 `unstable` 的 diff 文件标签带有
|
||
`.partial.md`,避免把部分结果误认成正式输出。
|
||
|
||
完整 residual proposal 仍保留在 `ReviewDocument` 中。reporter 默认最多列出前 20 条,只显示修改器身份、位置、原因、
|
||
span 和修改前后长度,不重复输出 `expected_text`、`replacement` 或正文摘要。调用方可以把限额设为非负整数,`0` 表示
|
||
只显示总数。
|
||
|
||
报告有意包含完整文档、实际修改的 `before` / `after` 和 diff,可能还原敏感内容。库不会自动打印、保存、上传或缓存
|
||
报告;持久化后的路径、访问权限、脱敏和保留周期属于调用项目。每个阶段保存完整前后快照,当前实现优先保证可复核性,
|
||
没有声称适合无限长度文档或无限修改器链。
|
||
|
||
## 6. 机器投影为什么不是 `dataclasses.asdict()`
|
||
|
||
`review_document_to_dict()` 是 `ReviewDocument` 的单向公共视图,不是内部 dataclass 的机械展开。它显式转换 enum、tuple、
|
||
modifier 参数、位置和哈希,并在根对象写入 `schema_name=mdpolish.review` 与独立的 `schema_version=1.0`。这样内部 Python
|
||
结构可以在不改变 schema 的前提下重构,调用项目也不需要猜测 enum、数组和阶段坐标。
|
||
|
||
投影有三个内容等级:
|
||
|
||
| detail | 增加的内容 |
|
||
| --- | --- |
|
||
| `summary` | 状态、哈希、计数、modifier 身份、阶段摘要和稳定错误代码;默认不含正文承载字段 |
|
||
| `changes` | modifier 参数和适用说明、实际修改片段、错误诊断、残留候选片段 |
|
||
| `full` | 完整输入、当前全文和每个完整阶段的前后全文 |
|
||
|
||
高等级只增加字段,不改变低等级已有字段的值和顺序。被 detail 隐藏的字段直接不存在,不使用 `null` 或空字符串假装隐藏,
|
||
因此空文档在 `full` 中仍能明确表示为 `markdown: ""`。
|
||
|
||
`summary` 不含正文,但仍包含 modifier identity、哈希和计数,只能称为“无正文投影”,不能称为脱敏或公开安全日志。
|
||
`changes` 和 `full` 都可能还原敏感内容;库不会自动打印、保存或发送任何投影。
|
||
|
||
`render_json_report()` 只把同 detail 的正式 dict 投影用标准 JSON 编码,不维护第二套字段。它返回内存字符串,使用 Unicode、
|
||
拒绝 NaN / Infinity,不写文件或添加 BOM。Markdown reporter 继续直接读取 `ReviewDocument`:它面向人类排版并包含 diff、
|
||
动态围栏和 residual 展示限额,不依赖机器 schema。
|
||
|
||
`parse_json_report()` 读取内存 JSON 字符串,返回新的普通 dict/list 容器。它不是 `ReviewDocument` 反序列化器,也不能用于
|
||
重新应用修改。三个 detail 都会检查字段、枚举、引用、顺序、计数和正文暴露边界;只有 `full` 带有完整阶段文本,因此还能
|
||
验证所有正文哈希、阶段链、Change 原文和行列,并使用 `edits.py` 的同一套精确编辑原语重放每个阶段。重放结果不等于
|
||
stage after 时直接抛出 `ReviewParseError`,不会因为攻击者同时更新正文和声明哈希就接受伪造阶段。
|
||
|
||
schema 同一 major 的未知 object 字段不改变已有字段解释;未知 major、detail、状态、坐标契约或 enum 会被拒绝。解析错误只
|
||
说明字段路径和契约类别,不复制正文、参数、reason 或诊断消息。
|
||
|
||
完整字段、坐标、错误代码、读取保证和兼容规则见
|
||
[`review-projection-schema-v1.md`](../reference/review-projection-schema-v1.md)。
|
||
|
||
## 7. 本地页面为什么仍然保持项目无关
|
||
|
||
本地 reviewer 只接受用户明确传入的一个目录,并读取其中直属的 `*.review.json`。它从文件名派生保守的页面标签,不读取
|
||
原始 Markdown 路径、项目 manifest、默认流水线或业务状态。Python 服务负责正式 JSON 校验和码点到 UTF-16 的只读定位,
|
||
React 页面只消费同源内部 API。
|
||
|
||
```text
|
||
项目保存的 full JSON
|
||
│
|
||
▼
|
||
官方 reader:验证 schema、哈希、阶段和 Change
|
||
│
|
||
▼
|
||
127.0.0.1 上的只读 API
|
||
│
|
||
▼
|
||
文档列表 ── Modifier 时间线 ── 双栏源码比较 ── Change 跳转
|
||
```
|
||
|
||
`editor_range` 只用于 CodeMirror 选择和滚动。正式 span 仍是所属 `stage.before.markdown` 中的 Python 码点半开范围,不写回
|
||
JSON,也不变成新的审计权威。
|
||
|
||
服务只绑定回环地址,只接受 `GET` / `HEAD`,校验 Host 和 Origin,不开放 CORS,也没有写入、重新清洗、上传或 shell 接口。
|
||
页面不渲染 Markdown 和 HTML,不加载图片或外部资源。它展示的是清洗证据,不记录批准、拒绝、批注或审核结论。
|