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):顺序为什么是承重的

View File

@@ -0,0 +1,158 @@
# 代码复用思考指南
> 目的:在新增 helper、常量、配置字段或枚举表之前先判断这个项目里"应该复用"和
> "刻意重复"的边界。这里的关键不是追求抽象,而是避免事实漂移。
---
## 先搜索,再决定
改任何值或新增类似逻辑前先跑:
```bash
grep -rn "关键字或现有值" scripts blender config
```
本项目的重复有两类:
- **危险重复**:同一事实被多处维护,漏改会静默错产物
- **可接受重复**:运行时边界不同或独立入口需要保留,抽象会扩大耦合
判断之前不要凭直觉抽取。
---
## 必须复用的事实源
### 九个 osm2streets 图层
JS 侧只认 `scripts/lib/scene-layers.js:15``SCENE_LAYERS`
需要文件名、合并场景、style JSON、QGIS 颜色时,使用同文件导出的派生函数:
- `layerFile(layer)``scene-layers.js:103`
- `mergeScene(getCollection)``scene-layers.js:109`
- `sceneStyle()``scene-layers.js:126`
- `qgisRgba(hex, alpha)``scene-layers.js:143`
不要在 `build-osm2streets-qgis.js``reimport-gpkg.js` 或 QGIS 项目生成代码里再枚举
九个图层。旧问题正是同一顺序复制到四处,漏一处不报错,只让 Blender/Cesium 场景错栈。
Python 侧必须有 `blender/osmassets/catalog.py:28``ROAD_LAYERS`,因为它还声明
Blender 高度与线性颜色。两侧靠 `catalog.check_layers()` 对账集合和顺序;颜色故意不同步。
### 区域输出路径
输出路径只在 `scripts/build-area.js:74``normalizeAreaConfig()` 推导。
低层脚本读取 `_pipeline/osm2streets-qgis.config.json`,不要重新读取
`config/areas/*.json` 或在阶段函数里现场拼路径。
新增产物时,在 `normalizeAreaConfig``outputs` 里加一项,再按需写入
`writeDerivedConfig()``build-area.js:189`)。这样 `intermediates``reimport`
`blender``cesium``preview` 仍然只通过磁盘产物耦合。
### 材质声明
Blender 内材质声明集中在 `catalog.MATERIALS``catalog.py:56`)。
真实 `bpy.types.Material``materials.from_spec()``materials.py:198`)构建。
注意当前有一个未完成迁移:`export_cesium.py:68``:80``:86``:95` 的四张表
仍按材质名字符串匹配。改材质名时不能只改 `catalog`;必须全仓 grep 材质名。
---
## 可接受的重复
### 三份 `parseArgs`
`parseArgs` 现在重复在三个独立入口:
- `scripts/build-area.js:50`
- `scripts/build-osm2streets-qgis.js:153`
- `scripts/reimport-gpkg.js:93`
语义一致:`--kebab-case value``kebabCase: "value"`,无值 flag 变字符串 `"true"`
这份重复目前是可接受技术债,因为三个脚本都能独立运行。改其中一处解析语义时,不要顺手
只改一份;要么保持三份一致,要么把"抽公共模块"作为独立重构并跑 parity。
### JS 与 Python 的图层颜色
`scene-layers.js` 的颜色是 QGIS 2D 调试 sRGB hex`catalog.py` 的颜色是 Blender
线性 RGB。`catalog.py:11-15` 明确说颜色不是同步目标。
把两边颜色抽成同一个表不是复用,是破坏两个运行时各自调过的视觉结果。
---
## 重复模式检查
### 看到第二份枚举表
问:
- 这份表是否已经能从 `SCENE_LAYERS``ROAD_LAYERS``MATERIALS` 或配置派生?
- 如果必须跨语言重复,是否已有对账机制?
- 追加顺序是否影响 GLB 材质索引?
没有对账机制的重复表必须特别谨慎。材质名覆盖就是当前已知风险:
`generate_scene.py` 创建材质,`export_cesium.py` 靠字符串覆盖,没有校验。
### 看到多个模块同样预处理
`water.py:9``grass.py:9``scrub.py:8` 都调用 `clip_polygon`,这是对要素模块签名的
统一要求:模块接收边界、自己裁剪、退化输入返回 0。
新增第四个要素模块时先照这个形状写,不要把裁剪逻辑上移到调用方。否则旧模块和新模块
的边界会不同,真实 OSM 的越界几何会按要素类型表现不一致。
### 看到多个地方解析同一格式
优先找已有解析器:
- OSM XML → `osmassets/osm.py:parse_osm()`
- 米制几何 → `osmassets/geom.py`
- GeoJSON 场景合并 → `scene-layers.js:mergeScene(getCollection)`
- 区域配置 → `build-area.js:normalizeAreaConfig()`
如果确实需要新解析器,把输入格式、容错语义和调用者写清楚,并给纯 Python 逻辑补测试。
---
## 什么时候抽象
抽象只在满足至少一条时做:
- 同一事实会被三处以上消费,且有真实漏改风险
- 同一段校验逻辑跨多个入口影响产物安全
- 抽出来后能保留运行时边界,比如纯 Python 逻辑进入 `geom.py` 后可被
`python3 -m unittest discover blender/tests` 覆盖
不要因为代码相似就抽象:
- 三份 `parseArgs` 当前保持独立入口价值
- `ROAD_LAYERS``SCENE_LAYERS` 跨语言且承载不同字段
- 每个要素模块各自调用 `clip_polygon` 是模块边界,不是可消除重复
---
## 提交前自检
- [ ] 已 grep 关键值或新字段
- [ ] 没有新增第二份九图层枚举
- [ ] 没有在阶段函数里重新拼输出路径
- [ ] 改材质名时已检查 `catalog.py``generate_scene.py``export_cesium.py`
- [ ] 新纯几何逻辑放进 `geom.py` 并补 `test_pure.py`
- [ ] 声称产物不变的重构已按[产物一致性指南](./artifact-parity-guide.md)校验
---
## 反模式
| 反模式 | 后果 |
|---|---|
| 新增一份图层名列表 | 回到旧的四份同步,漏改静默错栈 |
| 把两套颜色表统一 | 破坏 QGIS 与 Blender 各自调过的视觉结果 |
| 低层脚本直接读 `config/areas/*.json` | 两层配置边界失效 |
| 只改一份 `parseArgs` 的语义 | 三个入口行为分裂 |
| 把要素模块裁剪逻辑挪到调用方 | 不同要素的越界处理开始漂移 |
| 只在 `catalog.CESIUM_EXPORT` 加导出覆盖 | 当前不会生效;导出器没读它 |

