202 lines
8.2 KiB
Markdown
202 lines
8.2 KiB
Markdown
# 产物一致性(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 结论:
|
||
|
||
- `.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`
|
||
|
||
---
|
||
|
||
## 风险高的改动要分次提交
|
||
|
||
要素注册表这类高风险重构必须按要素分次提交:
|
||
|
||
> 逐要素分次提交,每次单独跑 parity。
|
||
|
||
一次改十个要素然后发现摘要有差异,你不知道是哪个引起的。**一次一个,每次跑校验。**
|
||
|
||
---
|
||
|
||
## 当前重构进度
|
||
|
||
临时施工计划已删除;长期状态以本指南和 `docs/changelog.md` 为准:
|
||
|
||
| 期 | 内容 | 状态 |
|
||
|---|---|---|
|
||
| P0 | 抽纯函数到 `osmassets/{osm,geom}.py` | ✅ 已完成 |
|
||
| P1 | `catalog.py` 单一定义源 + `check_layers` | ✅ 已完成 |
|
||
| P2 | 要素注册表 | ✅ 已完成(保守版)——`features.py` 注册 OSM way 分发顺序;`water/grass/scrub/tree/fountain/building/roads.py` 已拆出。材质、计数、metadata ownership 仍保留在 `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 打印格式 | 静默破坏契约 |
|
||
| 摘要里保留不稳定字段 | 检查变噪音,最终被忽略 |
|
||
| 顺手修 D1–D3 | 改变产物或扩大 diff |
|
||
|
||
---
|
||
|
||
## 相关
|
||
|
||
- [模块结构](../blender/module-structure.md):bpy 层为什么没有单元测试
|
||
- [测试](../blender/testing.md):纯 Python 层的防线
|
||
- [资产生成](../blender/asset-generation.md):为什么构建必须确定性
|
||
- [图层表](../pipeline/layer-registry.md):顺序为什么是承重的
|