[2026-09-18] eagle: personal/projects/zont-config-compiler.md personal/tech/roundtrip-key-verification.md

This commit is contained in:
Alexey Martemyanov
2026-09-18 00:32:31 +06:00
parent 9cf7a6f618
commit 573b1e2471
2 changed files with 256 additions and 4 deletions
@@ -221,3 +221,102 @@ grep -c '^\- id:' out.yml.after
```
Рост этой секции = форма не разобрана, а не «данных нет».
---
## 🔴 Авто-генерация id: обернуть сборку строки НЕДОСТАТОЧНО (2026-09-18, ZONT круг 50)
**Контекст:** учим энкодер **выдумывать** id записям, у которых его нет в промежуточном файле.
Пять ловушек, все найдены на живом коде.
### Ловушка 1 — `keep()` и `alloc()` пишут в РАЗНЫЕ хранилища занятости
У аллокатора было два хранилища: `_used` (зарезервированные) и `renames` (старый → новый).
`keep(id)` писал только в `_used`, а резолвер смотрел в `renames`**свой** id выглядел
незанятым → выдавался новый номер → строка уезжала под новым id, а **ссылки** в полях
оставались на старом.
```
round-trip: 759 → 693, ровно −66 строк (все одного типа)
симптом: тело выпущено с НОВЫМ id, ссылки несут СТАРЫЙ → объект молча осиротел
```
**Правило:** одно хранилище, одна функция. `keep(id)` регистрирует **тождественное
переименование** `id -> id`. Любой помощник, помечающий id занятым, обязан писать и в карту
переименований. Два хранилища = гарантированное расхождение.
### Ловушка 2 — барьер `if 'id' in obj` отсекает запись ДО аллокатора
```python
for obj in obj_list:
if isinstance(obj, dict) and 'id' in obj: # ← запись без id сюда не входит
zid = resolve_id(obj['id'])
```
Запись исчезает **при `exit=0`** и не появляется **ни под старым, ни под новым** id. Такой же
барьер обычно стоит в нескольких циклах — обход типов, обход орфанов/секций, индекс тел:
```bash
grep -n "'id' in \|\"id\" in \|'id' not in \|get('id')" encoder.py
```
**Правило:** при внедрении авто-генерации обернуть `build_line`/`add_z_line` — только половина
работы. Нужно снять **условия допуска** во **всех** циклах вывода, иначе запись не дойдёт до
аллокатора.
**Диагностический признак ловушки 2:** `exit=0`, в stderr пусто, строка отсутствует и под
старым, и под новым id. Если новый id всё-таки выдан — это ловушка 1, а не 2.
### Ловушка 3 — «id есть в файле» ≠ «запись можно оставить без id»
Замерено: снять id у одной записи каждой секции и пересобрать.
| Класс записи | Можно без id? |
|---|---|
| **Лист** — на неё никто не ссылается (ни по id, ни по типу) | ✅ да |
| На неё ссылаются **по типу** (`body_type(id)` для событий/параметров) | ❌ нет — тип ищется **по id** |
| В секции, чей цикл требует `id` у каждой записи | ❌ нет без ослабления цикла |
Безопасный первый заход — **только листья**. Записи, чей тип кто-то запрашивает, требуют, чтобы
тип выводился **до** присвоения id — это отдельная правка.
### Ловушка 4 — ссылка в никуда НЕ проверяется (опасная половина)
```yaml
- id: 8470
set_relay:
target: 99999 # ← объекта с таким id нет
```
Сборка **прошла, `exit=0`**, и `99999` уехал в выходной конфиг. Контроллер получил бы команду
в никуда. **Это опаснее потерянного объекта.**
`validate_config` проверяет кодируемость и дубли id — **ссылки он не резолвит**. Вместе с
авто-генерацией нужна валидация ссылок: каждое `target` / `output_id` / `object` / `left` /
`right` / список-id обязано быть в множестве существующих id, иначе падение.
Приёмка: подменить одну ссылку на свободный id и убедиться, что сборка **упала**.
### Ловушка 5 — проверять допущение МИНИ-КЕЙСОМ, а не чтением кода
Всё вышеперечисленное найдено **трассировкой одной записи на минимальном входе** (один сценарий,
`build_line` обёрнут логированием id), а не чтением эмиттера. Трассировка напечатала `id=4098`
там, где в источнике `8550` — это и назвало баг одной строкой.
```python
orig = mod.build_line
seen = []
mod.build_line = lambda t, i, f: (seen.append(i), orig(t, i, f))[1]
mod.convert(minimal_subset) # один сценарий / одна запись
print(seen) # сверить с id, которые есть на входе
```
**Проверка подозрения на регресс — против чистого HEAD, до обвинения своей правки:**
```bash
git stash push -u -m wip # работа спасается только stash'ем
python3 test_roundtrip.py # базовая линия на чистом HEAD
git stash pop
```
База тоже сломана → унаследовано; база зелёная → это твоя правка. И **коммитить сразу, как
только круг зелёный** — `git checkout --` затирает незакоммиченное без возврата.