feat: add native road parity baseline
This commit is contained in:
1
.trellis/tasks/08-25-rc-p0-contract-baseline/check.jsonl
Normal file
1
.trellis/tasks/08-25-rc-p0-contract-baseline/check.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
81
.trellis/tasks/08-25-rc-p0-contract-baseline/design.md
Normal file
81
.trellis/tasks/08-25-rc-p0-contract-baseline/design.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Phase 0 技术设计
|
||||
|
||||
契约本体见父任务 `design.md` §1,本文件只补 Phase 0 自身的实现设计。
|
||||
|
||||
## 1. 确定性核查的具体做法
|
||||
|
||||
编译器的潜在非确定性来源,按可疑度排序:
|
||||
|
||||
| 来源 | 表现 | 处置 |
|
||||
|---|---|---|
|
||||
| `fs.mkdtempSync(path.join(pipelineDir, "native-road-"))` | staging 目录名随机 | 若名字出现在 `compiled.json` 的 `source` 字段里,需归一化 |
|
||||
| `compiled.json` 内的绝对路径 | `source.osm` / `source.overrides` / `source.trafficSignals` 是绝对路径 | 归一化为相对 repo root |
|
||||
| 时间戳 | 未确认是否存在 | 核查 `compile-native-roads.js` 写入的 metadata |
|
||||
| `Map` / `Set` 迭代顺序 | JS 保证插入序,若插入源本身有序则确定 | 观测两次运行 `orderHash` 是否一致即可暴露 |
|
||||
| 浮点累加顺序 | 几何坐标末位抖动 | 若出现,说明存在顺序依赖,属真 bug,需修 |
|
||||
| `crypto` 随机(`traffic-signals.js` require 了 `crypto`) | 信号 ID 可能含随机成分 | 重点核查 —— `native-traffic-signals.json` 是 `loadOrGenerate`,已存在则复用,首次生成可能不确定 |
|
||||
|
||||
**核查顺序**:
|
||||
1. 备份两个有效区域的 `native-road/` 与 `native-traffic-signals.json`
|
||||
2. 删除 `native-road/`(保留 signals,因为 `loadOrGenerate` 语义是已存在则复用)
|
||||
3. 跑第一次 → snapshot A
|
||||
4. 再删 `native-road/`,跑第二次 → snapshot B
|
||||
5. diff A/B → volatile 字段清单
|
||||
6. 额外一轮:删掉 signals 文件也重生成一次,确认 signal ID 是否确定
|
||||
|
||||
## 2. `scripts/road-parity.js` 设计
|
||||
|
||||
```
|
||||
用法:
|
||||
node scripts/road-parity.js --config <area-config> --snapshot <out.json>
|
||||
node scripts/road-parity.js --config <area-config> --compare <baseline.json>
|
||||
```
|
||||
|
||||
### 归一化规则(写死在脚本里,并回写进契约文档)
|
||||
|
||||
1. **路径**:任何绝对路径 → 相对 repo root;staging 目录名 → 字面量 `<staging>`
|
||||
2. **JSON**:递归按 key 排序后 `JSON.stringify`,再 hash
|
||||
3. **GeoJSON**:
|
||||
- `contentHash` = features 按 `properties.native_id`(缺失则按几何首坐标)排序后 hash
|
||||
- `orderHash` = 原序 hash
|
||||
- 两者都记录;比对时 `contentHash` 不一致 = 语义变化(严重),
|
||||
仅 `orderHash` 不一致 = 顺序抖动(需解释但可能可接受)
|
||||
4. **浮点**:不做舍入。坐标变化就是变化,不允许用容差掩盖
|
||||
(拆分是纯搬迁,任何坐标位移都是 bug)
|
||||
|
||||
### 输出形状
|
||||
|
||||
```json
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"areaId": "fengshu-er-road",
|
||||
"files": {
|
||||
"compiled.json": { "contentHash": "…", "bytes": 12345 },
|
||||
"diagnostics.json": { "contentHash": "…", "bytes": 678, "count": 42 },
|
||||
"layers/road_surface.geojson": {
|
||||
"contentHash": "…", "orderHash": "…", "features": 128 }
|
||||
},
|
||||
"volatileExcluded": ["source.osm", "source.overrides", "<staging>"]
|
||||
}
|
||||
```
|
||||
|
||||
`count` / `features` 是冗余的人类可读字段 —— hash 变了但计数没变,
|
||||
说明是内容变化;计数也变了,说明是增删。加速归因。
|
||||
|
||||
## 3. 基线目录布局
|
||||
|
||||
```
|
||||
.trellis/tasks/08-25-road-compiler-extraction/baseline/
|
||||
fengshu-er-road.json
|
||||
nantaizi-lake-innovation-valley.json
|
||||
README.md ← 记录生成时的 git commit、命令、volatile 字段清单
|
||||
```
|
||||
|
||||
`README.md` 必须记录生成基线时的 commit hash,否则将来无法判断
|
||||
"基线是在哪个代码状态下产生的"。
|
||||
|
||||
## 4. 为什么校验脚本放宿主而不是直接放未来的编译器仓库
|
||||
|
||||
Phase 0 时编译器仓库还不存在。脚本先落宿主 `scripts/`,
|
||||
Phase 2 随编译器一起搬走,届时宿主保留一个薄封装(或直接调编译器包的 CLI)。
|
||||
这样 Phase 1 的 parity 验证不需要等仓库拆分。
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
118
.trellis/tasks/08-25-rc-p0-contract-baseline/implement.md
Normal file
118
.trellis/tasks/08-25-rc-p0-contract-baseline/implement.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Phase 0 执行计划
|
||||
|
||||
## 前置
|
||||
|
||||
- [ ] 分支:`experiment/road-compiler-rethink`(已在)
|
||||
- [ ] 确认工作区干净(`.trellis/tmp-*.js` 五个未跟踪文件与本任务无关,可先清理或忽略)
|
||||
- [ ] 阅读父任务 `design.md` §1 契约定义
|
||||
|
||||
## Step 1 — 摸清两区域当前状态
|
||||
|
||||
```bash
|
||||
ls config/areas/
|
||||
for d in outputs/*/native-road/layers; do echo "$d: $(ls $d | wc -l)"; done
|
||||
```
|
||||
|
||||
判定:`hanyang-block` 为废案,不纳入本阶段;只核查两个有效区域的配置与当前产物。
|
||||
|
||||
- [x] 逐区域检查 config 的 `nativeRoad.edgeLines` / `junctionTemplates.enabled`
|
||||
- [x] 将两个有效区域的配置差异记录在基线 README
|
||||
|
||||
**门槛**:两个有效区域的图层差异有书面解释。
|
||||
|
||||
## Step 2 — 确定性核查
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
cp -r outputs/<area>/native-road /tmp/rc-baseline-a
|
||||
# 重编
|
||||
rm -rf outputs/<area>/native-road
|
||||
npm run road:compile -- --config config/areas/<area>.json
|
||||
cp -r outputs/<area>/native-road /tmp/rc-baseline-b
|
||||
# 比对
|
||||
diff -r /tmp/rc-baseline-a /tmp/rc-baseline-b
|
||||
```
|
||||
|
||||
- [x] 对两个有效区域各跑一遍
|
||||
- [x] 记录所有差异字段 → volatile 清单
|
||||
- [x] 额外一轮:删掉 `native-traffic-signals.json` 重生成,
|
||||
确认信号 ID 是否确定(`traffic-signals.js` require 了 `crypto`,重点核查)
|
||||
- [ ] 若发现**几何坐标级**的非确定性 → 停止本阶段,先开 bug 任务修掉
|
||||
|
||||
**门槛** 🔴:确定性已证明,或非确定性已定位并修复。这是全案的地基。
|
||||
|
||||
## Step 3 — 写 `scripts/road-parity.js`
|
||||
|
||||
按 `design.md` §2 实现。
|
||||
|
||||
- [x] `--snapshot` 生成归一化 checksum 清单
|
||||
- [x] `--compare` 比对并逐文件报差异,不一致时非零退出
|
||||
- [x] GeoJSON 双 hash(`contentHash` 排序后 / `orderHash` 原序)
|
||||
- [x] volatile 字段按 Step 2 结论归一化
|
||||
- [x] 自验:同一区域连跑两次 snapshot,`--compare` 必须通过
|
||||
|
||||
```bash
|
||||
node scripts/road-parity.js --config config/areas/fengshu-er-road.json --snapshot /tmp/s1.json
|
||||
node scripts/road-parity.js --config config/areas/fengshu-er-road.json --compare /tmp/s1.json
|
||||
echo "exit=$?" # 必须为 0
|
||||
```
|
||||
|
||||
**门槛**:脚本对未改动的代码报告一致。
|
||||
|
||||
## Step 4 — 建立两区域基线
|
||||
|
||||
```bash
|
||||
mkdir -p .trellis/tasks/08-25-road-compiler-extraction/baseline
|
||||
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
|
||||
rm -rf outputs/$a/native-road
|
||||
npm run road:compile -- --config config/areas/$a.json
|
||||
node scripts/road-parity.js --config config/areas/$a.json \
|
||||
--snapshot .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json
|
||||
done
|
||||
```
|
||||
|
||||
- [x] 两个基线 JSON 生成
|
||||
- [x] 写 `baseline/README.md`:生成时的 git commit、命令、volatile 字段清单、
|
||||
两个有效区域图层差异的解释
|
||||
- [x] 反向自验:再跑一次 `--compare` 两个基线全部通过
|
||||
|
||||
## Step 5 — K2 决策
|
||||
|
||||
- [x] 检查 `comparison.json` 当前是否真被消费(grep 宿主与 workbench)
|
||||
- [x] 若有 → 方案 B(可选注入),在契约里定义 `comparisonDir` 为 optional
|
||||
|
||||
## Step 6 — 落契约文档
|
||||
|
||||
- [x] 写 `docs/native-road-package-v1.md`,内容源自父任务 `design.md` §1
|
||||
- [x] 补入 Step 2 的 volatile 字段清单
|
||||
- [x] 补入 Step 5 的 K2 决策
|
||||
- [x] 在 `docs/changelog.md` 记一行
|
||||
|
||||
## 验证命令汇总
|
||||
|
||||
```bash
|
||||
# parity 两区域全绿
|
||||
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
|
||||
node scripts/road-parity.js --config config/areas/$a.json \
|
||||
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/$a.json || echo "FAIL $a"
|
||||
done
|
||||
|
||||
# 既有测试未破
|
||||
npm run test:native-road
|
||||
npm run test:road-workbench
|
||||
|
||||
# 确认无生产代码改动
|
||||
git diff --stat -- scripts/lib blender
|
||||
```
|
||||
|
||||
最后一条必须为空输出 —— Phase 0 不碰生产代码。
|
||||
|
||||
## Review Gate
|
||||
|
||||
全部 AC0.1–AC0.6 打勾后,再进 Phase 1。**基线不可靠时不要前进。**
|
||||
|
||||
## Rollback
|
||||
|
||||
本阶段纯新增(文档 / 脚本 / 基线数据)。回滚 = 删除新增文件。
|
||||
唯一例外:Step 2 若发现并修复了非确定性 bug,那部分是真实代码改动,
|
||||
应拆成独立 commit 并单独保留。
|
||||
84
.trellis/tasks/08-25-rc-p0-contract-baseline/prd.md
Normal file
84
.trellis/tasks/08-25-rc-p0-contract-baseline/prd.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Phase 0:冻结契约与 parity 基线
|
||||
|
||||
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
|
||||
契约权威定义:父任务 `design.md` §1
|
||||
|
||||
## Goal
|
||||
|
||||
在任何代码移动之前,把 `native-road-package/v1` 契约落成仓库内文档,
|
||||
并为两个有效区域建立可复跑的逐文件 parity 基线,作为全案唯一 oracle。
|
||||
|
||||
**本阶段不移动、不重构任何生产代码。** 只新增文档、基线数据和一个校验脚本。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R0.1 契约文档
|
||||
|
||||
- 把父任务 `design.md` §1(输入契约 / 输出契约 / 消费方式)落成
|
||||
`docs/native-road-package-v1.md`。
|
||||
- 文档必须列全 12 个图层名、`compiled.json` 顶层结构、`diagnostics.json` 记录形状、
|
||||
stdout 标记格式。
|
||||
- Phase 2 会把该文档搬进编译器仓库,此处先落在宿主。
|
||||
|
||||
### R0.2 确定性前置核查 🔴
|
||||
|
||||
**基线只有在编译器确定性的前提下才有意义。** 必须先证明这一点。
|
||||
|
||||
- 对同一输入连续跑两次 `road:compile`,比较全部输出文件。
|
||||
- 识别并记录所有 volatile 字段(时间戳、绝对路径、staging 目录名、
|
||||
任何来自 `mkdtemp` 的随机名、Map/Set 迭代顺序敏感的输出)。
|
||||
- 校验脚本必须归一化或排除这些字段。
|
||||
- 若发现真实的非确定性(相同输入产出不同几何),**必须先修掉再继续**,
|
||||
否则整个拆分没有 oracle。
|
||||
|
||||
### R0.3 parity 校验脚本
|
||||
|
||||
- 新增 `scripts/road-parity.js`(宿主侧,Phase 2 随编译器搬走)。
|
||||
- 能力:
|
||||
- `--snapshot <out.json>` 遍历指定区域的 `native-road/` 全部文件 +
|
||||
`native-traffic-signals.json`,归一化后输出排序稳定的 checksum 清单
|
||||
- `--compare <baseline.json>` 与基线比对,差异逐文件报告
|
||||
- 非零退出码表示不一致
|
||||
- GeoJSON 需按稳定键排序后再 hash(避免 feature 顺序抖动造成假阳性),
|
||||
但**顺序本身若变化必须被报告**——用两个 hash:`contentHash`(排序后)
|
||||
与 `orderHash`(原序),分别报告。
|
||||
|
||||
### R0.4 两区域基线
|
||||
|
||||
- 区域:`fengshu-er-road`、`nantaizi-lake-innovation-valley`
|
||||
- 从干净状态跑 `npm run road:compile`,产出基线 JSON 提交进版本控制
|
||||
- 基线文件位置:`.trellis/tasks/08-25-road-compiler-extraction/baseline/<area>.json`
|
||||
(Phase 2 搬进编译器仓库当测试语料,见父任务 R4)
|
||||
|
||||
`hanyang-block` 是废案,不纳入编译、快照或后续 parity 兼容范围。
|
||||
|
||||
### R0.5 K2 决策定档
|
||||
|
||||
`comparison.json` 依赖宿主 osm2streets 产物(`compile-native-roads.js:87` 读
|
||||
`area.outputs.geojsonDir`),拆出后不可达。本阶段必须做出决策并写入契约文档:
|
||||
|
||||
- 方案 A:移除 `comparison.json`(osm2streets 已 legacy)
|
||||
- 方案 B:保留,`comparisonDir` 作为可选输入注入
|
||||
- 倾向 A。决策写入 `docs/native-road-package-v1.md` 的「已移除能力」一节。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] AC0.1 `docs/native-road-package-v1.md` 存在,覆盖输入契约、输出契约、
|
||||
12 图层清单、stdout 标记、已移除能力
|
||||
- [x] AC0.2 连续两次编译的 parity 比对通过(确定性已证明),volatile 字段清单已记录在文档内
|
||||
- [x] AC0.3 `scripts/road-parity.js` 可用,`--snapshot` / `--compare` 均工作,
|
||||
不一致时非零退出
|
||||
- [x] AC0.4 两个区域基线 JSON 已提交,且每个都能用 `--compare` 自比对通过
|
||||
- [x] AC0.5 K2 决策已定档并写入契约文档
|
||||
- [x] AC0.6 未修改任何生产代码(`git diff` 只含新增文档 / 脚本 / 基线)
|
||||
|
||||
## 依赖与顺序
|
||||
|
||||
- 无前置依赖,本阶段是全案入口
|
||||
- **阻塞** Phase 1、2、3、4 —— 没有基线就没有 oracle
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 任何生产代码改动(含"顺手修一下")
|
||||
- 目录结构调整
|
||||
- `compiled.json` 结构变更
|
||||
26
.trellis/tasks/08-25-rc-p0-contract-baseline/task.json
Normal file
26
.trellis/tasks/08-25-rc-p0-contract-baseline/task.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "rc-p0-contract-baseline",
|
||||
"name": "rc-p0-contract-baseline",
|
||||
"title": "Phase 0:冻结契约与 parity 基线",
|
||||
"description": "写死 native-road-package/v1 输入输出契约,并对两个有效区域建立逐文件 checksum 基线作为全案 oracle",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": "08-25-road-compiler-extraction",
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
# Native Road Parity Baselines
|
||||
|
||||
Generated from commit `25cf82e7c75ea8d881639c4a80648121efbd39f6` on 2026-08-25.
|
||||
|
||||
```bash
|
||||
npm run road:compile -- --config config/areas/fengshu-er-road.json
|
||||
node scripts/road-parity.js --config config/areas/fengshu-er-road.json \
|
||||
--snapshot .trellis/tasks/08-25-road-compiler-extraction/baseline/fengshu-er-road.json
|
||||
|
||||
npm run road:compile -- --config config/areas/nantaizi-lake-innovation-valley.json
|
||||
node scripts/road-parity.js --config config/areas/nantaizi-lake-innovation-valley.json \
|
||||
--snapshot .trellis/tasks/08-25-road-compiler-extraction/baseline/nantaizi-lake-innovation-valley.json
|
||||
```
|
||||
|
||||
Both samples emit all twelve native-road layers. `fengshu-er-road` enables its
|
||||
configured complex-junction template; `nantaizi-lake-innovation-valley` uses
|
||||
the default, template-free compiler path. `hanyang-block` is an abandoned area
|
||||
and deliberately excluded from this corpus.
|
||||
|
||||
The snapshot normalizes absolute source paths and random `native-road-*`
|
||||
staging names. It does not round coordinates or ignore feature ordering.
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"areaId": "fengshu-er-road",
|
||||
"files": {
|
||||
"../native-traffic-signals.json": {
|
||||
"contentHash": "88e7f0ef4fc7bdb9ef202af018b3fb3179035ffc8ed5eeba03276c263e1f0476",
|
||||
"bytes": 5886
|
||||
},
|
||||
"comparison.json": {
|
||||
"contentHash": "651eb4000a44a7521a79c3e1429795b83d20f8899e0c67c4bb25b56cf4a6f75d",
|
||||
"bytes": 1295
|
||||
},
|
||||
"compiled.json": {
|
||||
"contentHash": "2ab82eafcdab70155078fae3559692ef1fde0645a52c6decbaea762a9ffeb24a",
|
||||
"bytes": 159230
|
||||
},
|
||||
"diagnostics.json": {
|
||||
"contentHash": "1d9fdae06ddcbc19b5262b797a4a35c494fc30a98a66dbfa111533b12c5fdfc7",
|
||||
"bytes": 6862
|
||||
},
|
||||
"layers/center_lines.geojson": {
|
||||
"contentHash": "04db66df5821b319d59297b8515502fabcad95faf083cae19edb338be2f6024b",
|
||||
"orderHash": "04db66df5821b319d59297b8515502fabcad95faf083cae19edb338be2f6024b",
|
||||
"features": 5,
|
||||
"bytes": 6909
|
||||
},
|
||||
"layers/connectors.geojson": {
|
||||
"contentHash": "e7979d4642c7c10f33fb23e92721f22533a851ae1a2b4d35f224d50927bf2fe9",
|
||||
"orderHash": "c327b7b154d4d9ca04cd895523eb0d8ad67c37051ef1d2135b2b46c51e12df58",
|
||||
"features": 90,
|
||||
"bytes": 197885
|
||||
},
|
||||
"layers/crosswalks.geojson": {
|
||||
"contentHash": "354cd4d7ed105a11b95904b44848dce567b3a3383f76755936d8c00691fe29af",
|
||||
"orderHash": "d24376438ac956bde52fcbfa955dd72c2ba31a97995608aaed1c79f6fd3633ab",
|
||||
"features": 152,
|
||||
"bytes": 219286
|
||||
},
|
||||
"layers/direction_arrows.geojson": {
|
||||
"contentHash": "7b8362ad4912c997a7661befde55ddfe58757c3c8d139c15867e1938ea8f6918",
|
||||
"orderHash": "5274e8b433946410a7cdffdaa452c1750e04f53e73a86bf81d034cfb26d4c254",
|
||||
"features": 686,
|
||||
"bytes": 1053308
|
||||
},
|
||||
"layers/edge_lines.geojson": {
|
||||
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"features": 0,
|
||||
"bytes": 52
|
||||
},
|
||||
"layers/intersection_surface.geojson": {
|
||||
"contentHash": "b2022ca70339aa470f3be451bf3ba2f55b59c3900645c864538372173811309f",
|
||||
"orderHash": "b2022ca70339aa470f3be451bf3ba2f55b59c3900645c864538372173811309f",
|
||||
"features": 5,
|
||||
"bytes": 13048
|
||||
},
|
||||
"layers/lane_centerlines.geojson": {
|
||||
"contentHash": "19c550c794810c1071de55dee158295235106a56e3877a2ba10da920ee214593",
|
||||
"orderHash": "67f8e7d33ec578644416e4837dd4aafef62a55aaeb4d438d07ab07eddfbb1df1",
|
||||
"features": 84,
|
||||
"bytes": 73294
|
||||
},
|
||||
"layers/lane_separators.geojson": {
|
||||
"contentHash": "02fdf2c7b6e89c2850a60ea2ac72b9f62910259bdb2949681fcaa7e67d2b51f0",
|
||||
"orderHash": "81782a059c4ff9f69fdf7de9107d20a8d6cf55d34bb0620ec28a9a8c5106c982",
|
||||
"features": 3596,
|
||||
"bytes": 4164106
|
||||
},
|
||||
"layers/road_surface.geojson": {
|
||||
"contentHash": "5a2936a705e64338d0d79b395a13d761f1893dd72330a9b9ee4696ae75eab295",
|
||||
"orderHash": "d8402ae6e772e0778b862b190abfa291de1b02a15537d455f7dcd8596245dfd8",
|
||||
"features": 30,
|
||||
"bytes": 49554
|
||||
},
|
||||
"layers/sidewalk_surface.geojson": {
|
||||
"contentHash": "b6f8f876e9f18fd2ef6749125a09d654c73d986e916bd262ceb9967bca1a54c6",
|
||||
"orderHash": "3fd42a67e18e3b8743fa538428e039ef66a1f44150a6881a7aaa18326607b5cb",
|
||||
"features": 26,
|
||||
"bytes": 54997
|
||||
},
|
||||
"layers/turn_arrows.geojson": {
|
||||
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"features": 0,
|
||||
"bytes": 52
|
||||
},
|
||||
"layers/vehicle_stop_lines.geojson": {
|
||||
"contentHash": "cb904550ab8eec011cf33e5edfa81044de6dfca0280dbfe488e77229055ed590",
|
||||
"orderHash": "8a4b877a17a5af60adc8656465afad6756944736291664139e878d46c5a4f363",
|
||||
"features": 4,
|
||||
"bytes": 4191
|
||||
},
|
||||
"traffic-signal-assemblies.json": {
|
||||
"contentHash": "1788026a0c8c531b2fc7dd49628f7fbadd809c7b2f7f8819cffb3ac7d30689ca",
|
||||
"orderHash": "b409ea6b77b6c8639f63c33a156581f9b184a84fde34d265b5deb05705828251",
|
||||
"features": 7,
|
||||
"bytes": 5411
|
||||
},
|
||||
"traffic-signals.json": {
|
||||
"contentHash": "625b79b6eb82b41f2099ba51960717c7f794fdfbd87310eb12e99da966540d89",
|
||||
"bytes": 15491
|
||||
}
|
||||
},
|
||||
"volatileExcluded": [
|
||||
"absolute paths -> <repo> or <external>",
|
||||
"native-road-* staging directory -> <staging>"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"areaId": "nantaizi-lake-innovation-valley",
|
||||
"files": {
|
||||
"../native-traffic-signals.json": {
|
||||
"contentHash": "36cb9f069fb44a6e24f0af0d130ef8cb5b4a93250dc9fb0891191d1ba039a5b2",
|
||||
"bytes": 28329
|
||||
},
|
||||
"comparison.json": {
|
||||
"contentHash": "5aa11da443221a48288641ac6950c6e4d75d834713a93110bbf265859202a265",
|
||||
"bytes": 1153
|
||||
},
|
||||
"compiled.json": {
|
||||
"contentHash": "ea5ae4a04f1259b1ccec9fc1cca7ef8ba81426a77584a447480b6047b37f5e01",
|
||||
"bytes": 184985
|
||||
},
|
||||
"diagnostics.json": {
|
||||
"contentHash": "7e9327f0424dd612f389c560c114a4cafee1349bbfa36f49b2b4eab846e5ca91",
|
||||
"bytes": 9755
|
||||
},
|
||||
"layers/center_lines.geojson": {
|
||||
"contentHash": "2b2710fce06e75b0a3e4941c2da6119cea7b8bb7834345607bbee384de20ee6b",
|
||||
"orderHash": "ea71a8841370689d6fc2b75d0b30ec7104638fb2904da6d707dc82ff3131e065",
|
||||
"features": 511,
|
||||
"bytes": 694102
|
||||
},
|
||||
"layers/connectors.geojson": {
|
||||
"contentHash": "901a86bc6c536d38bb73ae4654ec1ce7d1c0790c2d6eba92543c48f584cd15ac",
|
||||
"orderHash": "4df882ad59b65a97d892df64de0156b35beaf071b04eed671ef7b535e9e2a7b9",
|
||||
"features": 76,
|
||||
"bytes": 160866
|
||||
},
|
||||
"layers/crosswalks.geojson": {
|
||||
"contentHash": "53fda76182c0d1db5f9e7ac284337ece62655d7fee8bb446fe170ea26abe8bbc",
|
||||
"orderHash": "53fda76182c0d1db5f9e7ac284337ece62655d7fee8bb446fe170ea26abe8bbc",
|
||||
"features": 48,
|
||||
"bytes": 50536
|
||||
},
|
||||
"layers/direction_arrows.geojson": {
|
||||
"contentHash": "38e80bbfe02d4f12ce40e7d0c8342e3bef6b602c64f81dd320b366b6a6b3d205",
|
||||
"orderHash": "579aa6c37585bf4edf9d6b6b1e120f976b93ec2d0c303d82a154b75c114a45f5",
|
||||
"features": 324,
|
||||
"bytes": 493431
|
||||
},
|
||||
"layers/edge_lines.geojson": {
|
||||
"contentHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"orderHash": "ad37fe6278e0c7caf3b77c1c5068a56e18a6c87b78ed5a840ee22a0f961ea7a8",
|
||||
"features": 0,
|
||||
"bytes": 52
|
||||
},
|
||||
"layers/intersection_surface.geojson": {
|
||||
"contentHash": "7f79e57215e3fd07c5c0f703c71e516873db770cb869efdf18925a3184059ca3",
|
||||
"orderHash": "d69df8d92d0fce8f7ee172959b978d941fd011e094857e9ab7304b3d1e98bf92",
|
||||
"features": 12,
|
||||
"bytes": 38528
|
||||
},
|
||||
"layers/lane_centerlines.geojson": {
|
||||
"contentHash": "3fea4ebfe62e0d720c6977e2289f4e6517ed7b3c5ab361270d03378971aec780",
|
||||
"orderHash": "69a8ebaa42a74da4f7d5940c5a81bd82ceb104efd3f858723dccfadbae12121e",
|
||||
"features": 50,
|
||||
"bytes": 43582
|
||||
},
|
||||
"layers/lane_separators.geojson": {
|
||||
"contentHash": "dc0b50b53da191e65f3dba01162acb7561966d1796a9c0789527cebfd6ed06a4",
|
||||
"orderHash": "0bfd873e5698869fb4855783fe30bb6db29456958e4d154b7264374a5fdb4a10",
|
||||
"features": 731,
|
||||
"bytes": 835933
|
||||
},
|
||||
"layers/road_surface.geojson": {
|
||||
"contentHash": "81af19c71cf6fc8a3884488c00cfb590e1a7fe9ed9e4351fce65825ceadd50fa",
|
||||
"orderHash": "f2ddec5258411fbdbc050b7ecefbb157b59e00b8226afcc30d2b772e1859e357",
|
||||
"features": 25,
|
||||
"bytes": 38275
|
||||
},
|
||||
"layers/sidewalk_surface.geojson": {
|
||||
"contentHash": "bb8f4516e27b16238d753b112d18b8acfa5cfad2cea522a65b7972a609be9563",
|
||||
"orderHash": "4fa773a6f39c5b6d23551e1338c6d99ef8a5ecd9ebfcafa16154c9765fef8c6e",
|
||||
"features": 73,
|
||||
"bytes": 122432
|
||||
},
|
||||
"layers/turn_arrows.geojson": {
|
||||
"contentHash": "a65bb8fe079f27e00bf2e6208148fc6b73d2d07e76d48b1cd1bf8a610d7e2d66",
|
||||
"orderHash": "66966ac2c65464f9c6715ca9cda5a3620520bf1d066cba2dee36114dc46af936",
|
||||
"features": 108,
|
||||
"bytes": 132536
|
||||
},
|
||||
"layers/vehicle_stop_lines.geojson": {
|
||||
"contentHash": "4f45caf3c0bf13bbf10d9cac15fa80b1a9be3969ed84803ba48e56c4a0080992",
|
||||
"orderHash": "4f45caf3c0bf13bbf10d9cac15fa80b1a9be3969ed84803ba48e56c4a0080992",
|
||||
"features": 8,
|
||||
"bytes": 8475
|
||||
},
|
||||
"traffic-signal-assemblies.json": {
|
||||
"contentHash": "9f8d032c00784bb72bb1322238cd7b0a80e621d435b43c728da86f1e77ca352a",
|
||||
"orderHash": "0b9cae13094dba1994faca4bf1d4d80e8bc5497daa46df5899bf5e6fc7f26d61",
|
||||
"features": 35,
|
||||
"bytes": 26398
|
||||
},
|
||||
"traffic-signals.json": {
|
||||
"contentHash": "c4b68cc1cefeb0d393a18eef6c7eb79933fba28630826745165ed433c25779d0",
|
||||
"bytes": 74151
|
||||
}
|
||||
},
|
||||
"volatileExcluded": [
|
||||
"absolute paths -> <repo> or <external>",
|
||||
"native-road-* staging directory -> <staging>"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
294
.trellis/tasks/08-25-road-compiler-extraction/design.md
Normal file
294
.trellis/tasks/08-25-road-compiler-extraction/design.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# 道路编译器独立化 — 技术设计(parent)
|
||||
|
||||
本文件是 **契约的权威定义**,供所有子任务引用。
|
||||
Phase 0 的交付物是把本节内容落成编译器仓库内的正式文档 + 校验脚本,
|
||||
而不是重新发明契约。
|
||||
|
||||
---
|
||||
|
||||
## 1. 契约:`native-road-package/v1`
|
||||
|
||||
### 1.1 输入契约
|
||||
|
||||
现状:`compile-native-roads.js` 接收整个 normalized areaConfig,但**实际只用 9 个字段**。
|
||||
|
||||
```
|
||||
area.id → areaId
|
||||
area.input → osmFile (OSM XML 路径)
|
||||
area.nativeRoad.edgeLines → options.edgeLines
|
||||
area.nativeRoad.junctionTemplates → options.junctionTemplates
|
||||
area.outputs.nativeRoadOverrides → overridesFile
|
||||
area.outputs.nativeTrafficSignals → trafficSignalsFile
|
||||
area.outputs.nativeRoadDir → outDir
|
||||
area.outputs.pipelineDir → stagingDir
|
||||
area.outputs.geojsonDir → comparisonDir (见 K2,倾向移除)
|
||||
```
|
||||
|
||||
目标形状:
|
||||
|
||||
```js
|
||||
// RoadCompilerInput —— 编译器唯一入口参数
|
||||
{
|
||||
areaId: string,
|
||||
osmFile: string, // 绝对路径
|
||||
outDir: string, // native-road/ 的目标位置
|
||||
stagingDir: string, // 原子提升用的临时目录父级
|
||||
overridesFile: string,
|
||||
trafficSignalsFile: string,
|
||||
options: {
|
||||
edgeLines: boolean,
|
||||
junctionTemplates: {
|
||||
enabled: boolean,
|
||||
references: [],
|
||||
clusters: [{ id, template, referenceFile, nodeIds, ...几何参数 }]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**宿主侧责任**:`scripts/lib/area-config.js` 把 areaConfig 映射成 `RoadCompilerInput`。
|
||||
**编译器侧责任**:不认识 areaConfig,不读 `config/areas/*.json`,不推导任何路径。
|
||||
|
||||
### 1.2 输出契约
|
||||
|
||||
`compiled.json` 顶层键(已核实):
|
||||
|
||||
```
|
||||
schema, areaId, source, model, movements, trafficSignals, diagnostics, layers
|
||||
```
|
||||
|
||||
`source` 含 `{ osm, overrides, trafficSignals }` 三个路径 —— 是 parity 归一化的重点对象。
|
||||
|
||||
```
|
||||
<outDir>/ # 原子提升:先写 staging 再 rename
|
||||
manifest.json # ← Phase 3 新增,图层自声明
|
||||
compiled.json # 单一读模型,含 model + movements
|
||||
diagnostics.json # severity / subject / sourceIds / rule / message / geometry?
|
||||
comparison.json # ← 见 K2,可能移除
|
||||
layers/
|
||||
road_surface.geojson
|
||||
intersection_surface.geojson
|
||||
sidewalk_surface.geojson
|
||||
edge_lines.geojson
|
||||
lane_separators.geojson
|
||||
center_lines.geojson
|
||||
crosswalks.geojson
|
||||
vehicle_stop_lines.geojson
|
||||
direction_arrows.geojson
|
||||
turn_arrows.geojson
|
||||
lane_centerlines.geojson # 语义层,不渲染
|
||||
connectors.geojson # 语义层,不渲染
|
||||
|
||||
<trafficSignalsFile> # 兄弟文件,不在 outDir 内
|
||||
```
|
||||
|
||||
**stdout 完成标记**(与宿主 `SCENE_DONE` / `CESIUM_EXPORT_DONE` 同约定):
|
||||
|
||||
```
|
||||
NATIVE_ROAD_COMPILE_DONE {"areaId":…,"roads":N,"endpoints":N,"diagnostics":N,"output":…,"comparison":…}
|
||||
```
|
||||
|
||||
### 1.3 消费方式:子进程为主契约
|
||||
|
||||
| 方式 | 定位 |
|
||||
|---|---|
|
||||
| CLI 子进程 + 读 `outDir` + 解析 stdout 标记 | **主契约**。宿主 pipeline 层本就是唯一能启动外部进程的层 |
|
||||
| `require()` in-process | 仅作性能优化,不得成为唯一路径 |
|
||||
|
||||
选子进程的理由:
|
||||
1. 语言无关 —— 编译器将来若换 TS/Rust,宿主零改动。
|
||||
2. 强制文件契约成为真契约,无法偷传对象绕过边界。
|
||||
3. 与既有 QGIS / GDAL / Blender 调用方式一致。
|
||||
|
||||
`build-area.js:7` 现在是 in-process `require`。Phase 2 改为子进程调用。
|
||||
|
||||
---
|
||||
|
||||
## 2. 模块清单:什么搬、什么留
|
||||
|
||||
### 2.1 搬(约 3400 行核心 + workbench)
|
||||
|
||||
| 文件 | 行数 | 依赖 |
|
||||
|---|---|---|
|
||||
| `scripts/lib/native-road.js` | 1695 | fs, path, turn-lane-arrows, complex-junction |
|
||||
| `scripts/lib/complex-junction.js` | 474 | fs, gaode-junction-reference |
|
||||
| `scripts/lib/turn-lane-arrows.js` | 502 | fs, path, lane-geometry |
|
||||
| `scripts/lib/gaode-junction-reference.js` | 231 | fs |
|
||||
| `scripts/lib/lane-geometry.js` | 161 | 无(纯函数) |
|
||||
| `scripts/lib/native-traffic-signals.js` | 49 | osm, traffic-signals ← **见 K1** |
|
||||
| `scripts/lib/osm.js` | 102 | 无 |
|
||||
| `scripts/compile-native-roads.js` | — | area-config ← **要换成窄契约** |
|
||||
| `scripts/check-native-roads.js` | — | area-config ← 同上 |
|
||||
| `scripts/road-workbench.js` + `scripts/workbench/app.js` | 173 + — | area-config ← 同上 |
|
||||
| `scripts/test-native-road.js`、`scripts/test-road-workbench.js` | — | fixture ← **见 K5** |
|
||||
|
||||
依赖图(已核实,`native-road.js` 对宿主零耦合):
|
||||
|
||||
```
|
||||
native-road.js ──→ turn-lane-arrows ──→ lane-geometry (纯)
|
||||
└──────────→ complex-junction ──→ gaode-junction-reference
|
||||
native-traffic-signals ──→ osm.js
|
||||
└──→ traffic-signals.js ← 共用,需拆
|
||||
```
|
||||
|
||||
### 2.2 留
|
||||
|
||||
- `scripts/lib/area-config.js` —— 宿主拥有,新增 `toRoadCompilerInput()` 映射
|
||||
- `scripts/build-area.js` —— 改为子进程调用编译器
|
||||
- `scripts/lib/traffic-signals.js` 的 legacy 读取器部分(见 K1)
|
||||
- `blender/` 全部 —— Phase 3 内部重组,但不搬出仓库
|
||||
- `scripts/lib/scene-layers.js`、osm2streets / QGIS legacy 链路 —— 完全不动
|
||||
|
||||
### 2.3 K1 的拆分建议(Phase 1 需细读确认)
|
||||
|
||||
`lib/traffic-signals.js` 当前混了两类东西:
|
||||
|
||||
| 类别 | 使用方 | 归属 |
|
||||
|---|---|---|
|
||||
| OSM 信号节点提取 + 信号文档 schema | `native-traffic-signals.js` | **随编译器走** —— 编译器生成该文档,就该拥有其契约 |
|
||||
| `readTrafficSignalFeatures` | `build-osm2streets-qgis.js` | 留宿主 |
|
||||
| `readTrafficSignals` | `build-area.js` | 留宿主 |
|
||||
| `buildTrafficSignals` | `test-preview-assets.js` | 待判定 |
|
||||
|
||||
拆完后宿主从编译器包 import 信号文档 schema,反向依赖为 0 不受影响
|
||||
(宿主依赖编译器是允许的方向)。
|
||||
|
||||
---
|
||||
|
||||
## 3. Phase 3:渲染分离的设计
|
||||
|
||||
### 3.1 现状问题
|
||||
|
||||
`blender/osmassets/catalog.py:53` 的 `NATIVE_ROAD_LAYERS` 是一张跨仓库重复表:
|
||||
|
||||
```python
|
||||
# 注释自陈:"It is a provider adapter, not a second scene-layer registry."
|
||||
NATIVE_ROAD_LAYERS = (
|
||||
{"source": "road_surface", "material_layer": "road_surface"},
|
||||
{"source": "edge_lines", "material_layer": "lane_separators"},
|
||||
... 共 10 项
|
||||
)
|
||||
```
|
||||
|
||||
编译器写 12 个 geojson,这张表只列 10 项 —— `lane_centerlines` / `connectors`
|
||||
是语义层不参与渲染。**但这个事实只存在于这张表的省略里,编译器侧没有任何声明。**
|
||||
|
||||
后果:编译器新增图层 → 必须有人记得去另一个仓库改 `catalog.py` → 忘了就静默少渲染一层。
|
||||
这正是 `.trellis/spec/pipeline/index.md` 首页警告的「最容易出静默错误」。
|
||||
|
||||
### 3.2 目标:编译器自声明图层
|
||||
|
||||
`<outDir>/manifest.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"contract": "native-road-package/v1",
|
||||
"areaId": "fengshu-er-road",
|
||||
"layers": [
|
||||
{ "source": "road_surface", "role": "surface",
|
||||
"materialLayer": "road_surface" },
|
||||
|
||||
{ "source": "lane_centerlines", "role": "semantic" },
|
||||
{ "source": "connectors", "role": "semantic" },
|
||||
|
||||
{ "source": "center_lines", "role": "marking",
|
||||
"materialLayer": "center_lines",
|
||||
"splitBy": { "prop": "color",
|
||||
"cases": [ { "match": "white", "material": "native_center_line_white" },
|
||||
{ "default": true, "material": "center_lines" } ] } },
|
||||
|
||||
{ "source": "lane_separators", "role": "marking",
|
||||
"materialLayer": "lane_separators",
|
||||
"splitBy": { "prop": "color",
|
||||
"cases": [ { "match": "yellow", "material": "native_lane_separator_yellow" },
|
||||
{ "default": true, "material": "lane_separators" } ] } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `role: "semantic"` 的图层 Blender 直接跳过 —— 把"不渲染"从省略变成显式声明。
|
||||
- `splitBy` 表达当前 `generate_scene.py:815-819` 里硬编码的
|
||||
`color != "white"` / `color == "yellow"` 分流逻辑。
|
||||
- 材质本体(颜色、z 高度、贴图)仍归宿主 `catalog.py::MATERIALS` —— 编译器不懂渲染。
|
||||
编译器只声明"我有这些图层、它们是什么角色、该用哪个材质槽"。
|
||||
|
||||
### 3.3 Blender 侧改造
|
||||
|
||||
- `generate_scene.py` 内道路分支抽为 `blender/osmassets/native_roads.py`,读 manifest 遍历。
|
||||
- 建筑(`handle_building` / `dispatch_ways`)、植被、水体保持原位不动。
|
||||
- `catalog.NATIVE_ROAD_LAYERS` 删除。
|
||||
|
||||
### 3.4 AC5 的验证方法
|
||||
|
||||
向编译器加一个 throwaway 图层(例如 `debug_probe.geojson` + manifest 声明),
|
||||
不改宿主任何代码,跑 blender 阶段,确认它被渲染出来。验证完回滚该图层。
|
||||
|
||||
---
|
||||
|
||||
## 4. 决策记录
|
||||
|
||||
### D1 不用 drawtonomy 替代编译器 ❌
|
||||
|
||||
评估结论(2026-08-25):
|
||||
|
||||
- 克隆仓库内**没有编辑器本体**。`packages/` 只有 SDK / dev-server / mcp-server;
|
||||
白板闭源,托管在 drawtonomy.com。README 卖点里的 topology-aware lanes、
|
||||
lane tool、intersection/roundabout templates、Map→lanes **全部不在开源代码内**。
|
||||
- **它完全不做 raw OSM 推导**。`exporter/osmParser.ts` 首行自陈是
|
||||
"Lanelet2 OSM (.osm XML) parser" —— 读的是 Lanelet2(车道左右边界已显式)。
|
||||
整个 `exporter/` 目录 grep `highway` 命中 1 次,是 `opendrive.ts:685` 的
|
||||
车道类型字符串,不是 OSM 标签解析。从不读 `highway=*` / `lanes` / `turn:lanes`。
|
||||
- 本编译器的核心能力恰是"从稀疏有歧义的中国 OSM 标签推导车道几何",方向垂直。
|
||||
- 语义成熟度对比:本编译器有逐值 provenance
|
||||
(`tag:lanes:forward` / `inferred:highway-default` / `override:<id>`)+ 31 条语义诊断规则;
|
||||
drawtonomy SDK 两者皆无,其 25 条校验规则全是 OpenDRIVE 结构/XML 完整性检查。
|
||||
|
||||
### D2 drawtonomy 作为**下游后端 + 编辑器**(后续独立 PoC)✅
|
||||
|
||||
可用能力(扩展 API,`types.ts:257` 8 个 capability):
|
||||
`shapes:write` + `ui:panel` 足以把编译产物注入编辑器;
|
||||
`snapshot:read` 回读后由本地跑其开源 `exportToOpenDrive` / `lanelet2`。
|
||||
|
||||
不可用能力(决定 junctionTools 不能做成扩展):
|
||||
- 无 canvas / overlay 能力,UI 只能是侧栏 iframe
|
||||
- 无工具注册,画布指针事件完全归宿主
|
||||
- **无任何 change 事件推送** —— `ExtensionClient.handleMessage` 入站只有
|
||||
`ext:init` / 按 requestId 匹配的 5 个 `*-response` / `ext:error`,所有读取靠轮询
|
||||
|
||||
结论:junctionTools 留在自有 workbench(画布自己的,可任意绘制)。
|
||||
drawtonomy 承担场景编排 + 工业格式导出。二者是不同的活,不强行合并 UI。
|
||||
|
||||
依赖风险记录:`drawtonomy-dev-server` 是 `https://www.drawtonomy.com` 的
|
||||
**缓存代理**(TTL 1 小时),非自托管。manifest 有 `minHostVersion` 字段,
|
||||
说明宿主协议会漂。后续 PoC 不把它放进关键路径。
|
||||
|
||||
### D3 值得从 drawtonomy 借用的(Apache-2.0,需保留 NOTICE/署名)
|
||||
|
||||
| 来源 | 行数 | 用途 | 建议阶段 |
|
||||
|---|---|---|---|
|
||||
| `exporter/odrGeometryFit.ts` | 610 | 折线→解析曲线拟合(中位数去噪 + 贪心生长 + 最简原语优先 + 逐拟合回验 + G1 硬不变量)。可替掉手调的 `approachWidthMultiplier=1.45` / `coreRadiusMeters=28`,改为对 `referenceFile` 拟合、残差作质量指标 | 拆分后独立任务 |
|
||||
| validator 的 mutation-proven 方法 | — | 故意破坏合法输入、断言校验器必须抓到。本编译器 31 条诊断规则目前无任何触发证明 | 拆分后独立任务,成本低 |
|
||||
| validator 的分层 + 命名空间(`xml.*` → `ref.*` → `junction.*` → `geom.*`) | — | 替代当前 31 条平铺规则 | 同上 |
|
||||
| OpenDRIVE + Lanelet2 导出器 | 3059 + 869 | 补齐工业格式输出(Lanelet2 是 Autoware 的输入格式) | 后续独立 PoC |
|
||||
|
||||
### D4 为什么 Phase 1 与 Phase 2 必须分开
|
||||
|
||||
Phase 1 只改"包边界与入口契约",仓库不变 → 若产物变化,成因必在代码改动。
|
||||
Phase 2 只改"仓库位置与消费方式",代码不变 → 若产物变化,成因必在搬迁。
|
||||
合并执行则两者混淆,parity oracle 失去诊断价值。
|
||||
|
||||
### D5 IR 重构推迟到拆分之后
|
||||
|
||||
见 prd.md C1。拆分的正确性完全建立在"产物逐字节不变"上,
|
||||
同期改 IR 会同时摧毁 oracle 与归因能力。
|
||||
|
||||
---
|
||||
|
||||
## 5. 回滚形状
|
||||
|
||||
| Phase | 回滚方式 |
|
||||
|---|---|
|
||||
| 0 | 无代码改动,仅新增文档与基线,无需回滚 |
|
||||
| 1 | `git revert`;`packages/road-compiler/` 与旧 `scripts/lib/*` 并存过渡期内可切回旧路径 |
|
||||
| 2 | 宿主依赖回指本仓库内路径(`file:packages/road-compiler`),编译器仓库保留不动 |
|
||||
| 3 | Blender 侧恢复 `catalog.NATIVE_ROAD_LAYERS`,manifest 保留但不消费 |
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
139
.trellis/tasks/08-25-road-compiler-extraction/prd.md
Normal file
139
.trellis/tasks/08-25-road-compiler-extraction/prd.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 道路编译器独立化(parent)
|
||||
|
||||
## Goal
|
||||
|
||||
把 native road compiler 从本仓库拆成可独立维护的项目,与本项目通过**版本化文件契约**相辅相成;
|
||||
同时把道路渲染与 OSM 建筑渲染分离,使二者可各自演进。
|
||||
|
||||
本任务是 parent:它拥有源需求、契约定义、子任务地图、跨子任务验收标准和最终集成评审。
|
||||
**它自身不承担实现工作**,所有可交付物在子任务中完成。
|
||||
|
||||
## 背景:为什么现在拆
|
||||
|
||||
- `scripts/lib/native-road.js` 已 1695 行,`compileGeometry()` 单函数编排 15+ 个 pass,
|
||||
顺序依赖只由行号编码,每加一个特性就往既有函数尾部挂位置参数
|
||||
(`compileLaneMarkings` 已 7 个位置参数)。
|
||||
- 道路与建筑渲染纠缠在 `blender/generate_scene.py` 一个脚本内,
|
||||
道路图层表 `NATIVE_ROAD_LAYERS` 硬编码在 `blender/osmassets/catalog.py:53`。
|
||||
- 道路编译的迭代节奏(几何、路口、标线、V2X 语义)与建筑/植被/水体渲染完全不同,
|
||||
放在一个仓库里互相牵制。
|
||||
|
||||
拆分的可行性前提(已核实):
|
||||
- `native-road.js` 只 require `fs` / `path` / 两个同族道路模块,**对宿主项目零耦合**。
|
||||
- 对宿主的唯一耦合是三个 CLI 入口里的 `readAreaConfig`。
|
||||
- 输出侧 `native-road/` **已经是文件契约**,`blender/generate_scene.py:801` 按图层名读取。
|
||||
- stdout 标记 `NATIVE_ROAD_COMPILE_DONE` 已存在,与 `SCENE_DONE` / `CESIUM_EXPORT_DONE`
|
||||
同一约定 —— 子进程边界事实上已预留。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1 契约先行
|
||||
|
||||
- R1.1 输入输出契约必须在任何代码移动之前写死并版本化为 `native-road-package/v1`,
|
||||
命名对齐既有 `osm-asset-package/v1` 约定。
|
||||
- R1.2 契约的权威定义见本任务 `design.md`;Phase 0 负责把它落成仓库内文档 + 校验脚本。
|
||||
|
||||
### R2 可证明的等价性
|
||||
|
||||
- R2.1 拆分过程中每一步都必须对两个有效区域
|
||||
(`fengshu-er-road` / `nantaizi-lake-innovation-valley`)
|
||||
验证产物与基线一致。
|
||||
- R2.2 任何差异必须逐条书面解释后才可接受,禁止"看起来差不多"。
|
||||
- R2.3 验证方法沿用既有 `.trellis/spec/guides/artifact-parity-guide.md`。
|
||||
|
||||
### R3 独立可维护
|
||||
|
||||
- R3.1 拆出的项目必须能脱离宿主自测(自带 fixture,不读宿主 `inputs/` 或 `outputs/`)。
|
||||
- R3.2 保留 git 历史(`git subtree split` 或 `git filter-repo`)——
|
||||
1695 行几何逻辑的 blame 是踩坑记录,丢失后无人敢改。
|
||||
- R3.3 编译器不得反向依赖宿主任何模块、配置或目录布局。
|
||||
|
||||
### R4 相辅相成(防漂移)
|
||||
|
||||
四个机制缺一不可:
|
||||
|
||||
| 机制 | 作用 | 落地于 |
|
||||
|---|---|---|
|
||||
| `native-road-package/v1` 版本号 | 破坏性变更必须升版本,宿主主动 opt-in | Phase 0 |
|
||||
| 编译器自声明图层(layer manifest) | 加图层不需要改宿主代码 | Phase 3 |
|
||||
| 两区域 parity 基线留在编译器仓库当测试语料 | 编译器无法静默弄坏宿主 | Phase 0 → Phase 2 |
|
||||
| 宿主锁版本依赖,不用 `latest` | 升级是决定,不是意外 | Phase 2 |
|
||||
|
||||
### R5 渲染分离
|
||||
|
||||
- R5.1 道路图层表必须从宿主 `catalog.py` 的硬编码变为编译器输出的 manifest。
|
||||
- R5.2 `blender/generate_scene.py` 内道路渲染分支抽离为独立模块;
|
||||
建筑 / 植被 / 水体保持原位。
|
||||
- R5.3 分离后,编译器新增图层不需要修改宿主任何代码即可被渲染。
|
||||
|
||||
## Constraints(硬约束)
|
||||
|
||||
### C1 不得在拆分过程中重构 IR 🔴
|
||||
|
||||
lane graph 提为一等 IR 是正确方向,但**必须在 Phase 2 完成之后作为独立任务**。
|
||||
|
||||
理由:拆分的正确性完全依赖"产物逐字节不变"这一 oracle。
|
||||
一旦同时改 IR,oracle 失效,且无法判断产物变化来自搬迁还是重设计。
|
||||
|
||||
未来若做,形式是**新增输出**而非替换 `compiled.json`,
|
||||
靠"解锁 drawtonomy / OpenDRIVE / Lanelet2 导出"赚取存在理由。
|
||||
|
||||
### C2 拆分期不引入构建步骤
|
||||
|
||||
编译器保持 CommonJS、无构建步骤,与宿主 pipeline 层运行时一致
|
||||
(见 `.trellis/spec/pipeline/index.md`)。
|
||||
TypeScript / ESM 是独立决策;若后续 drawtonomy PoC 需要 SDK 互操作,只能在其独立任务的扩展目录内处理。
|
||||
|
||||
### C3 Phase 1 必须先于 Phase 2
|
||||
|
||||
"包边界"与"换仓库"分两步做。合并执行时若产物变化,无法区分成因。
|
||||
|
||||
## 子任务地图
|
||||
|
||||
| Phase | 子任务 | 交付物 | 阻塞后续 |
|
||||
|---|---|---|---|
|
||||
| 0 | `08-25-rc-p0-contract-baseline` | 契约文档 + 两区域 checksum 基线 + 校验脚本 | 是 |
|
||||
| 1 | `08-25-rc-p1-package-boundary` | 本仓库内 `packages/road-compiler/`,窄输入契约 | 是 |
|
||||
| 2 | `08-25-rc-p2-repo-split` | 独立仓库 + 宿主锁版本消费 | 是 |
|
||||
| 3 | `08-25-rc-p3-render-separation` | layer manifest + Blender 道路模块抽离 | 否 |
|
||||
|
||||
顺序约束:**0 → 1 → 2 必须串行**。3 依赖 2 完成。
|
||||
|
||||
## 跨子任务验收标准
|
||||
|
||||
- [ ] AC1 Phase 0 基线建立后,两区域每个输出文件的 checksum 已提交进版本控制
|
||||
- [ ] AC2 Phase 1 结束时,`npm run build:area` 两区域产物对 AC1 基线逐字节一致
|
||||
- [ ] AC3 Phase 2 结束时,宿主从打包依赖构建,两区域 parity 仍成立
|
||||
- [ ] AC4 Phase 2 结束时,编译器仓库 `npm test` 在不访问宿主仓库的情况下通过
|
||||
- [ ] AC5 Phase 3 结束时,向编译器新增一个图层,宿主**零代码改动**即可渲染出来(实测验证)
|
||||
- [ ] AC6 Phase 3 结束时,`.blend` 结构摘要对基线一致(走 artifact-parity-guide)
|
||||
- [ ] AC7 全程未修改 `compiled.json` 的结构(C1 未被违反)
|
||||
- [ ] AC8 编译器仓库对宿主的反向依赖数为 0(grep 验证)
|
||||
|
||||
## 已识别风险(跨子任务,逐个必须有归属)
|
||||
|
||||
| # | 风险 | 归属 Phase | 处置 |
|
||||
|---|---|---|---|
|
||||
| K1 | `lib/traffic-signals.js` 是真共用模块:`reimport-gpkg.js` / `build-osm2streets-qgis.js` / `build-area.js` / `test-preview-assets.js` 都在用,而 `native-traffic-signals.js` 也依赖它 | 1 | 需细读后拆分:信号文档 schema + OSM 信号节点提取随编译器走(编译器生成它就该拥有契约),legacy QGIS/预览读取器留宿主。**全案唯一需要细读再动的地方** |
|
||||
| K2 | `comparison.json` 依赖宿主产物 —— `compile-native-roads.js:87` 读 `area.outputs.geojsonDir` 里 osm2streets 输出做对比,拆出后摸不到 | 0(决策)/ 1(执行) | osm2streets 已 legacy,倾向直接砍掉,用高德参考 + 规范校验取代。决策需在 Phase 0 定档 |
|
||||
| K3 | config 内绝对路径指向宿主仓库:`config/areas/fengshu-er-road.json` 的 `referenceFile: "/Users/que01/osm2streets-qgis-workflow/inputs/osm/珠山湖大道(枫树二路)口.geojson"` | 1 | 需定参考文件解析约定(相对 config 目录 / 显式 basePath) |
|
||||
| K4 | workbench 的 OpenLayers 来自宿主 `node_modules`(design.md 的 import map 方案) | 2 | 新仓库自带依赖 |
|
||||
| K5 | 测试 fixture 依赖宿主:`test-native-road.js:167` 读 `inputs/osm/枫树二路.osm`,其余为内联合成 OSM | 1 | 该文件(或裁剪版)作为测试数据提交进编译器仓库 |
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- lane graph IR 重构(见 C1,未来独立任务)
|
||||
- pass manager / 显式依赖声明重构(同上,属编译器内部演进,不属本次拆分)
|
||||
- 替换编译器为 drawtonomy 或 osm2streets —— 已评估否决:
|
||||
drawtonomy 开源部分只读 Lanelet2(显式车道边界),不做 OSM `highway=*` 推导,
|
||||
与本编译器的核心能力方向垂直
|
||||
- osm2streets / QGIS legacy 链路的任何改动
|
||||
- 宿主侧建筑 / 植被 / 水体渲染逻辑的改动(Phase 3 只抽离道路部分)
|
||||
|
||||
## Notes
|
||||
|
||||
- 评估结论与选型依据见 `design.md` 的「决策记录」一节。
|
||||
- 时间估计:Phase 0-2 约 3 天(搬家 + 证明没搬坏),Phase 3 约 1-2 天。
|
||||
拿到"道路与建筑渲染分离、可独立维护"是在 Phase 3 结束。
|
||||
- 父任务验收完成后,下一项候选任务是独立的
|
||||
`08-25-rc-p4-drawtonomy-ext`(drawtonomy 扩展 PoC);它不属于本任务完成条件。
|
||||
33
.trellis/tasks/08-25-road-compiler-extraction/task.json
Normal file
33
.trellis/tasks/08-25-road-compiler-extraction/task.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "road-compiler-extraction",
|
||||
"name": "road-compiler-extraction",
|
||||
"title": "道路编译器独立化(parent)",
|
||||
"description": "把 native road compiler 拆成可独立维护的项目,并与本项目通过版本化契约相辅相成;同时分离道路与 OSM 建筑渲染",
|
||||
"status": "planning",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P1",
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [
|
||||
"08-25-rc-p0-contract-baseline",
|
||||
"08-25-rc-p1-package-boundary",
|
||||
"08-25-rc-p2-repo-split",
|
||||
"08-25-rc-p3-render-separation"
|
||||
],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {
|
||||
"next_task": "08-25-rc-p4-drawtonomy-ext"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user