Compare commits
7 Commits
620127031e
...
experiment
| Author | SHA1 | Date | |
|---|---|---|---|
| a4dfbf884a | |||
| 82dcb83663 | |||
| 7e8fcd82ed | |||
| eaa7c08f6a | |||
| 113ea54535 | |||
| 009fe34f79 | |||
| 7883a451c5 |
@@ -47,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
|
||||
│
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Native Road Compiler Contract
|
||||
# Native Road Package Contract
|
||||
|
||||
The native-road package is maintained in the private `road-compiler` repository:
|
||||
|
||||
`https://git.app.que01.top/que01/road-compiler`
|
||||
|
||||
This host consumes the exact git tag recorded in `package.json` and lockfile. The host owns area configuration normalization and writes `RoadCompilerInput`; the compiler owns the `native-road-package/v1` file contract and CLI. Production code must invoke the installed compiler CLI through `scripts/lib/road-compiler-cli.js`, not import compiler source files or read `packages/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 compilation from `scripts/build-area.js`, `scripts/compile-native-roads.js`, and the road workbench fresh-compile callback.
|
||||
This contract applies to native-road consumption from `scripts/build-area.js`, diagnostics, parity, Blender, and native preview generation.
|
||||
|
||||
## Signatures
|
||||
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
"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-25-rc-p3-render-separation",
|
||||
"08-26-rc-p5-artifact-consumption"
|
||||
],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 产物结构。"}
|
||||
23
.trellis/tasks/08-26-rc-p5-artifact-consumption/design.md
Normal file
23
.trellis/tasks/08-26-rc-p5-artifact-consumption/design.md
Normal 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.
|
||||
@@ -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 的跨层数据流。"}
|
||||
14
.trellis/tasks/08-26-rc-p5-artifact-consumption/implement.md
Normal file
14
.trellis/tasks/08-26-rc-p5-artifact-consumption/implement.md
Normal 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.
|
||||
95
.trellis/tasks/08-26-rc-p5-artifact-consumption/prd.md
Normal file
95
.trellis/tasks/08-26-rc-p5-artifact-consumption/prd.md
Normal 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 耦合。
|
||||
29
.trellis/tasks/08-26-rc-p5-artifact-consumption/task.json
Normal file
29
.trellis/tasks/08-26-rc-p5-artifact-consumption/task.json
Normal 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": {}
|
||||
}
|
||||
1
.trellis/tasks/08-26-rc-p5a-zip-export/check.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5a-zip-export/check.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
22
.trellis/tasks/08-26-rc-p5a-zip-export/design.md
Normal file
22
.trellis/tasks/08-26-rc-p5a-zip-export/design.md
Normal 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.
|
||||
1
.trellis/tasks/08-26-rc-p5a-zip-export/implement.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5a-zip-export/implement.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
10
.trellis/tasks/08-26-rc-p5a-zip-export/implement.md
Normal file
10
.trellis/tasks/08-26-rc-p5a-zip-export/implement.md
Normal 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.
|
||||
34
.trellis/tasks/08-26-rc-p5a-zip-export/prd.md
Normal file
34
.trellis/tasks/08-26-rc-p5a-zip-export/prd.md
Normal 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
|
||||
项目中,不泄露为宿主依赖。
|
||||
26
.trellis/tasks/08-26-rc-p5a-zip-export/task.json
Normal file
26
.trellis/tasks/08-26-rc-p5a-zip-export/task.json
Normal 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": {}
|
||||
}
|
||||
1
.trellis/tasks/08-26-rc-p5b-zip-import/check.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5b-zip-import/check.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
19
.trellis/tasks/08-26-rc-p5b-zip-import/design.md
Normal file
19
.trellis/tasks/08-26-rc-p5b-zip-import/design.md
Normal 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.
|
||||
1
.trellis/tasks/08-26-rc-p5b-zip-import/implement.jsonl
Normal file
1
.trellis/tasks/08-26-rc-p5b-zip-import/implement.jsonl
Normal file
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
12
.trellis/tasks/08-26-rc-p5b-zip-import/implement.md
Normal file
12
.trellis/tasks/08-26-rc-p5b-zip-import/implement.md
Normal 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.
|
||||
40
.trellis/tasks/08-26-rc-p5b-zip-import/prd.md
Normal file
40
.trellis/tasks/08-26-rc-p5b-zip-import/prd.md
Normal 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` 可被导入;契约升级必须另开兼容任务。
|
||||
26
.trellis/tasks/08-26-rc-p5b-zip-import/task.json
Normal file
26
.trellis/tasks/08-26-rc-p5b-zip-import/task.json
Normal 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": {}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "rc-p3-render-separation",
|
||||
"title": "Phase 3:道路与建筑渲染分离",
|
||||
"description": "编译器输出 layer manifest 自声明图层,Blender 改为读 manifest;道路渲染从 generate_scene.py 抽离,消除跨仓库重复图层表",
|
||||
"status": "in_progress",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
@@ -11,7 +11,7 @@
|
||||
"creator": "dingkang",
|
||||
"assignee": "dingkang",
|
||||
"createdAt": "2026-08-25",
|
||||
"completedAt": null,
|
||||
"completedAt": "2026-08-26",
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<!-- @@@auto:current-status -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 44
|
||||
- **Total Sessions**: 45
|
||||
- **Last Active**: 2026-08-26
|
||||
<!-- @@@/auto:current-status -->
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<!-- @@@auto:active-documents -->
|
||||
| File | Lines | Status |
|
||||
|------|-------|--------|
|
||||
| `journal-1.md` | ~955 | Active |
|
||||
| `journal-1.md` | ~976 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
@@ -29,6 +29,7 @@
|
||||
<!-- @@@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` |
|
||||
|
||||
@@ -953,3 +953,24 @@ Split road compiler into private road-compiler repository with history, pushed v
|
||||
### 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**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"id": "fengshu-er-road",
|
||||
"input": "/Users/que01/osm2streets-qgis-workflow/inputs/osm/枫树二路.osm",
|
||||
"nativeRoadPackage": "../../inputs/native-road-packages/fengshu-er-road.zip",
|
||||
"outputRoot": "/Users/que01/osm2streets-qgis-workflow/outputs",
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
@@ -24,31 +25,6 @@
|
||||
"turnLaneArrows": {
|
||||
"enabled": false
|
||||
},
|
||||
"nativeRoad": {
|
||||
"edgeLines": false,
|
||||
"junctionTemplates": {
|
||||
"enabled": true,
|
||||
"references": [],
|
||||
"clusters": [
|
||||
{
|
||||
"id": "zhushanhu-fengshu-complex",
|
||||
"template": "complex-junction-v1",
|
||||
"referenceFile": "../../inputs/osm/珠山湖大道(枫树二路)口.geojson",
|
||||
"approachWidthMultiplier": 1.45,
|
||||
"approachLengthMeters": 32,
|
||||
"coreRadiusMeters": 28,
|
||||
"cornerRadiusMeters": 25,
|
||||
"outerRadiusExtraMeters": 24,
|
||||
"nodeIds": [
|
||||
"8005332807",
|
||||
"8024512135",
|
||||
"8024512145",
|
||||
"8024512147"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"osm2streets": {
|
||||
"debug_each_step": false,
|
||||
"dual_carriageway_experiment": false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"id": "nantaizi-lake-innovation-valley",
|
||||
"input": "/Users/que01/Desktop/南台子湖创新谷OSM.osm",
|
||||
"nativeRoadPackage": "../../inputs/native-road-packages/nantaizi-lake-innovation-valley.zip",
|
||||
"outputRoot": "/Users/que01/osm2streets-qgis-workflow/outputs",
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
@@ -24,9 +25,6 @@
|
||||
"turnLaneArrows": {
|
||||
"enabled": true
|
||||
},
|
||||
"nativeRoad": {
|
||||
"edgeLines": false
|
||||
},
|
||||
"osm2streets": {
|
||||
"debug_each_step": false,
|
||||
"dual_carriageway_experiment": false,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"id": "my-area",
|
||||
"input": "/absolute/path/to/input.osm",
|
||||
"nativeRoadPackage": "/absolute/path/to/exported-native-road-package.zip",
|
||||
"outputRoot": "/absolute/path/to/outputs",
|
||||
"qgisApp": "/Applications/QGIS.app",
|
||||
"blenderApp": "/Applications/Blender.app",
|
||||
@@ -24,14 +25,6 @@
|
||||
"turnLaneArrows": {
|
||||
"enabled": false
|
||||
},
|
||||
"nativeRoad": {
|
||||
"edgeLines": false,
|
||||
"junctionTemplates": {
|
||||
"enabled": false,
|
||||
"references": [],
|
||||
"clusters": []
|
||||
}
|
||||
},
|
||||
"osm2streets": {
|
||||
"debug_each_step": false,
|
||||
"dual_carriageway_experiment": false,
|
||||
|
||||
BIN
inputs/native-road-packages/fengshu-er-road.zip
Normal file
BIN
inputs/native-road-packages/fengshu-er-road.zip
Normal file
Binary file not shown.
BIN
inputs/native-road-packages/nantaizi-lake-innovation-valley.zip
Normal file
BIN
inputs/native-road-packages/nantaizi-lake-innovation-valley.zip
Normal file
Binary file not shown.
1
package-lock.json
generated
1
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^8.5.2",
|
||||
"@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.2.2",
|
||||
"fflate": "0.8.3",
|
||||
"ol": "^10.10.0",
|
||||
"osm2streets-js-node": "0.1.4"
|
||||
}
|
||||
|
||||
@@ -11,14 +11,10 @@
|
||||
"compress:glb": "node scripts/compress-glb.js",
|
||||
"diagnose:area": "node scripts/diagnose-area.js",
|
||||
"preflight:area": "node scripts/preflight-area.js",
|
||||
"road:compile": "node scripts/compile-native-roads.js",
|
||||
"reference:junction": "node scripts/inspect-junction-reference.js",
|
||||
"road:check": "node scripts/check-native-roads.js",
|
||||
"road:workbench": "node scripts/road-workbench.js",
|
||||
"serve:v2x-preview": "node scripts/v2x-preview-server.js",
|
||||
"test:road-workbench": "node scripts/test-road-workbench.js",
|
||||
"test:native-road": "node scripts/test-native-road.js",
|
||||
"test:road-compiler-cli": "node scripts/test-road-compiler-cli.js",
|
||||
"test:native-road-package": "node scripts/test-native-road-package.js",
|
||||
"test:road-parity": "node scripts/test-road-parity.js",
|
||||
"test:gaode-junction-reference": "node scripts/test-gaode-junction-reference.js",
|
||||
"test:preflight": "node scripts/test-area-preflight.js",
|
||||
@@ -38,6 +34,7 @@
|
||||
"dependencies": {
|
||||
"@inquirer/prompts": "^8.5.2",
|
||||
"@osm-asset/road-compiler": "git+https://git.app.que01.top/que01/road-compiler.git#v0.2.2",
|
||||
"fflate": "0.8.3",
|
||||
"ol": "^10.10.0",
|
||||
"osm2streets-js-node": "0.1.4"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { readAreaConfig } = require("./lib/area-config");
|
||||
const { compileArea: compileNativeRoads } = require("./lib/road-compiler-cli");
|
||||
const { importNativeRoadPackage } = require("./lib/native-road-package");
|
||||
const { resolveStages } = require("./lib/build-stages");
|
||||
const { validateManifest, addIntegrity } = require("./lib/package-contract");
|
||||
const { blenderExecutable: resolveBlenderExecutable } = require("./lib/tool-paths");
|
||||
@@ -225,10 +225,7 @@ function buildBlenderScene(area, roadProvider) {
|
||||
}
|
||||
fs.mkdirSync(path.dirname(area.outputs.blend), { recursive: true });
|
||||
fs.mkdirSync(path.dirname(area.outputs.render), { recursive: true });
|
||||
if (roadProvider === "native") {
|
||||
compileNativeRoads(area);
|
||||
ensureNativeRoadLayers(area);
|
||||
}
|
||||
const nativePackage = roadProvider === "native" ? importNativeRoadPackage(area) : null;
|
||||
|
||||
const blenderArgs = [
|
||||
"--background",
|
||||
@@ -246,8 +243,8 @@ function buildBlenderScene(area, roadProvider) {
|
||||
area.blender.treeStyle,
|
||||
];
|
||||
if (roadProvider === "native") {
|
||||
blenderArgs.push("--native-road", area.outputs.nativeRoadDir);
|
||||
blenderArgs.push("--traffic-signals", path.join(area.outputs.nativeRoadDir, "traffic-signals.json"));
|
||||
blenderArgs.push("--native-road", nativePackage.rootDir);
|
||||
blenderArgs.push("--traffic-signals", path.join(nativePackage.rootDir, "traffic-signals.json"));
|
||||
} else {
|
||||
blenderArgs.push("--geojson", area.outputs.geojsonDir);
|
||||
}
|
||||
@@ -270,7 +267,7 @@ function buildBlenderScene(area, roadProvider) {
|
||||
inputs: {
|
||||
config: fileRecord(configPath),
|
||||
osm: fileRecord(area.input),
|
||||
...(roadProvider === "native" ? nativeRoadRecords(area) : sceneGeojsonRecords(area)),
|
||||
...(roadProvider === "native" ? nativeRoadRecords(nativePackage) : sceneGeojsonRecords(area)),
|
||||
...(roadProvider === "osm2streets" ? {
|
||||
geojsonDir: fileRecord(area.outputs.geojsonDir),
|
||||
trafficSignalAssemblies: fileRecord(area.outputs.trafficSignalAssemblies),
|
||||
@@ -282,7 +279,7 @@ function buildBlenderScene(area, roadProvider) {
|
||||
render: fileRecord(area.outputs.render),
|
||||
},
|
||||
summary: {
|
||||
...(roadProvider === "native" ? { nativeRoad: nativeRoadFeatureCounts(area) } : { geojson: geojsonFeatureCounts(area) }),
|
||||
...(roadProvider === "native" ? { nativeRoad: nativeRoadFeatureCounts(nativePackage) } : { geojson: geojsonFeatureCounts(area) }),
|
||||
roadProvider,
|
||||
blendBytes: fileRecord(area.outputs.blend).bytes,
|
||||
renderBytes: fileRecord(area.outputs.render).bytes,
|
||||
@@ -291,17 +288,13 @@ function buildBlenderScene(area, roadProvider) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureNativeRoadLayers(area) {
|
||||
ensureFile(path.join(area.outputs.nativeRoadDir, "compiled.json"), "Native road compilation");
|
||||
for (const file of ["road_surface.geojson", "edge_lines.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson", "lane_separators.geojson", "center_lines.geojson", "direction_arrows.geojson", "turn_arrows.geojson", "crosswalks.geojson", "vehicle_stop_lines.geojson"]) {
|
||||
ensureFile(path.join(area.outputs.nativeRoadDir, "layers", file), `Native road layer ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
function nativeRoadRecords(area) {
|
||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
||||
function nativeRoadRecords(nativePackage) {
|
||||
const root = path.join(nativePackage.rootDir, "layers");
|
||||
return {
|
||||
nativeRoadCompiled: fileRecord(path.join(area.outputs.nativeRoadDir, "compiled.json")),
|
||||
nativeRoadZip: nativePackage.zipRecord,
|
||||
nativeRoadImport: fileRecord(nativePackage.rootDir),
|
||||
nativeRoadCompiled: fileRecord(path.join(nativePackage.rootDir, "compiled.json")),
|
||||
nativeRoadSignals: fileRecord(path.join(nativePackage.rootDir, "traffic-signals.json")),
|
||||
nativeRoadSurface: fileRecord(path.join(root, "road_surface.geojson")),
|
||||
nativeEdgeLines: fileRecord(path.join(root, "edge_lines.geojson")),
|
||||
nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")),
|
||||
@@ -315,8 +308,8 @@ function nativeRoadRecords(area) {
|
||||
};
|
||||
}
|
||||
|
||||
function nativeRoadFeatureCounts(area) {
|
||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
||||
function nativeRoadFeatureCounts(nativePackage) {
|
||||
const root = path.join(nativePackage.rootDir, "layers");
|
||||
return {
|
||||
roadSurface: featureCount(path.join(root, "road_surface.geojson")),
|
||||
edgeLines: featureCount(path.join(root, "edge_lines.geojson")),
|
||||
@@ -487,8 +480,9 @@ function compressCesiumGlb(area) {
|
||||
|
||||
function publishPackage(area, roadProvider) {
|
||||
ensureFile(area.outputs.packageStagingManifest, "Staged package manifest");
|
||||
const trafficSignalsSource = roadProvider === "native"
|
||||
? path.join(area.outputs.nativeRoadDir, "traffic-signals.json")
|
||||
const nativePackage = roadProvider === "native" ? importNativeRoadPackage(area) : null;
|
||||
const trafficSignalsSource = nativePackage
|
||||
? path.join(nativePackage.rootDir, "traffic-signals.json")
|
||||
: area.outputs.trafficSignals;
|
||||
ensureFile(trafficSignalsSource, "Traffic signal anchors");
|
||||
const started = Date.now();
|
||||
@@ -574,7 +568,7 @@ function writeCesiumPreview(area, roadProvider) {
|
||||
intersectionSurface: fileRecord(intersectionSurface),
|
||||
});
|
||||
} else {
|
||||
Object.assign(previewInputs, nativeRoadRecords(area));
|
||||
Object.assign(previewInputs, nativeRoadRecords(importNativeRoadPackage(area)));
|
||||
// This operational preview intentionally has no synthetic vehicle source.
|
||||
// A stale descriptor from an older build must not resurrect simulated routes.
|
||||
fs.rmSync(area.outputs.trafficSimulation, { force: true });
|
||||
|
||||
@@ -25,6 +25,10 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
const fileStem = outputOverrides.fileStem || id;
|
||||
const compress = normalizeCompressConfig(raw.compress);
|
||||
const budget = normalizeBudgetConfig(raw.budget);
|
||||
const roadProvider = roadProviderOption(raw.blender?.roadProvider ?? "native");
|
||||
if (roadProvider === "native" && raw.nativeRoadPackage === undefined) {
|
||||
throw new Error("Missing config key: nativeRoadPackage");
|
||||
}
|
||||
const pipelineDir = path.resolve(outputOverrides.pipelineDir || path.join(areaDir, "_pipeline"));
|
||||
const packageDir = path.resolve(outputOverrides.packageDir || path.join(areaDir, "package"));
|
||||
const packageStagingDir = path.resolve(outputOverrides.packageStagingDir || path.join(pipelineDir, "package-staging"));
|
||||
@@ -32,13 +36,9 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
const packageStagingRuntimeDir = path.resolve(outputOverrides.packageStagingRuntimeDir || path.join(packageStagingDir, "runtime"));
|
||||
const previewDir = path.resolve(outputOverrides.previewDir || path.join(areaDir, "_preview"));
|
||||
const geojsonDir = path.resolve(outputOverrides.geojsonDir || path.join(areaDir, "osm2streets_web_out"));
|
||||
const nativeRoadDir = path.resolve(outputOverrides.nativeRoadDir || path.join(areaDir, "native-road"));
|
||||
const outputs = {
|
||||
areaDir,
|
||||
geojsonDir,
|
||||
nativeRoadDir,
|
||||
nativeRoadOverrides: path.resolve(outputOverrides.nativeRoadOverrides || path.join(areaDir, "native-road-overrides.json")),
|
||||
nativeTrafficSignals: path.resolve(outputOverrides.nativeTrafficSignals || path.join(areaDir, "native-traffic-signals.json")),
|
||||
gpkg: path.resolve(outputOverrides.gpkg || path.join(areaDir, `${fileStem}.gpkg`)),
|
||||
qgisProject: path.resolve(outputOverrides.qgisProject || path.join(areaDir, `${fileStem}.qgz`)),
|
||||
qgisPreview: path.resolve(outputOverrides.qgisPreview || path.join(areaDir, `${fileStem}-preview.png`)),
|
||||
@@ -81,6 +81,9 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
|
||||
return {
|
||||
id,
|
||||
nativeRoadPackage: raw.nativeRoadPackage === undefined
|
||||
? null
|
||||
: path.resolve(options.configDir || repoRoot, requireText(raw.nativeRoadPackage, "nativeRoadPackage")),
|
||||
input,
|
||||
outputRoot,
|
||||
qgisApp: raw.qgisApp || (process.platform === "darwin" ? "/Applications/QGIS.app" : "/usr"),
|
||||
@@ -111,10 +114,6 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
turnLaneArrows: {
|
||||
enabled: booleanOption(raw.turnLaneArrows?.enabled, false, "turnLaneArrows.enabled"),
|
||||
},
|
||||
nativeRoad: {
|
||||
edgeLines: booleanOption(raw.nativeRoad?.edgeLines, false, "nativeRoad.edgeLines"),
|
||||
junctionTemplates: normalizeJunctionTemplates(raw.nativeRoad?.junctionTemplates, repoRoot, options.configDir || repoRoot),
|
||||
},
|
||||
osm2streets: raw.osm2streets || {
|
||||
debug_each_step: false,
|
||||
dual_carriageway_experiment: false,
|
||||
@@ -125,7 +124,7 @@ function normalizeAreaConfig(raw, options = {}) {
|
||||
blender: {
|
||||
treeStyle: raw.blender?.treeStyle || "natural",
|
||||
officeOverrides: raw.blender?.officeOverrides || raw.blender?.office_overrides || "",
|
||||
roadProvider: roadProviderOption(raw.blender?.roadProvider ?? "native"),
|
||||
roadProvider,
|
||||
},
|
||||
v2xPreview: normalizeV2xPreviewConfig(raw.v2xPreview),
|
||||
compress,
|
||||
@@ -272,24 +271,8 @@ function booleanOption(value, fallback, label) {
|
||||
throw new Error(`${label} must be boolean`);
|
||||
}
|
||||
|
||||
function toRoadCompilerInput(area) {
|
||||
return {
|
||||
areaId: area.id,
|
||||
osmFile: area.input,
|
||||
outDir: area.outputs.nativeRoadDir,
|
||||
stagingDir: area.outputs.pipelineDir,
|
||||
overridesFile: area.outputs.nativeRoadOverrides,
|
||||
trafficSignalsFile: area.outputs.nativeTrafficSignals,
|
||||
comparisonDir: area.outputs.geojsonDir,
|
||||
options: {
|
||||
edgeLines: area.nativeRoad.edgeLines,
|
||||
junctionTemplates: area.nativeRoad.junctionTemplates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
normalizeAreaConfig,
|
||||
readAreaConfig,
|
||||
toRoadCompilerInput,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { readAreaConfig } = require("./area-config");
|
||||
const { importNativeRoadPackage } = require("./native-road-package");
|
||||
const {
|
||||
SCENE_LAYERS,
|
||||
SCENE_FILE,
|
||||
@@ -324,6 +325,11 @@ function addEndpoint(map, ref) {
|
||||
|
||||
function artifactStatus(area) {
|
||||
const native = area.blender.roadProvider === "native";
|
||||
let nativePackage = null;
|
||||
let nativePackageError = null;
|
||||
if (native) {
|
||||
try { nativePackage = importNativeRoadPackage(area); } catch (error) { nativePackageError = error.message; }
|
||||
}
|
||||
const entries = [
|
||||
...(native ? [] : [
|
||||
["GeoJSON dir", area.outputs.geojsonDir, true, "dir"],
|
||||
@@ -332,15 +338,16 @@ function artifactStatus(area) {
|
||||
["QGIS preview", area.outputs.qgisPreview, true, "file"],
|
||||
["Traffic signal assemblies", area.outputs.trafficSignalAssemblies, true, "file"],
|
||||
]),
|
||||
["Native road directory", area.outputs.nativeRoadDir, native, "dir"],
|
||||
["Traffic signal runtime", native ? path.join(area.outputs.nativeRoadDir, "traffic-signals.json") : area.outputs.packageTrafficSignals, true, "file"],
|
||||
["Native road ZIP", area.nativeRoadPackage, native, "file"],
|
||||
["Native road import", nativePackage?.rootDir || path.join(area.outputs.pipelineDir, "native-road-import"), native, "dir"],
|
||||
["Traffic signal runtime", native ? path.join(nativePackage?.rootDir || "", "traffic-signals.json") : area.outputs.packageTrafficSignals, true, "file"],
|
||||
["Blend scene", area.outputs.blend, true, "file"],
|
||||
["Render PNG", area.outputs.render, true, "file"],
|
||||
["Package manifest", area.outputs.packageManifest, true, "file"],
|
||||
["Package primary GLB", area.outputs.packagePrimaryGlb, true, "file"],
|
||||
["Cesium preview", area.outputs.cesiumPreview, true, "file"],
|
||||
];
|
||||
return entries.map(([label, file, expected, type]) => {
|
||||
const result = entries.map(([label, file, expected, type]) => {
|
||||
const exists = fs.existsSync(file);
|
||||
const stat = exists ? fs.statSync(file) : null;
|
||||
const validType = !exists || (type === "dir" ? stat.isDirectory() : stat.isFile());
|
||||
@@ -358,6 +365,8 @@ function artifactStatus(area) {
|
||||
geojsonFiles,
|
||||
};
|
||||
});
|
||||
if (nativePackageError) result.push({ label: "Native road ZIP validation", path: area.nativeRoadPackage, expected: true, type: "file", exists: false, validType: false, error: nativePackageError });
|
||||
return result;
|
||||
}
|
||||
|
||||
function metadataSummary(file, warnings) {
|
||||
@@ -378,6 +387,10 @@ function metadataSummary(file, warnings) {
|
||||
|
||||
function stageManifestStatus(area, configPath = null) {
|
||||
const native = area.blender.roadProvider === "native";
|
||||
let nativePackage = null;
|
||||
if (native) {
|
||||
try { nativePackage = importNativeRoadPackage(area); } catch (_) { /* artifact status reports the validation failure */ }
|
||||
}
|
||||
const compressionComplete = fs.existsSync(stageManifestPath(area, "compress"));
|
||||
const reimportManifest = stageManifestPath(area, "reimport");
|
||||
const hasReimportManifest = fs.existsSync(reimportManifest);
|
||||
@@ -435,7 +448,7 @@ function stageManifestStatus(area, configPath = null) {
|
||||
inputs: {
|
||||
...(configPath ? { config: configPath } : {}),
|
||||
osm: area.input,
|
||||
...(native ? nativeRoadRecords(area) : {
|
||||
...(native && nativePackage ? nativeRoadRecords(nativePackage) : {
|
||||
geojsonDir: area.outputs.geojsonDir,
|
||||
...sceneGeojsonFiles(area),
|
||||
trafficSignalAssemblies: area.outputs.trafficSignalAssemblies,
|
||||
@@ -468,18 +481,16 @@ function stageManifestStatus(area, configPath = null) {
|
||||
glb: area.outputs.glb,
|
||||
metadata: area.outputs.metadata,
|
||||
}),
|
||||
...(native ? nativeRoadRecords(area) : {
|
||||
...(native && nativePackage ? nativeRoadRecords(nativePackage) : {
|
||||
lanePolygons: path.join(area.outputs.geojsonDir, "lane_polygons.geojson"),
|
||||
network: path.join(area.outputs.geojsonDir, "network.json"),
|
||||
intersectionSurface: path.join(area.outputs.geojsonDir, "intersection_surface.geojson"),
|
||||
}),
|
||||
...(native ? { trafficSimulation: area.outputs.trafficSimulation } : {}),
|
||||
previewCss: path.join(path.resolve(__dirname, ".."), "lib", "cesium-preview.css"),
|
||||
previewJs: path.join(path.resolve(__dirname, ".."), "lib", "cesium-preview.js"),
|
||||
},
|
||||
outputs: compressionComplete ? {
|
||||
vehicleRoute: native ? optionalExpectedFile(area.outputs.vehicleRoute) : area.outputs.vehicleRoute,
|
||||
...(native ? { trafficSimulation: area.outputs.trafficSimulation } : {}),
|
||||
vehicleModel: area.outputs.vehicleModel,
|
||||
} : {
|
||||
cesiumPreview: area.outputs.cesiumPreview,
|
||||
@@ -609,11 +620,13 @@ function sceneGeojsonFiles(area) {
|
||||
return files;
|
||||
}
|
||||
|
||||
function nativeRoadRecords(area) {
|
||||
const root = path.join(area.outputs.nativeRoadDir, "layers");
|
||||
function nativeRoadRecords(nativePackage) {
|
||||
const root = path.join(nativePackage.rootDir, "layers");
|
||||
return {
|
||||
nativeRoadCompiled: path.join(area.outputs.nativeRoadDir, "compiled.json"),
|
||||
nativeRoadSignals: path.join(area.outputs.nativeRoadDir, "traffic-signals.json"),
|
||||
nativeRoadZip: nativePackage.zipFile,
|
||||
nativeRoadImport: nativePackage.rootDir,
|
||||
nativeRoadCompiled: path.join(nativePackage.rootDir, "compiled.json"),
|
||||
nativeRoadSignals: path.join(nativePackage.rootDir, "traffic-signals.json"),
|
||||
nativeRoadSurface: path.join(root, "road_surface.geojson"),
|
||||
nativeEdgeLines: path.join(root, "edge_lines.geojson"),
|
||||
nativeIntersectionSurface: path.join(root, "intersection_surface.geojson"),
|
||||
|
||||
@@ -19,7 +19,8 @@ const DEFAULT_SETTINGS = {
|
||||
};
|
||||
|
||||
function buildNativeTrafficSimulation(area, options = {}) {
|
||||
const root = area.outputs.nativeRoadDir;
|
||||
const root = options.nativePackage?.rootDir;
|
||||
if (!root) throw new Error("Native traffic simulation requires an imported native road package");
|
||||
const compiledPath = path.join(root, "compiled.json");
|
||||
const signalPath = path.join(root, "traffic-signals.json");
|
||||
const connectorPath = path.join(root, "layers", "connectors.geojson");
|
||||
|
||||
87
scripts/lib/native-road-package.js
Normal file
87
scripts/lib/native-road-package.js
Normal file
@@ -0,0 +1,87 @@
|
||||
"use strict";
|
||||
|
||||
const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { unzipSync } = require("fflate");
|
||||
|
||||
const CONTRACT = "native-road-package/v1.1";
|
||||
const ROOT_FILES = ["manifest.json", "compiled.json", "diagnostics.json", "comparison.json", "traffic-signal-assemblies.json", "traffic-signals.json"];
|
||||
|
||||
function sha256(file) { return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); }
|
||||
|
||||
function readCentralEntries(bytes) {
|
||||
const entries = [];
|
||||
for (let i = 0; i + 46 <= bytes.length; i += 1) {
|
||||
if (bytes.readUInt32LE(i) !== 0x02014b50) continue;
|
||||
const nameLength = bytes.readUInt16LE(i + 28);
|
||||
const extraLength = bytes.readUInt16LE(i + 30);
|
||||
const commentLength = bytes.readUInt16LE(i + 32);
|
||||
const name = bytes.subarray(i + 46, i + 46 + nameLength).toString("utf8");
|
||||
const externalAttrs = bytes.readUInt32LE(i + 38);
|
||||
entries.push({ name, externalAttrs, directory: name.endsWith("/") });
|
||||
i += 45 + nameLength + extraLength + commentLength;
|
||||
}
|
||||
if (!entries.length) throw new Error("ZIP central directory is missing");
|
||||
return entries;
|
||||
}
|
||||
|
||||
function validateName(name) {
|
||||
if (!name || path.posix.isAbsolute(name) || name.split("/").includes("..") || name.includes("\\")) throw new Error(`Unsafe ZIP entry path: ${name}`);
|
||||
if (name.startsWith("./") || (name.includes("/") && !name.startsWith("layers/")) || (name.startsWith("layers/") && name.split("/").length !== 2)) throw new Error(`Native road ZIP must be root-flat: ${name}`);
|
||||
}
|
||||
|
||||
function validateManifest(rootDir, areaId) {
|
||||
let manifest;
|
||||
try { manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "manifest.json"), "utf8")); } catch (error) { throw new Error(`Invalid native road manifest: ${error.message}`); }
|
||||
if (!manifest || manifest.contract !== CONTRACT) throw new Error(`Unsupported native road contract: ${manifest?.contract || "missing"}`);
|
||||
if (manifest.areaId !== areaId) throw new Error(`Native road areaId mismatch: expected ${areaId}, got ${manifest.areaId}`);
|
||||
if (!Array.isArray(manifest.layers) || !manifest.layers.length) throw new Error("Native road manifest layers are missing");
|
||||
const declared = new Set();
|
||||
for (const layer of manifest.layers) {
|
||||
if (!layer || typeof layer.source !== "string" || layer.source !== path.posix.basename(layer.source) || declared.has(layer.source)) throw new Error("Invalid native road layer declaration");
|
||||
if (!["surface", "marking", "semantic"].includes(layer.role)) throw new Error(`Invalid native road layer role: ${layer.role}`);
|
||||
if (layer.role !== "semantic" && typeof layer.materialLayer !== "string") throw new Error(`Renderable layer ${layer.source} has no materialLayer`);
|
||||
declared.add(layer.source);
|
||||
}
|
||||
const published = new Set(fs.readdirSync(path.join(rootDir, "layers")).filter((name) => name.endsWith(".geojson")).map((name) => name.slice(0, -8)));
|
||||
if (declared.size !== published.size || [...declared].some((name) => !published.has(name))) throw new Error("Native road manifest/source mismatch");
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function importNativeRoadPackage(area) {
|
||||
const zipFile = area.nativeRoadPackage;
|
||||
if (!zipFile || !fs.existsSync(zipFile) || !fs.statSync(zipFile).isFile()) throw new Error(`Native road ZIP not found: ${zipFile || "missing nativeRoadPackage"}`);
|
||||
const bytes = fs.readFileSync(zipFile);
|
||||
const hash = crypto.createHash("sha256").update(bytes).digest("hex");
|
||||
const entries = readCentralEntries(bytes);
|
||||
const names = new Set();
|
||||
for (const entry of entries) {
|
||||
validateName(entry.name);
|
||||
if (names.has(entry.name)) throw new Error(`Duplicate ZIP entry: ${entry.name}`);
|
||||
names.add(entry.name);
|
||||
const unixMode = entry.externalAttrs >>> 16;
|
||||
if ((unixMode & 0xf000) === 0xa000) throw new Error(`Symbolic links are not allowed: ${entry.name}`);
|
||||
}
|
||||
let files;
|
||||
try { files = unzipSync(bytes); } catch (error) { throw new Error(`Invalid ZIP archive: ${error.message}`); }
|
||||
const expected = new Set([...ROOT_FILES, ...[...names].filter((name) => name.startsWith("layers/") && name.endsWith(".geojson"))]);
|
||||
for (const name of ROOT_FILES) if (!names.has(name)) throw new Error(`Missing native road ZIP entry: ${name}`);
|
||||
for (const name of names) if (!expected.has(name) || (!name.startsWith("layers/") && !ROOT_FILES.includes(name))) throw new Error(`Unexpected native road ZIP entry: ${name}`);
|
||||
const rootDir = path.join(area.outputs.pipelineDir, "native-road-import", hash);
|
||||
if (!fs.existsSync(rootDir)) {
|
||||
fs.mkdirSync(path.join(rootDir, "layers"), { recursive: true });
|
||||
for (const [name, data] of Object.entries(files)) {
|
||||
if (!names.has(name) || name.endsWith("/")) continue;
|
||||
const destination = path.join(rootDir, name);
|
||||
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
||||
fs.writeFileSync(destination, data);
|
||||
}
|
||||
}
|
||||
const manifest = validateManifest(rootDir, area.id);
|
||||
const records = {};
|
||||
for (const name of names) { const file = path.join(rootDir, name); if (fs.existsSync(file) && fs.statSync(file).isFile()) records[name] = { path: file, bytes: fs.statSync(file).size, sha256: sha256(file) }; }
|
||||
return Object.freeze({ zipFile, zipRecord: { path: zipFile, bytes: bytes.length, sha256: hash }, rootDir, manifest, records });
|
||||
}
|
||||
|
||||
module.exports = { CONTRACT, importNativeRoadPackage };
|
||||
@@ -5,6 +5,7 @@ const crypto = require("crypto");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { readAreaConfig } = require("./lib/area-config");
|
||||
const { importNativeRoadPackage } = require("./lib/native-road-package");
|
||||
|
||||
const repoRoot = path.resolve(__dirname, "..");
|
||||
|
||||
@@ -77,10 +78,11 @@ function filesBelow(directory) {
|
||||
|
||||
function snapshot(configPath) {
|
||||
const area = readAreaConfig(configPath, { repoRoot });
|
||||
if (!fs.existsSync(area.outputs.nativeRoadDir)) throw new Error(`Native road output not found: ${area.outputs.nativeRoadDir}`);
|
||||
const nativePackage = importNativeRoadPackage(area);
|
||||
const nativeRoadDir = nativePackage.rootDir;
|
||||
const files = {};
|
||||
for (const file of filesBelow(area.outputs.nativeRoadDir).sort()) {
|
||||
const relative = path.relative(area.outputs.nativeRoadDir, file).split(path.sep).join("/");
|
||||
for (const file of filesBelow(nativeRoadDir).sort()) {
|
||||
const relative = path.relative(nativeRoadDir, file).split(path.sep).join("/");
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
files[relative] = jsonRecord(text, Buffer.byteLength(text));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ assert.match(areaBuildSource, /exportCesium\(area, roadProvider\)/);
|
||||
assert.match(areaBuildSource, /"--dynamic-glb", area\.outputs\.trafficSignalsDynamicGlb/);
|
||||
assert.match(areaBuildSource, /trafficSignalsCountdown0Glb: fileRecord/);
|
||||
assert.match(areaBuildSource, /if \(roadProvider === "osm2streets"\) \{[\s\S]*?buildPreviewVehicleRoute/);
|
||||
assert.match(areaBuildSource, /Object\.assign\(previewInputs, nativeRoadRecords\(area\)\)/);
|
||||
assert.match(areaBuildSource, /Object\.assign\(previewInputs, nativeRoadRecords\(/);
|
||||
assert.match(areaBuildSource, /vehicleRoute: optionalFileRecord\(area\.outputs\.vehicleRoute\)/);
|
||||
|
||||
const gltf = {
|
||||
@@ -48,7 +48,7 @@ assert.equal(evaluateGlbBudget(digest, budget).violations[0].key, "glbBytes");
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "asset-budget-"));
|
||||
const input = path.join(tempDir, "input.osm");
|
||||
fs.writeFileSync(input, "<osm/>");
|
||||
const base = { id: "test-area", input, outputRoot: tempDir };
|
||||
const base = { id: "test-area", input, outputRoot: tempDir, blender: { roadProvider: "osm2streets" } };
|
||||
assert.equal(
|
||||
normalizeAreaConfig(base).outputs.trafficSignals,
|
||||
path.join(tempDir, "test-area", "osm2streets_web_out", "traffic_signals.json"),
|
||||
@@ -63,7 +63,7 @@ assert.equal(
|
||||
);
|
||||
assert.equal(normalizeAreaConfig({ ...base, budget: { nodes: 800 } }).budget.glbNodes, 800);
|
||||
assert.equal(normalizeAreaConfig(base).stages.intermediates, false);
|
||||
assert.equal(normalizeAreaConfig(base).blender.roadProvider, "native");
|
||||
assert.equal(normalizeAreaConfig(base).blender.roadProvider, "osm2streets");
|
||||
assert.deepEqual(normalizeAreaConfig(base).v2xPreview, {
|
||||
enabled: false,
|
||||
apiBaseUrl: "/api",
|
||||
@@ -80,7 +80,7 @@ assert.throws(
|
||||
() => normalizeAreaConfig({ ...base, v2xPreview: "enabled" }),
|
||||
/v2xPreview must be an object/,
|
||||
);
|
||||
assert.equal(normalizeAreaConfig({ ...base, blender: { roadProvider: "native" } }).blender.roadProvider, "native");
|
||||
assert.equal(normalizeAreaConfig({ ...base, nativeRoadPackage: path.join(tempDir, "roads.zip"), blender: { roadProvider: "native" } }).blender.roadProvider, "native");
|
||||
assert.throws(
|
||||
() => normalizeAreaConfig({ ...base, blender: { roadProvider: "other" } }),
|
||||
/blender\.roadProvider/,
|
||||
|
||||
@@ -25,7 +25,7 @@ fs.writeFileSync(path.join(layersDir, "connectors.geojson"), JSON.stringify({ ty
|
||||
fs.writeFileSync(path.join(layersDir, "vehicle_stop_lines.geojson"), JSON.stringify({ type: "FeatureCollection", features: [{ type: "Feature", properties: { road_id: "road:a" }, geometry: { type: "Polygon", coordinates: [] } }] }));
|
||||
fs.writeFileSync(path.join(layersDir, "lane_centerlines.geojson"), JSON.stringify({ type: "FeatureCollection", features: roads.map((road) => ({ type: "Feature", properties: { road_id: road.id, lane_index: 1 }, geometry: { type: "LineString", coordinates: road.centerline } })) }));
|
||||
|
||||
const descriptor = buildNativeTrafficSimulation({ id: "fixture", outputs: { areaDir, nativeRoadDir } });
|
||||
const descriptor = buildNativeTrafficSimulation({ id: "fixture", outputs: { areaDir } }, { nativePackage: { rootDir: nativeRoadDir } });
|
||||
assert.equal(descriptor.schema, SCHEMA);
|
||||
assert.equal(descriptor.coordinateSystem.route, "WGS84");
|
||||
assert.ok(descriptor.routes.length >= 1);
|
||||
|
||||
50
scripts/test-native-road-package.js
Normal file
50
scripts/test-native-road-package.js
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const { zipSync } = require("fflate");
|
||||
const { importNativeRoadPackage } = require("./lib/native-road-package");
|
||||
|
||||
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "native-road-package-"));
|
||||
const area = { id: "fixture", nativeRoadPackage: "", outputs: { pipelineDir: path.join(temp, "pipeline") } };
|
||||
const layers = ["road_surface", "connectors"];
|
||||
const manifest = { contract: "native-road-package/v1.1", areaId: "fixture", layers: [
|
||||
{ source: "road_surface", role: "surface", materialLayer: "road_surface" },
|
||||
{ source: "connectors", role: "semantic" },
|
||||
] };
|
||||
function validEntries() {
|
||||
return {
|
||||
"manifest.json": Buffer.from(JSON.stringify(manifest)),
|
||||
"compiled.json": Buffer.from("{}"), "diagnostics.json": Buffer.from("{}"), "comparison.json": Buffer.from("{}"),
|
||||
"traffic-signal-assemblies.json": Buffer.from("{}"), "traffic-signals.json": Buffer.from("{}"),
|
||||
...Object.fromEntries(layers.map((layer) => [`layers/${layer}.geojson`, Buffer.from('{"type":"FeatureCollection","features":[]}')])),
|
||||
};
|
||||
}
|
||||
function write(name, entries = validEntries()) { const file = path.join(temp, name); fs.writeFileSync(file, zipSync(entries)); area.nativeRoadPackage = file; return file; }
|
||||
write("valid.zip");
|
||||
const first = importNativeRoadPackage(area);
|
||||
assert.equal(first.manifest.areaId, "fixture");
|
||||
assert.equal(Object.keys(first.records).length, 8);
|
||||
assert.ok(first.rootDir.includes(first.zipRecord.sha256));
|
||||
assert.equal(importNativeRoadPackage(area).rootDir, first.rootDir);
|
||||
assert.throws(() => { area.nativeRoadPackage = path.join(temp, "missing.zip"); importNativeRoadPackage(area); }, /ZIP not found/);
|
||||
write("corrupt.zip", { "manifest.json": Buffer.from("{") });
|
||||
assert.throws(() => importNativeRoadPackage(area), /ZIP central directory|Invalid ZIP|Invalid native road manifest|Missing native road ZIP entry/);
|
||||
write("nested.zip", { "nested/manifest.json": Buffer.from("{}") });
|
||||
assert.throws(() => importNativeRoadPackage(area), /root-flat/);
|
||||
write("traversal.zip", { "../manifest.json": Buffer.from("{}") });
|
||||
assert.throws(() => importNativeRoadPackage(area), /Unsafe ZIP entry/);
|
||||
const wrongContract = validEntries(); wrongContract["manifest.json"] = Buffer.from(JSON.stringify({ ...manifest, contract: "v1" })); write("contract.zip", wrongContract);
|
||||
assert.throws(() => importNativeRoadPackage(area), /Unsupported native road contract/);
|
||||
const wrongArea = validEntries(); wrongArea["manifest.json"] = Buffer.from(JSON.stringify({ ...manifest, areaId: "other" })); write("area.zip", wrongArea);
|
||||
assert.throws(() => importNativeRoadPackage(area), /areaId mismatch/);
|
||||
const mismatch = validEntries(); delete mismatch["layers/connectors.geojson"]; write("mismatch.zip", mismatch);
|
||||
assert.throws(() => importNativeRoadPackage(area), /manifest\/source mismatch/);
|
||||
const symlink = write("symlink.zip");
|
||||
const data = fs.readFileSync(symlink); const index = data.indexOf(Buffer.from([0x50, 0x4b, 0x01, 0x02])); data.writeUInt32LE((0xa000 << 16) >>> 0, index + 38); fs.writeFileSync(symlink, data);
|
||||
assert.throws(() => importNativeRoadPackage(area), /Symbolic links/);
|
||||
fs.rmSync(temp, { recursive: true, force: true });
|
||||
console.log("Native road ZIP package tests passed.");
|
||||
Reference in New Issue
Block a user