Files
mdpolish/research-wiki/design/0011-generic-review-projection-and-reporting.md
T

504 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 0011:通用内存评审视图与报告
## 状态
已于 2026-08-27 获用户明确批准,按本文第 14 节实施。本文自批准起冻结;后续改变决策需新增 design 并使用
`supersedes` 指向本文。
`supersedes: 0008`(范围有限):本文拟修改 `0008` 中“报告与评审工具全部留在项目端”的当前决定,把“从
`TransformResult` 可信重放通用修改链,并生成不依赖项目界面的内存评审视图”重新纳入 `mdpolish``0008` 已确定的
核心无文件 I/O、项目拥有持久化决定权、无默认流水线和无项目规则继续有效。
`0007` 仍是已被 `0008` 取代的历史记录,本文不让它重新生效。本文不恢复 `0005` 的 artifact schema、实验目录和文件
写入,也不恢复 `0007` 的定位文件、HTTP 服务、React 页面、CodeMirror 或项目评审流程;只借鉴两份历史设计保存的
中间快照重放经验。
## 1. 问题与可观察现象
`v0.3.0``Pipeline.transform()` 返回内存中的 `TransformResult`。成功结果已经包含完整清洗后 Markdown、实际修改、
输入输出哈希和修改器元数据,但调用方仍不容易正确组织一份完整评审视图。
问题不只是“缺一个 HTML 页面”。每条 `Change.span` 都属于该修改器执行前的快照,不一定属于原始输入或最终输出。
如果多个修改器依次改变同一段附近的文本,调用方把所有范围直接画在最终 Markdown 上,会产生错误跳转或错误归属。
历史上的 `0007` 已经观察到同一个问题。当时的可靠做法是:从输入开始,按修改器批次重放实际修改,逐批验证
`before_sha256`、范围、原文和 `after_sha256`,再提供完整阶段文本。旧实现只服务 artifact 和同仓库评审器,已在
`0008` 中移出当前库;但“怎样可信解释 `TransformResult`”并不是 ClinDB 专属问题。
当前调用方有三种不理想选择:
1. 只展示 `output_markdown`,看不到修改来源和过程;
2. 自己重写修改排序、快照校验和中间阶段重放,形成第二套核心语义;
3. 直接展示 `changes`,却把中间快照坐标误当成最终坐标。
因此,当前缺少的不是文件输出,而是位于核心结果和项目界面之间的、可复用的内存评审投影。
## 2. 成熟项目的做法
2026-08-27 查阅的官方接口显示,成熟工具通常把“处理结果”和“展示/持久化”分层:
| 项目 | 核心返回 | 展示与写入 |
| --- | --- | --- |
| [mdformat](https://mdformat.readthedocs.io/en/stable/users/installation_and_usage.html) | `mdformat.text()` 返回完整格式化字符串 | `mdformat.file()` 才原地写文件 |
| [Prettier](https://prettier.io/docs/api/) | `format()` 返回完整字符串;`formatWithCursor()` 额外返回映射后光标 | CLI 或编辑器决定怎样保存和展示 |
| [unified / VFile](https://unifiedjs.com/explore/package/vfile/) | `VFile` 保存最终内容、元数据和消息 | 独立 reporter 接收 `VFile[]` 并返回字符串 |
| [markdownlint](https://github.com/DavidAnson/markdownlint) | lint 结果包含规则、范围、严重度和修复信息;`applyFixes()` 返回完整字符串 | CLI formatter 输出文本、JSON、SARIF、JUnit 等 |
| [textlint](https://github.com/textlint/textlint/blob/master/docs/formatter.md) | fix 结果包含完整 `output`、已应用消息和剩余消息 | 独立 formatter 负责编排 |
这些项目没有要求底层处理函数直接生成某个业务页面。它们共同提供了两点参考:
- 核心或同一工具生态应提供足够准确的内容、位置和消息,使调用方不必重新解释修改语义;
- reporter 可以属于通用工具,但文件路径、持久化和用户界面继续由外层决定。
`TransformResult` 已经比单纯返回字符串更接近 textlint 的 fix 结果和 unified 的 `VFile`。本文只补齐可信重放和通用
reporter,不把它改造成文件或页面对象。
## 3. 目标与非目标
### 3.1 目标
- 接收原始 Markdown 和一个已有 `TransformResult`,先验证输入哈希再建立评审视图;
- 按修改器批次重放实际修改,复用核心的范围、冲突、排序和哈希不变量;
- 提供完整原始 Markdown、完整当前 Markdown、实际修改定位和可信的修改器阶段;
- 明确区分成功输出与 `failed` / `unstable` 的部分或不稳定结果;
- 为每条实际修改提供其修改前快照中的行列位置,不把它伪装成最终输出位置;
- 提供一个确定、无文件 I/O 的 Markdown 源码报告 renderer
- 保持清洗结果、修改应用顺序和 `Modifier` 行为不变;为可靠区分尚未开始执行的预检失败,给 `ErrorStage` 增加
`PREFLIGHT`
- 保持核心安装零第三方运行依赖;
- 只用合成文本验证空文档、Unicode、不同换行、插入、删除、替换、多编辑和多修改器链。
### 3.2 非目标
- 不读取或写入文件,不接收文件路径,不决定文件名、输出目录或覆盖策略;
- 不建立 artifact、JSON 文件 schema、数据库、批处理协议或保留周期;
- 不提供 HTML renderer、Markdown 渲染预览、HTTP 服务、React 页面、编辑器或桌面应用;
- 不执行或重新运行 `Modifier`,不应用 `residual_proposals`,不启动第二轮流水线;
- 不增加默认流水线、项目 profile、业务文档 ID、审核状态、批准/拒绝动作或统计阈值;
- 不把 Python 字符范围转换成 JavaScript UTF-16、LSP 或其他外部坐标;
- 不把所有中间修改强行投影到最终 Markdown 的单一坐标系;
- 不修改清洗语义、规则顺序、终态或修改器版本;除新增 `preflight` 错误阶段外,不改变已有错误阶段的含义;
- 不读取、复制或修改 `data/``artifacts/`、消费测试仓或仓库外真实材料。
最终坐标投影不进入第一版,是因为后续修改器可能再次改写前一修改器生成的文本。此时“第一条修改在最终文本中的范围”
可能缩短、分裂或彻底消失。第一版使用准确的修改器阶段坐标,不用启发式 diff 伪造一一对应关系。
## 4. 方案比较
| 方案 | 优点 | 问题 | 选择 |
| --- | --- | --- | --- |
| 所有展示继续由调用项目实现 | `mdpolish` 表面积最小 | 每个项目都要复制重放、排序和哈希校验,容易产生不同解释 | 不采用 |
| 把原文、所有阶段和 renderer 直接塞进 `TransformResult` | 调用入口最短 | 流水线结果永久重复持有大文本;核心执行和展示投影耦合 | 不采用 |
| 恢复 `0005``0007` 的完整报告与评审器 | 已有历史实现和界面经验 | 重新引入 artifact、路径、HTTP、Node.js 和项目工作流 | 不采用 |
| 新建纯内存 `ReviewDocument` 和通用 Markdown reporter | 唯一可信重放留在 Python;项目仍控制 UI 和持久化 | 增加一组公共模型,并会额外占用中间快照内存 | 采用 |
| 另建独立 Python 分发包 | 边界最物理独立 | 第一版逻辑很小且必须紧跟核心模型版本,增加发布协调 | 本轮不采用 |
## 5. 职责边界
采用后的依赖方向为:
```text
项目选择 Modifier、参数和顺序
Pipeline.transform()
TransformResult
│ + 调用方持有的原始 Markdown
build_review_document()
ReviewDocument
│ │
│ └── render_markdown_report() ──► 内存字符串
项目 HTML / 终端 / 桌面界面
项目决定是否以及怎样保存
```
| 层次 | 负责 | 不负责 |
| --- | --- | --- |
| `Pipeline` | 执行修改并返回当前权威结果 | 为展示重放历史 |
| 评审投影 | 验证并解释已有结果,建立完整阶段 | 重新运行规则、读写文件 |
| 通用 reporter | 把评审投影编排成确定的 Markdown 源码报告 | 业务样式、权限、保存 |
| 使用项目 | 页面、文件名、目录、脱敏、审核流程 | 重写核心修改应用语义 |
`mdpolish` 仍然不知道“这份字符串来自哪个文件”。调用方可以把报告字符串写入文件,但该动作不属于本库保证。
## 6. 第一版公共接口
第一版拟提供受支持的模块路径:
```python
from mdpolish.review import (
ReviewBuildError,
ReviewChange,
ReviewCurrentKind,
ReviewDocument,
ReviewLocation,
ReviewStage,
build_review_document,
render_markdown_report,
)
```
本轮不把这些名称重新导出到包根,避免继续扩大 `mdpolish.__init__`。精确字段类型在实现时可以按严格类型检查机械调整,
但必须满足以下语义。
### 6.1 `ReviewLocation`
表示修改器执行前快照中的人类位置:
```python
@dataclass(frozen=True, slots=True)
class ReviewLocation:
line: int # 1-based
column: int # 1-basedPython Unicode 码点口径
```
权威修改范围仍是 `Change.span`。行列只用于人查看,不用于重新应用修改,也不转换成 UTF-16、字节偏移或终端显示宽度。
位置口径与执行器的 `len()` 和字符串切片一致:BOM 和组合字符各占一个 Python Unicode 码点;补充平面字符占一个码点,
不是两个 UTF-16 code unit。行列指向 `span.start`。LF、CRLF 和 CR 都作为物理换行;CRLF 是一个换行边界,但原始偏移中
仍占两个码点。计算前不规范化正文或换行。
### 6.2 `ReviewChange`
```python
@dataclass(frozen=True, slots=True)
class ReviewChange:
change: Change
location: ReviewLocation
```
它保留原 `Change`,不复制或改名其中的 modifier、proposal、reason、span、before、after 和哈希事实。
这里明确选择嵌入而不是复制字段:`Change` 是同一包内的权威已应用修改记录,另建一份近似字段会形成第二个事实来源。
代价是两个公共模型存在有意耦合;今后增加、删除或改变 `Change` 字段及语义时,必须在 design 中同步评审
`ReviewChange`、reporter 和兼容性,不能把它当作无关的内部改动。
### 6.3 `ReviewStage`
一个阶段表示某个修改器执行前后的可信快照:
```python
@dataclass(frozen=True, slots=True)
class ReviewStage:
modifier_position: int
modifier: ModifierInfo
before_sha256: str
after_sha256: str
before_markdown: str
after_markdown: str
changes: tuple[ReviewChange, ...]
```
同一阶段的修改范围全部相对于 `before_markdown`。零修改阶段的前后字符串和哈希相同,不能伪造 `Change`
### 6.4 `ReviewDocument`
```python
class ReviewCurrentKind(StrEnum):
SUCCESS_OUTPUT = "success_output"
PARTIAL_OUTPUT = "partial_output"
@dataclass(frozen=True, slots=True)
class ReviewDocument:
status: RunStatus
current_kind: ReviewCurrentKind
input_sha256: str
current_sha256: str
input_markdown: str
current_markdown: str
modifiers: tuple[ModifierInfo, ...]
stages: tuple[ReviewStage, ...]
stages_complete: bool
errors: tuple[RunError, ...]
residual_proposals: tuple[ResidualProposal, ...]
```
- `success` 使用 `output_markdown``current_kind``success_output`
- `failed``unstable` 使用 `partial_markdown`,明确标记为 `partial_output`
- `unstable` 仍然不是成功输出,reporter 不能省略这一提示;
- `residual_proposals` 只作为证据保留,绝不应用;
- `ReviewDocument` 不含路径、标题、业务 ID、时间或保存状态。
`ReviewDocument` 不再提供一份平铺的 `changes`。公共且权威的遍历顺序是先遍历 `stages`,再遍历每个
`ReviewStage.changes`reporter 不得按 `Change.modifier_position` 自行二次分组。构建时必须验证每条嵌入 `Change`
`modifier_position`、身份和版本与所属阶段一致。阶段内顺序与核心 `changes` 的报告顺序完全相同。
`stages_complete=True` 只在能够证明所有修改器都完成 transform 阶段时成立:`success``unstable`,以及错误全部发生在
`final_review``failed`。它不表示 final review 成功,也不表示结果可作为正式输出。
### 6.5 可判定的错误阶段与不完整阶段
当前 `ErrorStage.TRANSFORM` 同时表示预检失败和修改器运行失败。仅凭空 `changes` 无法区分“所有修改器均未运行”、
“首个修改器失败”和“前面已经完成了若干零修改器”。reporter 也不能解析可能变化的错误消息来猜测。因此本文给公共
`ErrorStage` 增加:
```python
class ErrorStage(StrEnum):
PREFLIGHT = "preflight"
TRANSFORM = "transform"
FINAL_REVIEW = "final_review"
```
`Pipeline._preflight_modifiers()` 产生的所有错误使用 `PREFLIGHT`。进入修改器循环后,元数据复核、`propose()`、批次验证和
应用错误继续使用 `TRANSFORM`final review 的既有错误继续使用 `FINAL_REVIEW`。这是错误分类的加法变更,不改变
`RunStatus`、当前文本或清洗结果。
评审投影按以下规则处理,不从 `changes` 数量推测执行进度:
| 情形 | `stages` | `current_markdown` | `stages_complete` |
| --- | --- | --- | --- |
| `PREFLIGHT` 失败 | 空元组;即使预检已经读取了部分元数据,也没有修改器阶段 | 必须等于输入 | `False` |
| 修改器位置 `p` 在执行期元数据复核或 `propose()` 失败 | 恰好包含已完成的 `0..p-1` 阶段,包括其中的零修改阶段 | 等于位置 `p` 的修改前快照 | `False` |
| 位置 `p` 已返回 proposals,但再次元数据复核、批次验证或应用失败 | 同上;位置 `p` 不建立阶段,也不伪装成零修改阶段 | 等于位置 `p` 的修改前快照 | `False` |
| 只有 `FINAL_REVIEW` 错误 | 包含全部 transform 阶段 | transform 后的当前文本 | `True` |
| `success``unstable` | 包含全部 transform 阶段 | 对应状态的当前文本 | `True` |
只有一个批次通过验证、原子应用并得到可信的 `after_sha256`,所属修改器阶段才算完成。已经调用 `propose()` 不算完成;
失败修改器已知的 before 快照通过 `ReviewDocument.current_markdown` 表达,错误通过 `RunError` 表达,不增加“半阶段”公共
模型。失败位置之后的修改器一律不进入 `stages`
### 6.6 构建函数
```python
def build_review_document(
input_markdown: str,
result: TransformResult,
) -> ReviewDocument:
...
```
原始全文由调用方显式传入。这样不会让每个 `TransformResult` 默认再保存一份输入,也能在构建评审视图时验证
`input_sha256`
## 7. 可信重放规则
`build_review_document()` 不调用 `Modifier.propose()`。它只解释已经存在的不可变结果,步骤固定为:
1. 验证 `input_markdown` 类型及 SHA-256 等于 `result.input_sha256`
2. 选择状态对应的当前文本,并验证其哈希等于 `result.current_sha256`
3. 根据明确的 `ErrorStage` 和错误位置确定已完成阶段的右开边界;preflight 或 transform 失败只能有一个同阶段错误,
final review 可以有多个错误,混合阶段或越界位置直接拒绝;
4. 验证 preflight 失败没有实际修改且当前文本等于输入;transform 失败的实际修改位置都严格小于失败位置;final review
失败、`success``unstable` 的完成边界必须覆盖全部修改器;
5. 验证 `changes``modifier_position` 不倒退,且身份、版本与 `result.modifiers` 对应;
6. 按修改器位置收集一个批次;同批实际修改必须具有相同的 `before_sha256``after_sha256`
7. 验证当前快照哈希、`proposal_ref`、范围、`before` 原文、重复和冲突;
8. 在修改前快照上计算每条修改的 1-based 行列;
9. 调用与核心执行器共用的私有应用原语,从后向前原子应用批次;
10. 验证新文本哈希等于批次 `after_sha256`
11. 对完成边界内没有实际修改的修改器建立前后相同的阶段;
12. 全链结束后验证文本和值都等于 `result` 中的当前结果。
范围冲突、排序和字符串应用不得在 `review.py` 中维护一套稍有不同的实现。批准后允许从 `edits.py` 提取一个私有的、
无审计副作用的应用原语,由正常执行和评审重放直接共用。规范排序键、冲突语义和字符串应用只在这个原语中存在;重放
不能复制排序 tuple,也不能另写一个“等价”应用函数。核心 `apply_modifier_batch()` 仍负责从 proposals 验证并生成审计
`Change`,重放则把已经校验的实际修改转换成该原语的严格输入。
重构前后 `validate_modifier_batch()``apply_modifier_batch()` 的公共签名、返回类型、异常类型及可观察行为必须保持不变。
除现有回归测试外,必须把同一组合成输入和批次序列分别送入正常执行路径和评审重放路径,逐阶段比较完全相同的
before/after 文本及 SHA-256,并比较最终文本及 SHA-256;只比较最终值不足以验收这次重构。
任一步验证失败都抛出 `ReviewBuildError`。错误消息只说明失败的阶段、修改器位置和契约类型,不包含完整 Markdown、
`before``after` 或周边文本,不通过文本搜索、diff 猜测或跳过错误继续生成近似报告。
## 8. Markdown 报告
第一版提供:
```python
def render_markdown_report(
review: ReviewDocument,
*,
residual_limit: int = 20,
) -> str:
...
```
它是纯函数,只返回一个 Markdown 字符串。固定内容包括:
1. 状态,以及当前内容是成功输出还是部分输出;
2. 输入和当前 SHA-256
3. 修改器顺序、版本和实际修改数量;
4. 完整原始 Markdown 的源码区块;
5. 完整成功输出或明确标注的完整部分输出源码区块;
6. 原始输入到当前文本的统一 diff;
7. 按可信修改器阶段分组的实际修改详情,包括理由、位置、`before``after`
8. 错误和残留候选的安全摘要。
报告展示的是 Markdown 源码,不渲染输入中的 Markdown、HTML、图片或链接。源码围栏长度必须根据内容动态选择,不能因为
正文包含三反引号或三波浪号而提前结束。空文档、无末尾换行和不同物理换行必须有明确、可测试的表示。
统一 diff 只是输入到当前文本的人工视图,不是修改权威,也不承担逐修改器归属。逐修改器归属只看 `ReviewStage`
`ReviewChange`
统一 diff 的固定文件标签用于直接暴露结果性质:成功为 `a/input.md``b/output.md``unstable``a/input.md`
`b/unstable.partial.md``failed``a/input.md``b/failed.partial.md`。diff 之前还必须输出明确的 `status`
`current_kind`,不能只依赖文件名提示。
`ReviewDocument.residual_proposals` 为调用方保留完整证据,但通用 reporter 不重复输出候选中的 `expected_text`
`replacement` 或正文摘要。它先输出残留总数,再按结果中的确定顺序最多列出 `residual_limit` 条;每条只显示
modifier id/version/position、proposal index、reason、edit 数量,以及每个 edit 的 span 和
`len(expected_text)` / `len(replacement)`。若有省略,必须输出省略数量。`residual_limit` 必须满足
`type(residual_limit) is int` 且非负,非法值直接报错;`0` 表示只显示统计。这个限额只影响字符串展示,不截断
`ReviewDocument` 中的证据。
第一版不提供 JSON reporter。稳定 JSON 会立即形成新的序列化 schema,而当前需求只要求内存模型和可读报告。调用项目若
要定义自己的 JSON、HTML 或 API,可以从 `ReviewDocument` 转换,并在自己的仓库维护契约。
## 9. 状态与失败边界
| `RunStatus` | 可以展示的全文 | 阶段语义 |
| --- | --- | --- |
| `success` | 正式成功输出 | 所有修改器 transform 阶段完整 |
| `unstable` | 明确标注的部分/不稳定当前文本 | transform 阶段完整;残留候选不应用 |
| `failed`,仅 final review 错误 | 明确标注的部分当前文本 | transform 阶段完整,final review 失败 |
| `failed`preflight 错误 | 与输入相同的部分当前文本 | 没有修改器运行,阶段为空,`stages_complete=False` |
| `failed`,含 transform 错误 | 明确标注的部分当前文本 | 只展示失败位置前已完成的阶段,`stages_complete=False` |
reporter 不把 `failed``unstable``partial_markdown` 命名为 cleaned、final、successful 或正式结果。
## 10. 隐私与资源边界
`ReviewDocument` 和 Markdown 报告有意包含完整输入、当前文本、`before``after` 和 diff,因此可能还原敏感文档。该能力
只改变内存表示,不改变数据权限:
- 库不自动记录、打印、缓存、保存或上传评审内容;
- 调用方决定是否生成 reporter 字符串,以及是否将它持久化;
- 异常消息和测试失败说明不得泄露正文;
- README 必须明确报告不是安全日志,保存时应遵守调用项目的数据边界;
- 本轮测试只使用虚构小文本,不读取真实数据或历史 artifact。
每个 `ReviewStage` 保存完整前后字符串,内存可能随修改器数量增长。第一版优先保证可信和接口简单,不声称支持无限长度
文档。实现验收应记录合成规模和内存风险;若真实消费出现瓶颈,再设计惰性阶段或紧凑编辑图,不在本轮提前增加两套模型。
## 11. 源码结构、版本与兼容
批准后拟新增:
```text
src/mdpolish/
├── review.py # 公共评审模型、构建函数和 Markdown reporter
└── _review_replay.py # 必要时保存私有可信重放辅助
tests/
└── test_review.py
research-wiki/explanation/
└── review-projection.md # 实现后解释当前机制
```
同时会修改现有 `models.py`(新增 `ErrorStage.PREFLIGHT`)、`pipeline.py`(预检错误归类)、`edits.py`(私有应用原语)及其
对应现有测试。除这些已列明位置和新增评审模块外,不借本设计改动其他核心契约。
若共用编辑辅助可以清楚留在 `edits.py`,则不强制创建 `_review_replay.py`。实现时以单一可信应用逻辑和可读性为准,不能仅为
匹配草图创建空壳模块。
本文增加公共模型和 reporter,并有限改变 `0008` 的职责边界,计划包版本为 `0.4.0`,不是 `0.3.1`。现有
`TransformResult` 字段、顶层导出、修改器版本和清洗语义保持不变;`ErrorStage` 增加 `PREFLIGHT`,原有两个枚举值语义
不变。调用方不使用 `mdpolish.review` 时,除预检错误获得更准确的阶段值外行为不变。
实现和验收完成后才更新根 README 的当前版本候选、能力边界和检查结果。能力表应增加
`build_review_document()` / `render_markdown_report()`;“当前不提供”应改为不提供 artifact、文件/JSON/HTML 输出、
Web 评审器和项目工作流,并明确这两个新函数只返回内存对象或字符串,不读取或写入文件。本文获批不自动授权 tag、
Release 或发布。
## 12. 测试与验收
### 12.1 结果重放
合成测试至少覆盖:
- 空文档、零修改和空流水线;
- 一个修改器的插入、删除、替换和多编辑原子批次;
- 多修改器依次修改,后一个修改器读取并修改前一个输出;
- 一个候选包含多条编辑,`proposal_ref` 关联保持不变;
- 首个、中间和末尾零修改器阶段;
- 中文、补充平面字符、组合字符、BOM、LF、CRLF、CR 和无末尾换行;
- 对 BOM、组合字符和补充平面字符断言精确行列,确认它们分别按 Python 码点而不是显示宽度、UTF-16 或字节计算;
- `success``unstable`、preflight failure、transform failure 和 final review failure
- preflight failure 的空阶段和原样当前文本;修改器执行期元数据、`propose()`、批次验证和应用分别失败时,失败位置之前的
阶段边界与当前文本;
- 批次验证失败时,即使 `propose()` 已经返回,失败修改器也不产生零修改阶段;
- 残留候选只展示不应用;
- 相同输入与结果得到值相等、顺序相同的评审对象;
- 同一组合成输入和批次序列通过核心执行与评审重放得到完全相同的逐阶段及最终文本和 SHA-256。
### 12.2 篡改和失败关闭
至少拒绝:
- 输入哈希不符;
- 状态对应文本或当前哈希不符;
- 错误阶段混合、preflight/transform 多错误、错误位置越界或状态与错误阶段不相容;
- preflight 失败却含实际修改或当前文本不等于输入;transform 失败却含失败位置或其后的实际修改;
- 修改器位置倒退、越界或身份不符;
- proposal 引用位置或快照不符;
- 批次前后哈希不一致;
- 范围越界、`before` 长度不符、原文不符、重复和冲突修改;
- 重放完成后文本或哈希与结果不符。
失败消息测试必须确认不会包含合成正文片段。
### 12.3 Markdown reporter
至少覆盖:
- 完整输入和完整当前文本都能在报告中找到并区分;
- success、failed、unstable 标签不会混淆;
- 统一 diff 标签分别使用 `output.md``unstable.partial.md``failed.partial.md`
- 修改器顺序、版本、计数、理由和阶段位置正确;
- reporter 只按 `ReviewStage` 遍历实际修改,不自行按 `modifier_position` 重组;
- 残留候选总数、`residual_limit=0`、截断及省略计数正确,且报告不包含其 `expected_text``replacement` 或正文摘要;
- 输入包含反引号围栏、波浪号围栏、HTML、链接和图片语法时只作为源码显示;
- 空内容、无末尾换行和混合 Unicode 不被静默规范化;
- 统一 diff 与输入和当前文本一致,但不被当作修改记录;
- renderer 不读文件、不访问网络、不调用修改器。
### 12.4 基础回归与交付
实现完成后必须实际运行根 README 当时列出的全部检查,并额外确认:
- 现有核心和修改器测试结果不变;
- Python 最低支持版本和当前支持版本都能构建评审视图;
- wheel 包含公共 `review.py` 和类型标记,不包含 tests、Wiki、artifact、页面或真实数据;
- 核心安装仍无第三方运行依赖;
- README 示例只操作内存字符串,不暗示文件已经写入;
- `AGENTS.md``CLAUDE.md` 除标题外正文一致;
- Git diff 不混入用户已有改动、真实文本、大文件或生成产物。
## 13. 风险与代价
- **公共表面积增加:** 新 dataclass 和函数一旦发布就需要兼容管理;第一版只提供完成当前问题所需的窄接口。
- **中间全文占用内存:** 完整阶段便于可信评审,但对大文档和长流水线有成本;README 必须诚实说明未定义极端规模保证。
- **Markdown 报告会放大敏感内容:** 它包含原文和当前全文;库不保存并不能替代调用方的数据治理。
- **错误枚举加值:** 精确区分 preflight 是公共契约变更;依赖方若对 `ErrorStage` 做穷举匹配,需要处理新值。相比让
reporter 猜测执行进度,这个显式兼容成本更可控。
- **报告可能被误当成权威:** 修改权威仍是 `TransformResult` 和重放校验;统一 diff 与 Markdown 排版只是派生视图。
- **旧代码容易被直接搬回:** 历史 `_artifact_replay.py` 可作为测试经验,但旧 artifact 类型、UTF-16 坐标和文件契约不得
复制进新的公共模型。
## 14. 批准后的实施边界
用户明确批准本文后,只授权:
1. 新增第 11 节所需的通用评审源码和合成测试;
2. 为单一可信批次语义做必要的私有机械重构,不改变公共函数签名和清洗结果;
3.`ErrorStage` 增加 `PREFLIGHT` 并调整预检错误归类,再实现第 6 至 9 节的内存模型、可信重放和 Markdown reporter
4. 更新 README 当前能力、示例和真实验证结果;
5. 新增实现后的 `explanation/review-projection.md`
6. 将包版本候选更新为 `0.4.0`,完成提交前 diff、测试和 wheel 检查。
批准本文不授权:
- 提交、push、创建 PR、创建 tag、GitHub Release 或发布 wheel
- 修改其他仓库、消费测试仓、外部系统或真实数据;
- 创建 artifact、报告文件、HTML 页面、CLI、服务、前端、配置或默认流水线;
- 改变清洗规则、误删容忍度、修改器顺序、`RunStatus` 或输入输出持久化协议。