Initialize Trellis project guidelines

This commit is contained in:
2026-08-03 10:56:21 +08:00
parent 7f4ebe8fb7
commit 4c5981c555
166 changed files with 28564 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
# 产物一致性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 材质索引 |
| 改材质名 | **必须**——可能静默断开 Cesium 侧的四张覆盖表 |
| 改几何构建、采样、实例化逻辑 | **必须** |
| 改 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 | ❌ **未做**——`export_cesium.py` 仍不 import `catalog`,靠四张材质名表;`catalog.CESIUM_EXPORT` 是死代码 |
### 已知缺陷(记录在案,本轮不修)
| # | 位置 | 现象 |
|---|---|---|
| D1 | `export_cesium.py:74,82,91,100` | `"Office White Metal Facade"` 四张表里都有,但 `catalog` 里已无此材质——死条目 |
| 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):顺序为什么是承重的