[2026-09-18] eagle: personal/projects/zont-config-compiler.md personal/tech/roundtrip-key-verification.md
This commit is contained in:
@@ -30,6 +30,18 @@ aliases:
|
||||
- ZONT расписание trigger
|
||||
- ZONT unresolved снят
|
||||
- ZONT битая ссылка id
|
||||
- ZONT авто-id
|
||||
- ZONT автогенерация id
|
||||
- ZONT круг 50
|
||||
- ZONT окно id 4098 19999
|
||||
- ZONT ID_MIN ID_MAX
|
||||
- ZONT _IdAllocator
|
||||
- ZONT дыры снизу вверх
|
||||
- ZONT keep renames питфолл
|
||||
- ZONT барьер id in obj
|
||||
- ZONT питфолл 104
|
||||
- ZONT валидация ссылок
|
||||
- ZONT ссылка в никуда
|
||||
- ZONT конец сценария
|
||||
- ZONT терминатор сценария
|
||||
- ZONT unresolved = конец сценария
|
||||
@@ -638,6 +650,14 @@ enabled = not (f5 & 8)
|
||||
> **(в) дорисовка «для читаемости»** — `type`, `object_name`, `_head`: имена и ярлыки, которых
|
||||
> в строке нет. Alex видит их мгновенно и считает выдумкой. **YAML = строка конфига + вывод из тела.**
|
||||
|
||||
> 🔴 **Питфоллы круга 50 (авто-id) — читать ПЕРЕД правкой id:**
|
||||
> **101** `keep()` не писал в `renames` → регресс −66 объектов (§25.7);
|
||||
> **102** «id есть в YAML» ≠ «объект можно без id» — граница по секциям измерена (§25.7);
|
||||
> **103** ссылка на несуществующий id **не проверяется** — мусор уезжает в конфиг, exit=0 (§25.7);
|
||||
> **104** барьер `'id' in obj` отсекает узел **до** аллокатора, молча, exit=0 (§25.9).
|
||||
> **Общий урок:** обернуть сборку строки недостаточно — надо снять условия допуска во **всех**
|
||||
> циклах вывода, и проверять факт трассировкой мини-кейса, а не чтением кода.
|
||||
|
||||
|
||||
**Корень:** я подменял решение Alex своим и считал это работой. Когда он говорит «наличием поля X» —
|
||||
это **ответ**, а не повод искать обходной путь.
|
||||
@@ -959,7 +979,8 @@ https://lk.zont-online.ru/download/firmwares/H2000_PRO_<HW>__<FW>_<PROFILE>.zip
|
||||
| **`objcmd`** | ✅ **РАЗОБРАН** — `descr` + `args` + `target: <id>` (`10029`/`10033`/`10036`/`10053`). `args` остаётся **только** у `objcmd` |
|
||||
| **`args` у `set_var`** | ⛔ **ОТМЕНЁН** (круг 40, `4b99d8e`) — был выдумкой: `0` = поле 4, `1` = поле 5 (**производное**: «в поле 2 литерал, а не id»). Энкодер ставит поле 5 сам (§21.2.1) |
|
||||
| **`action` / шаг 46 в `steps`** | ⛔ **ОТМЕНЁН** (круг 39, `8f2edd4`) — шаг 46 в `steps` не пишется; его id едет в **`trigger.step_id`**, `steps` = **тела действий** (§21.3) |
|
||||
| Коммит | **`4b99d8e` ← текущий HEAD** · `8f2edd4` (step_id) · `272f4c6` (trigger.step) · `6d0b6a5` · `4c6a6f5` (орфаны-param) · `806ed2e` (end) · `b770bed` (revert) · `ba6ef44` · `0cbbeb8` (расписание 50) · `2007771` (условие 47) · `0c4b9a6` (`var` поле 2) · `16ec710` (круг 32) · `375d01a` · `07c076f` |
|
||||
| Коммит | **`5d31d6e` ← текущий HEAD** (круги 46-49, запушен в Gitea) · `9c83651` (exit) · `5d31d6e` · `75f46f3` (тип 9 / set_contour_target) · `5fdedc9` (тип 5: `action`) · `4b99d8e` (`args` у set_var снят) · `8f2edd4` (step_id) · `272f4c6` (trigger.step) · `6d0b6a5` · `4c6a6f5` (орфаны-param) · `806ed2e` (end) · `b770bed` (revert) · `ba6ef44` · `0cbbeb8` (расписание 50) · `2007771` (условие 47) · `0c4b9a6` (`var` поле 2) · `16ec710` (круг 32) · `375d01a` · `07c076f` |
|
||||
| 🔴 Незакоммичено | **авто-id (круг 50)** — `yml-to-config.py` изменён, **113+/26−**, поверх `5d31d6e`. Аллокатор + окно + hard fail написаны, регресс `759 → 759` зелёный, авто-id end-to-end **НЕ работает** (§25.9-25.11). Коммит — по команде Alex |
|
||||
| Откачено | `87e315c` («set-var target into `set_var`») — снят `git reset --soft` без разрешения Alex (§10.1) |
|
||||
| Ранее | `823fabd` — форма сценария §5; `1cc010a` — **содержит сломанные версии**; закрыт §9.1 |
|
||||
| Документация | ✅ **три дока сведены в один** — `personal/projects/zont-config-compiler.md` (§9.2) |
|
||||
@@ -3709,9 +3730,141 @@ Alex: **«если id есть то очевидно ничего не прид
|
||||
**Правило одной строкой:** `id` в YAML = решение человека, неприкосновенно. Нет `id` — выдаём сами.
|
||||
Ссылка на несуществующий id — всегда ошибка, автозамены нет ни в одном случае.
|
||||
|
||||
**Риски:** (а) ссылки в новые объекты — реальная проблема, требует решения шага 3; (б) порядок
|
||||
эмиссии: `S`-блоки первыми, `Z` — после, id нужен раньше строки; (в) **верхняя граница** —
|
||||
`max+1 = 20556` перескочит железо, вопрос к Alex открыт.
|
||||
### 25.7. ✅ ВЫПОЛНЕНО (круг 50, 2026-09-18) — аллокатор написан, регресс зелёный
|
||||
|
||||
| Что | Файл / место | Статус |
|
||||
|---|---|---|
|
||||
| `ID_MIN = 4098`, `ID_MAX = 19999` | `yml-to-config.py`, модульные константы | ✅ |
|
||||
| Класс `_IdAllocator` — дыры снизу вверх, hard fail «id window exhausted» | там же | ✅ |
|
||||
| Врезка в 3 узких места: `add_z_line`, `build_line('Z', ...)` ×7, `reconstruct_from_raw` ×16 | там же | ✅ |
|
||||
| `_alloc_id` — 3 источника «своего» id: `_known_ids`, `renames`, `known=True` | там же | ✅ |
|
||||
| Регресс | `test_roundtrip.py` → **759 → 759, расхождений 0** | ✅ |
|
||||
|
||||
Правки внесены тремя идемпотентными скриптами `/tmp/patch_autoid{,2,3}.py`
|
||||
(проверка `count == 1` на каждую замену, печатают OK/SKIP).
|
||||
|
||||
#### 🔴 ПИТФОЛЛ 101 — `keep()` не писал в `renames`: регресс −66 объектов
|
||||
|
||||
**Симптом:** регресс `759 → 693`, потеряно ровно **66** строк, **все типа 46** формы
|
||||
`#Z8550=46,0,8548,[8549],[]`.
|
||||
|
||||
**Как искалось (правильный порядок):** сначала трассировка **мини-кейса** (один сценарий `8547`),
|
||||
а не чтение кода. Диагностика `/tmp/dbg50.py` подменила `build_line` и напечатала все id, дошедшие
|
||||
до сборки: `#Z4098=46,0,8548,[8549],[]` — строка рождалась **с новым номером**, а ссылка в поле 2
|
||||
сценария оставалась `[8550]`. Объект уезжал в никуда.
|
||||
|
||||
**Причина:** `_alloc_id` считает id «своим», если он есть в `renames`. `keep()` писал id только
|
||||
в `_used`, **но не в `renames`** → для `_alloc_id` id выглядел незанятым → выдавался новый номер.
|
||||
|
||||
**Лечение:** `keep()` регистрирует **тождественное переименование** `id -> id`
|
||||
(`self.renames.setdefault(obj_id, obj_id)`). После этого все три источника своего id
|
||||
обрабатываются одинаково.
|
||||
|
||||
**Урок:** `keep()` и `alloc()` обязаны писать в **один и тот же** словарь. Два словаря занятости
|
||||
(`_used` и `renames`) — источник расхождения; любая функция, помечающая id занятым, регистрирует
|
||||
его и в `renames`.
|
||||
|
||||
#### 🔴 ПИТФОЛЛ 102 — «отдельный id есть в YAML» ≠ «объект можно оставить без id»
|
||||
|
||||
Замерено `/tmp/test_autoid_granica.py` (снять id у одного объекта секции, собрать, посмотреть вердикт):
|
||||
|
||||
| Секция | Результат | Причина |
|
||||
|---|---|---|
|
||||
| `relay_commands` | ❌ **объект пропал** (старая строка ушла, новая не появилась) | см. §25.8 |
|
||||
| `heating_circuits` | ❌ `unknown param 'target_temp' ... (type None)` | на объект ссылаются **по типу** |
|
||||
| `temperature_sensors` | ❌ `unknown event 'lost' ... (type None)` | то же |
|
||||
| `actions` / `mqtt_topics` / `gui_switches` | ❌ `Ошибка: 'id'` | цикл по секции требует `id` **у всех** записей |
|
||||
|
||||
**Граница авто-id (подтверждено данными):**
|
||||
|
||||
| Класс объекта | Можно без id? |
|
||||
|---|---|
|
||||
| Объект-**лист**, на который никто не ссылается (по id или по типу) | ✅ да |
|
||||
| Объект, на который ссылаются **по типу** (`_body_type(id)` — события/параметры 49) | ❌ нет: тип берётся по id |
|
||||
| Объект в секции, чей цикл вывода требует `id` у каждой записи | ❌ нет без правки цикла |
|
||||
|
||||
#### 🔴 ПИТФОЛЛ 103 — ссылка на несуществующий id НЕ проверяется (дыра до круга 50)
|
||||
|
||||
Кейс B приёмки: `target: 9464` подменён на `target: 99999` → **сборка прошла, exit=0**, мусор
|
||||
`99999` уехал в выходной конфиг.
|
||||
|
||||
**Это опаснее потери объекта:** контроллер примет команду в никуда. Правило Alex — «ссылка на
|
||||
несуществующий id = ошибка» — **в коде отсутствует**, его надо написать:
|
||||
`validate_config` обязан сверять ссылки (`target`, `output_id`, `object`, `left`/`right`, поля-списки)
|
||||
с множеством существующих id и падать, если ссылка висит в воздухе.
|
||||
|
||||
### 25.8. ⏳ ОСТАТОК круга 50 (не сделано)
|
||||
|
||||
1. ~~**`relay_commands` без id теряется** — причина не установлена~~ → ✅ **ПРИЧИНА УСТАНОВЛЕНА**
|
||||
(см. §25.9, питфолл 104).
|
||||
2. **Валидация ссылок** (питфолл 103) — `validate_config` не сверяет ссылки с существующими id.
|
||||
3. **Секции, требующие `id` у каждой записи** (`actions`, `mqtt_topics`, `gui_switches`) — для
|
||||
авто-id нужно ослабить циклы до `.get('id')`.
|
||||
4. **Объекты-источники по типу** — авто-id возможен только вместе с правкой `_body_type`
|
||||
(тип должен выводиться до присвоения id, из самой записи).
|
||||
|
||||
### 25.9. 🔴 ПИТФОЛЛ 104 — барьер `'id' in obj` отсекает узел ДО аллокатора
|
||||
|
||||
Решение пункта 1 остатка — **не догадкой, а диагностикой** `/tmp/dbg_relaycmd.py` (печатает узел до/после
|
||||
снятия id, exit энкодера, наличие нового id в выводе, попадание в `_known_ids`).
|
||||
|
||||
**Факт:** аллокатор **не вызывается вообще**. Узел `relay_commands` без `id` выбрасывается молча
|
||||
раньше — условием цикла вывода:
|
||||
|
||||
```python
|
||||
# LINE 2046, цикл TYPE_ORDER
|
||||
for obj in obj_list:
|
||||
if isinstance(obj, dict) and 'id' in obj: # ← узел без id сюда не входит
|
||||
zid = _alloc_id(obj['id'])
|
||||
```
|
||||
|
||||
**Тот же барьер стоит в ТРЁХ местах** — именно он давал `Ошибка: 'id'` в замерах §25.6:
|
||||
|
||||
| Место (строка) | Условие | Что молча отсекает |
|
||||
|---|---|---|
|
||||
| 2046, цикл `TYPE_ORDER` | `if isinstance(obj, dict) and 'id' in obj` | **все секции**: `relay_commands`, `heating_circuits`, … |
|
||||
| 1546, цикл орфанов | `if not isinstance(_obj, dict) or 'id' not in _obj: continue` | `actions`, `mqtt_topics`, `gui_switches` |
|
||||
| 1308, `_body_index` | `if ... and 'id' in _obj and 'raw' in _obj` | тела без id (ссылки на них не находятся) |
|
||||
|
||||
**Урок:** при внедрении авто-id недостаточно обернуть **сборку** строки (`build_line`/`add_z_line`) —
|
||||
надо снять **условия допуска** во всех циклах вывода. Иначе объект не доходит до аллокатора и
|
||||
пропадает **без единого сообщения об ошибке**. Признак: `exit=0`, строка отсутствует и в старом, и в
|
||||
новом номере.
|
||||
|
||||
### 25.10. 🎯 ОТКРЫТОЕ РЕШЕНИЕ (вопрос задан Alex, ответа нет)
|
||||
|
||||
Правка трёх барьеров = открыть авто-id для всех секций. Выбор объёма:
|
||||
|
||||
| Вариант | Что делается | Объём / риск |
|
||||
|---|---|---|
|
||||
| **1. Только объекты-листья** *(рекомендация)* | 3 правки условий допуска; объекты, на которые ссылаются по типу, продолжают требовать явный `id` | ~30 мин, риск низкий |
|
||||
| **2. Все секции** | дополнительно снять барьер «`id` у каждой записи» в 6+ циклах + разобрать `_body_type` (тип объекта нужен ДО присвоения id) | ~2 ч, риск средний — можно молча сломать ссылки по типу |
|
||||
|
||||
Аргумент за вариант 1: закрывает реальный сценарий («написал новую реле-команду, номер придумался
|
||||
сам»). Объекты-источники всегда заводит прибор, и номер им выдаёт контроллер, а не мы.
|
||||
|
||||
### 25.11. 📌 Статус на конец сессии 2026-09-18 (для следующей)
|
||||
|
||||
| Позиция | Состояние |
|
||||
|---|---|
|
||||
| Аллокатор, окно, hard fail | ✅ **написано** в `yml-to-config.py` (**НЕ закоммичено**) |
|
||||
| Регресс 9/9 | ✅ `759 → 759`, расхождений 0 |
|
||||
| Авто-id end-to-end | ❌ **не работает**: барьеры §25.9 не сняты |
|
||||
| Валидация ссылок | ❌ нет (питфолл 103) |
|
||||
| Рабочее дерево | `yml-to-config.py` изменён, **113+/26−**, поверх `5d31d6e` |
|
||||
|
||||
🔴 **Работа НЕ закоммичена.** Коммит — по команде Alex (питфолл: он сам скажет).
|
||||
Правки внесены скриптами `/tmp/patch_autoid.py`, `/tmp/patch_autoid2.py`, `/tmp/patch_autoid3.py`
|
||||
(идемпотентные, проверка `count == 1`, печатают OK/SKIP).
|
||||
|
||||
**Инструменты диагностики (в `/tmp`, не в репо):** `/tmp/dbg50.py` (трассировка `build_line`,
|
||||
минимальный кейс сценария), `/tmp/dbg_relaycmd.py` (барьер `'id' in obj`),
|
||||
`/tmp/test_autoid.py` (тест авто-генерации), `/tmp/test_autoid_granica.py` (граница: какие объекты
|
||||
можно без id), `/tmp/accept_autoid.py` (приёмка: замена id + ссылка в никуда).
|
||||
|
||||
**Риски (актуальны):** (а) ссылки в новые объекты — реальная проблема, требует решения шага 3;
|
||||
(б) порядок эмиссии: `S`-блоки первыми, `Z` — после, id нужен раньше строки;
|
||||
(в) ~~верхняя граница~~ — ✅ **ЗАКРЫТО**: фикс-окно `4098..19999`, решение Alex.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 --` затирает незакоммиченное без возврата.
|
||||
|
||||
Reference in New Issue
Block a user