# 实施计划 每步是一个独立提交,带自己的验证命令、门禁和回滚点。门禁不过就停在该步,不带着已知缺陷进入下一步。 约束与合约的权威定义在 `design.md`;本文只管执行顺序与验证。 ## 全局回滚策略 - 任务分支上一步一提交,回滚等于 `git revert` 该提交,不做跨步大回滚。 - 客户端编辑能力全程挂在 `directEdit` 开关后,默认关闭,直到第 8 步手测清单通过才默认开启。开关关闭时工作台行为与当前 main 完全一致。 - 服务端新增端点与新文件是纯增量:`active/` 与 `revisions/` 不存在时,旧代码路径照常工作。任何一步回滚都不会让既有 import 目录无法打开。 - v1 `native-road-overrides.json` 始终保持权威且格式不变,任何步骤都不迁移它。 ## 0. 前置:依赖与测试基础设施 - 目标:先把验证能力建好,避免后续步骤"写完没法测"。 - 范围:`package.json` 增加 `vitest`(固定版本)与 `test:client:unit` 脚本;建立客户端纯逻辑测试目录。不碰任何产品代码。 - 验证:`npm run test:client:unit`(空套件通过)、`npm run format:check`、`npm run test`。 - 门禁:新脚本可跑通且不影响现有 `test` / `test:client` / `build`。 - 回滚点:仅 `package.json` 与测试目录,revert 无副作用。 ## 1. `ol-ext` 限时探针 - 目标:只回答一个问题——能否复用通用 handle 的命中、pointer 生命周期与视觉反馈。 - 范围:隔离探针,不修改生产基线图层,不进依赖清单直到通过。 - 验证:手动跑探针页;确认 OL `Map` 未重建、proxy 拖拽稳定、基线 source 未被写入、事件能转成 draft 值。 - 门禁:三条同时成立才把 `ol-ext` 固定为依赖。超过一个工作日或任一条不成立,立刻停止排障。 - 回滚点:删除探针目录,改用原生 OL `Snap` + 小型 `PointerInteraction` adapter。`HandleManifest -> RoadEditOperation -> RoadConstraint -> preview solver` 合约不变,因此本步失败最多换输入层,不动 schema、求解器或已保存编辑。 ## 2. v2 文档、活动副本与 revision 基础 - 目标:落地 `native-road-edits/v2`、operations、`anchorSnapshot` 与 5 态重放状态机。 - 范围:新增文档读写、`active/` 与 `revisions/` 布局、内容寻址 OSM 副本、`documentVersion`、命名检查点与恢复读取。惰性迁移既有 import 目录,不移动既有文件。v1 overrides 完全不动。 - 验证:`npm run test`(新增 fixture:文档往返、`documentVersion` 递增、惰性迁移在只有旧文件的目录上生成 `rev-0001`、内容相同的 OSM 不产生第二份副本)。 - 门禁:既有 `workbench-data/import-*` 目录在新代码下可正常打开,且未被改写;schema 校验拒绝越界 `boundaryIndex` 与非法 station。 - 回滚点:revert 本步后 `active/` 与 `revisions/` 只是残留目录,旧代码忽略它们照常运行。 ## 3. area config 快照 - 目标:切断编译对外部区域配置文件的运行期依赖,让 revision 真正可复现。 - 范围:活动副本与每个 revision 各写一份 `area-config.snapshot.json`;编译与预览改读快照;`/api/junction-clusters` 改写快照并在响应里返回可复制片段。 - 验证:`npm run test` 新增 fixture——冻结 revision 后修改外部 area config,重新编译该 revision 结果不变;快照缺失时给出明确错误而非静默用外部文件。 - 门禁:现有 `/api/junction-clusters` 的用户可见行为(返回可复制片段)不退化。 - 回滚点:revert 后编译回到读外部配置文件;已写的快照文件被忽略,不影响 v1 链路。 ## 4. 约束求解边界 - 目标:在 `compileGeometry()` 前插入纯函数 `resolveDirectEditConstraints`,输出 editable profiles、junction plans、handle manifest 与 diagnostics。 - 范围:实现 6 个 kind 的求解;interval 默认范围推导(避开两端 reserve);junction reserve 计算;`junction-approach-width` 对道路 profile 的优先级与边界连续过渡。米制换算复用 `src/geometry/lane-geometry.js`,不新增第二套。 - 验证:`npm run test` 新增 fixture——每个 kind 的重放、最小车道宽 2.4m、左右外缘不交叉、道路/路口连续、路口面不自交、connector 包含性、`boundaryIndex` 越界转 `stale`、锚点缺失/歧义分别转 `stale`/`conflicted`。 - 门禁:求解是纯函数,无文件写入、无网络;`npm run test` 全绿且既有 baseline 输出在无 v2 约束时逐字节不变。 - 回滚点:无 v2 约束时该步是恒等变换,因此 revert 前后编译输出一致,可安全单独回退。 ## 5. 预览与编辑 API - 目标:草稿预览与持久化分离,预览绝不写文件。 - 范围:`GET /api/edit-state`、`POST /api/edit-preview`、`POST /api/edits`、`POST /api/revisions`、`POST /api/revisions/:id/rebase`;`previewSeq` 回显;`degraded` 标记;`expectedDocumentVersion` 前置条件与 409;`/api/state`、`/api/session`、`/api/import` 的增量字段。 - 验证:`npm run test` 新增 server integration——预览调用前后目录 mtime 与内容不变;`expectedDocumentVersion` 不匹配返回 409 且不写入;原子写入在中途失败时不留半份文件;rebase 返回各 status 计数;预览与正式编译对同一约束集给出相同几何。 - 门禁:预览零写入;409 路径有测试覆盖;现有 `/api/overrides`、`/api/compile`、`/api/state` 的既有字段与行为不变。 - 回滚点:新端点是增量,revert 后客户端开关已关闭,工作台回到 v1 行为。 ## 6. 主地图 Road editor - 目标:外缘、步行带、车道分隔与区间范围可拖,禁入 junction reserve。 - 范围:selection → handle manifest;handles / ghost / preview 三个独立 source;`EditSession` 命令栈与 undo/redo;80ms 防抖与 `pointerup` 最终请求;`previewSeq` 乱序丢弃与 `AbortController` 取消;无效草稿反馈;应用/保存/取消。全部挂在 `directEdit` 开关后。 - 验证:`npm run test:client`、`npm run test:client:unit`(命令栈与 undo/redo、乱序响应丢弃、handle 事件到约束值的投影、米制换算的纬度正确性)、`npm run build`。 - 门禁:单元测试覆盖上述四类纯逻辑;拖拽期间 `Map` 未重建且只有受影响 source 被替换(手测清单第 1-3 项)。 - 回滚点:关掉 `directEdit` 开关即可现场止损,无需回滚代码;必要时 revert 本步。 ## 7. 最小 JunctionTools - 目标:单一普通 node 路口的进口宽度、cutback、单角部圆角,闭合预览/应用/取消。 - 范围:单 `JunctionRef` 会话的路由与返回主地图的上下文;session draft;完整局部预览;应用合并到活动副本;取消回到进入前状态;切换路口前强制应用或取消。不做 cluster 高级编辑、不做多路口联动。 - 验证:`npm run test:client:unit`(会话状态机:draft → 应用 → 主工作区未保存约束;取消后状态复原;未处理草稿时切换被拒绝)、`npm run test`(局部求解与全量编译一致)、`npm run build`。 - 门禁:预览包含全部受影响派生对象(道路面、路口面、步行带、车道中心线、connector、停止线、斑马线、标线);拟合失败时返回最后有效几何加结构化诊断,不把非法图形显示成已应用。 - 回滚点:路由与开关独立,revert 不影响第 6 步的主地图编辑能力。 ## 8. 全量验证与开关默认开启 - 目标:跑完交付级验收标准,确认可默认开启 `directEdit`。 - 范围:补齐 `prd.md`「验收标准(交付阶段)」逐条证据;开关默认值改为开启。 - 验证:`npm run format:check`、`npm run test`、`npm run test:client`、`npm run test:client:unit`、`npm run build`,加下方手测清单。 - 门禁:交付级验收标准全部勾选;任一条不过则开关保持关闭,任务不进入 Phase 3。 - 回滚点:只改开关默认值,回滚成本为一行。 ## 手测清单 自动化测试覆盖不到的地图行为,每次进入第 8 步都要跑一遍: 1. 选中道路 → 出现手柄,浏览器 devtools 中 `Map` 实例未变(拖拽前后同一对象引用)。 2. 拖动外缘 → 只有道路面、步行带、车道线、标线、connector 的 source 被替换,底图与未受影响图层无重绘闪烁。 3. 连续快速拖拽后松手 → 最终几何与松手位置一致,无回跳(验证乱序丢弃与最终请求)。 4. 拖到违反最小车道宽 → 出现阻塞诊断,地图停在最后一个有效预览。 5. 手柄落在 junction reserve 内 → 不可拖动并提示进入 `JunctionTools`。 6. 进入 `JunctionTools` 调三项 → 预览完整;应用 → 返回主地图仍显示同一几何;取消 → 状态复原。 7. 未处理草稿时尝试切换相邻路口 → 被拒绝并提示先应用或取消。 8. 保存 → 重新编译 → 几何不变,约束状态 `exact`。 9. 重新导入同一份 OSM → 约束全部 `exact`;导入删掉某条道路的 OSM → 相关约束 `stale`,旧 revision 仍可打开复现原结果。 10. 两个标签页同时保存 → 后者 409 提示,前者结果保留。 11. 关闭 `directEdit` 开关 → 工作台行为与当前 main 一致。 ## 步骤依赖 0 → 1 可并行于 2;2 → 3 → 4 → 5 是硬顺序;6 依赖 1、4、5;7 依赖 6;8 依赖全部。第 1 步失败不阻塞 2-5,只改变第 6 步的输入 adapter 选择。