Files
osmWorkflow/.trellis/spec/guides/artifact-parity-guide.md

202 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 产物一致性Parity指南
> **触发条件**:任何声称"纯重构、产物不变"的改动。
>
> 这条管线的产物是 `.blend` / `.glb` / `.png`——**二进制、无法 code review、
> 肉眼看不出 5% 的几何漂移**。parity 校验是这一层唯一的回归防线。
---
## 为什么不能直接比字节
三类文件全都**不是位级可复现**的,同一份代码跑两次就会不一样:
| 产物 | 为什么不稳定 |
|---|---|
| `.blend` | 内嵌绝对路径;图片按哈希表顺序打包 |
| `.png` | EEVEE 渲染非位级可复现 |
| `.glb` | glTF 导出器会去重相同的 accessor`smart_project` 的 UV 带浮点噪声——实测两次跑出 399 vs 398 个 accessor、差 720 字节,而 node/mesh/primitive/material/image **完全一致** |
所以比对的是**结构摘要**,不是字节。
---
## 三件套
| 工具 | 位置 | 作用 |
|---|---|---|
| 场景摘要 | `blender/tools/scene_digest.py` | 在 Blender 内打开 `.blend`,输出稳定 JSON对象名/顶点数/面数/材质槽/自定义属性、材质参数、场景属性 |
| GLB 摘要 | `scripts/glb-digest.js` | 纯 Node 读 GLB 的 JSON chunk输出 node/mesh/material 清单与 PBR 参数,附 buffer 字节长度 |
| 驱动 | `scripts/parity.js` | 跑构建 → 采集摘要 → 快照 / 比对 |
```bash
node scripts/parity.js capture <label> [--areas a,b] [--stages blender,cesium]
node scripts/parity.js compare <labelA> <labelB>
```
基线落在 `outputs/_refactor-baseline/<label>/`,在 `.gitignore` 里——
**本地草稿,不是产物,不入库**`parity.js:16-17`)。
默认样本区域两个(`parity.js:26``nantaizi-lake-innovation-valley`(主,
OSM + osm2streets GeoJSON 齐全)、`hanyang-block`(次)。
---
## 必须先做 control 实验
**这是最容易被跳过、跳过之后整个校验就是假的一步。**
用**未改动**的代码连跑两次diff 两份摘要。这一步确定哪些字段天然不确定,
把它们列入忽略名单。
```bash
node scripts/parity.js capture control-1
node scripts/parity.js capture control-2
node scripts/parity.js compare control-1 control-2 # 必须全绿
```
没做这步就开始改代码,你会分不清一个差异是"重构引入的 bug"还是"本来就每次都不一样"。
已完成的 control 结论(`docs/refactor-plan.md`
- `.blend` **结构摘要两次完全一致** ← 这是主校验信号,可信
- `.blend` 文件 sha256 不一致
- 渲染 PNG sha256 不一致
- GLB 结构node / mesh / primitive / material / image两次完全一致
但 accessor 数 399 vs 398、buffer 差 720 字节
---
## 忽略名单:必须附理由
`parity.js:25-54``IGNORED_PATHS`,每一条上面都写着为什么被忽略:
```
files.blend.sha256 / files.glb.{sha256,bytes} / files.render.{sha256,bytes}
glbDigest.fileBytes / glbDigest.buffers / glbDigest.counts.accessors
capturedAt / durationMs / label
```
这些字段**仍然被记录**——人读快照时想看到它们——只是不参与比对
`parity.js:25-27`)。
> **往忽略名单里加东西是有代价的动作。**
> 加之前先确认这个字段是**真的**每次都变(用 control 实验证明),
> 而不是你的改动让它变了。注释里必须写清楚证据。
---
## 真正的契约
忽略名单之外剩下的就是契约,**改动它们 = 改动产物**
| 契约项 | 谁产生 |
|---|---|
| `SCENE_DONE` stdout 标记及其 JSON 内容 | `generate_scene.py` |
| `CESIUM_EXPORT_DONE` stdout 标记及其 JSON 内容 | `export_cesium.py` |
| `.blend` 全量结构摘要(对象、网格、材质、自定义属性) | `scene_digest.py` |
| GLB 的 node / mesh / material / image 结构 | `glb-digest.js` |
| `<area>.json` 放置元数据 | `export_cesium.py` |
**改动 stage 的打印格式会静默破坏 parity 契约**——`parity.js:118-121` 解析这两个标记。
---
## 摘要工具本身的两条约定
`scene_digest.py` / `glb-digest.js` 时:
1. **浮点数四舍五入到 6 位**`scene_digest.py:17-18`。Blender 会把浮点数
round-trip 过单精度repr 的最后几位不是有意义的信号
2. **不稳定字段属于 `UNSTABLE_*` / `IGNORED_*` 名单,不属于摘要**
`scene_digest.py:11-13`。Blender 自己加的对象自定义属性
`_RNA_UI``cycles`)就是这么排除的(`scene_digest.py:24-25`
> 否则校验就是噪音,然后就会被忽略。一个天天报红的检查等于没有检查。
---
## 什么时候必须跑
| 改动 | 要不要跑 |
|---|---|
| 挪函数、拆模块、改导入 | **必须**——纯重构的定义就是产物不变 |
| 调整 `ROAD_LAYERS` / `MATERIALS` 的**顺序** | **必须**——会平移 GLB 材质索引 |
| 改材质名 | **必须**——可能影响新 `.blend``cesium_export` 契约或旧 `.blend` 的回退表 |
| 改几何构建、采样、实例化逻辑 | **必须** |
| 改 stage 的 stdout 打印 | **必须**——标记本身是契约 |
| 改 `.trellis/` 下的文档 | 不用 |
| 改 README / changelog | 不用 |
| **有意**改变产物(新功能、修渲染 bug | 跑,但目的是**看清差异范围**,不是要全绿 |
最后一行很重要parity 不只是"证明没变"的工具,也是"确认只变了预期的那部分"的工具。
加一种新植被,应该只看到新增对象,不该看到道路网格的顶点数也动了。
---
## 有意改变产物时怎么做
1. 先 capture 一份改动前的基线
2.
3. capture 改动后
4. compare**逐条读差异**
5. 差异要么是预期的,要么就是 bug——**没有第三种**
6. 把结论写进 `docs/changelog.md`
---
## 风险高的改动要分次提交
`docs/refactor-plan.md` 对风险最高的一期写着:
> 逐要素分次提交,每次单独跑 parity。
一次改十个要素然后发现摘要有差异,你不知道是哪个引起的。**一次一个,每次跑校验。**
---
## 当前重构进度(`docs/refactor-plan.md`
那份计划是**临时工作文档**P3 收尾后会并入 changelog 并删除。当前状态:
| 期 | 内容 | 状态 |
|---|---|---|
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
| P2 | 要素注册表 | ⚠️ **部分**——`water/grass/scrub/tree.py` 已拆出,但**没有 `features/` 注册表**`building` / `fountain` / `roads` 仍在 `generate_scene.py` 里 |
| P3 | 材质契约化(自定义属性传递 spec | ✅ 已完成——`catalog.MATERIALS[*]["cesium"]``materials.from_spec()` 写入 `material["cesium_export"]``export_cesium.py` 优先读该属性;四张材质名表仅作旧 `.blend` 回退 |
### 已知缺陷(记录在案,本轮不修)
| # | 位置 | 现象 |
|---|---|---|
| D1 | `export_cesium.py` 旧回退表 | `"Office White Metal Facade"` 四张表里都有,但 `catalog` 里已无此材质——只作为旧 `.blend` 回退兼容保留 |
| D2 | `scene-layers.js` vs `catalog.py` | 同一批图层的颜色两侧各自手调,无一致性保证(**这是刻意的**,见[图层表](../pipeline/layer-registry.md#为什么颜色刻意不同步) |
| D3 | `generate_scene.py` `tuft_density_wave` | 注释仍在跟已删除的 hedge banding 作对比 |
**碰到它们不要顺手修**——修复会改变产物或扩大 diff属于独立决定。
---
## 反模式
| 反模式 | 后果 |
|---|---|
| 跳过 control 实验直接开始改 | 分不清真回归和天然噪声 |
| 因为"老是报红"往忽略名单里加字段 | 把真回归一起忽略掉 |
| 忽略名单不写理由 | 下一个人无法判断该不该移出来 |
| 直接 diff 文件字节 | 永远红,然后所有人都不看了 |
| 一次改十个地方再跑校验 | 差异定位不到具体改动 |
| 改 stage 打印格式 | 静默破坏契约 |
| 摘要里保留不稳定字段 | 检查变噪音,最终被忽略 |
| 顺手修 D1D3 | 改变产物或扩大 diff |
---
## 相关
- [模块结构](../blender/module-structure.md)bpy 层为什么没有单元测试
- [测试](../blender/testing.md):纯 Python 层的防线
- [资产生成](../blender/asset-generation.md):为什么构建必须确定性
- [图层表](../pipeline/layer-registry.md):顺序为什么是承重的