24 KiB
0014:项目无关的本地清洗评审器
状态
已于 2026-08-28 获用户明确批准,按本文第 15 节实施。本文自批准起冻结;后续改变决策需新增 design 并使用
supersedes 指向本文。
用户在批准本文时同时明确要求:完成实施和验收后提交 Git,创建并推送 v0.7.0 tag,再用同一个已验收 wheel 及其
SHA-256 校验文件创建 GitHub Release。该授权不包括 PyPI、其他包索引、PR 或其他仓库修改。
supersedes: 0008(范围有限):本文拟改变“Web 评审器全部留在项目端”的边界。项目规则、流水线、文件写入和业务审核流程
仍由使用项目拥有;mdpolish 只增加读取正式 review JSON 的通用本地查看工具。
supersedes: 0011(范围有限):本文拟增加 HTML 浏览器界面和本机只读服务,但不改变 ReviewDocument、可信重放、
Markdown reporter 或核心无文件 I/O 的决定。
supersedes: 0012(范围有限):本文拟增加正式 JSON 的只读解析与校验入口。它不会把 JSON 恢复为 ReviewDocument,
不会重新运行 Modifier,也不会把机器投影变成可重新应用修改的权威输入。
历史 0007 已经被 0008 替代,不因本文重新生效。本文只借鉴其本机服务安全边界,以及
/home/lihaoze/work/mdpolish-wheel-pilot 中已经实现的双栏界面;不恢复旧 artifact、locator、manifest 或项目实验系统。
1. 问题与可观察现象
mdpolish v0.6.0 已经能把可信的 ReviewDocument 生成为 schema 1.0 的 full JSON。这个 JSON 包含完整输入、当前文本、
所有 Modifier 阶段和实际 Change,足以支持准确的浏览器评审。
但是当前库仍明确不提供 JSON 读取器、本地服务或评审页面。每个项目如果要查看结果,还要重复完成以下工作:
- 解析并校验
mdpolish.reviewJSON; - 验证正文哈希、阶段链、Change 范围和计数;
- 把 Python Unicode 码点坐标转换成浏览器编辑器使用的 UTF-16 坐标;
- 编写本机只读服务、双栏界面和 Change 跳转;
- 持续跟随 review schema 和前端依赖变化。
wheel-pilot 已经按自己的 0004 design 实现一版 React + CodeMirror 评审器。只读调查确认它的主要交互是通用的:选择文档、
查看总体输入/输出、按 Modifier 查看完整阶段、保留零修改阶段、滚动长文档,以及点击 Change 跳转。当前参考生产构建约
842 KiB;这只是本次方案比较的观测值,不是未来 wheel 大小承诺。
wheel-pilot 中真正属于项目的部分是论文 Modifier、七步顺序、批处理、artifacts/v0.6.0/ 路径和五篇文档身份。
页面和只读服务不需要理解这些业务事实。因此,让每个项目继续复制整套 viewer 会形成重复实现和不一致的校验口径。
2. 决定摘要
第一版采用以下边界:
使用项目
├── 选择 Modifier、参数和顺序
├── Pipeline.transform()
├── build_review_document()
├── render_json_report(..., detail="full")
└── 自行保存 *.review.json、决定权限与保留周期
│
▼
mdpolish-reviewer --review-dir <明确目录>
├── mdpolish 官方 JSON 解析与语义校验
├── Python 码点 → UTF-16 只读定位
├── 回环地址上的只读 HTTP 服务
└── React + CodeMirror 双栏页面
评审器随同一个 mdpolish wheel 交付,但与内存清洗核心隔离。安装 wheel 不会启动服务、读取文件或改变任何 Markdown;
只有用户显式运行评审器命令时,工具才读取明确传入的目录。
项目无需复制前端或 Python 服务。项目只要保存正式 full JSON,就能使用同一界面。
3. 目标与非目标
3.1 目标
- 为所有使用项目提供同一套本地只读清洗结果页面;
- 保留 wheel-pilot 当前已经验证的双栏布局、Modifier 时间线、完整滚动和 Change 跳转体验;
- 直接消费
mdpolish.review正式机器投影,不建立项目 artifact schema; - 由
mdpolish提供正式 JSON 的解析和校验,不让 viewer 私下维护另一套 schema 解释; - 对
full数据验证正文哈希、阶段首尾、Change 批次、位置、计数和状态一致性,失败时拒绝近似展示; - 保持 Modifier 阶段坐标的原语义,只为 CodeMirror 额外派生 UTF-16 范围;
- 只绑定本机回环地址,只提供同源静态页面和只读 API;
- 前端生产资源随 wheel 提供,使用项目运行页面时不需要 Node.js,也不从 CDN 下载资源;
- 保持普通 Python 核心安装零第三方运行依赖;
- 使用合成文本覆盖 Unicode、不同换行、空文档、零修改阶段、失败和不稳定状态。
3.2 非目标
- 不替项目读取原始 Markdown、运行 Pipeline、选择 Modifier 或保存 review JSON;
- 不定义项目的目录层级、批处理协议、文档 ID、标题、审核状态、权限模型或保留周期;
- 不编辑、接受、拒绝、撤销或重新应用 Change,不从页面触发清洗;
- 不渲染 Markdown 排版,不执行原文中的 HTML,不加载图片、字体或其他外部资源;
- 不提供上传、远程访问、账户、数据库、多人协作、批注或生产部署接口;
- 不把 JSON 恢复为
ReviewDocument、TransformResult或 Pipeline; - 不改变 schema
1.0的字段、哈希、坐标、detail 或正文暴露语义; - 不增加默认流水线、项目 profile、业务规则或真实样本;
- 不在本轮修改或删除 wheel-pilot 的 reviewer。它的迁移与清理必须在该仓库另行批准;
- 不读取、复制、修改或提交真实文档、历史报告和外部数据。
4. 职责边界
| 能力 | mdpolish |
使用项目 |
|---|---|---|
| Modifier 规则、参数和顺序 | 不拥有 | 拥有 |
| 清洗执行与内存审计 | 提供通用核心 | 显式调用 |
ReviewDocument 与正式 JSON 生产 |
提供 | 决定是否生成 |
| review JSON 文件名、目录和覆盖策略 | 不决定 | 拥有 |
| review JSON 内容解析与通用一致性校验 | 提供 | 不再重复实现 |
| UTF-16 编辑器定位 | reviewer 内部提供 | 不需要实现 |
| 双栏页面、Modifier 时间线和 Change 跳转 | 提供 | 直接使用 |
| 文档业务名称、审核结论、批注和权限 | 不拥有 | 如有需要自行实现 |
| 数据脱敏、访问控制和保留周期 | 只说明风险 | 拥有 |
reviewer 不知道 JSON 对应哪个原始文件。页面展示的文档标签只能来自 review JSON 文件名,不能猜测输入路径、论文标题或业务 身份。项目如果需要额外业务字段,应建设自己的外层页面;第一版不为此增加 sidecar manifest 或配置插件。
5. 方案比较
| 方案 | 优点 | 代价 | 决定 |
|---|---|---|---|
| 每个项目继续复制 wheel-pilot reviewer | 上游 wheel 最小 | 校验、API、前端和依赖重复;行为会漂移 | 不采用 |
单独发布 mdpolish-reviewer wheel |
核心分发物最小 | 两个包必须配对版本、安装和发布;当前 reviewer 无第三方 Python 依赖 | 第一版不采用 |
| 同一 wheel 内放独立 reviewer 模块和静态资源 | 一个版本同时约束 producer、reader 和页面;项目只安装一个 wheel | 所有人下载的 wheel 都会增加静态资源体积 | 采用 |
| 运行时从网络下载页面 | wheel 较小 | 引入网络、版本漂移、隐私和供应链风险 | 不采用 |
| pip 构建 wheel 时自动运行 npm | 不提交生产 bundle | Git direct install 需要 Node.js 和网络,破坏现有 Python 安装体验 | 不采用 |
| 提交经过检查的生产 bundle并打进 wheel | 使用者不需要 Node.js;Python 构建保持简单 | 源码与生成资源必须同步检查,Git diff 会包含压缩文件 | 采用 |
这里的“同一 wheel”不表示 reviewer 成为 Pipeline 的一部分。依赖方向固定为 reviewer 可以导入 review JSON 解析能力,
models.py、edits.py、modifier.py 和 pipeline.py 不导入 reviewer、HTTP 或前端资源。
6. 正式 JSON 读取入口
第一版拟在受支持的 mdpolish.review 路径增加:
class ReviewParseError(ValueError):
"""机器投影 JSON 不能被安全读取。"""
def parse_json_report(
report: str,
*,
expected_detail: ReviewDetail | str | None = None,
) -> ReviewProjection:
...
它接收内存字符串并返回只含 JSON 基本值的新 dict/list 容器。它不接收路径,不读取文件,不返回 ReviewDocument,也不重新运行
Modifier。expected_detail=None 接受 schema 支持的任一已知 detail;reviewer 必须显式要求 full。
解析至少执行以下通用检查:
- 拒绝重复 object key、非标准
NaN/Infinity、孤立 surrogate 和非 JSON 值; - 要求
schema_name == "mdpolish.review",并按 schema major 兼容规则处理版本; - 验证已知 detail 的必需字段、类型、枚举、整数范围、引用位置和正文暴露边界;
- 对同一 schema major 的未知 object 字段按现有兼容规则忽略其语义,不改变已知字段解释;
- 遇到未知状态、detail、坐标契约或无法安全解释的 enum 时失败,绝不把它降级成
success; - 错误消息只给字段路径和契约类别,不拼入正文、参数、reason 或诊断消息。
summary 和 changes 不含完整阶段文本,解析器只能验证它们实际携带的结构和引用。full 还必须验证:
- input、current 和每个阶段全文的 UTF-8 SHA-256 与码点长度;
- 第一个完成阶段从 input 开始,相邻阶段首尾完全相接;
- 每个 Change 的 modifier 引用、哈希、范围、原文、行列和顺序;
- 使用核心共用的精确编辑应用原语重放当前阶段 Change,结果必须等于 stage after;
- 零修改阶段的 before 与 after 完全相同;
- 完成阶段末尾等于 current,阶段数量与
stages_complete及错误阶段相容; - counts、错误、残留候选和
success/failed/unstable状态相容。
第 4 点比 wheel-pilot 当前服务只检查“before 中能找到片段”更严格。它防止攻击者同时篡改 stage after 正文和哈希后,页面仍把 一个并非由所列 Change 产生的结果展示为可信阶段。
解析器可以从 edits.py 复用私有验证与应用原语,但不得复制一份不同的冲突、排序或字符串应用规则。正常清洗公共接口和
schema 1.0 生产结果必须保持不变。
7. 输入集合与文件边界
第一版本地入口拟为:
mdpolish-reviewer --review-dir <review_directory> [--port <port>]
同时支持等价的模块入口:
python -m mdpolish.reviewer --review-dir <review_directory>
--review-dir 必填,没有当前目录或 artifacts/ 的隐式默认值。服务只读取该目录直属的 *.review.json 普通文件,按文件名
确定顺序,不递归、不扫描父目录、不跟随目录或文件符号链接。目录为空时明确失败。
文件适配器负责严格 UTF-8、BOM、读取错误和路径检查,然后把内存字符串交给 parse_json_report(..., expected_detail="full")。核心解析函数不知道路径。
第一版使用去掉末尾 .review.json 后的文件名作为页面标签和内部文档身份。例如 paper-01.review.json 显示为
paper-01;它不自动补 .md,也不声称这是原输入文件名。重名、空身份或无法安全形成 URL 身份时启动失败。
目录名只作为页面顶部的本地集合标签,不成为运行 ID、项目 ID 或 schema 字段。绝对路径不返回给浏览器,也不打印正文。
review JSON 可能包含完整敏感正文。mdpolish 不自动创建、复制、移动、删除或清理这些文件;项目继续负责把它们保存在
合适的本地目录,设置权限、Git 忽略和保留周期。
8. 本机服务和内部 API
服务必须保持以下边界:
- 只绑定
127.0.0.1,默认端口0由操作系统选择; - 只接受
GET和HEAD,其他方法返回405; - 校验
Host与可选Origin,不开放 CORS; - 不提供任意文件路径、写入、删除、移动、重新运行或 shell 接口;
- 静态资源只来自 wheel 内固定目录,拒绝路径穿越和符号链接;
- 页面和 API 设置
no-store、CSP、nosniff、no-referrer和禁止 frame 的响应头; - 日志不输出正文、修改片段、参数或绝对 review 目录;
- 退出时不修改项目目录或浏览器外状态;
- 不自动打开浏览器,终端只打印明确的回环 URL 和不含敏感路径的文档数量。
浏览器使用版本化但只服务同一 reviewer 的内部 /api/v1/。API 至少提供:
| 资源 | 内容 |
|---|---|
| 集合摘要 | 集合标签、聚合状态、文档顺序和计数,不含正文 |
| 文档比较 | 输入、成功 current、Modifier 摘要、全部 Change 和诊断 |
| Modifier 阶段 | 指定完整阶段的 before、after、Change 和 UTF-16 定位 |
这是 Python 服务与同 wheel 页面之间的内部契约,不承诺给第三方项目直接调用。跨项目稳定数据契约仍是
mdpolish.review schema 和第 6 节的解析入口,不能把本地 HTTP API 变成第二个公共 artifact schema。
第一版启动时校验目录内全部 review。任何一份损坏都会阻止服务启动,并指出不含正文的文件标签和错误类别;不在同一次集合 中混合“已可信”和“猜测展示”的文档。若真实项目证明需要隔离单篇坏文件,再新增 design 改变失败策略。
9. UTF-16 编辑器定位
schema 1.0 的 span.start / span.end 是所属阶段 before 文本中的 Python Unicode 码点半开范围。CodeMirror 使用
JavaScript UTF-16 code unit。reviewer 在 full 阶段已经通过校验后,派生:
"editor_range": {
"start": 10,
"end": 12
}
这个范围只存在于内部 API,用于左栏选区和滚动,不写回正式 review JSON,不成为新的修改权威。中文基本平面字符通常不改变 数值,emoji 等补充平面字符会占两个 UTF-16 code unit。组合字符仍按原字符串逐码点转换,不做 Unicode 规范化。
转换必须以该 Change 所属的 stage.before.markdown 为输入。不得把中间阶段 span 套到原始输入、最终 current 或其他
Modifier 阶段。
10. 页面行为
第一版以 wheel-pilot 当前页面为迁移基线,保留用户已经满意的视觉和主要交互:
- 启动后选择第一份文档,默认比较完整 input 与成功 current;
- 左侧列出文档和按位置排序的全部 Modifier;
- 选择 Modifier 后比较该阶段完整 before / after;
- 零修改 Modifier 仍显示,前后全文相同;
- 不折叠未修改区域,长文由 MergeView 容器完整纵向滚动;
- 总结果列出全部 Change,阶段视图只列当前 Modifier 的 Change;
- 点击 Change 时先切换所属阶段,等待 MergeView 用新 before/after 重建,再选中并居中左栏范围;
failed和unstable只显示准确的 partial/错误/残留证据,不把 current 命名为“清洗后”;- Markdown、HTML、图片和脚本语法只作为只读源码,不渲染、不请求外部资源;
- 页面文案使用简体中文,第一版不建设主题、国际化或项目定制接口。
前端仍采用 React、TypeScript、Vite、CodeMirror MergeView、Vitest 和 React Testing Library。准确版本只在
reviewer/package.json 与锁文件中维护,不在 design 和 README 复制第二份易漂移清单。Node.js 24 只用于仓库开发、测试和
生成生产 bundle;使用 wheel 查看结果不需要 Node.js。
11. 源码和交付结构
批准后拟增加:
reviewer/
├── .nvmrc
├── package.json
├── package-lock.json
├── src/ # React、API client、运行时响应校验
└── tests/ # 合成前端测试
src/mdpolish/
├── review.py # 增加正式 JSON 解析入口
├── reviewer.py # 路径适配、本地 API、HTTP 服务和 CLI
└── _reviewer_static/ # 经检查并提交的生产 HTML/JS/CSS
tests/
├── test_review_parsing.py # schema 与 full 语义校验
└── test_reviewer.py # 目录、HTTP、安全和 UTF-16
最终文件拆分可以在不改变职责的前提下机械调整,例如把 HTTP handler 放入私有模块;不得把 viewer 逻辑塞进
pipeline.py 或让核心导入前端资源。
生产 bundle 提交到 _reviewer_static/ 并包含在 wheel,使从 Git 地址或 Release wheel 安装时不调用 npm。前端源码或锁文件
变化后必须重新构建并检查 bundle;Python wheel 构建只打包现有已验证资源。测试必须发现缺失或陈旧入口资源,不能在没有
页面时静默构建一个“成功”wheel。
前端 bundle 引入的第三方代码必须在仓库和 wheel 中保留适用的版权与许可证说明。实现验收要列出实际 bundle 和 wheel 大小,
检查 wheel 不包含 node_modules、前端测试、coverage、source map、真实数据或 review JSON。
pyproject.toml 增加 mdpolish-reviewer console script 和静态 package data。普通 mdpolish 导入路径不重新导出服务对象;
公共 Python 读取入口仍位于 mdpolish.review。
12. 兼容与版本
本文增加公共 JSON 读取函数、公共 CLI、HTML 页面和 wheel 文件,属于 0.x 阶段的功能性次版本变化。实施候选版本计划从当前
未发布的 0.6.1 更新为 0.7.0;不改变现有 Modifier 版本、Pipeline 结果或 schema 1.0。
reviewer 和 producer 随同一个 wheel 发布,避免建立第二套版本配对规则。页面内部 API 可以随同一 wheel 修改,但必须同步 Python、TypeScript 运行时校验和测试。
正式 mdpolish.review schema 继续独立版本。若未来 schema major 改变,reader 必须拒绝;同 major 的加法字段按
0012 的兼容规则处理。任何修改现有字段含义、正文暴露等级或坐标口径的工作仍需新的 design,不能借 reviewer 页面绕过。
13. 测试与验收
13.1 JSON 解析与失败关闭
只使用虚构小文本,至少覆盖:
summary、changes、full的必需字段和暴露边界;- 空文档、空流水线、零修改 Modifier 和多 Modifier 链;
- success、unstable、preflight failure、transform failure 和 final review failure;
- 中文、emoji、组合字符、BOM 字符、LF、CRLF、CR 和无末尾换行;
- full 中所有文本哈希、阶段链、Change 重放、位置、计数和状态;
- 重复键、BOM 文件、错误 UTF-8、非有限数、孤立 surrogate、未知 schema major/detail/enum;
- 损坏 input/current/stage 哈希、断裂阶段、错误 span、冲突 Change、篡改 after、零修改阶段却改变文本;
- 解析错误不包含正文、参数、reason 或诊断消息哨兵;
- 解析器不读文件、不访问网络、不调用 Modifier,不改变传入或返回外部容器。
同一组合成 full report 应分别通过 ReviewDocument 生产路径和 JSON 读取路径,逐阶段比较相同的 before、after、哈希与 Change
顺序。只比较最终 current 不足以证明 reader 与 producer 一致。
13.2 本地服务
- 明确目录的多份合法 full JSON 可以得到集合、文档和 Modifier 阶段响应;
- 文件名顺序、标签、零修改阶段和聚合计数正确;
- 空目录、递归文件、符号链接、未知文件、损坏 JSON 和重复身份失败;
- 路径穿越、异常 Host/Origin、未知路由和非 GET/HEAD 请求被拒绝;
- 服务只绑定
127.0.0.1,安全响应头和媒体类型正确; - API 和日志不返回绝对目录,不输出正文到终端;
- 码点到 UTF-16 的转换覆盖 emoji、组合字符、不同换行和空插入;
- 缺失静态资源时明确失败,不访问 CDN 或任意磁盘路径。
13.3 前端
- 文档列表、聚合状态和修改数显示正确;
- 总结果、Modifier 阶段和零修改阶段切换正确;
- Change 筛选、同阶段跳转、跨 Modifier 跳转和重建后聚焦正确;
- 长文不生成折叠区,MergeView 保持纵向滚动;
- Markdown 中的 HTML、图片和脚本保持惰性文本;
- failed、unstable、API 错误和未知内部响应不显示虚构成功结果;
- ESLint、TypeScript、Vitest 和 Vite 生产构建通过;
- 真实浏览器滚动和视觉仍需人工验收,jsdom 结果不能替代。
13.4 回归与交付
实施完成后运行根 README 当时列出的全部检查,并额外确认:
- 现有 Pipeline、Modifier、ReviewDocument、dict/JSON producer 和 Markdown reporter 行为不变;
- 普通核心安装仍没有第三方 Python 运行依赖;
- 最低和当前支持的 Python 环境都能启动 reviewer 并读取合成 full JSON;
- wheel 能从仓库外安装和启动页面,静态资源、console script 与版本正确;
- 记录 wheel 文件清单、压缩/解压大小和相对
0.6.1候选的增量; - wheel 不包含
node_modules、前端测试、source map、真实报告、项目规则或数据; - 前端第三方许可证说明完整;
AGENTS.md与CLAUDE.md除标题外正文一致;- Git diff 不混入 wheel-pilot、真实文本、大型实验产物或用户已有改动。
真实项目数据不是实现正确性的必要条件。批准本文也不授权读取或复制 wheel-pilot 的 local-data/ 和 artifacts/。
若用户随后希望确认页面视觉,可以由 wheel-pilot 继续使用自己的已有结果,或在该仓库另行批准改用上游候选 wheel。
14. 风险与代价
- wheel 明显变大: 当前参考 bundle 约 842 KiB,实际实现仍需记录。换来的是项目不再安装 Node.js 或复制页面。
- 公共 CLI 需要兼容维护:
--review-dir和只读行为一旦发布就不能随意更名;第一版参数保持最少。 - 提交生成资源会增加 diff: 这是保证 Git direct install 不依赖 npm 的代价,必须用构建和 wheel 测试防止陈旧 bundle。
- 解析器公共表面积增加: 它需要长期跟随 schema,但比每个项目各写一套校验更可控。
- full JSON 占用内存: 每阶段重复全文,reader 和页面还会产生额外容器;第一版不承诺无限文档或批量规模。
- 本机 HTTP 仍有攻击面: 回环、Host/Origin 校验、无 CORS、CSP、无写接口和明确目录都是必需边界。
- 文件名不等于业务身份: 通用 schema 没有路径和标题;第一版宁可显示保守标签,也不引入项目 manifest。
- 页面可能被误当成审核系统: 它只展示清洗证据,不记录批准、拒绝、责任人或结论。
- wheel-pilot 暂时重复: 上游实现和发布前,两边 reviewer 会并存。迁移应在消费者仓库单独评审,不能同时删除以制造大爆炸变更。
15. 批准后的实施边界
用户明确批准本文后,只授权:
- 在当前
mdpolish仓库实现第 6 至 11 节的 JSON reader、本机服务、前端、静态资源和 console script; - 从 wheel-pilot 已提交的页面与服务中参考或迁移项目无关代码,但不修改该仓库,不读取其真实数据与 artifacts;
- 为共享精确编辑语义做必要的私有机械复用,不改变公共清洗结果;
- 新增合成 Python/TypeScript 测试,并完成第 13 节的构建、wheel 和仓库外 smoke test;
- 实现完成后更新 README 当前能力、
review-projection.md、schema reference 和经实际验证的 guide; - 把候选包版本更新为
0.7.0,报告实际 diff、测试、bundle、wheel 和许可证检查。
本次批准还授权在全部必需验收通过后:
- 提交本文及其实施,使用中文 Git commit subject;
- 把当前分支提交和
v0.7.0tag 推送到origin; - 只把提交前已经验收的同一个
mdpolish-0.7.0-py3-none-any.whl和 SHA-256 校验文件上传到v0.7.0GitHub Release,不能在 tag 后重新构建另一份 wheel 冒充已验收产物。
本次批准不授权:
- 创建 PR,发布到 PyPI、GitHub Packages 或其他包索引;
- 修改 wheel-pilot、其他仓库、真实数据或历史 artifacts;
- 增加远程绑定、写接口、上传、认证、数据库、项目 metadata、审核工作流或 Markdown 渲染;
- 改变清洗规则、Modifier 顺序、误删容忍度、
RunStatus、ReviewDocument字段或 schema1.0语义。