Planning: parent design.md becomes the single authoritative contract (constraint model with 6 kinds, handle manifest, coordinate/unit layering, preview sequencing, storage layout and lazy migration, area config snapshot, API contract). Work is split into four independently verifiable child tasks with per-step gates and rollback points. Test infra: pin vitest 4.1.11, add test:client:unit for client pure logic, extend prettier globs to root *.ts so vitest.config.ts is checked. Add .gitignore: the repo had none, so inputs/, outputs/, workbench-data/ and the client build output were untracked rather than ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
9.4 KiB
Markdown
108 lines
9.4 KiB
Markdown
# 实施计划
|
||
|
||
每步是一个独立提交,带自己的验证命令、门禁和回滚点。门禁不过就停在该步,不带着已知缺陷进入下一步。
|
||
|
||
约束与合约的权威定义在 `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 选择。
|
||
|
||
|
||
|