Files
road-compiler/.trellis/tasks/08-26-direct-manipulation-road-editor/implement.md
que01 93f09e399e chore(road-editor): plan direct-edit task tree and add client test infra
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>
2026-08-26 17:33:42 +08:00

108 lines
9.4 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.
# 实施计划
每步是一个独立提交,带自己的验证命令、门禁和回滚点。门禁不过就停在该步,不带着已知缺陷进入下一步。
约束与合约的权威定义在 `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 默认范围推导(避开两端 reservejunction 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 manifesthandles / ghost / preview 三个独立 source`EditSession` 命令栈与 undo/redo80ms 防抖与 `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 可并行于 22 → 3 → 4 → 5 是硬顺序6 依赖 1、4、57 依赖 68 依赖全部。第 1 步失败不阻塞 2-5只改变第 6 步的输入 adapter 选择。