View File

@@ -0,0 +1,217 @@
# 跨层思考指南
> **目的**:动手前把数据流走一遍,把"没想到"变成"想过了"。
>
> 本项目的层是**跨语言、跨进程、跨运行时**的,边界比普通应用多得多。
---
## 本项目的层与边界
```
config/areas/*.json JSON 数据
↓ ①
build-area.js Node宿主机
↓ ② 派生配置 JSON
build-osm2streets-qgis.js Node + osm2streets WASM
↓ ③ 子进程 + 环境变量
ogr2ogr / ogrinfo / QGIS Python GDAL/QGIS 运行时
↓ ④ GeoJSON / GeoPackage 文件
generate_scene.py Blender 内嵌 Python
↓ ⑤ .blend 文件 + 材质名字符串
export_cesium.py Blender 内嵌 Python
↓ ⑥ GLB + JSON
cesium-preview.js 浏览器
```
| # | 边界 | 常见问题 |
|---|---|---|
| ① | 用户配置 → 归一化 | `??` vs `\|\|`、相对路径、字段整体替换 |
| ② | 两层配置 | 低层脚本读错配置源 |
| ③ | Node → 外部进程 | 环境变量缺失、退出码与信号、0 字节产物 |
| ④ | 文件交换 | 图层集合/顺序漂移、精度丢失 |
| ⑤ | Python → Python | **材质名字符串**,无校验 |
| ⑥ | Blender → 浏览器 | 坐标系约定、材质在两种光照下的差异 |
---
## 什么时候该读这篇
- [ ] 改动同时出现在 `scripts/``blender/`
- [ ] 你在改九个 osm2streets 图层中的任何一个
- [ ] 你在改材质名、材质顺序
- [ ] 你在往配置里加字段
- [ ] 你在改任何被 `execFileSync` / `spawnSync` 调起的东西
- [ ] 你在改 stage 的 stdout 打印
- [ ] 你要新增一种在 Blender 里生成、要在 Cesium 里看的资产
---
## Step 1画数据流
对每一个箭头问三件事:
- **格式是什么**——JSONGeoJSON FeatureCollectionGeoPackage 图层?字符串键?
- **可能出什么错**——文件不存在0 字节?字段名对不上?
- **谁负责校验**——上游写的时候,还是下游读的时候?
本项目的答案通常是:**上游写完就走,下游读的时候校验**。因为上游经常是外部工具
ogr2ogr、Blender改不动。
## Step 2找出"约定型"边界
最危险的不是有 schema 的边界,是**靠约定连接**的边界:
| 边界 | 靠什么连接 | 有没有校验 |
|---|---|---|
| `scene-layers.js``catalog.py` | 图层 `id` 的集合与顺序 | ✅ `check_layers()`warn |
| `generate_scene.py``export_cesium.py` | **材质名字符串** | ❌ **无** |
| GeoJSON 文件名 ↔ 图层 id | `layerFile()``<id>.geojson` | 部分reimport 会检查 gpkg 图层是否齐全) |
| stage stdout ↔ `parity.js` | `SCENE_DONE` / `CESIUM_EXPORT_DONE` 字面量 | ❌ 无 |
| GLB 材质索引 ↔ `MATERIALS` 顺序 | 隐式的创建顺序 | ❌ 无(靠 parity 事后发现) |
**没有校验的那几行就是本项目最容易静默出错的地方。**
## Step 3定契约
对每个边界写清楚:输入格式、输出格式、能出什么错。
本项目已定的契约见[产物一致性指南](./artifact-parity-guide.md#真正的契约)。
---
## 本项目真实踩过的坑
### 坑 1同一份事实存了四份
九个图层的顺序曾同时存在于 z_index 表、样式 JSON、QGIS 工程、README。
改一处漏三处,**不报错**,只是下游场景悄悄错栈。
**修法**`scripts/lib/scene-layers.js` 单一事实源 + 四个派生函数。
跨语言那一份(`catalog.py`)无法消除,改用运行时对账。
→ [图层表](../pipeline/layer-registry.md)
### 坑 2外部工具失败但留下了文件
`ogr2ogr` 遇到不存在的图层退出码非零,**但已经创建了一个 0 字节文件**。
直接覆盖目标目录就会用空文件冲掉好数据,而且看起来像成功。
**修法**staging 目录 → 全部校验 → 才落盘。
→ [外部工具调用](../pipeline/external-tools.md#2-导出失败会留下-0-字节文件)
### 坑 3为了"输出干净"加了个参数
`ogr2ogr` 显式设 `COORDINATE_PRECISION`,触发了 GDAL 的精度裁剪,
7 个箭头多边形丢了 28 个顶点。默认行为本来就能完整往返双精度。
**教训**:跨边界时,**显式设置一个"看起来更安全"的参数,可能触发上游的另一条代码路径**。
### 坑 4新资产在 Blender 里对、在 Cesium 里发黑
场景里每一个材质都被手工提亮过(草往亮绿混 72%、建筑自发光 0.18
因为 Cesium 默认光照偏白。新资产没调过,是唯一如实渲染的东西,
放在旁边就显得发黑。
**教训****同一份数据在两个运行时里的"正确"可能不一样**。
第二个运行时如果有一整套补偿,新东西必须也进那套补偿。
→ [资产生成](../blender/asset-generation.md#为什么新资产总是发黑)
### 坑 5两个 Python 脚本靠字符串对接
`export_cesium.py` 不 import `catalog`,靠材质名字符串匹配四张覆盖表。
改个材质名Cesium 侧的调色**静默失效**。`catalog.CESIUM_EXPORT` 想解决这个问题,
但迁移没做完,它现在是死代码。
**教训****字符串键的跨模块耦合必须配一个对账机制**,否则重命名就是定时炸弹。
---
## 加东西时的检查清单
### 加一个图层
- [ ] `scene-layers.js:SCENE_LAYERS` 末尾追加
- [ ] `catalog.py:ROAD_LAYERS` 末尾追加,**顺序一致**
- [ ] 确认 osm2streets 拆分结果里有对应的 `splitKey`
- [ ] 跑一次构建,确认日志里没有 `Layer catalog warning:`
- [ ] 跑 parity确认只多了预期的对象
### 加一个材质
- [ ] `catalog.MATERIALS` **末尾**追加(中间插入会平移 GLB 材质索引)
- [ ] 若在 Cesium 里需要调色,去 `export_cesium.py` 的四张表加
- [ ] 跑 parity
### 加一个配置字段
- [ ] `normalizeAreaConfig` 里用 `??` 不用 `||`
- [ ] 需要传给低层脚本?加进 `writeDerivedConfig`
- [ ] 数值?在消费侧加 `Number.isFinite` + 范围校验,**在任何副作用之前**
- [ ] 更新 `config/examples/template.json`
- [ ] 更新 [config spec](../config/index.md) 的字段表
### 加一个 stage
- [ ] `normalizeAreaConfig``stages` + `resolveStages``aliases`
- [ ] 想清楚进不进 `all`(恢复类/补丁类不进)
- [ ] 与已有 stage 有覆盖关系?加互斥检查
- [ ] 产出新文件?加进 `outputs` 路径推导
- [ ] 阶段函数开头 `ensureFile` 校验依赖产物
### 加一种 OSM 要素
- [ ] 新模块放 `osmassets/``assemble(...)` 签名照抄现有三个
- [ ] 只 import 需要的,**纯几何逻辑放 `geom.py` 并补测试**
- [ ] 材质加进 `catalog.MATERIALS` 末尾
- [ ] `generate_scene.py` 分发处加一行
- [ ] 需要"随机"外观?用 index 的纯函数或显式 seed**不要用 `random`**
- [ ] 跑 parity
---
## 通用的四个错误
### 隐式格式假设
跨边界时假设"上游肯定给的是 X 格式"。本项目的做法是**读的时候验**
`reimport-gpkg.js:175` 明确检查 `type === "FeatureCollection" && Array.isArray(features)`
### 校验散在各处
同一个约束在三个地方各写一遍,改的时候漏一个。
本项目把参数校验集中在脚本开头(`build-osm2streets-qgis.js:41-70`
**在任何副作用之前一次验完**
### 抽象泄漏
低层脚本如果直接读 `config/areas/*.json`,两层配置的边界就白设了。
它们只该读派生配置。
### 每个消费方各自解析同一份数据
看到两处代码用各自的方式从同一份 payload 里挖同一个字段,
就该有一个共享的解析函数了。`mergeScene(getCollection)` 用回调而不是数组,
就是为了让 build 和 reimport 共用一套合并逻辑。
---
## 一条铁律
> **改任何值之前,先全仓 grep 一遍。**
```bash
grep -rn "要改的值" scripts blender config
```
本项目跨两种语言IDE 的"查找引用"帮不上忙。这一个习惯能挡掉大部分
"忘了同步 X" 的 bug。
---
## 相关
- [代码复用思考指南](./code-reuse-thinking-guide.md)
- [产物一致性指南](./artifact-parity-guide.md)
- [图层表](../pipeline/layer-registry.md)

View File

@@ -0,0 +1,76 @@
# 思考指南索引
> 目的:在改代码前补一遍"跨层会不会断、重复事实会不会漂、产物是否仍一致"。
> 本目录不替代包级 spec它用于那些单看一个文件容易误判的改动。
---
## 可用指南
| 指南 | 关注点 | 什么时候读 |
|---|---|---|
| [跨层思考指南](./cross-layer-thinking-guide.md) | JS、GDAL/QGIS、Blender Python、浏览器之间的数据契约 | 改图层、材质名、配置字段、stage 输出、外部工具调用 |
| [代码复用思考指南](./code-reuse-thinking-guide.md) | 单一事实源、重复解析、可接受重复与应抽取重复的边界 | 改 `parseArgs`、图层表、配置归一化、几何工具 |
| [产物一致性指南](./artifact-parity-guide.md) | `.blend` / `.glb` / metadata 的结构摘要校验 | 任何声称"纯重构、产物不变"的改动 |
---
## 本项目触发点
### 读跨层思考指南
- [ ]`scripts/lib/scene-layers.js:15``SCENE_LAYERS`
- [ ]`blender/osmassets/catalog.py:28``ROAD_LAYERS``catalog.py:56``MATERIALS`
- [ ]`blender/export_cesium.py:68` 等四张按材质名字符串匹配的覆盖表
- [ ]`build-area.js:74``normalizeAreaConfig()``config/examples/template.json`
- [ ] 改任何 `execFileSync` / `spawnSync` 调起的脚本或参数
- [ ]`SCENE_DONE` / `CESIUM_EXPORT_DONE` 的 stdout 标记
### 读代码复用思考指南
- [ ] 准备新增第二份或第三份图层、材质、配置字段枚举
- [ ] 修改三份重复的 `parseArgs` 之一:
`build-area.js:50``build-osm2streets-qgis.js:153``reimport-gpkg.js:93`
- [ ] 多个要素模块都要做同一件几何预处理,比如
`water.py:9``grass.py:9``scrub.py:8` 都先 `clip_polygon`
- [ ] 低层脚本想直接读取 `config/areas/*.json`,绕开派生配置
- [ ] 新增 helper 前没有先 `grep -rn` 找现有函数
### 读产物一致性指南
- [ ] 挪函数、拆模块、改导入,且声称产物不变
- [ ] 重排 `ROAD_LAYERS` / `MATERIALS`
- [ ] 改材质名或导出调色逻辑
- [ ] 改几何、采样、实例化、UV、材质构建
- [ ]`scripts/parity.js``scripts/glb-digest.js``blender/tools/scene_digest.py`
---
## 改值前的固定动作
```bash
grep -rn "要改的值" scripts blender config
```
本仓库跨 JS、Blender Python、浏览器 JS 和 JSON很多连接靠字符串或文件名约定。
例如 `scene-layers.js``catalog.py` 只靠 `id` 集合和顺序对账;
`generate_scene.py``export_cesium.py` 的材质覆盖目前靠材质名字符串,没有自动校验。
---
## 审查 AI 结果时
- 先看它有没有读到对应包的 index 和本目录指南
- 对任何"行为没变"的结论,要求说明是否需要 parity需要却没跑就是风险
- 对任何"可以合并重复"的建议,先判断重复是不是刻意边界:
三份 `parseArgs` 目前是可接受技术债JS/Python 图层颜色则是刻意不同步
- 对任何"加精度、加默认值、直接覆盖文件"的建议,回到真实代码注释验证;
`reimport-gpkg.js:152-156``reimport-gpkg.js:11-13` 都是反直觉约束
---
## 维护规则
- 发现新的跨层坑,优先补到相关指南,再补包级 spec
- 指南只写本项目已发生或代码已体现的约束,不写通用工程格言
- 每条新约束至少带两个真实路径或函数名,方便后续 grep 定位