# 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 读取器、本地服务或评审页面。每个项目如果要查看结果,还要重复完成以下工作: 1. 解析并校验 `mdpolish.review` JSON; 2. 验证正文哈希、阶段链、Change 范围和计数; 3. 把 Python Unicode 码点坐标转换成浏览器编辑器使用的 UTF-16 坐标; 4. 编写本机只读服务、双栏界面和 Change 跳转; 5. 持续跟随 review schema 和前端依赖变化。 wheel-pilot 已经按自己的 `0004` design 实现一版 React + CodeMirror 评审器。只读调查确认它的主要交互是通用的:选择文档、 查看总体输入/输出、按 Modifier 查看完整阶段、保留零修改阶段、滚动长文档,以及点击 Change 跳转。当前参考生产构建约 842 KiB;这只是本次方案比较的观测值,不是未来 wheel 大小承诺。 wheel-pilot 中真正属于项目的部分是论文 Modifier、七步顺序、批处理、`artifacts/v0.6.0/` 路径和五篇文档身份。 页面和只读服务不需要理解这些业务事实。因此,让每个项目继续复制整套 viewer 会形成重复实现和不一致的校验口径。 ## 2. 决定摘要 第一版采用以下边界: ```text 使用项目 ├── 选择 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` 路径增加: ```python 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` 还必须验证: 1. input、current 和每个阶段全文的 UTF-8 SHA-256 与码点长度; 2. 第一个完成阶段从 input 开始,相邻阶段首尾完全相接; 3. 每个 Change 的 modifier 引用、哈希、范围、原文、行列和顺序; 4. 使用核心共用的精确编辑应用原语重放当前阶段 Change,结果必须等于 stage after; 5. 零修改阶段的 before 与 after 完全相同; 6. 完成阶段末尾等于 current,阶段数量与 `stages_complete` 及错误阶段相容; 7. counts、错误、残留候选和 `success` / `failed` / `unstable` 状态相容。 第 4 点比 wheel-pilot 当前服务只检查“before 中能找到片段”更严格。它防止攻击者同时篡改 stage after 正文和哈希后,页面仍把 一个并非由所列 Change 产生的结果展示为可信阶段。 解析器可以从 `edits.py` 复用私有验证与应用原语,但不得复制一份不同的冲突、排序或字符串应用规则。正常清洗公共接口和 schema `1.0` 生产结果必须保持不变。 ## 7. 输入集合与文件边界 第一版本地入口拟为: ```text mdpolish-reviewer --review-dir [--port ] ``` 同时支持等价的模块入口: ```text python -m mdpolish.reviewer --review-dir ``` `--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 阶段已经通过校验后,派生: ```json "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. 源码和交付结构 批准后拟增加: ```text 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. 批准后的实施边界 用户明确批准本文后,只授权: 1. 在当前 `mdpolish` 仓库实现第 6 至 11 节的 JSON reader、本机服务、前端、静态资源和 console script; 2. 从 wheel-pilot 已提交的页面与服务中参考或迁移项目无关代码,但不修改该仓库,不读取其真实数据与 artifacts; 3. 为共享精确编辑语义做必要的私有机械复用,不改变公共清洗结果; 4. 新增合成 Python/TypeScript 测试,并完成第 13 节的构建、wheel 和仓库外 smoke test; 5. 实现完成后更新 README 当前能力、`review-projection.md`、schema reference 和经实际验证的 guide; 6. 把候选包版本更新为 `0.7.0`,报告实际 diff、测试、bundle、wheel 和许可证检查。 本次批准还授权在全部必需验收通过后: 1. 提交本文及其实施,使用中文 Git commit subject; 2. 把当前分支提交和 `v0.7.0` tag 推送到 `origin`; 3. 只把提交前已经验收的同一个 `mdpolish-0.7.0-py3-none-any.whl` 和 SHA-256 校验文件上传到 `v0.7.0` GitHub Release,不能在 tag 后重新构建另一份 wheel 冒充已验收产物。 本次批准不授权: - 创建 PR,发布到 PyPI、GitHub Packages 或其他包索引; - 修改 wheel-pilot、其他仓库、真实数据或历史 artifacts; - 增加远程绑定、写接口、上传、认证、数据库、项目 metadata、审核工作流或 Markdown 渲染; - 改变清洗规则、Modifier 顺序、误删容忍度、`RunStatus`、`ReviewDocument` 字段或 schema `1.0` 语义。