35 Commits

Author SHA1 Message Date
a4dfbf884a chore: remove deprecated native compiler commands 2026-08-26 13:24:31 +08:00
82dcb83663 chore: keep ZIP import task active 2026-08-26 13:04:32 +08:00
7e8fcd82ed chore: record ZIP artifact task completion 2026-08-26 12:59:31 +08:00
eaa7c08f6a fix: finalize ZIP preview freshness and parity 2026-08-26 12:58:36 +08:00
113ea54535 feat: import native road packages from ZIP 2026-08-26 12:51:58 +08:00
009fe34f79 chore: record journal 2026-08-26 11:01:54 +08:00
7883a451c5 chore(task): archive 08-25-rc-p3-render-separation 2026-08-26 10:58:41 +08:00
620127031e feat: consume manifest-driven native roads 2026-08-26 10:55:47 +08:00
5704b51315 chore: record journal 2026-08-26 09:32:22 +08:00
969b57106f chore(task): archive 08-25-rc-p2-repo-split 2026-08-26 09:28:33 +08:00
7d4b3b566a docs: specify external road compiler contract 2026-08-26 09:27:09 +08:00
5cde08090e refactor: consume external road compiler 2026-08-26 09:23:46 +08:00
b78cb6e990 chore: record journal 2026-08-25 17:28:26 +08:00
b44c995457 chore(task): archive 08-25-rc-p1-package-boundary 2026-08-25 17:27:53 +08:00
2469cfdbff test: make road compiler fixtures self contained 2026-08-25 17:27:19 +08:00
284d3dbedb chore: remove migrated workbench sources 2026-08-25 17:22:50 +08:00
f542a64924 refactor: move road workbench into compiler package 2026-08-25 17:18:49 +08:00
d5c58b5b0c refactor: move native road checks into package 2026-08-25 17:15:15 +08:00
73707dabbe refactor: move road compiler core into package 2026-08-25 17:12:58 +08:00
5220c6b2a2 chore: record journal 2026-08-25 16:55:17 +08:00
d7c2e13124 chore(task): archive 08-25-rc-p0-contract-baseline 2026-08-25 16:53:04 +08:00
e7bc7f82b1 feat: add native road parity baseline 2026-08-25 16:52:25 +08:00
25cf82e7c7 fix: render live V2X vehicles in Cesium preview 2026-08-25 12:57:35 +08:00
bc845444bb fix: restore live V2X signal and vehicle fidelity in Cesium preview
The ported overlay used the correct REST paths but lost the data handling
from the source dashboard's live-intersection view (HologramCross), so
signals rendered permanently red and vehicles often never appeared.

- Lamp status codes now follow the dashboard dictionary (11/21/22/23/31).
  The previous 2/3 reading made every real push fall through to red. The
  dictionary lives only in the overlay; the preview consumes normalized
  {nodeKeys, color, countDown} entries so the two copies cannot drift.
- Bind V2X phases to native signal heads geometrically. The runtime
  document has no phaseNo, so the old lookup fell back to signal.id and
  never matched, leaving the dynamic assembly dark. Travel heading is
  recovered as faceHeadingDegrees + 180, per the generator's
  mast = travel - 90 / face = travel + 180. Verified 7/7 exact matches
  against the fengshu-er-road runtime document.
- A phase now lights every approach it drives; the phase -> single entity
  map silently overwrote all but the last.
- Drive the countdown assets from the push's countDown field.
- All three sockets heartbeat every 30s and reconnect with backoff,
  replaying their subscription frame. Without this the service dropped
  the connection and the scene emptied after about a minute.
- The OBU socket sends its bounds frame on connect and on camera move;
  it previously sent nothing at all.
- Vehicles are swept when a push goes stale and their slots reused, so
  they no longer accumulate as ghosts. Models follow the dashboard's
  car_obu.glb / ${type}${subType}.glb naming.
- Parse vehicle pushes leniently, since the dashboard uses saferEval and
  the payload is not guaranteed to be strict JSON. Failures are counted
  and surfaced rather than dropped; no eval is introduced.
- Drop FlowTravelRatio/queryListWeek, which is not part of this view.

Also corrects a stale spec rule that required vehicleModelNames to be
empty. Live V2X vehicles need packaged models; the real invariant is no
generated routes or traffic simulation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 09:20:50 +08:00
bb2a1449ac fix: keep V2X intersection code out of login UI 2026-08-24 14:47:25 +08:00
1c2d091ad2 chore: record journal 2026-08-24 14:44:57 +08:00
e997ece188 chore(task): archive 08-24-v2x-amap-intersection-data 2026-08-24 14:43:12 +08:00
b7031f145a fix: use live V2X vehicles only in preview 2026-08-24 14:42:31 +08:00
ee273c5cd6 feat: add live V2X Cesium preview overlay 2026-08-24 14:20:16 +08:00
71ba536c7c chore: record journal 2026-08-24 13:52:21 +08:00
903b34b5ae chore(task): archive 08-18-native-preview-traffic-simulation 2026-08-24 13:52:00 +08:00
f1f829c2c6 fix: stabilize fengshu native road compilation 2026-08-24 13:51:21 +08:00
a16400ff96 fix: correct native road edge and junction boundary geometry
修复四处几何缺陷,并新增复杂路口候选识别与工作台调试视图。

几何修复:
- junctionBoundary 的进口边改用 cutback 处朝向 (headingAtCutback)。原先取节点端
  首段朝向, 与 roadRing 的端边不平行, 15 个进口有偏差 (最大 152°), 路口面与道路面
  之间张开楔形。凸包兜底路口 11 -> 8。
- compileEdgeLines 偏移量改用 totalWidth/2。原用单方向 road.widthMeters/2,
  双向段的路缘线画在车行道正中间 (实测中位偏差 1.63m = w/2)。同时以实际发出的
  路面为准过滤无路面的路, 并统一复杂路口的裁剪口径。
  顶点偏离 >0.5m: 64% -> 0%, 最大偏离 4.53m -> 0.06m (= 半个带宽, 理论最优)。
- roadRing 新增 removeOffsetFolds。定宽法线偏移无斜接限制, 转弯处相邻段较短时
  偏移点前后颠倒, 多边形折回自身, 按 even-odd 渲染成叶片状空洞。
  道路面自交 1 -> 0, 最尖角 0.1° -> 18.8°。
- junctionBoundary 新增 mergeParallelApproachPoints。双幅路两幅同向汇入时,
  四个侧点按角度排序后交错, 在交错点之间插入的圆角曲线从外圈深挖回节点,
  渲染成拱形凹口。合并近平行 (<25°) 进口为单一面。
  路口面最尖角 6.9° -> 全部 >=82.6°, 自交 0。

新增:
- detectComplexJunctionCandidates: 按"短链接连接的路口节点"聚类并做紧凑度过滤
  (直径 <=45m), 输出 complex-junction-candidate 诊断与建议参数。纯诊断,
  不启用模板、不改几何; test:native-road 断言了这条契约。
- road:workbench --debug: 地图标注候选编号与包络, 点选给出可粘贴配置片段,
  并提供 POST /api/junction-clusters 一键写入区域配置。写入前用 readAreaConfig
  校验、检测节点冲突、要求候选存在于最新编译结果, 原子落盘。

Regression: test:native-road / test:road-workbench / test:gaode-junction-reference /
test:preflight / test:native-preview-traffic / test:package-contract /
test:traffic-signals 全绿; road:check ok=true, errors=[]。
源 OSM 与参考 GeoJSON 校验和未变。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-08-21 17:45:20 +08:00
8ecd794633 chore(task): archive 08-19-gaode-junction-reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 12:00:05 +08:00
12aeda9a63 feat: parameterize complex junction geometry with Gaode reference
- 高德 GeoJSON 参考流程: `scripts/lib/gaode-junction-reference.js`
  与 `scripts/inspect-junction-reference.js` 将 GCJ-02 参考转换为 WGS84,
  按 node id/最近距离关联 OSM, 支持普通路口面和 `complex-cluster` 两种匹配。
- 复合路口模板 `complex-junction-v1`: `scripts/lib/complex-junction.js` 用参考
  几何校准 core 半径, 生成路口面、进口路面、斑马线、停止线、角部圆角与安全岛;
  拓扑/信号/连接全部沿用 OSM/native。
- 车道中心线控制要素避让: `compileLaneCenterlines` 现接收模板已产出的斑马线/停止线,
  新增 `trimLaneOutsideControls` 按到路口中心的半径定向裁剪; 标线源几何同步裁剪, 不再
  越过斑马线继续画到核心区。拓扑几何不变, connector 集合前后一致。
- 复合路口人行道转角: `buildComplexJunctionGeometry` 沿已定义的路缘生成 2m 宽转角带,
  复用圆角曲线, 通过 `islands` 通道并入 `sidewalk_surface`; 自交或坐标非有限时报
  `complex-junction-sidewalk-corner-fallback` 并跳过。
- 新增诊断: `complex-junction-configured-radius-ignored`、
  `lane-centerline-fully-inside-control`、`complex-junction-sidewalk-corner-fallback`。
- 死码清理: 移除未被调用的 `clusterApproachRing`。
- spec 更新: `.trellis/spec/pipeline/cli-and-stages.md` 复合路口小节补充控制要素
  避让顺序、人行道转角契约、Validation 矩阵三行; 索引新增导航。
- 任务产物 `08-19-gaode-junction-reference`: 8 条验收标准全部实测记录,
  Scope Drift / Verification Log / Known Gaps 三节沉淀本次工作。

Regression: test:native-road / test:road-workbench / test:preflight /
test:native-preview-traffic / test:package-contract / test:traffic-signals /
test:gaode-junction-reference 全绿; road:check ok=true, errors=[]。
2026-08-21 11:59:03 +08:00
161 changed files with 11648 additions and 11782 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ outputs/
__pycache__/
assets/models/speedtree/
assets/models/lyrog/
inputs/osm

View File

@@ -54,6 +54,8 @@ cp config/examples/template.json config/areas/my-area.json
| `turnLaneArrows` | | 见下 | 从 OSM `turn:lanes:*` 生成自定义车道箭头的发布开关 |
| `osm2streets` | | 见下 | 透传给 osm2streets 的选项 |
| `blender` | | 见下 | Blender 侧选项 |
| `nativeRoad` | | 见下 | 原生道路编译选项 |
| `v2xPreview` | | 见下 | 可选的 Cesium 预览实时 V2X 叠加设置 |
| `compress` | | 见下 | 默认交付压缩阶段的 GLB 压缩选项 |
| `budget` | | 见下 | 区域 GLB 性能与体量预算 |
| `outputs` | | 从 `id` 推导 | 输出路径覆盖,逃生舱 |
@@ -134,6 +136,40 @@ cp config/examples/template.json config/areas/my-area.json
| `officeOverrides` | `""` | 旧名 `office_overrides` 仍被接受 |
| `roadProvider` | `"native"` | Blender 道路来源。`"native"` 时仅使用 `native-road/` 的道路、路口和人行道面;`"osm2streets"` 仅用于显式 legacy/debug 构建。 |
### `nativeRoad`
| 字段 | 默认 | 说明 |
|---|---|---|
| `edgeLines` | `false` | 是否输出道路边缘线。 |
| `junctionTemplates.enabled` | `false` | 启用显式绑定的参数化路口模板。 |
| `junctionTemplates.references` | `[]` | 仅支持 `cross-v1`;每项必须给出 OSM `nodeId`,可带 GCJ-02 `referenceFile` 作校准与有效性检查。 |
| `junctionTemplates.clusters` | `[]` | `cross-cluster-v1` 的相邻 OSM 节点簇。输出各外部进口的参数化渐变道路面,簇内短段与节点级路口面保留;`approachWidthMultiplier` / `approachLengthMeters` 控制渐变。可选 `referenceFile` 记录 GCJ-02 校准来源不合并节点级道路、connector、信号或停止线语义。 |
| `junctionTemplates.clusters[].cornerRadiusMeters` | `12`425 | 仅 `complex-junction-v1`:相邻进口夹角处路缘圆角的半径。圆角切于两侧最外道路边缘,只补齐夹角处的路面,不改变 connector、信号或停止线。 |
| `junctionTemplates.clusters[].outerRadiusExtraMeters` | `18`1835 | 仅 `complex-junction-v1`:路口中心到外部进口交接边界的额外半径。用于让圆角包住角部斑马线;未配置时保持原有 18 m。 |
`junctionTemplates.references[*].referenceFile`
`junctionTemplates.clusters[*].referenceFile` 是唯一允许相对写法的路径:相对当前
`config/areas/<id>.json` 解析。`readAreaConfig()` 会先将其规范化为绝对路径,再通过
`toRoadCompilerInput()` 传入 road compiler包内不得自行解析相对路径或读取配置文件。
### `v2xPreview`
这是 Cesium 验证预览的可选实时叠加层,不进入发布的 `package/`。默认值:
```json
{
"enabled": false,
"apiBaseUrl": "/api",
"wsBaseUrl": "/websocket",
"crossCode": ""
}
```
`apiBaseUrl``wsBaseUrl` 应使用同源反向代理路径,不能写入私有上游主机、账号或令牌。
V2X 接口返回的地图数据是 GCJ-02浏览器预览在创建 Cesium entity 前一次性转为 WGS84
而 native package 的 WGS84/ENU 契约保持不变。详见
[`docs/v2x-cesium-preview.md`](../../../docs/v2x-cesium-preview.md)。
### `compress`
完整构建和显式 `--stages compress` 都使用此配置。默认压缩链是 texture resize + WebP

View File

