feat: add native road parity baseline

This commit is contained in:
2026-08-25 16:52:25 +08:00
parent 25cf82e7c7
commit e7bc7f82b1
20 changed files with 1373 additions and 0 deletions

View 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."}

View 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 rootstaging 目录名 → 字面量 `<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 验证不需要等仓库拆分。

View 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."}

View 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.1AC0.6 打勾后,再进 Phase 1。**基线不可靠时不要前进。**
## Rollback
本阶段纯新增(文档 / 脚本 / 基线数据)。回滚 = 删除新增文件。
唯一例外Step 2 若发现并修复了非确定性 bug那部分是真实代码改动
应拆成独立 commit 并单独保留。

View 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` 结构变更

View 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": {}
}

View File

@@ -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.

View File

@@ -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>"
]
}

View File

@@ -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>"
]
}

View 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."}

View 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 保留但不消费 |

View 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."}

View 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。
一旦同时改 IRoracle 失效,且无法判断产物变化来自搬迁还是重设计。
未来若做,形式是**新增输出**而非替换 `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 编译器仓库对宿主的反向依赖数为 0grep 验证)
## 已识别风险(跨子任务,逐个必须有归属)
| # | 风险 | 归属 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它不属于本任务完成条件。

View 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"
}
}