@@ -815,6 +815,32 @@ area.outputs.trafficSignals
- `layout.mastHeightMeters``layout.headCenterHeightMeters` 必须相等,表示横杆与灯壳的
中心对齐;`lensVerticalOffsetsMeters` 以灯壳中心为基准,正值向上、负值向下。当前倒计时牌
垂直偏移为 `0`,必须贴在横杆上而非悬挂。
- 复合路口中,具有相同 `cluster_id` 的停止线按道路轴线匹配 OSM control arm轴线的两个
极性必须视为等价,但每条候选停止线仍只能消费一次。普通路口继续使用有向进口匹配,不能
用复合路口的无向规则覆盖普通路口。
- 复合路口的中心绿化带/中央分隔带不由 native 复合模板伪造;模板只生成 OSM 道路和路口
内必要的安全岛。每条生成的复合进口车道须在停止线外侧上游约 `8m` 生成一支与车道拓扑
一致的左转、直行或右转箭头;不得额外叠加一套固定直行箭头。
- **控制要素避让顺序**`buildComplexJunctionGeometry``compileLaneCenterlines` 之前生成,
产出的 `complex-crosswalk` / `complex-corner-crosswalk` / `complex-stop-line` 必须以
`{ crosswalks, stopLines }` 参数传入 `compileLaneCenterlines`。车道中心线、车道分隔线、
中心虚线、导向箭头都在这一裁剪之后才落地。这里的裁剪对象包括两类车道几何:一是簇
边界上的 OSM 车道,二是模板自造的 `cluster-approach-lane` 预览车道。裁剪必须先按到
路口中心的距离把线定向为“外向内”,再复用 `trimLineBeforeFirstControl`:因为 OSM 车道
由簇边缘向外指、预览车道由外圈向核心指,两种朝向都存在,直接裁会裁反。裁剪后的几何
同时喂给车道标线源(`markingLanes[i].coordinates`),否则分隔线/箭头会画到斑马线之外
继续侵入核心区。拓扑几何 `coordinates`(供 connector 使用)不得裁剪,否则 turn path
会断。发布几何 vs 拓扑几何的双轨在 `compileLaneCenterlines` 内并存,别合并。
- **复合路口人行道转角**:直步行带被 `trimLineAtComplexCluster` 裁在簇外圈,两条相邻的
臂之间会留一个楔形无覆盖区。转角带由 `buildComplexJunctionGeometry``complex-corner-fillet`
共用同一 `curve` / `tangents` / `edges` / `apex` 复用生成,不重新拟合曲线;宽度
`SIDEWALK_WIDTH_METERS = 2` 必须与 `native-road.js:DEFAULT_SIDEWALK_WIDTH_METERS` 保持
一致,法线方向按“离簇中心更远”选取(路缘相对中心是星形的)。判定两侧臂是否都带步行带
时必须按臂的外向方向翻转 road 自身的 `sidewalkLeft/Right`,因为那是相对数字化方向的。
端头以 `SIDEWALK_CORNER_OVERRUN_METERS = 6` 冗余越过簇边界,让转角带和直条带自然重叠,
不追求精确缝合。自交(`ringSelfIntersects`)或任一坐标非有限时跳过并报
`complex-junction-sidewalk-corner-fallback`;不允许硬塞坏几何。产物经 `islands` 通道
与安全岛一同并入 `generatedComplexSidewalks`,最终落入 `sidewalk_surface.geojson`
### 4. Validation & Error Matrix
@@ -824,6 +850,9 @@ area.outputs.trafficSignals
| 直接运行 `blender` / `preview` 但锚点不存在 | 在启动外部工具前报 `Traffic signal anchors not found` |
| `signal_uid` 缺失/重复、非空 `display_id` 重复、字段或 Point 无效 | 重导入在替换任何输出前失败 |
| 用户仅修改 QGIS 后运行 `reimport` | 重新生成锚点,不沿用旧坐标 |
| 复合路口车道整体落在斑马线/停止线内 | 报 `lane-centerline-fully-inside-control`,回退为未裁剪发布几何,不写空 LineString |
| 复合路口人行道转角自交或坐标非有限 | 报 `complex-junction-sidewalk-corner-fallback`,跳过该角,两侧步行带保持断开 |
| 有 `referenceFile` 时同时配置 `coreRadiusMeters` | 校准值胜出,报 `complex-junction-configured-radius-ignored` 记录被忽略的配置值 |
### 5. Good/Base/Bad Cases
@@ -1435,6 +1464,72 @@ parity 校验依赖 stage 的 stdout 标记来判断阶段是否跑到(如 `SC
---
## Native Road Parity
### 1. Scope / Trigger
拆分 native road compiler、移动其模块或声称 native-road JSON / GeoJSON 未变化时,
`scripts/road-parity.js`。它独立于 build stage只读现有 native-road 输出。
### 2. Signatures
```bash
node scripts/road-parity.js --config config/areas/<area>.json --snapshot <out.json>
node scripts/road-parity.js --config config/areas/<area>.json --compare <baseline.json>
```
`--config` 必填,且 `--snapshot``--compare` 必须二选一。
### 3. Contracts
- 快照覆盖 `outputs/<area>/native-road/` 下全部文件和同级
`native-traffic-signals.json`
- 普通 JSON 递归按对象 key 排序后生成 `contentHash`
- GeoJSON 同时生成排序 feature 的 `contentHash` 与原序 `orderHash`;任一不同都算
parity 失败。
- 绝对路径归一化为 `<repo>/...``<external>/<basename>`,随机
`native-road-*` staging 名归一化为 `<staging>`。坐标不做容差或舍入。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 缺 `--config` 或同时/均未传 snapshot、compare | 打印 usage退出非零 |
| native-road 输出目录不存在 | 抛出 `Native road output not found`,退出非零 |
| baseline 缺文件、出现额外文件或任一 hash/count/bytes 不同 | 每项打印 `ROAD_PARITY_DIFF`,退出非零 |
| 完全一致 | 打印 `ROAD_PARITY_OK`,退出 0 |
### 5. Good/Base/Bad Cases
- Good: 移动编译器前后,两个 hash 均一致。
- Base: 绝对 OSM 输入路径变化但归一化后内容一致。
- Bad: 只比较排序后的 GeoJSON 内容并忽略 `orderHash`,会掩盖输出顺序漂移。
### 6. Tests Required
- `npm run test:road-parity`:相等基线、顺序 hash 变化与缺文件都应被断言。
- `node --check scripts/road-parity.js`
- 对每个受支持区域运行 `--compare` 对已提交基线自比对。
- 迁移前先连续编译两次;只有 control 实验全绿,基线才可作为 oracle。
### 7. Wrong vs Correct
Wrong:
```js
// Sorting hides a changed feature order.
assert.equal(sortedContentHash, baseline.contentHash);
```
Correct:
```js
assert.equal(contentHash, baseline.contentHash);
assert.equal(orderHash, baseline.orderHash);
```
---
## 反模式
| 反模式 | 后果 |

View File

@@ -15,8 +15,10 @@
| 加阶段、加 CLI 参数、改配置字段 | [CLI 与阶段](./cli-and-stages.md) |
| 改区域诊断/质量门命令或共享区域配置归一化 | [CLI 与阶段](./cli-and-stages.md#区域诊断命令) 和 [质量门](./cli-and-stages.md#区域质量门命令) |
| 改 stage manifest 写入、读取或 stale 判断 | [CLI 与阶段](./cli-and-stages.md#stage-manifest-契约) |
| 改复合路口几何(`buildComplexJunctionGeometry`、车道控制避让、人行道转角) | [CLI 与阶段](./cli-and-stages.md#可编辑信号设施与运行时锚点的跨阶段消费) 的复合路口小节 |
| 改预览页生成 | [../preview/](../preview/index.md) |
| 声称"纯重构,产物不变" | [产物一致性指南](../guides/artifact-parity-guide.md) |
| Native road compiler CLI、输入或完成标记 | [编译器契约指针](./native-road-package.md) |
---
@@ -45,8 +47,8 @@ config/areas/<id>.json
│ → 重建 scene.geojson + scene_style.json
│ → _pipeline/stages/reimport.manifest.json
├─[blender]───────▶ compile-native-roads.js + Blender + blender/generate_scene.py
│ 默认读 .osm + native-road/
├─[blender]───────▶ native-road ZIP importer + Blender + blender/generate_scene.py
│ 默认读 .osm + validated ZIP cache/
│ → <id>.blend + <id>.png
│ → _pipeline/stages/blender.manifest.json

View File

@@ -0,0 +1,62 @@
# Native Road Package Contract
The native-road package is maintained in the private `road-compiler` repository:
`https://git.app.que01.top/que01/road-compiler`
The host consumes a read-only ZIP exported by the private `road-compiler` repository. The host never installs or invokes the compiler. `nativeRoadPackage` is resolved relative to the area config, hashed, validated, and extracted into `_pipeline/native-road-import/<sha256>/` before native consumers start.
The completion marker is `NATIVE_ROAD_COMPILE_DONE <json>`. `build-area.js` validates the marker's count, JSON payload, area id, output path, process status, and signal before starting Blender.
## Scope / Trigger
This contract applies to native-road consumption from `scripts/build-area.js`, diagnostics, parity, Blender, and native preview generation.
## Signatures
```text
road-compiler --input <RoadCompilerInput.json>
```
`RoadCompilerInput` contains `areaId`, absolute `osmFile`, `outDir`, `stagingDir`, `overridesFile`, `trafficSignalsFile`, optional `comparisonDir`, and `options`.
## Contracts
The compiler writes `compiled.json`, `diagnostics.json`, `comparison.json`, signal runtime files, `manifest.json`, and GeoJSON layers under `outDir`. Since `native-road-package/v1.1`, `manifest.json` is required: it declares every GeoJSON source, whether its role is renderable (`surface` / `marking`) or semantic, and the host material slot for renderable layers. The Blender adapter rejects a missing or invalid manifest, a declared/published GeoJSON mismatch, and an unknown material slot. On success stdout contains exactly one marker whose JSON includes `areaId`, `roads`, `endpoints`, `diagnostics`, and `output`.
## Validation & Error Matrix
| Condition | Required result |
|---|---|
| CLI cannot start | throw the spawn error |
| non-zero exit or signal | fail with status/signal |
| zero completion markers | fail |
| multiple completion markers | fail |
| malformed marker JSON | fail |
| marker `areaId` or `output` mismatch | fail |
## Good / Base / Bad Cases
- Good: host writes an absolute input JSON, runs the installed exact-tag CLI, validates one marker, then starts Blender.
- Base: compiler emits deterministic files and no optional comparison source exists.
- Bad: host imports compiler source, reads `config/areas` inside the package, or accepts a missing/duplicate marker.
## Tests Required
- `scripts/test-road-compiler-cli.js` asserts marker success, missing, duplicate, malformed, area mismatch, and output mismatch cases.
- `scripts/road-parity.js` compares `contentHash`, `orderHash`, feature counts, and byte counts for both supported areas.
- `npm run test:road-workbench` asserts the workbench is loaded from the installed package.
## Wrong vs Correct
Wrong:
```js
require("../packages/road-compiler/src/compile/compiler");
```
Correct:
```js
spawnSync(process.execPath, [compilerCli(), "--input", inputFile]);
```

View File

@@ -307,6 +307,105 @@ npm run build:area -- --config config/areas/<area>.json --stages cesium
---
## 实时 V2X 车辆仅数据源
### 1. Scope / Trigger
适用于开启 `v2xPreview.enabled` 的 Cesium 运营预览。触发:修改
`v2x-cesium-overlay.js`、preview descriptor 的 `routeName`、或实时车辆展示。
### 2. Signatures
```text
WS /network/ws/network/signal?authorization=<token>
WS /network/ws/network/obuPosition?authorization=<token>
WS /network/ws/network/targetPosition?authorization=<token>
```
OBU 消息使用 `carCode|obuCode``lon``lat``angle``speed`;目标识别消息使用
`data[deviceId][]` 内的 `id``longitude``latitude``type``subType``angle``speed`
两类车辆消息的根部还带 `interval`(推送间隔,`0` 视为 `500`)。
信号灯消息使用 `lamps[]` 内的 `phaseNo``status``countDown`
### 3. Contracts
- 两条车辆流均为 GCJ-02必须在实体创建前恰好调用一次 `gcj02ToWgs84`
- 本预览的 `routeName` 必须为 `null`,且不得生成 `trafficSimulation` 描述符。
真正的不变量是**不得有生成路线或交通仿真**,而不是「没有车模型」——
实时 V2X 车辆需要打包的车模型才能渲染,故 `vehicleModelNames`
`writeVehicleModel(area)` 正常写出(`build-area.js:writeCesiumPreview`
`vehicleModelName` 取其首项。早期版本靠清空模型列表来阻止仿真车辆,
该机制已不适用,不要再用它作为约束手段。
- 每辆实时车保留最多 24 个已转换的位置作为实际轨迹;轨迹不是推测路径。
- **灯色码字典只有一份**,取自源看板 `HologramCross/components/utils.ts`
`11=灭 21=红 22=黄 23=绿 31=其他`,未知码归入「其他」,**不得**落到红色。
overlay 归一化为 `{nodeKeys, color, countDown}` 后单向传给 `cesium-preview.js`
预览层不得自带第二份字典。
- **三条 socket 必须发心跳** `{"heartBeat":"ping"}` / 30000ms并在断线后退避重连
且**重连后重放订阅帧**signal 发 `junctionId`obu 发 boundstarget 发 `deviceId`)。
缺心跳会被服务端断开,画面在一分钟后静默变空。
- **相位到原生灯头靠几何绑定**`traffic-signals.json``phaseNo`,用 link 末段停止线点与
航向匹配原生 `stopLongitude/stopLatitude` 与行车方向;行车方向 =
`faceHeadingDegrees + 180`(生成侧 `scripts/lib/traffic-signals.js` 定义
`mast = travel - 90``face = travel + 180`)。
一相位可点亮多条进口道,一条 link 可点亮同进口道多个灯头,两侧都取并集。
未绑定相位须计数上报,可用 `v2xPreview.phaseSignalMap` 显式覆盖。
- **车辆生命周期**:超过 `interval * 1.5` 未更新即隐藏(不删除),隐藏项作为同模型的可复用槽位;
socket 断开清空车辆。OBU 数据 hook 标记为 `car_obu.glb`,但 dashboard 的最终
`CrossCars` 模板实际渲染 `11.glb`,预览必须以 `11.glb` 为准;目标车为
`${type}${subType}.glb``11/12/13/14/15/16/17/31/221/222.glb` 必须打入
`_preview/v2x-vehicles/`
- **可见性是运行时契约**:有效推送创建的模型必须在路口概览下至少为 `28px`,并允许最大
`8x` 放大;位置高度须高于静态道路表面。首次有效车辆推送应聚焦到车群附近,标签须不受
静态 GLB 的深度遮挡。仅验证 WS 收包或 registry 条数不算通过。
- 车辆推送体解析必须容忍非严格 JSON源看板用 `saferEval`),失败须计数并在面板可见,
**不得**静默丢弃;实现不得使用 `eval` / `new Function`
- 周流量比 `FlowTravelRatio/queryListWeek` **不属于**实时路口范围。
### 4. Validation & Error Matrix
| 条件 | 结果 |
|---|---|
| 未登录、令牌失效、REST/WS 不可用 | 静态路口继续显示,车辆层为空,并显示实时数据不可用状态 |
| 消息不是 JSON、心跳、坐标无效 | 忽略该消息,不创建车辆;非心跳的解析失败须计数 |
| 收到有效车辆坐标 | 创建或更新真实车辆与实际轨迹 |
| 车辆超过 `interval * 1.5` 未更新 | 隐藏该车并保留槽位,不得堆积幽灵车 |
| 相机视野变化 | 节流后向 obu socket 发送 GCJ-02 四角 `bounds` |
| 视野矩形不可用 | 发空帧(等于不过滤),不得跳过发送导致空场景 |
| 相位一个都没绑定 | 面板显性提示并建议配置 `phaseSignalMap` |
### 5. Good / Base / Bad Cases
- GoodOBU 和感知目标连续推送,页面只显示对应车辆的实际行驶轨迹。
- Base服务无数据页面没有车辆或线路。
- Bad将旧路线 JSON 或 `native-preview-traffic-simulation` 用作回退展示。
### 6. Tests Required
- `npm run test:v2x-cesium-preview`:校验灯色字典(含 `2`/`3` 不再是黄/绿的回归断言)、
宽松解析、几何相位绑定(对真实 `traffic-signals.json` 须达成精确一对一)、
同相位多进口道并集、车辆超时与槽位复用、心跳间隔与重连重放订阅帧、bounds 报文。
- `npm run test:preview-assets`:断言三条 WebSocket 存在runtime 不调用
`addVehicleCruises`、不显示 simulation 诊断,且预览层**不再自带** `lampColorName`
`Number(status) === 3` 判断;断言实时车辆最小像素尺寸和诊断可见数量读数存在。
- 对目标区域运行 `npm run build:area -- --config config/areas/<area>.json --stages preview`
检查 descriptor 中 `routeName` 为空、不存在 traffic-simulation 文件,
`vehicleModelNames` 已写出(实时车辆渲染需要)。
### 7. Wrong vs Correct
错误:接口不可用时恢复构造路线。
```js
const cruise = addVehicleCruises(viewer, routeData, signalData, start);
```
正确:保持空车辆层,等待真实流。
```js
const cruise = createLiveVehicleState();
```
## 本地预览必须走 HTTP
```bash

View File

@@ -0,0 +1,4 @@
{"file": ".trellis/spec/preview/index.md", "reason": "校验改动未违反预览层约束零依赖、无构建步骤、浏览器原生语法、IIFE。"}
{"file": "docs/v2x-cesium-preview.md", "reason": "校验文档与实现一致:接口清单、坐标契约、配置项、回滚说明。"}
{"file": ".trellis/tasks/08-24-v2x-realtime-cross-fidelity/prd.md", "reason": "AC1-AC10 是验收依据,逐条核对。"}
{"file": ".trellis/tasks/08-24-v2x-realtime-cross-fidelity/design.md", "reason": "核对灯色字典、相位映射算法、心跳与订阅帧是否与源项目逐项对齐。"}

View File

@@ -0,0 +1,235 @@
# Design — 还原 V2X 实时路口信号灯与车辆展示
## 1. 现状与根因
`scripts/lib/v2x-cesium-overlay.js`418 行,单个 IIFE承担了登录、REST、三个 WS、坐标转换、
实体渲染的全部职责。REST 路径与登录契约与源项目一致,失真集中在数据处理与渲染层。
逐项根因(对应 PRD 的 R1R10
| # | 现象 | 根因位置 |
| --- | --- | --- |
| R1 | 连接几十秒后静默中断 | `openSocket()` 无心跳、无重连 |
| R2 | 灯永远红 | `lampColor()``2`/`3`;真实码是 `21/22/23` |
| R3 | 原生信号灯模型从不亮 | `signalByPhase` 回退 `signal.id`,与 `phaseNo` 永不相等 |
| R4 | 同相位只亮一条进口道 | `state.linkPhases``Map<phase, entity>`,后写覆盖 |
| R5 | 无倒计时 | `updateSignalPhases()` 丢弃 `countDown` |
| R6 | OBU 可能一辆车都没有 | `connectObuSocket()``onOpen``null`,未发订阅帧 |
| R7 | 幽灵车堆积 | `state.vehicles` 只增不删,未用推送 `interval` |
| R8 | 车型错配 | 按数组下标取模型,非 `${type}${subType}` 语义 |
| R9 | 推送可能整条被丢 | `JSON.parse` + 静默 `catch`,源项目用 `saferEval` |
| R10 | 多余请求 | `FlowTravelRatio/queryListWeek` 不属实时路口 |
## 2. 边界与不变量
保持不变:
- overlay 仍是**单文件零依赖 IIFE**,由 `build-area.js:561` 原样拷入产物。不新增文件,避免改动
`area-preview.js` 的 HTML 装配与包契约。
- 纯逻辑继续通过 `createV2xCesiumOverlay.utils` 导出,供 `scripts/test-v2x-cesium-overlay.js`
`vm.runInNewContext` 沙箱直接测试(无 DOM、无 Cesium
- 坐标契约不变GCJ-02 → WGS84 只在建实体前转一次。
- 令牌只进 `sessionStorage`,页面始终从登录门进入。
新增的可测试纯函数一律**不触碰 `window` / `Cesium` / `document`**,副作用留在薄薄的适配层。
## 3. 模块内部分层
文件内按四层组织,自下而上:
```
L1 纯工具 gcj02ToWgs84 / joinUrl / md5 / toWebSocketUrl / getAngle / haversine
L2 纯解析 lampColorName / parseLoosePayload / normalizeObuVehicle /
normalizeTargetVehicles / normalizeSignalLamps
L3 纯状态机 createVehicleRegistry / createPhaseBinding / buildPhaseSignalMap
L4 副作用层 createSocket(心跳/重连) / 实体渲染 / UI 面板
```
L1L3 全部导出到 `.utils`,测试只打 L1L3。
## 4. 关键设计决策
### 4.1 灯色状态字典R2
直接采用源项目 `HologramCross/components/utils.ts` 的字典,语义化返回而非直接返回颜色,
使 overlay 与 `cesium-preview.js` 共用同一套判定:
```
11 -> "off" 灭灯
21 -> "red"
22 -> "yellow"
23 -> "green"
31 -> "other"
其他 -> "other" // 不再落到 red
```
`lampColorName(status)` 放在 overlay 的 L2 并导出。`cesium-preview.js` 中重复的
`lampColorName()` 删除,改为消费 overlay 通过 `setSignalState` 回调传入的**已归一化**灯态
`{nodeKeys, color, countDown}`),避免两处字典漂移。
> 取舍:也可以把字典抽到共享文件让两边 require但 overlay 必须是浏览器端零依赖 IIFE
> `cesium-preview.js` 同样是浏览器端脚本两者无模块系统。让数据流单向overlay → preview
> 比共享常量更简单,也消除了双字典。
### 4.2 相位 → 原生信号灯映射R3本任务最难的一处
`traffic-signals.json` 的信号灯项只有 `phaseGroup: 0`**不含 V2X `phaseNo`**,两侧没有公共 ID。
唯一可靠的连接是**几何**。
源项目的做法给了线索:`CrossTrafficLights3D.vue:96-104` 对每条 link 取 `geom.coordinates`
最后两点,末点即停止线位置,两点连线的方位角即进口道航向。原生信号灯项恰好也有
`stopLongitude/stopLatitude``headingDegrees`
因此定义纯函数:
```
buildPhaseSignalMap(links, nativeSignals, options) -> {
byPhase: Map<phaseNo, string[]>, // nodeKey 列表
bound: number,
unbound: Array<{phaseNo, reason}>,
diagnostics: Array<{linkId, matchedNodeKeys, distanceMeters, headingDeltaDegrees}>
}
```
算法:
1. 对每条 link解析 `geom`,取末两点,**先 GCJ-02 → WGS84**,再算 `stopPoint`
`approachHeading = getAngle(prePoint, lastPoint)`
2. 对每个原生 signal`(stopLongitude, stopLatitude)` 与**行车方向**。
行车方向的语义由生成侧 `scripts/lib/traffic-signals.js` 定死,不是猜的:
- `:54` `headingDegrees = atan2(axis)`,而 `axis = roadAxis(stopLineCenter → intersectionCenter)`
即**停止线指向路口中心的行车方向**。
- `:82` `mast_heading_deg = heading - 90`
- `:83` `face_heading_deg = heading + 180`
运行时 `traffic-signals.json``headingDegrees === mastHeadingDegrees`(实测 7/7 相等,
`150.658`),即存的是 **mast 值**`faceHeadingDegrees` 比它小 90`60.658`)。因此:
```
travelHeading = (faceHeadingDegrees + 180) % 360 // 首选
= (mastHeadingDegrees + 90) % 360 // face 缺失时的等价回退
```
实测对全部 7 个信号自洽。
3. 候选条件:`haversine(stopPoint, signalStop) <= maxDistanceMeters`
且 `angleDelta(approachHeading, travelHeading) <= maxHeadingDeltaDegrees`。
4. 一条 link **可匹配多个** signal同一进口道的多个灯头满足 R4 的一半。
5. link 的 `phaseList` 中每个 `phase` → 该 link 匹配到的全部 nodeKey**并集累加**
`Map<phaseNo, Set<nodeKey>>`),满足 R4 的另一半(同相位多进口道)。
6. 默认容差:`maxDistanceMeters = 30`、`maxHeadingDeltaDegrees = 45`。
7. 未匹配上的相位进入 `unbound`面板显示「N 已绑定 / M 未绑定」。
**实现期修正**:设计初稿认为朝向语义不可判定,打算用「三字段 ± 180 的候选集,任一命中即可」。
实测该方案在凤树二路路口产生**严重误匹配** —— 一条 link 同时命中 3 条不同 `approachId` 的进口道
(距离 1.57 / 13.01 / 17.56 m角差 3.8 / 7.8 / 5.4°),因为 ±180 × 三字段几乎放行任意 90° 朝向。
改为上述单一确定式后7 条 link 对 7 个信号达成 **7/7 精确一对一**,误匹配为 0。
候选集方案已废弃。
**配置覆盖**`v2xPreview.phaseSignalMap = { "<phaseNo>": ["<nodeKey>", …] }` 存在时,
该 phaseNo 直接用配置值,跳过几何匹配。这是几何匹配失败时的逃生舱,也让映射可人工固化。
> 取舍:不用 `approachId` / `sourceWayId` 匹配,因为那是 OSM 侧标识V2X link 不携带;
> 不用最近邻唯一指派(匈牙利算法),因为一进口道多灯头是正常情况,强制一对一会漏灯。
> 容差可配是因为不同路口的停止线标注精度差异较大。
### 4.3 WebSocket 连接层R1、R6
新增 `createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect})` 副作用封装:
- 心跳:`setInterval(() => send('{"heartBeat":"ping"}'), 30000)`,与源项目一致。
- 重连:指数退避 `1s, 2s, 4s, 8s`,上限 `8s`**重连成功后重新执行 `onOpen`**
确保订阅帧(`junctionId` / `deviceId` / `bounds`)被重放。这是源项目 `onConnected` 的语义。
- `close()` 主动关闭时不触发重连;`dispose()` 清心跳与退避计时器。
- 计时器通过参数注入(`setIntervalFn` / `setTimeoutFn`),使重连与心跳逻辑可在测试中用假时钟驱动。
订阅帧内容严格对齐源项目:
| socket | 连接后首帧 | 来源 |
| --- | --- | --- |
| signal | `{"junctionId":"<crossCode>"}` | `CrossTrafficLights3D.vue:129` |
| obu | `bounds \|\| ""`dashboard 传 `null` → 空串) | `useObuCars.ts:31` |
| target | `{"deviceId":"<ids>"}` 或 `{"deviceId":null}` | `useTargetCars.ts:50-53` |
OBU 的 bounds源项目取 AMap 视野四角。本项目用 Cesium `camera.computeViewRectangle()`
换算成同格式 `lng,lat;lng,lat;lng,lat;lng,lat`**WGS84 → GCJ-02 反向转换**,因为服务端按 GCJ-02
过滤),在 `camera.moveEnd` 时节流发送 `{"bounds": "<…>"}`。视野不可用时退回空串,等价于不过滤。
> 取舍Cesium 是倾斜视角,`computeViewRectangle()` 在极端俯仰下可能返回 `undefined`
> 此时退回空串而非跳过发送,保证仍能收到全量推送——宁可多收也不要空场景。
### 4.4 车辆注册表R7、R8
纯状态机 `createVehicleRegistry({now})`
```
registry.ingest(vehicles, interval) -> {added, updated, hidden}
registry.sweep(nowMs) -> hiddenKeys
registry.list() -> VehicleRecord[]
```
- 复刻源项目:`interval = Number(payload.interval) === 0 ? 500 : Number(payload.interval)`。
- `sweep()``Math.abs(now - timeStamp) >= interval * 1.5` 时 `visible = false`。
- 槽位复用:新车优先占用同模型的 `visible === false` 记录(源项目 `useTargetCars.ts:110`
减少 Cesium 实体的增删抖动。
- `now` 注入,测试用假时钟推进。
- socket `onClose` 时 `registry.clear()`,对齐源项目 `onDisconnected: cars.value = []`。
渲染适配层据 `visible` 切 `entity.show`**不删实体**,与槽位复用配套。
模型选择R8OBU 的数据 hook 虽标记 `car_obu.glb`,但最终的 `CrossCars/index.vue` 模板固定
传入 `11.glb`,因此 `modelNameFor(vehicle)` 对 OBU 返回 `11.glb`;目标车辆返回
`${type}${subType}.glb`。预览阶段把 dashboard 的 `11/12/13/14/15/16/17/31/221/222.glb`
复制到 `_preview/v2x-vehicles/`,并在 `vehicleModelNames` 中按文件名匹配;未知车型回退 `11.glb`
并累加 `missingModels` 计数供面板展示。
平滑:用 `Cesium.SampledPositionProperty` 按 `duration` 插值,替代当前的直接赋值跳变。
### 4.5 宽松解析R9
源项目用 `saferEval`。浏览器端零依赖不能引入该库,也不应引入 `eval`。改为
`parseLoosePayload(text)`:先 `JSON.parse`;失败则做一次受限规范化(单引号→双引号、
裸键补引号、去尾逗号、`NaN`/`Infinity` → `null`)后重试;仍失败返回 `null` 并
`state.parseFailures += 1`面板显示失败计数R9 要求可见)。
> 取舍:不用 `new Function` 还原 `saferEval` 的完整语义——那等于在预览页开一个任意代码执行面。
> 受限规范化覆盖实际会遇到的非严格 JSON 形态,且失败可观测,比静默丢弃安全得多。
### 4.6 移除范围外请求R10
`loadLiveData()` 的 `Promise.allSettled` 去掉 `FlowTravelRatio/queryListWeek`
`state.metrics` 及面板中的 `flow metrics` 文案一并移除。
## 5. 数据流
```
登录 → resolveCrossCode()
→ queryCrossLinkInfo / queryPoles / findDeviceByCrossCode / crossDeviceConfig
→ buildPhaseSignalMap(links, nativeSignals) [纯]
→ connectLiveSockets()
signal → normalizeSignalLamps → lampColorName → setSignalState({nodeKeys,color,countDown})
→ cesium-preview 点亮原生灯头 + 倒计时
obu → parseLoosePayload → normalizeObuVehicle ┐
target → parseLoosePayload → normalizeTargetVehicles ┴→ registry.ingest → 渲染适配
registry.sweep → entity.show
```
## 6. 兼容性与回滚
- `v2xPreview.enabled=false` 时 `createV2xCesiumOverlay()` 仍返回 `null`,生成页不含面板,
包产物字节不变AC10
- 原生信号灯在**无 V2X 灯态时**的行为保持现状(`hideUnconfirmedSignalAssets` 语义不变),
只有收到实时灯态才点亮,不会退回模拟相位。
- 回滚点:本任务对 `cesium-preview.js` 的改动限于删除重复的 `lampColorName` 与调整
`setSignalState` 入参形状,可单独 revert 而不影响 overlay。
- 新增 `phaseSignalMap`、`maxDistanceMeters`、`maxHeadingDeltaDegrees` 均为可选配置,
缺省即当前默认行为。
## 7. 风险
| 风险 | 影响 | 缓解 |
| --- | --- | --- |
| 几何匹配在实际路口全部落空 | 灯仍不亮 | 面板显性报「0 已绑定」;`phaseSignalMap` 配置覆盖兜底 |
| `computeViewRectangle()` 返回 undefined | OBU 过滤异常 | 退回空串=不过滤 |
| 宽松解析未覆盖真实非法形态 | 丢消息 | `parseFailures` 计数可见,便于按真实样本补规则 |
| 服务端心跳格式与源项目不符 | 仍被断开 | 心跳报文与间隔取自源项目源码AC1 用模拟服务端验证 |
| 真实推送 `interval` 缺失 | 车辆过早隐藏 | 缺失时回退 500ms与源项目一致 |

View File

@@ -0,0 +1,4 @@
{"file": ".trellis/spec/preview/index.md", "reason": "预览层契约:无构建步骤、零依赖 IIFE、参数经 window.OSM_ASSET_PREVIEW_CONFIG 注入。overlay 与 cesium-preview.js 的所有改动都受这些约束。"}
{"file": ".trellis/spec/pipeline/index.md", "reason": "确认 build-area.js 如何把 v2x-cesium-overlay.js 拷入产物,避免误增文件破坏包契约。"}
{"file": ".trellis/spec/config/index.md", "reason": "新增可选配置 v2xPreview.phaseSignalMap / maxDistanceMeters / maxHeadingDeltaDegrees 需符合区域配置约定。"}
{"file": "docs/v2x-cesium-preview.md", "reason": "现有 V2X 预览文档:配置项、坐标契约、代理要求、回滚方式。移除 FlowTravelRatio 与新增配置后需同步更新。"}

View File

@@ -0,0 +1,171 @@
# Implement — 还原 V2X 实时路口信号灯与车辆展示
## 参照实现(只读)
`/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/`
改动前先读对应源文件,不要凭记忆写契约。
## 改动文件
| 文件 | 性质 |
| --- | --- |
| `scripts/lib/v2x-cesium-overlay.js` | 主要改动L1L4 分层重写 |
| `scripts/lib/cesium-preview.js` | 删除重复 `lampColorName`,调整 `setSignalState` 入参 |
| `scripts/test-v2x-cesium-overlay.js` | 扩充单测,覆盖 AC2AC7 |
| `.trellis/tasks/.../prd.md` `design.md` | 如实现中发现偏差需回写 |
不改:`build-area.js``area-preview.js`、包产物生成逻辑。
## 执行顺序
每步结束跑一次 `npm run test:v2x-cesium-preview`,保持绿色再进下一步。
### 步骤 1 — L2 灯色字典 + 宽松解析R2、R9
1. 在 overlay 加 `LAMP_STATUS``11/21/22/23/31`)与 `lampColorName(status)`,未知返回 `"other"`
2. 删除旧 `lampColor()``Number(status)===3 / ===2` 的判断。
3.`parseLoosePayload(text)``JSON.parse` → 受限规范化重试 → `null`
4. 心跳报文 `pong` 短路判定保留(与源项目一致)。
5. 二者导出到 `.utils`
验证:`node scripts/test-v2x-cesium-overlay.js`
- `lampColorName(21)==="red"`, `(22)==="yellow"`, `(23)==="green"`, `(11)==="off"`, `(31)==="other"`
- **`lampColorName(2)!=="yellow"``lampColorName(3)!=="green"`**防回归AC2
- `parseLoosePayload("{'a':1,}")` 解析成功;`parseLoosePayload("{bad")===null`AC7
**审查点 A**:灯色字典必须与 `HologramCross/components/utils.ts`
`lightStatusColorDict` 逐项一致,不得增删码值。
### 步骤 2 — L1 几何工具(支撑 R3
1.`getAngle(start, end)`:直接对齐源项目 `utils.ts` 的实现(返回 0360
2.`haversineMeters(a, b)``angleDeltaDegrees(a, b)`(后者处理 350° vs 10° 的环绕)。
3. 导出到 `.utils`
验证:正北 `getAngle([0,0],[0,1])≈0`;正东 `≈90`
`angleDeltaDegrees(350,10)===20``haversineMeters` 对已知两点误差 < 0.5m。
### 步骤 3 — 相位映射R3、R4
1. 实现 `buildPhaseSignalMap(links, nativeSignals, options)`,返回
`{byPhase, bound, unbound, diagnostics}`,算法见 `design.md` §4.2。
2. link 末两点**先 GCJ-02→WGS84 再算角度与距离**(原生信号是 WGS84
3. 一 link 匹配多 signal`Map<phaseNo, Set<nodeKey>>` 并集累加。
4. 支持 `options.phaseSignalMap` 配置覆盖,命中则跳过几何匹配。
5. 默认 `maxDistanceMeters=30``maxHeadingDeltaDegrees=45`
6. 导出到 `.utils`
验证AC3、AC4
-`outputs/fengshu-er-road/package/runtime/traffic-signals.json` 的真实 7 条信号做夹具,
配一组构造的 link末点取自 `stopLongitude/stopLatitude` 反算 GCJ-02、
航向取自 `headingDegrees`),断言 `bound > 0`
- 构造两条不同 link 共用同一 `phaseNo`,断言 `byPhase.get(phaseNo)` 含**两条 link 的全部 nodeKey**。
- 构造一条远离所有信号的 link断言其相位进入 `unbound` 且带 `reason`
- 配置覆盖生效:`phaseSignalMap` 指定的 phaseNo 取配置值。
**审查点 B**:这是全任务最易做错的一步。合入前必须用真实 `traffic-signals.json` 跑出
非零绑定数,若为 0 则说明容差或坐标方向反了,**不得靠放大容差蒙混过关**——先打印
`diagnostics` 的实际距离/角差,确认量级合理(距离应是米级而非千米级)。
### 步骤 4 — 车辆注册表R7、R8
1. 实现 `createVehicleRegistry({now})`,含 `ingest / sweep / list / clear`
2. `interval` 取自推送体,`0` 或缺失回退 `500`
3. `sweep``|now - timeStamp| >= interval*1.5``visible=false`
4. 槽位复用:优先占用同 `model` 的不可见记录。
5. `modelNameFor(vehicle)`OBU → dashboard 最终模板使用的 `11.glb`target → `${type}${subType}.glb`。把 dashboard 车辆 GLB 复制到预览包。
6. `normalizeTargetVehicles` 补上源项目的 `type==1 && isNil(subType) → subType=1`
7. 导出 `createVehicleRegistry``modelNameFor`
验证AC5
- 注入一次推送 → `list()` 一条 `visible=true`
- 假时钟推进 `interval*1.5``sweep()` → 该车 `visible=false`
- 再注入同 key → 复用原槽位,`list().length` 不变。
- `clear()` 后为空。
- `modelNameFor({kind:"obu"})==="11.glb"``{kind:"target",type:1,subType:2}``"12.glb"`
### 步骤 5 — WebSocket 连接层R1、R6
1. 实现 `createSocket({url, onOpen, onMessage, onClose, heartbeatMs, reconnect, socketFactory, setIntervalFn, setTimeoutFn})`
2. 心跳 `{"heartBeat":"ping"}` / 30000ms。
3. 退避重连 `1s→2s→4s→8s` 封顶,**重连成功重放 `onOpen`**。
4. 主动 `close()` 不重连;`dispose()` 清所有计时器。
5. 三个 socket 的订阅帧按 `design.md` §4.3 的表格实现。
6. OBU bounds`camera.computeViewRectangle()` → WGS84→GCJ-02 → `"lng,lat;…"` 四角,
`camera.moveEnd` 节流发送;不可用时发空串。
7. 导出 `createSocket``buildBoundsMessage`
验证AC1、AC6
- 假 socket + 假时钟:推进 30s 断言收到一帧 `{"heartBeat":"ping"}`;推进 90s 断言三帧。
- 模拟 `onclose` 后推进退避时间,断言重新建连**且订阅帧被重放**。
- signal 首帧 `{"junctionId":"<code>"}`target 无配置首帧 `{"deviceId":null}`
有配置 `{"deviceId":"1,2"}`obu 首帧为空串。
- `dispose()` 后推进时钟不再产生任何发送。
**审查点 C**:心跳报文与间隔必须逐字节对齐源项目
`useObuCars.ts:28-31``useTargetCars.ts:27-30``CrossTrafficLights3D.vue:122-130`)。
### 步骤 6 — 接线与渲染适配R4、R5、R10
1. `loadLiveData()` 移除 `FlowTravelRatio/queryListWeek`,删 `state.metrics` 与面板 flow 文案。
2.`buildPhaseSignalMap` 结果替换 `state.linkPhases``Map<phase, entity>` 整体删除)。
3. `updateSignalPhases` → 归一化为 `{nodeKeys, color, countDown}` 列表后调
`context.setSignalState(...)`,同时给 link polyline 上色(同相位多 link 全部上色)。
4. 车辆渲染改为消费 `registry``visible``entity.show`,位置用
`SampledPositionProperty``duration` 插值。
5. 面板状态行补:已绑定/未绑定相位数、缺失模型数、解析失败数。
6. `dispose()` 清 socket、计时器、实体、registry。
### 步骤 7 — `cesium-preview.js` 对接R2、R5
1. 删除 `cesium-preview.js` 中的 `lampColorName()`(与 overlay 重复且映射错误)。
2. `createLiveTrafficSignals().update(lamps)` 改签名为
`update(entries)``entries = [{nodeKeys:string[], color:"red"|"yellow"|"green"|"off"|"other", countDown:number}]`
3.`nodeKeys` 直接点亮 `TrafficSignalDynamic_<nodeKey>_<color>` 节点,
删除失效的 `signalByPhase` 回退逻辑。
4. `countDown` 驱动 `category === "countdown"` 资产显示。
5. `color === "off" | "other"` 时三色节点全灭。
验证:`npm run test:preview-assets`
### 步骤 8 — 全量校验
```bash
npm run test:v2x-cesium-preview
npm run test:v2x-preview-server
npm run test:preview-assets
npm run test:traffic-signals
npm run test:native-preview-traffic
npm run test:package-contract
```
AC10 校验:将区域配置 `v2xPreview.enabled``false` 重新生成,
确认生成页无 V2X 面板且 `package/` 下产物字节不变。
## 人工验收AC9需真实上游
```bash
V2X_UPSTREAM=http://<host>:<port> \
npm run serve:v2x-preview -- --root outputs/fengshu-er-road --host 0.0.0.0 --port 7862
```
登录后逐项确认:
- [ ] 信号灯随相位变色,颜色与 dashboard 实时路口一致
- [ ] 倒计时数字走动
- [ ] 车辆出现、移动平滑、离开视野后消失(无幽灵车堆积)
- [ ] 连接持续 5 分钟以上不掉线
- [ ] 车辆与路口位置无明显偏移(坐标未二次转换)
- [ ] 面板绑定数、失败数符合预期
## 回滚点
- 步骤 7 独立可 revert只动 `cesium-preview.js`)。
- 步骤 16 全在 overlay 单文件内,`git checkout scripts/lib/v2x-cesium-overlay.js` 即回到当前状态。
- `v2xPreview.enabled=false` 是运行时总开关。
## 不做(留待后续任务)
- AMap 底图 / 矢量瓦片图层
- `CrossDevices/Model3D.vue` 3D 杆件模型(设备维持点位)
- `useV2XEvents` 事件气泡

View File

@@ -0,0 +1,99 @@
# 还原 V2X 实时路口信号灯与车辆展示
## Goal
`v2x_web` dashboard「实时路口」(`HologramCross`) 的实时数据行为完整搬到本项目的 Cesium 预览中,使
`outputs/<area>/…-cesium-preview.html` 在登录后能持续、正确地显示 **信号灯相位灯色与倒计时**
**OBU / 目标识别车辆**
当前实现Codex 移植)接口地址是对的,但数据处理与渲染层基本失真,实际效果是:灯永远红、车可能一辆都不出现、
出现后也不消失、连接几十秒后被服务端断开。本任务修复这条链路。
## Source of Truth
参照实现(只读,不修改):
`/Users/que01/Project/v2x_web/applications/dashboard/src/views/dashboard/components/Content/Map/HologramCross/`
- `components/CrossTrafficLights3D.vue` — 信号灯link 相位、WS 订阅、灯色/倒计时
- `components/utils.ts` — 灯色状态字典、航向角计算
- `components/CrossCars/hooks/useObuCars.ts` — OBU 车辆
- `components/CrossCars/hooks/useTargetCars.ts` — 目标识别车辆
- `components/CrossCars/type.ts` — 车辆数据结构
## Requirements
### R1 WebSocket 连接层对齐
- 三个 socket`signal` / `obuPosition` / `targetPosition`)均按源项目发送心跳
`{"heartBeat":"ping"}`,间隔 30000ms。
- 断线自动重连重连后重新发送各自的订阅消息signal 发 `junctionId`target 发 `deviceId`obu 发 bounds
- 页面 `visibilitychange` 回到可见时行为不得导致重复连接堆积;`dispose()` 必须清掉心跳与重连计时器。
### R2 信号灯灯色映射修正
- 采用源项目 `utils.ts` 的状态字典:`11=灭灯 21=红 22=黄 23=绿 31=其他`
- 现有 `lampColor()`overlay`lampColorName()``cesium-preview.js`)中基于 `2`/`3` 的判断必须移除。
- 未知状态码按「其他/灭灯」处理,不得静默落到红色。
### R3 相位到原生信号灯的映射
- `traffic-signals.json` 中不存在 V2X `phaseNo`(只有 `phaseGroup: 0`),当前回退到 `signal.id` 导致永不匹配。
必须建立一条真实可用的 V2X `phaseNo` → 原生信号灯头的映射。
- 映射结果必须可诊断面板要能显示「N 个相位已绑定 / M 个未绑定」。
- 允许区域配置提供显式覆盖映射。
### R4 一个相位可点亮多条进口道
- 同一 `phaseNo` 对应的所有 link / 信号灯头都要被点亮,当前 `Map<phase, entity>` 的覆盖行为必须消除。
### R5 倒计时
- 使用推送中的 `countDown` 字段(源项目 `phaseValue['time'] = signal.countDown`),驱动预览中已有的
countdown 资产或等效显示。
### R6 车辆订阅正确建立
- OBU socket 连接后必须发送订阅消息(源项目 `send(bounds || '')`),相机视野变化时发送
`{"bounds": "<lng,lat;…>"}`
- target socket 连接后发送 `{"deviceId": "<ids joined by comma>"}`,无配置时发送 `{"deviceId": null}`
### R7 车辆生命周期
- 按源项目实现超时隐藏:超过 `interval * 1.5` 未更新的车辆隐藏并可复用槽位;`interval` 取自推送体,
`0` 时回退 `500`
- socket 断开时清空车辆列表。
- 车辆位置更新使用推送间隔做平滑插值,避免逐帧跳变。
### R8 车辆模型命名与资产对齐
- OBU 数据层标记为 `car_obu.glb`,但 dashboard 的实际 `CrossCars` 模板固定渲染 `11.glb`;预览必须遵从该最终渲染契约。目标识别车辆使用 `${type}${subType}.glb`
- 预览包必须携带 dashboard 实际使用的车辆模型;包内缺少未知车型时回退到 `11.glb`,并在面板提示,不得静默显示错误车型。
### R9 推送体解析健壮性
- 源项目对两个车辆 socket 使用 `saferEval` 而非 `JSON.parse`,说明载荷不保证严格 JSON。
- 解析必须容忍非严格 JSON且解析失败要计数并在面板可见不得整条静默丢弃。
### R10 范围纠正
- `/facilities/api/FlowTravelRatio/queryListWeek`(周流量比)不属于「实时路口」,从实时加载链路移除。
## Non-Goals
- 不移植 AMap 底图 / 矢量瓦片图层(`linkVectorHdMapTileUrl`)。
- 不移植 `CrossDevices/Model3D.vue` 的 3D 杆件模型;设备维持点位显示(可后续独立任务)。
- 不移植 `useV2XEvents` 的 V2X 事件气泡。
- 不改变原生包产物GLB / metadata / traffic-signals.json 的生成逻辑)。
## Constraints
- 不得提交上游地址、账号、令牌、AMap key`v2xPreview` 配置保持非敏感。
- 令牌只存 `sessionStorage`,页面始终从登录门进入。
- 坐标契约不变V2X 数据 GCJ-02进 Cesium 前转一次 WGS84原生数据不得二次转换。
- `v2xPreview.enabled` 默认 `false`,关闭时生成页不含 V2X 面板。
- 生成的预览为静态页无构建步骤overlay 保持零依赖 IIFE。
## Acceptance Criteria
- [ ] AC1 三个 socket 均可见 30s 心跳;模拟服务端在 60s+ 后连接仍存活。
- [ ] AC2 单测覆盖灯色映射:`21→红 22→黄 23→绿 11→灭 31→其他`,且 `2`/`3` 不再被当作黄/绿。
- [ ] AC3 单测覆盖相位映射:给定 V2X link 相位与 `traffic-signals.json`,输出的绑定数 > 0
未绑定项被明确列出。
- [ ] AC4 单测覆盖同相位多进口道:一个 `phaseNo` 对应 2 条 link 时,两条都被点亮。
- [ ] AC5 单测覆盖车辆超时:注入两次推送并推进时间超过 `interval*1.5`,车辆转为不可见。
- [ ] AC6 单测覆盖 OBU/target 订阅消息:连接后发出的首帧内容与源项目一致。
- [ ] AC7 单测覆盖非严格 JSON 推送体能被解析,且失败计数可读。
- [ ] AC8 `npm run test:v2x-cesium-preview``test:v2x-preview-server``test:preview-assets` 全绿。
- [ ] AC9 真机(`npm run serve:v2x-preview` 接真实上游)登录后可见:信号灯随相位变色并显示倒计时、
车辆出现并移动、离开后消失。此项由开发者人工确认。
- [ ] AC10 `v2xPreview.enabled=false` 重新生成后,页面不含 V2X 面板,包产物字节不变。

View File

@@ -0,0 +1,26 @@
{
"id": "v2x-realtime-cross-fidelity",
"name": "v2x-realtime-cross-fidelity",
"title": "还原 V2X 实时路口信号灯与车辆展示",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-24",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,96 @@
# 后续计划drawtonomy 扩展 PoC
前置任务:`08-25-road-compiler-extraction` 的 Phase 0-3 完成并完成验收。
评估依据:父任务归档前的 `design.md` §4 决策记录 D1 / D2 / D3。
## Goal
在**编译器仓库内**实现一个 drawtonomy 扩展:把编译产物注入其浏览器编辑器,
回读快照后走其开源导出器产出 OpenDRIVE / Lanelet2。
定位是 **PoC不进关键路径**。目的是验证「编译器当 scene generator +
drawtonomy 当编辑器与工业格式后端」这条链路是否值得投入。
## 前提认知(已核实)
必须先明确,否则会做错方向:
- **drawtonomy 编辑器闭源**,克隆仓库只有 SDK / dev-server / mcp-server。
README 卖点里的 topology-aware lanes、lane tool、intersection templates、
Map→lanes 全部不在开源代码内。
- **它不做 raw OSM 推导**。`exporter/osmParser.ts` 是 Lanelet2 解析器
(车道左右边界已显式)。整个 exporter 目录 grep `highway` 命中 1 次且是
车道类型字符串。→ **不能替代本编译器**,只能做下游。
- **junctionTools 不能做成扩展**:无 canvas/overlay 能力、无工具注册、
**无任何 change 事件推送**`ExtensionClient.handleMessage` 入站只有
`ext:init` / 5 个 `*-response` / `ext:error`,读取全靠轮询)。
→ junction 编辑留在自有 workbench。
- **`drawtonomy-dev-server` 是 drawtonomy.com 的缓存代理**TTL 1 小时),
非自托管。宿主协议会漂manifest 有 `minHostVersion`)。→ 不进关键路径。
## Requirements
### R4.1 扩展:编译产物 → 编辑器
- manifest capabilities`shapes:write``ui:panel``snapshot:read``ui:notify`
- 面板调编译器的本地 HTTP 服务(`road-workbench` 已经是一个 HTTP 服务)取产物
- 车道边界 → `createLaneWithBoundaries(leftPoints, rightPoints, opts)``addShapes()`
- 坐标转换:本项目 WGS84/ENU→ drawtonomy 画布像素。
沿用其 `drawtonomy_origin_lat/lon` + `latLonToCanvas` 约定
- ⚠️ **借它的结构,不借它的坐标系**:其 `BaseShape.x/y` 是画布像素
`z` 注释明确写 "world units — NOT canvas pixels like x/y")。
编译器内部坐标系不得因此改变
### R4.2 回读 → 工业格式导出
- `requestSnapshot()` 取回快照
- 本地跑其开源 `exportToOpenDrive` / `lanelet2`Apache-2.0
- 产出 `.xodr` / `.osm`(Lanelet2),用其 `validateOpenDrive` 自校验
- Lanelet2 是 Autoware 的输入格式 —— 对本项目 V2X 方向有实际价值
### R4.3 许可与依赖合规
- drawtonomy SDK 是 Apache-2.0:借用代码需保留 NOTICE / 署名
- 若依赖 drawtonomy.com 托管服务需查其服务条款app 非 Apache-2.0
- ESM/CJS 互操作SDK 是 ESM+TS编译器是 CJS。
本阶段可**局部**引入构建步骤,但仅限扩展目录,不得污染编译器核心
(父任务 C2 的边界)
### R4.4 归属
扩展代码放**编译器仓库**,不放宿主 —— 符合父任务「独立维护」目标。
## Acceptance Criteria
- [ ] AC4.1 扩展能把一个区域的编译产物注入 drawtonomy 编辑器并正确显示车道
- [ ] AC4.2 编辑器内手改后回读快照,能导出通过 `validateOpenDrive``.xodr`
- [ ] AC4.3 能导出 Lanelet2 `.osm`
- [ ] AC4.4 编译器核心未引入构建步骤(构建仅限扩展目录)
- [ ] AC4.5 编译器内部坐标系未因适配画布像素而改变
- [ ] AC4.6 Apache-2.0 署名 / NOTICE 已按要求保留
- [ ] AC4.7 结论记录:这条链路是否值得继续投入,写入任务 `research/`
## 启动条件
- 道路编译器独立化父任务Phase 0-3已完成验收。
- 编译器仓库、稳定 CLI 与版本化输出契约已可用。
- 此任务不阻塞道路编译器抽离或渲染分离的交付。
## 明确不做
- junctionTools 做成扩展API 不支持,见前提认知)
- 把 drawtonomy 放进生产构建链路
- 用 drawtonomy 替代编译器D1 已否决)
- 自建通用白板编辑器
## 后续可能(不属本阶段)
父任务 design §4 的 D3 列了三项值得从 drawtonomy 借用的东西,
它们与本阶段独立,应各自开任务:
- `odrGeometryFit.ts` 的拟合器 → 替掉手调的
`approachWidthMultiplier=1.45` / `coreRadiusMeters=28`,改为对 `referenceFile`
拟合、残差作质量指标
- validator 的 mutation-proven 方法 → 给现有 31 条诊断规则建触发证明
- validator 的分层命名空间(`xml.*` / `ref.*` / `junction.*` / `geom.*`
→ 替代当前平铺的 31 条规则

View File

@@ -0,0 +1,28 @@
{
"id": "rc-p4-drawtonomy-ext",
"name": "rc-p4-drawtonomy-ext",
"title": "后续计划drawtonomy 扩展 PoC",
"description": "道路编译器独立化完成后,评估在编译器仓库内实现 drawtonomy 扩展:编译产物注入编辑器,快照回读后走开源 OpenDRIVE/Lanelet2 导出",
"status": "planning",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P3",
"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": null,
"relatedFiles": [],
"notes": "不属于道路编译器独立化父任务的完成条件。父任务验收完成后,将此任务作为下一项候选任务提示。",
"meta": {
"follow_up_after": "08-25-road-compiler-extraction"
}
}

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,34 @@
{
"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",
"08-26-rc-p5-artifact-consumption"
],
"parent": null,
"relatedFiles": [],
"notes": "P3 已完成。P5 artifact-only parent 已建立P5A ZIP export 已在 road-compiler v0.3.0 完成P5B 宿主 ZIP import 待实施。",
"meta": {
"next_task": "08-25-rc-p4-drawtonomy-ext"
}
}

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/blender/testing.md","reason":"P3 scene digest parity 是消费迁移的回归门槛。"}
{"file":".trellis/spec/pipeline/native-road-package.md","reason":"验证 ZIP 解压后的 v1.1 contract、areaId 和 source 集合。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"验证导入迁移不改变 Blender 产物结构。"}

View File

@@ -0,0 +1,23 @@
# P5 Artifact-Only Road Package Design
P5 replaces the current runtime dependency boundary with a file-only boundary:
```text
road-compiler Web editor
-> deterministic root-flat native-road-package/v1.1 ZIP
-> osmWorkflow config nativeRoadPackage
-> private verified extraction cache
-> Blender / Cesium / preview
```
The road-compiler repository owns export and all editing/compiler internals. osmWorkflow owns ZIP safety,
contract validation, Blender materials and downstream scene construction. Neither repository reads the
other's source, configuration, overrides or runtime implementation.
The ZIP is immutable input and the host's `_pipeline/native-road-import/<sha256>` is disposable cache.
The host rejects unsafe archive topology before writing files, then validates v1.1 `manifest.json` and
area identity before starting Blender. Stage manifests record the ZIP hash and resolved package records.
P5A publishes a fixture/exporter first. P5B uses that fixture to remove compiler CLI installation and
invocation from the host. The migration is intentionally a contract-preserving transport change: rendered
geometry and material behavior must remain at the P3 baseline.

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/pipeline/native-road-package.md","reason":"宿主与编译器之间的现有文件契约。"}
{"file":".trellis/spec/pipeline/external-tools.md","reason":"ZIP 导入必须在 Blender 副作用前完成安全校验。"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"导出、Node 导入、Blender 和 preview 的跨层数据流。"}

View File

@@ -0,0 +1,14 @@
# P5 Integration Plan
1. Complete P5A in `road-compiler`: deterministic Web/CLI ZIP export plus supported-area fixtures and a
tagged compiler release.
2. Complete P5B in `osmWorkflow`: safe importer, config migration, CLI/dependency removal and downstream
descriptor migration.
3. Run the shared end-to-end gate on `fengshu-er-road` and `nantaizi-lake-innovation-valley`:
import ZIP -> Blender -> Cesium -> package -> preview -> area checks.
4. Compare Blender scene digests to P3 control; run negative ZIP tests and legacy provider tests.
5. Record contract docs in both repositories, commit each repository independently, then archive both
children and this integration parent.
Integration rollback: restore the host's previous tagged compiler dependency and compile adapter. ZIP export
is additive and its generated files remain valid standalone artifacts.

View File

@@ -0,0 +1,95 @@
# 道路包导入与纯消费管线
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
本任务是集成 parent导出与导入分别在 road-compiler 和宿主仓库实现,只有两个子任务的
端到端 ZIP 契约都通过后才完成。
## Goal
让 road-compiler Web 导出的 `native-road-package/v1.1` 成为 Blender 管线的唯一
native-road 输入。宿主只校验并消费完整道路包,不再安装、调用或理解道路编译器。
用户价值:人工在 road-compiler Web 编辑后,导出道路包即可稳定交给 Blender / Cesium
构建;编辑成果不再通过发布新的编译器代码版本传播。
## Confirmed Facts
- P3 已使 Blender 通过 `manifest.json` 消费 `layers/*.geojson`,并拒绝无效 manifest、
source/file 不一致和未知宿主材质槽。
- 宿主当前仍通过 `scripts/build-area.js` 调用 `scripts/lib/road-compiler-cli.js`;后者安装并
运行 `@osm-asset/road-compiler`,将 `RoadCompilerInput` 写入 `_pipeline/`
- native Blender 构建当前强制使用 `outputs/<area>/native-road/`,并在每次构建前重新编译;
宿主还持有 `nativeRoad.edgeLines``junctionTemplates`、overrides 和 compiler runtime
路径等编译器内部概念。
- `osm2streets` 是显式 legacy/debug provider应保持现有行为。
- 有效验证区域只有 `fengshu-er-road``nantaizi-lake-innovation-valley`
`hanyang-block` 不参与。
## Requirements
### R1 纯产物边界
- native provider 只接收一个完整 `native-road-package/v1.1` 目录。
- 宿主不得依赖 `@osm-asset/road-compiler`,不得调用 compiler CLI不得写
`RoadCompilerInput`,不得读取或解释 compiler overrides / junction templates。
- 宿主以 manifest 作为道路图层事实源Blender 不增加 compiler 专用图层表。
### R2 ZIP 导入配置和校验
- 每个 native 区域配置一个道路包 ZIP 路径;路径相对于区域配置文件解析并可指向工作区外的
road-compiler Web 导出文件。
- ZIP 根目录必须直接包含 `manifest.json` 和契约定义的文件;不得接受多余顶层目录、绝对路径、
`..` 路径或符号链接。
- 在启动 Blender 前,宿主将 ZIP 安全解压到其私有 pipeline staging 目录,校验 manifest
契约版本、`areaId` 和当前区域一致,并记录 ZIP hash 和展开文件记录到 stage manifest。
- ZIP 永远只读:宿主不复制、覆盖或修改 road-compiler Web 的导出文件;成功或失败后仅清理
自己的临时解压目录。
- Blender 继续校验 manifest 的图层、source、semantic 角色、材质槽和 splitBy 规则。
### R3 后续阶段
- Cesium、package、preview 和区域诊断从导入道路包读取原生车道中心线、signals 和
其他既有 native-road 输入;不重新编译或复制道路包。
- build stage manifest 将道路包作为外部输入记录,而非宿主输出。
### R4 清理和迁移
- 删除宿主对 compiler npm 包、CLI 封装和编译脚本的生产依赖。
- 删除或替换只测试宿主编译行为的测试;保留 package-contract、Blender 和下游消费测试。
- 更新示例配置、README、pipeline spec 和道路包契约,明确 Web export -> host consume
的单向关系。
## Acceptance Criteria
- [ ] AC1 `package.json` / lockfile 和生产脚本不再引用 `@osm-asset/road-compiler` 或其 CLI。
- [ ] AC2 两个有效区域均可从配置指定的已导出 ZIP 道路包运行 Blender、Cesium、package 和 preview
构建期间没有 compiler 子进程。
- [ ] AC3 宿主在 Blender 前拒绝缺失包、错误 contract、错误 areaId 和无效 manifest错误说明
指向导入包和契约,而非 compiler 内部。
- [ ] AC4 Blender scene digest 与 P3 control 基线一致native semantic 层仍不生成几何。
- [ ] AC5 stage manifest / diagnose / check area 将导入道路包记录为输入并能识别缺失或变更。
- [ ] AC6 `osm2streets` legacy provider 行为未变。
- [ ] AC7 文档和配置将人工编辑传播路径表述为 road-compiler Web export -> host consume。
## Out of Scope
- road-compiler Web 的编辑器功能、认证、部署或 UI 改造。
- 修改道路几何、材质、Blender 建筑/植被/水体渲染。
- 新的 manifest 破坏性版本或 lane graph IR。
- 将道路包上传到远程制品库或实现同步服务。
## Key Decision
- Web 导出的是标准 ZIP宿主配置直接指向 ZIP 而不是目录。ZIP 是跨仓库的不可变交付物,
宿主只在私有 staging 目录中解压和消费,因而没有导出目录 ownership 或覆盖歧义。
## Subtask Map
| 子任务 | 仓库 | 交付物 | 依赖 |
|---|---|---|---|
| `rc-p5a-zip-export` | road-compiler | Web / CLI 可导出的 root-flat `native-road-package/v1.1` ZIP | 无 |
| `rc-p5b-zip-import` | osmWorkflow | 配置、受控解压、校验和纯消费管线 | p5a 的 ZIP fixture |
跨任务验收:同一个 ZIP fixture 必须被 p5b 的两个有效区域构建消费;宿主不得保留
compiler npm 或 CLI 耦合。

View File

@@ -0,0 +1,29 @@
{
"id": "rc-p5-artifact-consumption",
"name": "rc-p5-artifact-consumption",
"title": "道路包导入与纯消费管线",
"description": "让宿主通过配置指向 road-compiler Web 导出的 ZIP 道路包,只校验和消费该版本化文件契约,不再安装或调用编译器。",
"status": "planning",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-26",
"completedAt": null,
"branch": null,
"base_branch": "experiment/road-compiler-rethink",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [
"08-26-rc-p5b-zip-import",
"08-26-rc-p5a-zip-export"
],
"parent": "08-25-road-compiler-extraction",
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,22 @@
# P5A ZIP Export Design
The compiler owns a pure `exportNativeRoadPackage(result, destination)` function. It writes a
temporary root-flat directory from the already compiled Web/CLI state, validates it with the same
manifest/source validator used by compiler output, then writes a deterministic ZIP. The Web download
handler and CLI are thin callers of this function.
ZIP layout:
```text
manifest.json
compiled.json
diagnostics.json
comparison.json
traffic-signal-assemblies.json
traffic-signals.json
layers/<12 declared sources>.geojson
```
`manifest.generator` is informational only. The host accepts solely the `contract`, `areaId`, and
declared files. ZIP creation normalizes entry order and timestamps so the export is hash-stable.
Failure never replaces a chosen output file.

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,10 @@
# P5A Execution Plan
1. Inspect workbench state ownership and compiler output writer; keep export sourced from the current
compiled state, not host paths.
2. Add deterministic root-flat ZIP writer plus validation and CLI entry point.
3. Add Web download action using the same writer.
4. Add fixtures for root layout, content determinism, manifest/source parity and edited-state export.
5. Export both supported-area fixtures for P5B, run compiler tests, then publish a compiler release.
Rollback: remove the export entry point; no host behavior changes in this subtask.

View File

@@ -0,0 +1,34 @@
# Web 导出原生道路 ZIP 包
## Goal
让 road-compiler Web/CLI 以 root-flat native-road-package/v1.1 ZIP 导出人工编辑后的道路包。
## Requirements
- 从 Web 当前编辑状态导出完整道路包而不是源代码、npm 包或仅 overrides。
- ZIP 根直接包含 `manifest.json`,并包含全部 manifest 声明的 `layers/<source>.geojson`
`compiled.json``traffic-signals.json` 和 preview 所需的 signal / diagnostics 文档。
- `manifest.json``contract` 保持 `native-road-package/v1.1`;新增可选 `generator` 元数据
记录应用版本和 revision但不得让宿主依赖该版本。
- Web 提供用户可见的下载操作CLI 提供等价、可测试的 ZIP 导出入口。
- ZIP 条目顺序、JSON 序列化和压缩时间戳必须确定性,重复导出未编辑内容的文件 hash 一致。
## Acceptance Criteria
- [x] 从 Web 编辑后的区域可下载一个 root-flat ZIP且无需宿主或 npm 发布。
- [x] CLI fixture 导出 ZIP 后,解压内容通过 compiler 的 `validatePublishedLayers()`
- [x] ZIP 具有恰好一份 manifest 声明的 layer source无额外 compiler source / overrides 文件。
- [ ] 两个有效区域导出的 ZIP 可被 p5b 导入并完成 Blender/Cesium/preview 构建。
- [x] 不修改 `compiled.json` 与 v1.1 manifest 的既有渲染语义。
## Out of Scope
- 远程存储、上传、项目历史或多用户协作。
- 修改道路编辑器交互、几何算法或 manifest 主版本。
## Key Decisions
- ZIP 是单个标准交付物,根目录平铺 package 文件,不包裹 area-id 顶层目录。
- ZIP 面向宿主消费,只携带已编译渲染/预览数据编辑源、OSM 与 overrides 留在 road-compiler
项目中,不泄露为宿主依赖。

View File

@@ -0,0 +1,26 @@
{
"id": "rc-p5a-zip-export",
"name": "rc-p5a-zip-export",
"title": "Web 导出原生道路 ZIP 包",
"description": "让 road-compiler Web/CLI 以 root-flat native-road-package/v1.1 ZIP 导出人工编辑后的道路包。",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-26",
"completedAt": null,
"branch": null,
"base_branch": "experiment/road-compiler-rethink",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-26-rc-p5-artifact-consumption",
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,19 @@
# P5B ZIP Import Design
`scripts/lib/native-road-package.js` becomes the single host adapter. It resolves the config-relative
ZIP path, hashes it, validates central-directory entry names and metadata, and extracts only accepted
regular files to `_pipeline/native-road-import/<sha256>/`. It validates the unpacked manifest and
returns an immutable package descriptor:
```text
{ zipFile, zipRecord, rootDir, manifest, records }
```
The descriptor replaces `area.outputs.nativeRoadDir` as native input for Blender, native preview traffic,
diagnostics and stage manifests. The configured ZIP and unpacked cache are inputs, never published area
outputs. Cache invalidation is by ZIP SHA-256; cleanup only deletes importer-owned hash directories.
The importer rejects any non-root-flat layout, duplicate path, symlink, unsupported compression/error,
path traversal, invalid manifest, area mismatch or declared/published mismatch before Blender starts.
The native compiler CLI dependency and host-owned `nativeRoad` compilation options are removed. Legacy
`osm2streets` remains an explicit separate provider.

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,12 @@
# P5B Execution Plan
1. Add config normalization for `nativeRoadPackage`, migrate two supported configs and the template.
2. Add the tested ZIP importer with bounded extraction, root-flat/path/symlink validation, SHA cache and
manifest/area validation.
3. Route Blender, preview traffic, stage manifests and diagnostics through the imported descriptor.
4. Delete compiler CLI/compile production paths and package dependency; replace their tests with ZIP fixture
and negative importer tests.
5. Update docs/specs, run native/legacy unit suites, build two exported packages, then run Blender parity.
Rollback: restore the old compiler dependency and native compile path in one revert; ZIP exports remain
non-destructive standalone files.

View File

@@ -0,0 +1,40 @@
# ZIP 道路包安全导入与纯消费
## Goal
让 osmWorkflow 配置、校验、解压并消费 road-compiler Web 导出的 ZIP 道路包,移除 compiler runtime 耦合。
## Requirements
- 区域配置以 `nativeRoadPackage` 指向 ZIP路径相对于该区域配置文件native provider 不再
接受 compiler options、overrides 或输出目录作为输入。
- 宿主在任何 Blender / Cesium 副作用前,验证 ZIP 文件、条目数量/大小、root-flat 文件路径、
无重复路径、无符号链接、无绝对路径与 `..` 路径。
- 仅将已验证的文件解压至 `<area>/_pipeline/native-road-import/<zip-sha256>/`;解压目录是宿主
私有缓存,绝不写回 ZIP 或 road-compiler 导出目录。
- 共享 importer 为 Blender、preview traffic、diagnose/check 和 stage manifest 提供同一包路径与
文件记录;不能让下游自行按字符串拼接另一份 native-road 目录。
- 移除 compiler package、CLI wrapper、compile script、编译器输入配置、以及只覆盖旧调用链的测试。
## Acceptance Criteria
- [ ] 两个有效区域均从 ZIP 完成 `blender,cesium,package,preview`,构建日志没有 compiler CLI。
- [ ] 缺 ZIP、错误 ZIP、nested root、path traversal、symbolic link、错误 contract、错误 areaId 和
manifest/source 不匹配均在启动 Blender 前失败。
- [ ] stage manifest、`diagnose:area``check:area` 记录 ZIP hash 和已验证的导入目录ZIP
修改后会被判 stale。
- [ ] `package.json`、lockfile、生产脚本与宿主测试不再引用 `@osm-asset/road-compiler`
- [ ] Blender digest 对 P3 control 基线一致legacy `osm2streets` 路径的单元测试不变。
## Out of Scope
- p5a 的 ZIP 生成与 Web 下载实现。
- ZIP 远程下载、解密、签名验证或制品库管理。
- 改道路内容、材质或 legacy QGIS 行为。
## Key Decisions
- ZIP 是只读输入;宿主缓存的生命周期和目录 ownership 完全属于 `_pipeline/`
- ZIP 解码由明确声明的 Node ZIP 库完成,而不是依赖系统 `unzip`;这使路径/符号链接校验可控并
可在单元测试中覆盖。
- 只有 `native-road-package/v1.1` 可被导入;契约升级必须另开兼容任务。

View File

@@ -0,0 +1,26 @@
{
"id": "rc-p5b-zip-import",
"name": "rc-p5b-zip-import",
"title": "ZIP 道路包安全导入与纯消费",
"description": "让 osmWorkflow 配置、校验、解压并消费 road-compiler Web 导出的 ZIP 道路包,移除 compiler runtime 耦合。",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-26",
"completedAt": null,
"branch": null,
"base_branch": "experiment/road-compiler-rethink",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": "08-26-rc-p5-artifact-consumption",
"relatedFiles": [],
"notes": "",
"meta": {}
}

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 @@
{"_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

@@ -3,7 +3,7 @@
"name": "native-preview-traffic-simulation",
"title": "Native preview traffic simulation and migration contract",
"description": "",
"status": "in_progress",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
@@ -11,7 +11,7 @@
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-18",
"completedAt": null,
"completedAt": "2026-08-24",
"branch": null,
"base_branch": "feature/native-road-compiler",
"worktree_path": null,

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,57 @@
# Technical Design
## Boundary
The native compiler remains authoritative for OSM road identity, directed lanes,
connections, traffic signals, stop lines, and preview routes. A reference layer is
an optional geometry-calibration input consumed before junction surface publication.
## Proposed Flow
```text
optional reference GeoJSON (GCJ-02)
-> validate schema and coordinate bounds
-> GCJ-02 to WGS84 conversion
-> associate reference footprint with an OSM junction node
-> classify geometry by observable shape, not opaque vendor type alone
-> derive bounded junction-shape parameters
-> native compiler creates surface/connectors as usual
-> diagnostics compare generated geometry with reference
```
The first production validation keeps the reference overlay/diagnostic, then enables a
bounded `cross` junction template for explicitly configured references. Connector
endpoints and movement enumeration still come from the native model. T and complex
junctions remain on the existing algorithm until their templates are separately validated.
## Input Contract
Use an optional area-level reference configuration rather than a hard-coded filename.
The sample points to `inputs/osm/珠山湖大道(枫树二路)口.geojson`, declares `GCJ-02`,
and identifies the target through OSM node `8005332807` or a stable spatial/name match.
The file is not assumed to contain a CRS member; the configured source CRS is required.
## Geometry Safety
- Convert all reference coordinates to WGS84 before distance/overlap tests.
- Reject malformed or non-finite coordinates and references outside the OSM bounds plus a
bounded tolerance.
- Never let a reference polygon create a movement, lane, signal, or stop-line record.
- Keep a fallback path to the current `compileGeometry()` junction algorithm.
## Compatibility And Rollback
No-reference configs and existing output schemas remain unchanged. The experiment can be
disabled by omitting the reference section or setting it disabled. Generated diagnostics
must record source file hash, conversion mode, matched OSM node, and whether fallback was used.
No source reference file is rewritten.
## Open Technical Work
1. Select the smallest reusable geometry parameter set from the sample (approach cutback,
corner radius/rounding, island/marking envelopes, and tolerances).
2. Implement and test GCJ-02 conversion independently from compiler logic.
3. Compare the sample against all native junctions near the converted footprint before any
production surface override is enabled.
4. Add topology classification and template dispatch; only cross is enabled in this validation.
5. Preserve native connectors and controls while replacing only the bounded junction surface geometry.

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,53 @@
# Validation Implementation Plan
进度标记于 2026-08-21。
1. [x] Add a read-only reference inspection utility or library helper that validates GeoJSON,
converts GCJ-02 to WGS84, computes bounds/center, and associates the sample with OSM node
`8005332807`. — `scripts/lib/gaode-junction-reference.js` + `scripts/inspect-junction-reference.js`
2. [x] Add focused tests for coordinate conversion, malformed input rejection, spatial association,
and the no-reference fallback. — `npm run test:gaode-junction-reference`
3. [x] Add a diagnostic comparison between the reference footprint and native
`intersection_surface.geojson`, including center offset, envelope overlap, and approach headings.
— 复杂路口的面写在 `road_surface.geojson``complex_part`)而非 `intersection_surface`
原实现匹配不到、恒返回 `nativeIntersection: null`。已扩展为可回落到复杂路口簇
(新增 `--native-road-surface` / `--cluster-id`schema 升至 v2
4. [x] Add a reference-backed, parameterized junction template and dispatch it only for
explicitly enabled samples. — 实际实现为 `complex-junction-v1`,非原计划的 cross 模板,
范围变更见 prd.md「Scope Drift」。
5. [x] Preserve connector, movement, signal, and stop-line generation; validate containment against
the new surface. — A/B 实测通过,数据见 prd.md「Verification Log」。
6. [x] Run native-road, preflight, preview-traffic, and package-contract tests, plus the sample
comparison. Inspect the generated overlay/diagnostic manually.
— 2026-08-21 全绿:`test:gaode-junction-reference``test:native-road``test:road-workbench`
`test:preflight``test:native-preview-traffic``test:package-contract``test:traffic-signals`
外加 `road:check``ok: true, errors: []`。人工视觉确认仍待用户。
7. [x] Stop at validation. Do not commit,
and do not modify the source OSM or reference GeoJSON. — 未 commit参考 GeoJSON 未改;
源 OSM 有改动但来自人工 JOSM 编辑,非管线写入。
## 本次会话追加的修复2026-08-21
- 车道中心线与车道标线越过斑马线:`compileLaneCenterlines` 现接收模板已产出的斑马线/停止线,
新增 `trimLaneOutsideControls`(按到路口中心的半径定向后再裁剪,因为车道几何两种朝向都存在)。
拓扑几何 `coordinates` 保持完整,仅裁剪发布几何与标线源几何。
实测越线要素 20 → 0分隔线越界 2 → 0connector 356 / movement 370 前后一致。
- 新增诊断 `complex-junction-configured-radius-ignored``lane-centerline-fully-inside-control`
- 删除死函数 `clusterApproachRing`
- **复杂路口人行道转角**`complex-junction.js`):直条带被裁在簇边界后,相邻两臂的步行带
各自留一个断头,中间整个楔形无覆盖。现沿圆角已定义的路缘补一条 2 m 宽转角带。
- 复用同一函数作用域内已算好的 `curve` / `tangents` / `edges` / `apex`,不重新拟合曲线
- 新增 `armCarriesSidewalk`(按臂的外向方向翻转道路自身的 `sidewalkLeft/Right`)、
`edgeRunToRadius``offsetPolylineAwayFromCenter``ringSelfIntersects`
-`islands` 通道并入 `generatedComplexSidewalks`
- 自交或几何无效时跳过并报 `complex-junction-sidewalk-corner-fallback`,不硬塞坏几何
- 实测:人行道要素 7 → 8生成角 1平分线 286°241°→331°57 顶点);无自交;
与两侧条带均相接;面积采样 331 点中 0 点落在车行道内
- **限制**:全区 139 条路仅 3 条带人行道4 个角中只有 286° 两侧齐备,
其余 3 个角当前不产出,缺少验证样本
## Rollback Points
- Delete/disable the optional reference configuration to restore the default native path.
- Remove only the reference diagnostic output; existing native-road outputs remain authoritative.
- Do not overwrite existing layers until the comparison is accepted.

View File

@@ -0,0 +1,110 @@
# Parameterize junction geometry with optional Gaode reference
## Goal
验证一种通用的、参数化的路口几何生成范式:借鉴高德路口 GeoJSON 的规整边界和标线组织方式,
但保持 OSM/native compiler 对道路身份、连接关系、车道可行驶性、信号和停止线的权威性。
当前阶段先做一个可回滚的生产几何验证,不提交代码或产物。
## Requirements
- R1: native 路口在没有参考数据时行为不变,现有 OSM/native 输出仍是默认路径。
- R2: 支持可选的路口参考输入至少记录输入路径、坐标来源GCJ-02和目标 OSM 路口关联方式。
- R3: 参考输入先转换为 WGS84禁止将 GCJ-02 坐标直接与 OSM/native 几何叠加。
- R4: 参考数据只允许影响路口面的规整化参数、边界形态和标线布局;不得改变 OSM 道路、车道、connector、
signal_uid、停止线归属或车辆路线拓扑。
- R5: 不机械复制高德 feature 的 `type=1..5`;先建立可审计的几何类别映射或只使用其可验证的形态特征。
- R6: 以“珠山湖大道(枫树二路)”路口作为验证样本,输出参考前后对比、偏差诊断和可回滚结果。
- R6.1: 生成器按路口拓扑选择模板,至少区分 cross、T 和复杂/多臂路口;不能用一个固定几何参数机械覆盖所有路口。
- R6.2: 首个生产几何验证只启用 cross 模板,利用参考样本校准 cutback、圆角和进口过渡T/复杂路口先保留现有 native 行为并记录诊断。
- R6.3: 模板生成的路口面必须由 OSM/native 道路宽度、方向和车道结构驱动;高德几何只用于校准参数或对比,不直接复制其 feature。
- R7: 参考文件缺失、坐标转换失败、无法关联 OSM 路口或几何越界时,构建必须回退默认 native 算法并给出诊断,
不得静默覆盖生产几何。
- R8: 保持当前阶段 manifest、GLB/package 契约和无参考区域的 parity 行为不变。
## Acceptance Criteria
> 措辞修订说明见文末「Scope Drift」。勾选项均为 2026-08-21 实测证据记录在「Verification Log」。
- [x] 不配置参考输入时,现有 native-road 测试和样本区域输出保持默认行为。
- [x] 配置枫树二路参考后,能稳定关联 OSM node `8005332807`,并生成 WGS84 参考诊断。
- [x] 参考几何与 native 路口的叠加结果可检查:路口中心、边界、进口方向有数值和 SVG 记录。
- [x] 参考模式不改变 OSM 衍生的 connector、车道连接、信号或停止线集合模板只新增自有
`complex-*` 命名空间要素。(原措辞「不会新增或删除…停止线」严于 R4 本意,已修订)
- [x] 参考模式失败时有明确 warning并成功回退默认 native 几何。
- [x] 模板生成的路口面视觉上接近高德参考。connector / 信号 / 停止线集合一致性已实测通过;
视觉接近度由用户 2026-08-21 在工作台确认(「基本 ok」其间提出的人行道转角缺口已修复并复核通过。
- [x] 无参考路口继续走默认 native 几何,不被模板误套。
- [x] 验证产物写入区域输出目录,未执行 git commit。源 OSM 有改动,但来自人工 JOSM 编辑,
非管线写入;原措辞未区分这两者,已修订)
## Scope Drift2026-08-21 记录)
R6.2 原定「首个生产几何验证只启用 cross 模板T/复杂路口先保留现有 native 行为」。
实际实现走的是 `complex-junction-v1`,对枫树二路 4 节点簇做整体路口面、斑马线、停止线、
角部安全岛和圆角生成。范围已越过 R6.2本文档据实修订R6.2 视为被本次决策取代。
cross 模板(`cross-v1` / `cross-cluster-v1`)代码路径仍在,未被删除。
## Verification Log2026-08-21
参考模式 A/B 对照(同一 OSM 输入,`junctionTemplates.enabled` 开/关,独立 outputRoot
| 指标 | ON | OFF | 判定 |
| --- | --- | --- | --- |
| connector 要素 | 356 | 356 | 集合逐一相同,无新增/缺失 |
| movement / published | 370 / 356 | 370 / 356 | 一致 |
| connection | 215 | 215 | 一致 |
| 信号 uid 集合 | 20 | 20 | 集合完全一致 |
| 普通斑马线 `native-road-crosswalk/v1` | 6 | 6 | 一致 |
| 模板新增 `complex-crosswalk` | 128 | 0 | 模板自有命名空间 |
| 模板新增 `complex-corner-crosswalk` | 24 | 0 | 同上 |
| 模板新增 `complex-stop-line` | 4 | 0 | 同上 |
| 预览车道 `cluster-approach-lane` | 24 | 0 | 同上 |
参考关联与叠加(`npm run reference:junction`
- 匹配方式 `node-id`,命中 `8005332807`,参考中心到节点 **8.98 m**
- 匹配到 `complex-cluster` = `zhushanhu-fengshu-complex`13 个要素
- native core 半径 **24 m**(配置 `coreRadiusMeters: 28`),进口 4 / 车行道 8
进口朝向 `[-118.5, -28, 62.2, 151.9]`
- 已知口径差异:检查工具用参考文件**全部** feature 算 bounds约 380×330 m含长引道
`readReferenceCalibration` 只取 `type ∈ [1,2,3,4]` 的子集。两者不同量级,
故 bbox IoU0.070)当前**不是**可用的质量指标,仅中心偏差和朝向可比。
失败回退(`referenceFile` 指向不存在路径):
- 编译成功不抛错;诊断由 `complex-junction-reference-calibrated` 降级为 `complex-junction-generated`
- connector 356 / movement 370 不变
- 该路径下 `lane-centerline-fully-inside-control` 触发 10 次:无参考时 coreRadius 仅 14.56 m
10 条车道整体落在控制带内,回退为未裁剪发布。非回归(改动前同样未裁剪),但属已知限制。
非簇路口未受模板影响A/B `intersection_surface`
| | ON | OFF |
| --- | --- | --- |
| 普通路口面总数 | 41 | 45 |
| 簇成员节点出现次数 | 0预期 0 | 4模板关闭时预期 4 |
| 非簇节点41 个) | — | 与 ON 逐点几何完全一致,无增无缺 |
| 模板 `complex_part` 要素 | 13全部归属唯一簇 `zhushanhu-fengshu-complex` | 0 |
结论:模板严格限定在 `nodeIds` 列表内,无外溢。
## Known Gaps2026-08-21
1. `way/858770823:segment/2`(长 19.2 m远端为 service 路口 `8613058676`)被 `native-road.js`
的 cluster 丢弃判据误伤,整段无路面,该节点周边 25×25 m 内 25.8% 露底。
判据应从「远端在 outerRadius 内」改为「远端也是 cluster 成员节点」,实测该判据分界干净。
**用户 2026-08-21 明确暂缓,未修改。**
2. `coreRadiusMeters` 在有参考文件时不参与计算(被 calibration 覆盖并 clamp 到 24
已加诊断 `complex-junction-configured-radius-ignored` 使其可见,行为未改。
3. `approachLengthMeters``complex-junction-v1` 无效(仅 `cross-v1` / `cross-cluster-v1` 使用)。
死函数 `clusterApproachRing` 已删除。
4. 无参考路径下普通停止线为 0`crossing-no-safe-stop-line` 告警 1 条),先于本任务存在。
## Constraints And Deferred Scope
- OSM WGS84 与 native 拓扑是交通语义的单一事实源。
- 高德参考暂不作为完整道路网络输入,也不直接替代 road surface 或 connector 几何。
- 暂不定义所有高德 `type` 值的业务语义;先验证几何范式和校准边界。
- 暂不提交代码、配置或生成产物;本阶段完成后由用户决定是否进入正式实现。

View File

@@ -0,0 +1,26 @@
{
"id": "gaode-junction-reference",
"name": "gaode-junction-reference",
"title": "Parameterize junction geometry with optional Gaode reference",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-19",
"completedAt": "2026-08-21",
"branch": null,
"base_branch": "fengshu-er-road",
"worktree_path": null,
"commit": "12aeda9a63867ff659cec2310c3e4c586907d005",
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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,82 @@
# Design: Live V2X Cesium Intersection View
## Architecture
The native pipeline remains the source of static scene geometry and package
placement. The existing Cesium preview gains an operations overlay and a compact
V2X sign-in gate. Vehicle entities and their trace lines exist only after the
authenticated V2X streams provide valid positions; no generated traffic route or
simulation is rendered. The implementation stays in the current generated static
preview architecture and does not import the source dashboard's Vue, AMap, or
Three dependencies.
The generated preview runtime is divided into four browser concerns:
1. `auth`: presents the V2X sign-in form, MD5-hashes the password to match the
source contract, stores a successful token in `sessionStorage`, and clears it
on sign-out or authorization failure.
2. `v2x-client`: owns configured REST/WS origins, adds the raw `Authorization`
request header, normalizes the source API envelope, and reports a capability
status rather than blanking the preview if an optional resource fails.
3. `coordinates`: labels external values as GCJ-02 and native values as WGS84 or
ENU. It converts V2X GCJ-02 positions to WGS84 exactly once before Cesium
entity creation; the existing native package placement contract is unchanged.
4. `cesium-v2x-overlay`: adds V2X links, devices, pole/configuration evidence,
metrics, and real signal phase updates to the existing Cesium viewer, with
independent visibility controls and diagnostics.
## Data Flow
```text
V2X sign-in -> session token
| |
| +--> Authorization header / WS query parameter
v
configured V2X REST + WS endpoints -> GCJ-02 V2X geometry, devices, metrics, lamps
|
v
GCJ-02-to-WGS84 adapter
|
native package + route/signal descriptors -> WGS84/ENU -> existing Cesium viewer
^
|
Cesium V2X operations overlay
```
## Endpoint Contract
The first delivery consumes `queryCrossLinkInfo`, `queryPoles`, bound-device,
cross-device-config, and weekly flow-ratio APIs. It subscribes to
`/network/ws/network/signal` only after link data establishes usable phases.
Endpoint host/prefixes and selected `crossCode` are deployment configuration. A
development proxy forwards REST and WebSocket traffic so the browser never needs
a hard-coded private origin.
## Coordinate Contract
| Producer | Source CRS | Consumer rule |
| --- | --- | --- |
| Native compiler package, traffic signals, route descriptor | WGS84 / local ENU | Preserve the existing Cesium package placement and WGS84 entity contracts. |
| V2X dashboard link, road, device, and pole data | GCJ-02 | Convert to WGS84 once before creating Cesium entities. |
| High德 reference GeoJSON | GCJ-02 | Calibration-only compiler input; no runtime map or SDK dependency. |
The existing `gaode-junction-reference` inverse conversion is an appropriate
source for the V2X adapter. It will be factored or safely reused only when doing
so preserves its compiler behavior and test coverage.
## Compatibility And Failure Handling
- Existing Cesium preview and its static-only workflow remain usable.
- The V2X overlay starts only after successful sign-in.
- An expired token returns the user to sign-in and removes live entities rather
than presenting stale data as current.
- A REST/WS capability failure is shown in diagnostics; native scene and signals
remain usable, while the vehicle layer stays empty rather than simulating data.
- Removing the optional overlay/support files restores current preview behavior
without changing static package contracts.
## Security
`sessionStorage` limits the upstream token to the current tab session. Credentials
are never persisted. Configuration examples use placeholders, and deployment
documentation requires an HTTPS same-origin reverse proxy for V2X API/WS traffic.

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,41 @@
# Implementation Plan: Live V2X Cesium Intersection View
1. Read preview and pipeline specifications, then add a focused V2X preview
configuration. Keep secrets and private origins out of defaults and tracked
area configuration.
2. Add browser-safe GCJ-02-to-WGS84 conversion and focused tests for source
coordinate labels and prevention of duplicate conversion. Preserve the current
compiler reference conversion behavior.
3. Implement the V2X browser client: login, session token lifecycle, REST
envelope handling, capability errors, and signal WebSocket lifecycle.
4. Extend the generated Cesium preview with V2X entities and operations controls
for links, devices, poles/configuration, metrics, live signal state, and the
OBU/target-vehicle streams. Do not load or generate simulated vehicle routes.
5. Add a local development/proxy configuration example and deployment
documentation for V2X REST/WS proxying. Do not add an AMap key, SDK, or page.
6. Add focused tests for HTML generation, configuration escaping, login/request
construction, coordinate conversion, source-layer diagnostics, and graceful
unavailable/auth states. Run existing preview and native-road regressions.
7. Validate in a browser with a real V2X account and configured proxy: sign in,
inspect aligned native/V2X features, verify REST panels and signal updates,
then verify sign-out/token expiry behavior.
## Validation Commands
```bash
node --check scripts/lib/area-preview.js
node --check scripts/lib/cesium-preview.js
npm run test:gaode-junction-reference
npm run test:preview-assets
npm run test:native-preview-traffic
npm run test:traffic-signals
npm run road:compile -- --config config/areas/fengshu-er-road.json
npm run build:area -- --config config/areas/fengshu-er-road.json --stages preview
```
## Review Gates
- Confirm no token, account, or private host is committed.
- Verify V2X GCJ-02 coordinates convert once before Cesium use.
- Verify the existing native WGS84/ENU scene contract remains unchanged.
- Verify the current Cesium preview works without V2X configuration or sign-in.

View File

@@ -0,0 +1,85 @@
# Integrate live V2X intersection data into Cesium preview
## Goal
Provide a Cesium-based operational view of the current complex intersection in
this repository. It must show live data from the existing V2X platform while
retaining the native road compiler's WGS84/ENU output as the geometry authority.
## Confirmed Facts
- The source dashboard's holographic-intersection view uses V2X REST resources for
road/link geometry, device bindings, poles, configuration, traffic metrics, and
a token-authenticated signal WebSocket.
- The dashboard development proxy targets `172.16.1.159:50400` under `/api`,
`/websocket`, and `/vectortile`; the application sends the upstream token as the
`Authorization` header.
- The source dashboard uses AMap and therefore its road/link data is GCJ-02. Its
road-network exporter converts that data from GCJ-02 to WGS84 before export.
- This repository's native road, signal, and simulation artifacts are WGS84/ENU.
- The target is the existing Cesium preview, not AMap. High德 data in this
repository is a calibration/reference GeoJSON only, not a target runtime map.
- The requested page has a simple login gate; V2X data must remain real live
interface data, not copied mock data.
## Requirements
- R1: Add a Cesium-based complex-intersection operations view to this repository, protected by
a simple application login.
- R2: Integrate the real V2X REST endpoints used by the source dashboard, including
current intersection/link geometry, devices, poles/configuration, traffic metrics,
and signal state where upstream authorization permits it.
- R3: Keep the V2X platform credentials separate from the simple application login;
do not put a reusable upstream credential into browser source or committed files.
- R4: Treat V2X map-facing coordinates as GCJ-02. Treat native compiler artifacts
as WGS84/ENU, and convert V2X GCJ-02 coordinates to WGS84 exactly once before
adding them to Cesium.
- R5: Surface a clear unavailable/authentication state when live data cannot be
loaded while leaving the native complex-intersection view usable.
- R6: Make endpoint origin, selected V2X intersection code, and authorization
mechanism deployment configuration rather than hard-coded source values.
- R7: Vehicle lines and moving vehicles must come only from the authenticated
V2X vehicle streams. Do not render or fall back to generated route/simulation
data when the service is unavailable.
## Candidate Upstream Resources
- `GET /network/api/link/network/queryCrossLinkInfo/{crossCode}`: intersection
link geometry and signal phase association.
- `GET /network/api/pole/network/queryPoles/{crossCode}`: pole and light placement
configuration.
- `GET /facilities/api/crossDevice/findDeviceByCrossCode/{crossCode}`: bound devices.
- `GET /facilities/api/crossDeviceConfig/{crossCode}`: target-device configuration.
- `GET /facilities/api/FlowTravelRatio/queryListWeek?code={crossCode}` and related
cross-monitor resources: operational metrics.
- `WS /network/ws/network/signal?authorization={token}`: live signal lamps.
## Acceptance Criteria
- [ ] A user can pass the local login gate and open the current complex-intersection
Cesium view.
- [ ] The page retrieves and renders live V2X data for a configurable intersection
code without source-dashboard runtime dependencies.
- [ ] The page identifies the coordinate reference of each source and visually
aligns native WGS84 geometry with V2X data converted from GCJ-02.
- [ ] A missing/expired upstream credential or unavailable service is visible and
does not make the native intersection view unusable.
- [ ] Upstream URL, intersection code, and credentials/tokens are excluded from
tracked application source and documented for deployment.
## Out Of Scope
- Copying the source dashboard's Vue, AMap/Three, or proprietary UI component stack.
- Building a full user/role management service.
- Replacing native road geometry or traffic-signal contracts with V2X data.
- Generated vehicle-route or traffic-simulation fallback in the operations preview.
## Authentication Decision
- The page signs in to the upstream V2X service with the user-entered account,
posting `userName` and MD5-hashed `password` to
`/facilities/api/sys/login` through the configured API prefix.
- The returned token lives only in `sessionStorage`, is sent as the raw
`Authorization` header for REST requests, and is supplied as the
`authorization` WebSocket query parameter for the signal subscription.
- No default credentials, reusable token, or API secret may be committed.

View File

@@ -0,0 +1,26 @@
{
"id": "v2x-amap-intersection-data",
"name": "v2x-amap-intersection-data",
"title": "Integrate live V2X intersection data into AMap preview",
"description": "",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-24",
"completedAt": "2026-08-24",
"branch": null,
"base_branch": "fengshu-er-road",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

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": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P1",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": "2026-08-25",
"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 @@
{"_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,53 @@
# Phase 1 Technical Design
## Boundary
`packages/road-compiler/` becomes the single implementation owner for native
road compilation. It is a CommonJS workspace package with explicit public
entrypoints for compiler APIs, traffic-signal schema, turn-lane helpers, OSM
parsing, reference conversion, and CLIs. It never imports `area-config`, reads
`config/areas`, or derives host paths.
The host owns `scripts/lib/area-config.js` and maps normalized configuration to
the v1 `RoadCompilerInput` shape using `toRoadCompilerInput(area)`. During P1,
`build-area.js` calls the package in-process; P2 is the only phase that changes
that call to a child process.
## Source Ownership and Compatibility
The following current `scripts/lib` implementations move into package `src`:
`native-road`, `complex-junction`, `turn-lane-arrows`, `lane-geometry`,
`gaode-junction-reference`, `native-traffic-signals`, and `osm`.
Some moved modules also serve legacy host paths: QGIS uses turn-lane arrows,
vehicle preview and reimport use OSM parsing, and preview tests use lane
geometry. Those consumers import the package entrypoints after the move. This
preserves one implementation while allowing host -> package dependency.
Traffic signal ownership follows the matrix in
`research/traffic-signals-split-matrix.md`: schema, geometry, validation, and
runtime construction move to the package. The host retains only two file-I/O
adapters, `readTrafficSignalFeatures()` and `readTrafficSignals()`, both
delegating to package exports. No duplicated schema code remains.
## Input and Path Handling
The package compiler API accepts only `RoadCompilerInput` from
`docs/native-road-package-v1.md`. Package CLI commands accept an explicit JSON
input file (`--input`) and never load area configuration. The compatibility
host CLI still accepts `--config`, but immediately maps it through
`toRoadCompilerInput()` before entering the package.
`junctionTemplates.*.referenceFile` changes to a path relative to its area
configuration. `readAreaConfig()` resolves it while normalizing configuration;
the package receives an absolute path and does no path resolution.
## Tests and Rollout
The package receives the required Fengshu OSM fixture plus migrated unit tests.
Its `npm test` must run from the package directory without reading host inputs
or outputs. After each source batch, run the two Phase 0 parity baselines. The
existing host command names remain compatibility wrappers through P1.
Rollback is a normal Git revert of the latest batch commit. No module has a
parallel host copy after its batch lands, preventing divergence.

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 1 执行计划
分批搬迁,每批后跑 parity。**不要一次性大搬。**
## Step 0 — 前置
```bash
# 基线必须先绿
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
```
- [ ] 两个有效区域基线全绿(否则回 Phase 0
## Step 1 — K1 矩阵(动代码之前)
- [ ]`scripts/lib/traffic-signals.js` 全文,列出所有 export
- [ ] 对每个 export `grep -rn` 找使用方
- [ ] 产出 `research/traffic-signals-split-matrix.md`:函数 × 使用方 × 归属判定
- [ ] `buildTrafficSignals``test-preview-assets.js`)归属定档
**门槛**:矩阵完成,无"待定"项。
## Step 2 — 建包骨架
```
packages/road-compiler/
package.json # CommonJS无构建步骤父任务 C2
src/
bin/
test/fixtures/
docs/
```
- [ ] `package.json` 名称、入口、`test` script
- [ ]`package.json` 加 workspace 或 `file:` 依赖
## Step 3 — 分批搬迁(每批一个 commit + 一次 parity
**批 1纯函数零风险**
- [ ] `lib/lane-geometry.js``src/geometry/lane-geometry.js`
- [ ] `lib/gaode-junction-reference.js``src/reference/gaode.js`
- [ ] 宿主侧改 require 指向包
- [ ] parity ✓
**批 2几何模块**
- [ ] `lib/turn-lane-arrows.js`
- [ ] `lib/complex-junction.js`
- [ ] parity ✓
**批 3信号K1 执行)**
- [ ] 按 Step 1 矩阵拆 `lib/traffic-signals.js`
- [ ] `lib/native-traffic-signals.js` 与 schema 部分入包
- [ ] `lib/osm.js` 入包
- [ ] 宿主保留 legacy 读取器,从包 import schema
- [ ] parity ✓ + `npm run test:traffic-signals`(若存在)
**批 4编译器核心**
- [ ] `lib/native-road.js``src/compile/native-road.js`
- [ ] parity ✓
**批 5CLI + 窄契约R1.2**
- [ ] `compile-native-roads.js``bin/compile.js`,入参改 `RoadCompilerInput`
- [ ] `check-native-roads.js``bin/check.js`
- [ ] 宿主 `area-config.js``toRoadCompilerInput(area)`
- [ ] `build-area.js` 改为传 input 对象
- [ ] K3`referenceFile` 改为宿主解析后传绝对路径config 内改相对路径
- [ ] parity ✓
**批 6workbench**
- [ ] `road-workbench.js` + `workbench/app.js` 入包
- [ ] OpenLayers 依赖暂沿用宿主 `node_modules`(同仓库内可行),记入 Phase 2 K4
- [ ] `npm run test:road-workbench`
**批 7测试 fixtureR1.5**
- [ ] `inputs/osm/枫树二路.osm` 复制进 `test/fixtures/`
- [ ] `test-native-road.js` 改读包内 fixture
- [ ] 在包目录内独立跑 `npm test`
## Step 4 — 清理与验证
- [ ] 删除 `scripts/lib/` 下所有搬迁文件的残留AC1.6
- [ ] `grep -rn "area-config\|config/areas" packages/road-compiler/` 必须为空AC1.4
- [ ] `grep -rn "\.\./\.\./scripts\|osm2streets-qgis-workflow" packages/road-compiler/` 检查反向依赖
## 验证命令汇总
```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
(cd packages/road-compiler && npm test)
# 端到端(含 blender / cesium
npm run build:area -- --config config/areas/fengshu-er-road.json
# 反向依赖为 0
grep -rn "area-config\|config/areas" packages/road-compiler/ && echo "VIOLATION"
```
## Review Gate
AC1.1AC1.8 全绿。**parity 有任何一项不绿都不得进 Phase 2** —— 那意味着搬迁改变了行为,
而在同一仓库内查因比拆仓库后查因便宜得多。
## Rollback
每批一个 commit可逐批 `git revert`
过渡期内若需紧急回退,宿主 require 改回 `scripts/lib/` 旧路径即可
(前提:该批尚未执行 Step 4 清理)。

View File

@@ -0,0 +1,98 @@
# Phase 1包边界不换仓库
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
技术设计:父任务 `design.md` §2模块清单 / K1 拆分建议)
## Goal
在**当前仓库内**建立 `packages/road-compiler/`,移入编译器全部模块,
把对 `readAreaConfig` 的耦合换成窄输入契约,产物对 Phase 0 基线逐字节一致。
**本阶段不换仓库。** 目的是把"改坏了编译器"与"拆坏了仓库"分成两个可归因的步骤
(父任务 D4
## Requirements
### R1.1 目录与模块搬迁
`packages/road-compiler/` 内建立结构,移入父任务 `design.md` §2.1 列出的全部文件
(约 3400 行核心 + workbench
搬迁后 `scripts/lib/` 下不应再留下这些文件的副本 —— 并存会导致
"改了一份忘了另一份"的静默不一致。
### R1.2 窄输入契约
- 编译器入口只接受父任务 `design.md` §1.1 定义的 `RoadCompilerInput`
- 编译器内不得出现 `require("area-config")`、不得读 `config/areas/*.json`
不得从 areaId 推导任何路径
- 宿主 `scripts/lib/area-config.js` 新增 `toRoadCompilerInput(area)` 做映射
- 三个 CLI`compile` / `check` / `workbench`)改为接受显式参数或 input JSON
### R1.3 K1`traffic-signals.js` 拆分 🔴
**全案唯一需要细读再动的地方。** 按父任务 `design.md` §2.3
- 随编译器走OSM 信号节点提取 + 信号文档 schema
- 留宿主:`readTrafficSignalFeatures``build-osm2streets-qgis.js`)、
`readTrafficSignals``build-area.js`
- 待判定:`buildTrafficSignals``test-preview-assets.js`)—— 需读代码确认归属
- 拆完后宿主从编译器包 import schema宿主→编译器是允许方向
动手前必须先产出一份「函数 × 使用方」矩阵,确认无遗漏。
### R1.4 K3参考文件路径约定
`config/areas/fengshu-er-road.json`
`junctionTemplates.clusters[].referenceFile` 是指向宿主仓库的绝对路径。
- 定义解析约定:相对 config 文件所在目录,或由宿主在 `toRoadCompilerInput()`
中解析为绝对路径后传入
- 倾向后者:**编译器只接受已解析的绝对路径,不做路径推导**(符合 R1.2
- config 内改为相对路径,宿主负责解析
### R1.5 K5自带测试 fixture
- `test-native-road.js:167``inputs/osm/枫树二路.osm`
- 该文件(或裁剪版)复制进 `packages/road-compiler/test/fixtures/`
- 测试改为读包内 fixture不再访问宿主 `inputs/`
- 目标:编译器包 `npm test` 不依赖宿主任何目录
### R1.6 宿主侧接线保持不变
- `build-area.js` 本阶段继续 in-process `require`(子进程改造留到 Phase 2
- `npm run road:compile` / `road:check` / `road:workbench` 行为对用户不变
- `blender/` 完全不改
## Acceptance Criteria
- [x] AC1.1 两个有效区域 `road-parity --compare` 对 Phase 0 基线全绿
- [x] AC1.2 `npm run test:native-road``npm run test:road-workbench` 通过
- [x] AC1.3 `npm run build:area` 枫树二路完整跑通(含 blender / cesium 阶段)
- [x] AC1.4 `grep -rn "area-config\|config/areas" packages/road-compiler/` 无命中
- [x] AC1.5 `packages/road-compiler/``npm test` 不访问宿主 `inputs/``outputs/`
- [x] AC1.6 `scripts/lib/` 下无搬迁文件的残留副本
- [x] AC1.7 K1 的「函数 × 使用方」矩阵已产出并归档到 `research/`
- [x] AC1.8 `compiled.json` 结构未变(父任务 AC7 / C1
## 依赖与顺序
- **前置**Phase 0 必须完成且基线可靠
- **阻塞**Phase 2
- 与 Phase 3、4 无直接依赖,但它们都在 Phase 2 之后
## 风险
| 风险 | 缓解 |
|---|---|
| K1 拆分遗漏某个使用方 → 运行时才炸 | 先做矩阵,再动代码;`grep -rn` 全仓验证 |
| 搬迁过程中相对 require 路径改错 | 分小步 commit每步跑 parity |
| `workbench/app.js` 的 OpenLayers import map 指向宿主 node_modules | 本阶段仍在同仓库,可暂不处理;记入 Phase 2K4 |
| 一次性大搬迁难以归因 | 按模块分批先纯函数lane-geometry / gaode-reference再 complex-junction最后 native-road 与 CLI |
## Out of Scope
- 换仓库Phase 2
- layer manifestPhase 3
- IR / pass manager 重构(父任务 C1
- 引入 TypeScript 或构建步骤(父任务 C2

View File

@@ -0,0 +1,21 @@
# Traffic Signals Split Matrix
Evidence gathered from `scripts/lib/traffic-signals.js` and repository-wide
call-site search on 2026-08-25.
| Export | Current users | Owner after P1 | Migration |
|---|---|---|---|
| `SIGNAL_LAYOUT` | `buildTrafficSignalsFromFeatures` | compiler package | Export from `@osm-asset/road-compiler/traffic-signals` |
| `signalNodeKey` | `buildTrafficSignalsFromFeatures` | compiler package | No host caller today; preserve public export for runtime identity contract |
| `buildTrafficSignalFeatures` | `native-traffic-signals`, `readTrafficSignalFeatures` | compiler package | Host QGIS adapter imports it from the package |
| `validateTrafficSignalFeatures` | `native-traffic-signals`, signal tests | compiler package | Tests import package entrypoint |
| `validateTrafficSignalSourceReferences` | `native-traffic-signals`, `reimport-gpkg`, `readTrafficSignals` | compiler package | Reimport adapter imports package function |
| `buildTrafficSignalsFromFeatures` | `native-traffic-signals`, `readTrafficSignals`, `buildTrafficSignals` | compiler package | Host preview adapter imports package function |
| `buildTrafficSignals` | `test-preview-assets` | compiler package | Test imports package function; it is geometry/schema behavior, not host I/O |
| `readTrafficSignalFeatures` | `build-osm2streets-qgis` | host adapter | Keep a thin file-reading wrapper under `scripts/lib/traffic-signals.js`; it imports package functions |
| `readTrafficSignals` | `build-area` | host adapter | Keep a thin file-reading wrapper under `scripts/lib/traffic-signals.js`; it imports package functions |
`scripts/lib/traffic-signals.js` must not retain a second implementation of
the schema or geometry. Its only P1 responsibility is host file I/O for the
legacy QGIS / preview paths. This preserves the one-way dependency:
host -> compiler package; the package never imports the host.

View File

@@ -0,0 +1,26 @@
{
"id": "rc-p1-package-boundary",
"name": "rc-p1-package-boundary",
"title": "Phase 1包边界不换仓库",
"description": "在本仓库内建 packages/road-compiler移入编译器模块用窄输入契约替换 readAreaConfig 耦合,产物对基线逐字节一致",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P1",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": "2026-08-25",
"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,4 @@
{"file":".trellis/spec/pipeline/external-tools.md","reason":"检查 CLI 子进程错误、signal 与日志处理是否符合宿主管线约定。"}
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"检查编译阶段、完成标记和 stage manifest 的跨层影响。"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"检查配置、Node pipeline、Blender 消费链的边界没有漂移。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"检查两个有效区域的严格基线 parity。"}

View File

@@ -0,0 +1,44 @@
# Phase 2独立仓库与 CLI 边界设计
## Architecture
P1 已将实现收敛到 `packages/road-compiler/`。P2 把该目录以保留历史的方式导出为独立 package 仓库,保留两个明确方向的边界:
```
宿主 area config
-> scripts/lib/area-config.js
-> RoadCompilerInput JSON宿主 staging
-> @osm-asset/road-compiler CLI子进程
-> native-road/ + NATIVE_ROAD_COMPILE_DONE
-> build-area.js / Blender / preview
```
编译器不知道区域配置、宿主仓库、Blender 或宿主输出目录约定以外的输入路径。宿主不知道 compiler 内部源文件;它只拥有输入 JSON、CLI 的完成标记和写出的契约文件。
## CLI Contract
- CLI 接收一个 JSON 文件中的 `RoadCompilerInput`,而不是宿主的 area config。
- `comparisonDir` 继续作为可选输入,以维持现有 `comparison.json` 产物和 P0 baseline 的可比性。
- 成功只打印一次 `NATIVE_ROAD_COMPILE_DONE <JSON>`。payload 必须至少含 `areaId``output``roads``endpoints``diagnostics`
- `build-area.js` 保持同步批处理模式,子进程日志对用户可见;为了验证标记,输出同时被捕获或 tee。它对启动失败、非零状态、signal、缺失/重复/非法标记和 payload 与当前输入不一致都报错。
- 产物先由 compiler 的现有 staging/atomic promotion 逻辑写入目标 `native-road/`;宿主不接管这一写入过程。
## Repository Contents
独立仓库拥有compiler source、public API、CLI、check、workbench、fixture、两个 parity baseline、契约文档、README、package lockfile 和 workbench 的 `ol` 依赖。测试和 workbench 均必须在没有宿主 checkout 的环境运行。
宿主保留area-config 读取/归一化、`RoadCompilerInput` 映射、pipeline 阶段编排、Blender/preview、区域配置、运行产物和一份升级回归 baseline。宿主 spec 仅保留指针,契约正文随 compiler 仓库维护。
## History Extraction
导出只允许 `git subtree split``git filter-repo`。必须在独立仓库验证 `git log --follow src/compile/native-road.js`;验证的是迁移前的逻辑历史,而非仅 P1 的边界移动提交。导出和验证先在临时分支/clone 完成,外部远端创建和 push 必须使用用户给出的地址和授权范围。
## Dependency and Rollout
初始发布源定为私有 `https://git.app.que01.top/que01/road-compiler.git`。仓库以带注释 tag `v0.1.0` 首发,宿主以该精确 tag 作为依赖并写入 lockfile。开发临时形态可以指向独立 checkout 的本地 `file:` 依赖;它不能成为验收时的唯一消费方式。两个区域 parity 通过后删除 `packages/road-compiler/`;回滚由 revert 宿主消费改造实现,不能让 production host 继续直接 import 该目录。
## Compatibility and Risks
这是位置和调用方式迁移,不改变 compiler 行为。两个区域的 strict content/order hash 是主要 oracle信号文档的确定性生成仍应通过 parity 覆盖。P2 不处理 `check:area` 的历史 stage-manifest freshness warning也不把 Blender/Metal 沙箱问题归因为道路产物。
若外部 CLI 无法及时使用,回滚宿主消费 commit 即可;外部仓库历史和 tag 不需要删除。对于完整 Blender 构建macOS Metal 初始化必须在提权的宿主环境运行。

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/pipeline/external-tools.md","reason":"CLI 子进程必须沿用管线的启动、日志和失败状态语义。"}
{"file":".trellis/spec/pipeline/cli-and-stages.md","reason":"编译是 Blender 前的 pipeline 输入阶段,需保持阶段与 stdout 契约。"}
{"file":".trellis/spec/config/index.md","reason":"宿主拥有区域配置归一化及 RoadCompilerInput 映射。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"本阶段声称产物不变,必须用严格 parity 验证。"}

View File

@@ -0,0 +1,55 @@
# Phase 2 执行计划
执行前提:用户已在最新 P2 规划摘要后明确批准。发布目标已定为私有 `https://git.app.que01.top/que01/road-compiler.git`,首发 tag 为 `v0.1.0`
## 1. 导出并核验历史
- [ ] 创建临时 export 分支或临时 clone使用 `git subtree split --prefix=packages/road-compiler``git filter-repo --path packages/road-compiler/ --path-rename packages/road-compiler/:` 导出。
- [ ] 在导出仓库验证 `git log --follow src/compile/native-road.js` 包含 2026-08-13 以来的 native compiler 历史。
- [ ] 在私有 `https://git.app.que01.top/que01/road-compiler.git` 创建并推送独立仓库;创建带注释的 `v0.1.0` tag。
## 2. 使编译器仓库自持
- [ ] 将 CLI 作为 package `bin` 入口,接受由宿主写出的 `RoadCompilerInput` JSON并继续输出唯一的 `NATIVE_ROAD_COMPILE_DONE` JSON 标记。
- [ ] 补全 `package.json`、锁文件和 `ol` 依赖workbench vendor 文件从自身安装目录提供。
- [ ] 迁入 `docs/native-road-package-v1.md`、两个 baseline 和它们的测试驱动,新增独立 README。
- [ ] 在不含宿主文件的临时 clone 内执行依赖安装、单元测试、两区域 parity 与 workbench 启动测试。
- [ ] grep 验证没有 `area-config``config/areas`、宿主项目名或宿主相对路径引用。
## 3. 宿主改为外部 CLI 消费
- [ ] 将 compiler 以精确 git tag `v0.1.0` 安装到宿主并记录 lockfile禁止范围版本、`latest``*` 和裸分支。
- [ ]`toRoadCompilerInput()` 序列化到每个区域的 pipeline staging 目录,传给 compiler CLI。
- [ ]`build-area.js` 的 blender 前置阶段调用 CLI。沿用管线的 `spawnSync` / `runCommand` 失败语义启动错误、exit status、signal和直通日志缓冲或 tee stdout 后,解析恰好一个完成标记并验证其 `areaId``output` 与当前区域匹配。
- [ ] 将 host workbench 的 fresh compile 回调切换到同一 CLI不改变 fresh-process 行为。
- [ ] 把所有 host 对 `packages/road-compiler/**` 的直接 import 替换成已安装包的公开 API 或 CLI两个区域 parity 均通过后删除 in-host 副本。
- [ ] 将宿主 pipeline spec 改为指向编译器仓库所拥有的契约文档。
## 4. 验证与发布门
新仓库(干净 clone
```bash
npm ci
npm test
npm run test:road-parity
npm run road:workbench -- --config test/fixtures/fengshu-er-road.input.json --no-compile
```
宿主两个有效区域的逐字节 parity
```bash
node scripts/road-parity.js --config config/areas/fengshu-er-road.json \
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/fengshu-er-road.json
node scripts/road-parity.js --config config/areas/nantaizi-lake-innovation-valley.json \
--compare .trellis/tasks/08-25-road-compiler-extraction/baseline/nantaizi-lake-innovation-valley.json
```
- [ ] 为 CLI 标记成功、缺失、重复、非法 JSON、areaId 不匹配和 output 不匹配补测试。
- [ ] 跑 package 单测、宿主 native-road / workbench / traffic signals / turn-lane-arrows 测试,以及两个 parity 检查。
- [ ] 使用 `trellis-check` 做最终跨层检查Blender 完整构建需以提权方式运行,原因是 macOS Metal 初始化不能在沙箱内启动。
## Rollback
- 外部仓库和已发布 tag 保留不动;在宿主 revert 消费改造即可回到 P1 边界。
- 若按决定临时保留 in-host 副本,可将依赖回指该副本以恢复;该退路不取代 host parity 验证。

View File

@@ -0,0 +1,62 @@
# Phase 2拆仓库
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
## Goal
把当前 `packages/road-compiler/` 拆为保留 git 历史、可脱离宿主独立测试和运行的仓库;宿主改为消费一个锁定版本的编译器 CLI。这样道路编译器可以独立演进宿主依然通过版本化文件契约稳定构建场景。
## Confirmed Facts
- P1 已完成:编译器实现和 workbench 的所有权在 `packages/road-compiler/`;宿主负责将区域配置映射为 `RoadCompilerInput`
- `git log --follow packages/road-compiler/src/compile/native-road.js` 已能追溯到 2026-08-13 的 native-road compiler 历史。
- 契约为 `native-road-package/v1`,完成标记为 `NATIVE_ROAD_COMPILE_DONE <json>``comparisonDir` 是可选输入,继续保留 `comparison.json`
- 当前宿主远端为内部 Git 服务 `https://git.app.que01.top/que01/osmWorkflow.git`P2 独立编译器仓库定为私有 `https://git.app.que01.top/que01/road-compiler.git`
- 可维护范围只有 `fengshu-er-road``nantaizi-lake-innovation-valley``hanyang-block` 是废案,不参与基线、测试、验收或迁移语料。
## Requirements
### R2.1 保留 Git 历史
-`git subtree split``git filter-repo` 导出,不得以复制目录再 `git init` 替代。
- 新仓库中 `git log --follow src/compile/native-road.js` 必须能追溯到 P1 之前的 native-road compiler 提交。
### R2.2 新仓库可独立运行
- 新仓库拥有 compiler、CLI、check、workbench、其测试 fixture、两个 parity baseline、契约文档和 README。
- 新仓库 own `ol` 及其 workbench 所需的运行依赖workbench 不得从宿主 `node_modules` 提供浏览器资源。
- 新仓库不得引用宿主的 `area-config``config/areas``scripts/` 或本仓库绝对路径。
### R2.3 宿主的消费边界
- `scripts/lib/area-config.js` 继续是唯一的区域配置归一化与 `RoadCompilerInput` 映射位置。
- `scripts/build-area.js` 用已安装编译器的 CLI 子进程编译,继承 stdout/stderr检查启动错误、退出状态和 signal并解析且校验唯一的 `NATIVE_ROAD_COMPILE_DONE` 标记。
- 宿主只依赖 CLI、写入的文件和版本化输入 JSON不再以相对路径 import 编译器内部模块。保留的宿主 signal 文件 I/O 适配层改为只调用公开包 API。
- `road-workbench` 保持每次编译均启动新进程的语义,但改为调用已安装 CLI。
### R2.4 版本与迁移
- 宿主依赖必须锁定一个具体编译器版本,禁止 `latest``*`;包版本与 `native-road-package/v1` 的契约版本独立演进。
- 初始分发使用带注释的 git tag `v0.1.0`;宿主锁定该 tag。完整 parity 验证后移除 `packages/road-compiler/`,不保留 in-host 副本作为正常消费路径。
- 宿主保留两份 baseline用来验证今后升级编译器版本后没有产物漂移。
## Acceptance Criteria
- [ ] AC2.1 独立仓库的 `git log --follow src/compile/native-road.js` 可见 P1 前的 compiler 历史。
- [ ] AC2.2 在未 clone 宿主的干净目录中,新仓库 `npm ci`(或等价锁文件安装)和 `npm test` 均通过。
- [ ] AC2.3 新仓库的两区域 parity`fengshu-er-road``nantaizi-lake-innovation-valley`)均与迁入 baseline 完全一致。
- [ ] AC2.4 宿主用锁版本依赖运行 CLI`build-area.js` 成功解析一个有效的 `NATIVE_ROAD_COMPILE_DONE` 标记,缺失、重复或非法标记会失败。
- [ ] AC2.5 宿主的两个区域 parity 均对保留 baseline 全绿。
- [ ] AC2.6 宿主 `package.json` 的 compiler 依赖是具体、可复现版本,不是 `latest``*` 或裸分支。
- [ ] AC2.7 编译器仓库对宿主反向依赖为 0且其 workbench 可独立启动。
## Out of Scope
- 编译器几何、规则或输出内容改动。
- 图层 manifest 和道路/建筑渲染分离Phase 3
- drawtonomy 扩展Phase 4
- `hanyang-block` 修复、迁移或作为验收样本。
## Release Decision
独立仓库为私有 `https://git.app.que01.top/que01/road-compiler.git`。首发以带注释 tag `v0.1.0` 发布,宿主依赖精确锁定该 tag两个区域 parity 均通过后删除 `packages/road-compiler/`。该决定授权 P2 在该内部远端创建和推送仓库,但不授权发布到 npm 或其他托管平台。

View File

@@ -0,0 +1,26 @@
{
"id": "rc-p2-repo-split",
"name": "rc-p2-repo-split",
"title": "Phase 2拆仓库",
"description": "保留 git 历史拆出独立仓库,宿主以锁版本依赖消费,子进程为主契约",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P1",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": "2026-08-26",
"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,5 @@
{"file":".trellis/spec/blender/module-structure.md","reason":"检查道路 adapter 与建筑/植被/水体模块的边界没有漂移。"}
{"file":".trellis/spec/blender/testing.md","reason":"检查 Blender 结构摘要、纯 Python schema 测试和提权运行要求。"}
{"file":".trellis/spec/pipeline/index.md","reason":"检查 compiler 输出与 Blender 消费的 manifest 数据流。"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"检查三层契约、材质槽和 source 文件名一致性。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"检查两个区域和 throwaway 图层验证后的产物差异。"}

View File

@@ -0,0 +1,61 @@
# Phase 3Manifest 驱动的道路渲染设计
## Boundary
道路编译器是图层事实源,输出 `<nativeRoadDir>/manifest.json`Blender 是渲染适配器,读取 manifest 并用宿主 `catalog.MATERIALS` / `ROAD_LAYERS` 完成材质与高度映射。建筑、植被、水体、OSM legacy 图层不改变。
```
compiler CLI
-> native-road/manifest.json + 12 declared GeoJSON sources
-> generate_scene.py
-> osmassets/native_roads.py
-> catalog material/z lookup
-> roads.assemble_geojson_layer
```
## Manifest v1.1
`manifest.json` 顶层包含 `contract: "native-road-package/v1.1"``areaId``layers``layers` 必须恰好声明编译器输出的 12 个 source
- `road_surface`, `intersection_surface`, `sidewalk_surface`, `edge_lines`, `lane_separators`, `center_lines`, `crosswalks`, `vehicle_stop_lines`, `direction_arrows`, `turn_arrows`
- `lane_centerlines`, `connectors`
每项字段:
```json
{
"source": "center_lines",
"role": "marking",
"materialLayer": "center_lines",
"splitBy": {
"prop": "color",
"cases": [
{"match": "white", "material": "native_center_line_white"},
{"default": true, "material": "center_lines"}
]
}
}
```
`role``surface``marking` 时必须有 `materialLayer``semantic` 不得有材质要求Blender 必须跳过其 GeoJSON。`splitBy` 只支持精确属性匹配和一个 default 分支,避免把渲染表达式语言复制进编译器。
编译器在原子提升前校验:每个声明 source 都对应 `layers/<source>.geojson`,目录中没有未声明的 GeoJSONmanifest 自身必须是有效 JSON。新增 manifest 不改变已有文件内容。
## Blender Adapter
新建 `blender/osmassets/native_roads.py`,公开一个接收 `native_road_dir`, `projector`, `collection`, `road_mats`, `material_layers` 的装配函数。模块负责读取/校验 manifest、解析 split cases、调用既有 `roads.assemble_geojson_layer`;不拥有图层注册表、不创建材质、不维护场景总计数。
`generate_scene.py` 仅负责 native-road 目录存在性、catalog material lookup、调用 adapter 和合并 `road_counts`。删除 `catalog.NATIVE_ROAD_LAYERS`;保留 `catalog.ROAD_LAYERS` 作为宿主材质和 z 高度事实源。
未知 `materialLayer`、缺少 source 文件、重复/遗漏 source、非法 role 或非法 splitBy 必须抛出 `RuntimeError`不能静默跳过。semantic 图层必须不读取 GeoJSON、不创建对象、不增加 road count。
## Compatibility and Rollback
编译器契约从 `native-road-package/v1` 增加到 `v1.1`manifest 是 v1.1 的必需输出。Blender 不提供旧 manifest fallback因为新消费边界必须尽早暴露旧 compiler回滚时恢复 `NATIVE_ROAD_LAYERS` 内联分支即可。旧 v1 产物仍可由旧 compiler tag 生成,不被本阶段改写。
## Verification
- 两个支持区域的 native-road 文件 parity除新增 `manifest.json` 外所有现有 hash、orderHash、bytes 不变。
- Blender 结构摘要和预览图与 Phase 0 基线一致;完整 Blender/Metal 运行需提权。
- 纯 Python schema/manifest 测试覆盖 12 source、semantic skip、splitBy white/yellow、未知材质、缺文件和额外文件。
- throwaway `debug_probe.geojson` + manifest 实测:不改宿主代码即可生成 Blender 几何;验证后删除 probe 并再次跑 parity。

View File

@@ -0,0 +1,5 @@
{"file":".trellis/spec/blender/module-structure.md","reason":"native_roads.py 必须遵守 osmassets 的 bpy 依赖边界和 roads adapter 责任。"}
{"file":".trellis/spec/blender/testing.md","reason":"Blender 层以结构摘要/parity 为主要回归防线,纯 schema 逻辑需可用系统 Python 测试。"}
{"file":".trellis/spec/pipeline/index.md","reason":"native-road manifest 是跨 compiler/Blender 的 pipeline 文件契约。"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"改动同时触及 compiler JSON、Blender Python 和契约文档。"}
{"file":".trellis/spec/guides/artifact-parity-guide.md","reason":"本阶段必须证明除新增 manifest 外渲染产物不变。"}

View File

@@ -0,0 +1,85 @@
# Phase 3 执行计划
## Step 1 — 枚举现有分流逻辑(定 schema 之前)
- [x]`blender/generate_scene.py:796-834` 全部道路分支
- [x] 列出每个 `source` 的处理方式:直接渲染 / 按属性分流 / 跳过
- [x] 已知分流:
- `center_lines``color != "white"``center_lines` 材质;
`color == "white"``native_center_line_white`
- `lane_separators``color != "yellow"``lane_separators`
`color == "yellow"``native_lane_separator_yellow`
- [x] 确认 `lane_centerlines` / `connectors` 确实未被消费(→ `role: "semantic"`
- [x] 产出 `research/native-layer-consumption-matrix.md`
**门槛**manifest schema 能表达矩阵里的每一行,无"特例硬编码"残留。
## Step 2 — 定 manifest schema 并落契约
- [x] 按父任务 `design.md` §3.2 定稿 schema
- [x] 写入编译器仓库契约文档,版本标为 `native-road-package/v1.1`
- [x] 记录v1.1 起 `manifest.json` 为必需输出
## Step 3 — 编译器侧:生成 manifest
- [x] 编译器内建立图层注册表(单一事实源),`compileGeometry` 的输出键与之对应
- [x]`manifest.json` 到 outDir
- [x] 编译器自校验manifest 声明的每个 `source` 都必须有对应 geojson 文件,
反之亦然(缺一即报错,不静默)
- [x] parity此步只**新增** manifest.json其余文件应逐字节不变hanyang-block 不参与
```bash
# 只应多出 manifest.json
node scripts/road-parity.js --config config/areas/fengshu-er-road.json \
--compare .../baseline/fengshu-er-road.json
```
## Step 4 — Blender 侧:抽模块 + 读 manifest
- [x] 新建 `blender/osmassets/native_roads.py`
- [x]`generate_scene.py:796-834` 的道路分支迁入
- [x] 改为读 manifest 遍历;`role == "semantic"` 跳过
- [x] `splitBy` 通用化实现,替掉两处硬编码 lambda
- [x] **加断言**manifest 里的 `materialLayer` 若不在 `catalog.MATERIALS` /
`ROAD_LAYERS` 中,直接 raise不静默跳过风险表第二条
- [x] 删除 `catalog.NATIVE_ROAD_LAYERS`
- [x] `generate_scene.py` 只保留一行调用
## Step 5 — 验证
```bash
# .blend 结构摘要对基线
npm run build:area -- --config config/areas/fengshu-er-road.json
# 走 artifact-parity-guide 的结构摘要比对
# 两个有效区域跑通hanyang-block 为废案,不参与)
for a in fengshu-er-road nantaizi-lake-innovation-valley; do
npm run build:area -- --config config/areas/$a.json || echo "FAIL $a"
done
# 建筑/植被/水体未被改动
git diff --stat -- blender/osmassets/ | grep -vE "native_roads|catalog"
```
- [x] 渲染预览图人工对照(结构摘要粒度不足的兜底)
## Step 6 — AC3.4 实测(可扩展性证明)
这一步是整个 Phase 3 的价值证明,不可跳过。
- [x] 编译器加一个 throwaway 图层 `debug_probe.geojson`(复用 road-surface 几何仅验证 wiring
+ manifest 声明 `role: "marking"`,复用现有材质槽
- [x] **不改宿主任何代码**,跑 blender 阶段
- [x] 确认 Blender 场景中出现该图层几何
- [x] 截图/记录证据到任务 `research/`
- [x] 回滚 throwaway 图层
## Review Gate
AC3.1AC3.7 全绿,特别是 AC3.4 有实测证据。
## Rollback
- 恢复 `catalog.NATIVE_ROAD_LAYERS``generate_scene.py` 恢复内联分支
- `manifest.json` 可保留输出(无消费方时无害)
- 编译器侧图层注册表保留(它本身是改进)

View File

@@ -0,0 +1,90 @@
# Phase 3道路与建筑渲染分离
父任务:`.trellis/tasks/08-25-road-compiler-extraction/`
技术设计:父任务 `design.md` §3含 manifest 形状与 Blender 侧改造方案)
## Goal
让编译器自声明图层Blender 改为读 manifest 而非硬编码表;
道路渲染从 `generate_scene.py` 抽离为独立模块,建筑/植被/水体保持原位。
**这是全案价值最高的一步** —— 它把一张跨仓库重复的表变成生成物,
使"编译器加图层忘了改宿主"这类静默错误在结构上不可能。
## 背景:当前的静默错误风险
`blender/osmassets/catalog.py:53``NATIVE_ROAD_LAYERS` 有 10 项,
而编译器写 12 个 geojson —— `lane_centerlines``connectors` 是语义层不参与渲染。
**但这个事实只存在于这张表的省略里,编译器侧没有任何声明。**
拆仓库之后,这张表和它描述的对象分处两个仓库,漂移只是时间问题。
`.trellis/spec/pipeline/index.md` 首页已经把图层表标为「最容易出静默错误」。
## Requirements
### R3.1 编译器输出 layer manifest
`<outDir>/manifest.json`,形状见父任务 `design.md` §3.2。要点:
- 每个图层声明 `source`geojson 文件名)、`role`、以及渲染角色所需的 `materialLayer`
- `role: "semantic"` 显式标记不渲染的图层 —— 把省略变成声明
- `splitBy` 表达按属性分流到不同材质的逻辑
(当前 `generate_scene.py:815-819` 硬编码的 `color != "white"` / `color == "yellow"`
- 材质本体颜色、z 高度、贴图)**仍归宿主** `catalog.py::MATERIALS`
编译器不懂渲染,只声明"我有哪些图层、什么角色、该用哪个材质槽"
### R3.2 Blender 侧改造
- `generate_scene.py` 内道路分支抽为 `blender/osmassets/native_roads.py`
- 新模块读 manifest 遍历,不再查硬编码表
- 删除 `catalog.NATIVE_ROAD_LAYERS`
- 建筑(`handle_building` / `dispatch_ways`)、植被、水体**保持原位不动**
### R3.3 契约版本处理
新增 `manifest.json` 是**向后兼容的增量**(旧消费方忽略未知文件即可),
但 Blender 改为依赖它之后就成了必需项。
- 契约文档记录:`manifest.json` 自 v1.1 起为必需输出
- 宿主声明所需最低契约版本
### R3.4 跨层一致性检查
`.trellis/spec/guides/cross-layer-thinking-guide.md`
本改动同时触碰 JS编译器输出、PythonBlender 消费)、契约文档三层,
必须走该指南的检查清单。
## Acceptance Criteria
- [x] AC3.1 编译器输出 `manifest.json`,两个有效区域(`fengshu-er-road``nantaizi-lake-innovation-valley`)均含全部 12 图层声明
- [x] AC3.2 `catalog.NATIVE_ROAD_LAYERS` 已删除Blender 无硬编码道路图层表
- [x] AC3.3 `.blend` 结构摘要对 Phase 0 基线一致
(走 `.trellis/spec/guides/artifact-parity-guide.md`
- [x] AC3.4 **实测**:向编译器加一个 throwaway 图层 + manifest 声明,
不改宿主任何代码,跑 blender 阶段确认它被渲染出来;验证后回滚该图层
(父任务 AC5见 design §3.4
- [x] AC3.5 `role: "semantic"` 的图层被 Blender 正确跳过(不产生几何)
- [x] AC3.6 `center_lines` 的 white/非 white 与 `lane_separators` 的 yellow/非 yellow
分流行为与改造前一致
- [x] AC3.7 建筑/植被/水体渲染代码未被修改(`git diff` 验证范围)
## 依赖与顺序
- **前置**Phase 2 完成编译器已独立manifest 才有"跨仓库契约"的意义)
- 与 Phase 4 无依赖,可并行
- 完成本阶段后,父任务 Goal 中的「道路与建筑渲染分离」才算达成
## 风险
| 风险 | 缓解 |
|---|---|
| `splitBy` 表达力不足,覆盖不了现有分流逻辑 | 先枚举 `generate_scene.py` 里全部分流分支,再定 manifest schema |
| 材质槽名在两侧漂移 | manifest 里的 `materialLayer` 必须能被宿主校验Blender 侧加断言,未知材质槽直接报错而非静默跳过 |
| `.blend` 结构摘要对比粒度不够,漏掉几何差异 | 除结构摘要外,加渲染预览图人工对照 |
## Out of Scope
- 材质定义搬去编译器(材质属渲染,归宿主)
- osm2streets legacy 图层表 `ROAD_LAYERS` 的改动
- 建筑/植被/水体渲染逻辑

View File

@@ -0,0 +1,19 @@
# P3 Debug Probe Evidence
Date: 2026-08-26
A temporary, uncommitted compiler registry entry declared `debug_probe` as a
`marking` layer using the existing `road_surface` material slot. Its generated
GeoJSON reused the compiled road-surface collection only for this wiring test.
No host source was changed.
The compiler emitted 13 declared sources. Blender then generated
`/private/tmp/p3-debug-probe.blend`; `SCENE_DONE` reported
`"debug_probe": 30`. The Blender scene digest contains:
```json
{"name":"Road_debug_probe","vertices":274,"polygons":30}
```
The temporary registry entry was removed immediately after the proof. The
standard v0.2.0 compiler was then rerun to restore the normal 12-source output.

View File

@@ -0,0 +1,34 @@
# Native Road 图层消费矩阵
调研日期2026-08-25
## 证据
- 编译器在 `scripts/compile-native-roads.js:57,65-76` 声明并写出 12 个图层。
- Blender 在 `blender/generate_scene.py:796-834` 遍历
`catalog.NATIVE_ROAD_LAYERS`,只消费其中 10 个图层。
- 当前映射及基础高度来自 `blender/osmassets/catalog.py:53-64``ROAD_LAYERS`
## 当前消费与 manifest 目标
| 编译器 source | 当前 Blender 行为 | 当前材质槽 / z 来源 | manifest 目标 |
|---|---|---|---|
| `road_surface` | 直接渲染 | `road_surface` / `ROAD_LAYERS.road_surface` | `role: surface`, `materialLayer: road_surface` |
| `edge_lines` | 直接渲染 | `lane_separators` / `ROAD_LAYERS.lane_separators` | `role: marking`, `materialLayer: lane_separators` |
| `intersection_surface` | 直接渲染 | `intersection_surface` / `ROAD_LAYERS.intersection_surface` | `role: surface`, `materialLayer: intersection_surface` |
| `sidewalk_surface` | 直接渲染 | `sidewalks` / `ROAD_LAYERS.sidewalks` | `role: surface`, `materialLayer: sidewalks` |
| `lane_separators` | 按 `color` 分流 | 非 yellow: `lane_separators`; yellow: `native_lane_separator_yellow`; z 取 `lane_separators` | `role: marking`, `materialLayer: lane_separators`, `splitBy.color`yellow 特例) |
| `center_lines` | 按 `color` 分流 | 非 white: `center_lines`; white: `native_center_line_white`; z 取 `center_lines` | `role: marking`, `materialLayer: center_lines`, `splitBy.color`white 特例) |
| `direction_arrows` | 直接渲染 | `lane_arrows_webscale` / `ROAD_LAYERS.lane_arrows_webscale` | `role: marking`, `materialLayer: lane_arrows_webscale` |
| `turn_arrows` | 直接渲染 | `lane_arrows_webscale` / `ROAD_LAYERS.lane_arrows_webscale` | `role: marking`, `materialLayer: lane_arrows_webscale` |
| `crosswalks` | 直接渲染 | `crosswalks` / `ROAD_LAYERS.crosswalks` | `role: marking`, `materialLayer: crosswalks` |
| `vehicle_stop_lines` | 直接渲染 | `vehicle_stop_lines` / `ROAD_LAYERS.vehicle_stop_lines` | `role: marking`, `materialLayer: vehicle_stop_lines` |
| `lane_centerlines` | 不进入 Blender 遍历 | 无 | `role: semantic`Blender 跳过 |
| `connectors` | 不进入 Blender 遍历 | 无 | `role: semantic`Blender 跳过 |
## 结论与实施约束
- manifest 必须声明全部 12 个编译器图层,不能再用“未出现在列表中”表达不渲染。
- `role: semantic` 的两个图层不得读取 GeoJSON 或创建 Blender 几何。
- `splitBy` 至少需表达属性 `color` 的精确匹配与默认分支,且分流后沿用同一个基础材质槽的 z 值。
- Blender 消费 manifest 时,未知 `materialLayer`、缺失 source 文件、或 source 未在 manifest 声明都必须报错;不能静默跳过。

View File

@@ -0,0 +1,26 @@
{
"id": "rc-p3-render-separation",
"name": "rc-p3-render-separation",
"title": "Phase 3道路与建筑渲染分离",
"description": "编译器输出 layer manifest 自声明图层Blender 改为读 manifest道路渲染从 generate_scene.py 抽离,消除跨仓库重复图层表",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "dingkang",
"assignee": "dingkang",
"createdAt": "2026-08-25",
"completedAt": "2026-08-26",
"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

@@ -8,8 +8,8 @@
<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 39
- **Last Active**: 2026-08-18
- **Total Sessions**: 45
- **Last Active**: 2026-08-26
<!-- @@@/auto:current-status -->
---
@@ -19,7 +19,7 @@
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~845 | Active |
| `journal-1.md` | ~976 | Active |
<!-- @@@/auto:active-documents -->
---
@@ -29,6 +29,12 @@
<!-- @@@auto:session-history -->
| # | Date | Title | Commits | Branch |
|---|------|-------|---------|--------|
| 45 | 2026-08-26 | Complete manifest-driven native road rendering | `6201270` | `experiment/road-compiler-rethink` |
| 44 | 2026-08-26 | Complete external road compiler split | `5cde080`, `7d4b3b5` | `experiment/road-compiler-rethink` |
| 43 | 2026-08-25 | Extract road compiler package boundary | `73707da`, `d5c58b5`, `f542a64`, `284d3db`, `2469cfd` | `experiment/road-compiler-rethink` |
| 42 | 2026-08-25 | Freeze native road parity baseline | `e7bc7f8` | `experiment/road-compiler-rethink` |
| 41 | 2026-08-24 | Use live V2X vehicle streams only | `b7031f1` | `fengshu-er-road` |
| 40 | 2026-08-24 | Stabilize Fengshu native road compilation | `f1f829c` | `fengshu-er-road` |
| 39 | 2026-08-18 | Complete native-only pipeline replacement | `9e0e25a` | `feature/native-road-compiler` |
| 38 | 2026-08-18 | Complete native traffic signal workflow | `5accc0a` | `feature/native-road-compiler` |
| 37 | 2026-08-18 | Complete rounded native junction verification | `e41bfd1`, `f7e71cf` | `feature/native-road-compiler` |

View File

@@ -843,3 +843,134 @@ Made native road compilation the default provider and full-build stage set; deco
### Status
[OK] **Completed**
## Session 40: Stabilize Fengshu native road compilation
**Date**: 2026-08-24
**Task**: Stabilize Fengshu native road compilation
**Branch**: `fengshu-er-road`
### Summary
Validated and committed the Fengshu Er Road native compilation fixes, including stale override tolerance, corrected unreferenced complex-junction sizing, updated OSM input, and focused road/traffic preview regression coverage.
### Git Commits
| Hash | Message |
|------|---------|
| `f1f829c` | (see git log) |
### Status
[OK] **Completed**
## Session 41: Use live V2X vehicle streams only
**Date**: 2026-08-24
**Task**: Use live V2X vehicle streams only
**Branch**: `fengshu-er-road`
### Summary
Replaced the Cesium preview's simulated vehicle route with authenticated OBU and target-vehicle V2X streams, converting GCJ-02 coordinates to WGS84 exactly once. Removed native traffic-simulation output, regenerated Fengshu Er Road preview, documented the no-fallback contract, and verified focused preview, V2X, budget, native-road, and signal tests.
### Git Commits
| Hash | Message |
|------|---------|
| `b7031f1` | (see git log) |
### Status
[OK] **Completed**
## Session 42: Freeze native road parity baseline
**Date**: 2026-08-25
**Task**: Freeze native road parity baseline
**Branch**: `experiment/road-compiler-rethink`
### Summary
Completed and archived Phase 0: documented native-road-package/v1, added strict road parity snapshots and two valid-area baselines, excluded abandoned hanyang-block, and verified deterministic recompilation including signal regeneration.
### Git Commits
| Hash | Message |
|------|---------|
| `e7bc7f8` | (see git log) |
### Status
[OK] **Completed**
## Session 43: Extract road compiler package boundary
**Date**: 2026-08-25
**Task**: Extract road compiler package boundary
**Branch**: `experiment/road-compiler-rethink`
### Summary
Completed and archived Phase 1: moved native road compiler, checks, workbench, signal schema, OSM parser, and fixtures into packages/road-compiler; introduced RoadCompilerInput mapping; verified two-area parity and a privileged full Fengshu build.
### Git Commits
| Hash | Message |
|------|---------|
| `73707da` | (see git log) |
| `d5c58b5` | (see git log) |
| `f542a64` | (see git log) |
| `284d3db` | (see git log) |
| `2469cfd` | (see git log) |
### Status
[OK] **Completed**
## Session 44: Complete external road compiler split
**Date**: 2026-08-26
**Task**: Complete external road compiler split
**Branch**: `experiment/road-compiler-rethink`
### Summary
Split road compiler into private road-compiler repository with history, pushed v0.1.2, migrated host to exact-tag CLI consumption, removed in-host package, passed regression tests and two-area parity.
### Git Commits
| Hash | Message |
|------|---------|
| `5cde080` | (see git log) |
| `7d4b3b5` | (see git log) |
### Status
[OK] **Completed**
## Session 45: Complete manifest-driven native road rendering
**Date**: 2026-08-26
**Task**: Complete manifest-driven native road rendering
**Branch**: `experiment/road-compiler-rethink`
### Summary
Published compiler v0.2.2, switched Blender to validated native-road manifests, proved debug-probe extensibility, and verified Blender parity for fengshu-er-road.
### Git Commits
| Hash | Message |
|------|---------|
| `6201270` | (see git log) |
### Status
[OK] **Completed**

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -55,6 +55,20 @@ npm run build:area -- --config config/areas/nantaizi-lake-innovation-valley.json
## 导出为 Cesium GLB
导出脚本会优先使用 `tqdm` 显示固定在终端底部的进度条、速率和 ETA。它是可选依赖未安装
时脚本自动回退为普通的 `CESIUM_EXPORT_PROGRESS` 日志,不会阻止导出。
macOS Blender 可用自带 Python 安装依赖:
```bash
/Applications/Blender.app/Contents/Resources/4.5/python/bin/python3.11 \
-m pip install -r blender/requirements.txt
```
若 Blender 安装路径或版本不同,使用对应的 `Resources/<version>/python/bin/python3.*`
安装后必须使用同一 Blender 安装启动导出脚本,系统 Python 中安装的 `tqdm` 不会自动被
Blender 发现。
```bash
/Applications/Blender.app/Contents/MacOS/Blender \
--background \

Some files were not shown because too many files have changed in this diff Show More