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

This commit is contained in:
Alexey Martemyanov
2026-09-17 23:51:44 +06:00
parent 4736965eff
commit b44e948d7c
2 changed files with 182 additions and 18 deletions
+134 -18
View File
@@ -66,7 +66,23 @@ aliases:
- ZONT objcmd args - ZONT objcmd args
- ZONT три точки входа энкодера - ZONT три точки входа энкодера
- ZONT круг 44 - ZONT круг 44
- ZONT круг 45
- ZONT имя действия ключом
- ZONT set_sensor
- ZONT set_analog_output
- ZONT set_contour_temp
- ZONT suffix поля 2
- ZONT _body_index не видит тело
- ZONT _known_ids фильтр
- ZONT raw костыль
- ZONT _reg_inline_body
- ZONT тип 9 set_relay
- ZONT круг 45
- ZONT рабочее дерево 2026-09-18 - ZONT рабочее дерево 2026-09-18
- ZONT имя действия ключом
- ZONT три точки входа энкодера
- ZONT _body_index питфолл
- ZONT _known_ids питфолл
related: related:
- '[[family/tech/zont-api]]' - '[[family/tech/zont-api]]'
- '[[family/how-to/home-automation]]' - '[[family/how-to/home-automation]]'
@@ -3084,13 +3100,18 @@ Alex прочитал `git show --stat ba6ef44` (2 файла, 10 строк) и
| 4 | ✅ **ЗАКРЫТО**`args: [0, 1]` у `#Z10069` была выдумкой; поле 5 записи 59 производное, энкодер ставит сам (§21.5, `4b99d8e`) | — | | 4 | ✅ **ЗАКРЫТО**`args: [0, 1]` у `#Z10069` была выдумкой; поле 5 записи 59 производное, энкодер ставит сам (§21.5, `4b99d8e`) | — |
| 5 | Поле 4 = `2` у `expr` (`10077``10085`) — то же производное поле, что поле 5 (§21.5). Имени не дано, энкодер восстанавливает по признаку «второй операнд — литерал» | форма не согласована | | 5 | Поле 4 = `2` у `expr` (`10077``10085`) — то же производное поле, что поле 5 (§21.5). Имени не дано, энкодер восстанавливает по признаку «второй операнд — литерал» | форма не согласована |
| 6 | Push в Gitea — **15** коммитов не запушены | по команде Alex | | 6 | Push в Gitea — **15** коммитов не запушены | по команде Alex |
| 7 | **`objcmd`: `args` `source`** — форма согласована, в код НЕ внесена (§26) | делать (план готов) | | 7 | **ЗАКРЫТО** (круг 45) — `objcmd` переведён на форму **«имя действия — ключ»**: `set_sensor:` / `set_analog_output:` / `set_contour_temp:` с `target` + `source` (+ `suffix`). `cmd` и `raw` снесены. Круг **759 → 759** (§26.3) | |
| 8 | **Авто-id** — согласовано, план готов (§25), не реализовано | делать (нужен ответ про верхнюю границу `20556`) | | 8 | **Авто-id** — согласовано, план готов (§25), не реализовано | делать (нужен ответ про верхнюю границу `20556`) |
| 9 | **Тип 9 (`relay_commands`) в ту же форму**`set_relay:` с `descr`/`target`/`value` (§26.6) | делать (нужно имя ключа от Alex) |
> ⚠️ Пункты **7** и **8** добавлены 2026-09-18 — оба согласованы, но не реализованы. > ⚠️ Пункт **8** добавлен 2026-09-18 — согласован, но не реализован.
> ✅ Единственный «нерешённый по форме» пункт — 5 (поле 4 у `expr`), и он **не блокирует** круг: > ✅ Единственный «нерешённый по форме» пункт — 5 (поле 4 у `expr`), и он **не блокирует** круг:
> 9/9 зелёный. Всё в этом доке проверено на 9 конфигах. > 9/9 зелёный. Всё в этом доке проверено на 9 конфигах.
**Круг 45 (2026-09-18):** `objcmd` доведён до формы «имя — ключ», круг **759 → 759 чистый**.
Вскрыты 3 новых питфолла: 93 (`_oc_mask` переименован, проверка на старом имени),
94 (`_body_index` не видит тела внутри сценариев), 95 (`_known_ids` как фильтр собственного обхода).
--- ---
## 21. 📌 HEAD и состояние на конец 2026-09-17 ## 21. 📌 HEAD и состояние на конец 2026-09-17
@@ -3569,30 +3590,125 @@ next_id = max(все существующие id) + 1 ← НЕ «перв
верно. Round-trip доказывает «вывод парсера == вход», про энкодер не говорит ничего (см. док верно. Round-trip доказывает «вывод парсера == вход», про энкодер не говорит ничего (см. док
`roundtrip-key-verification`). `roundtrip-key-verification`).
### 26.3. Целевая форма (вариант A, согласована) ### 26.3. ✅ ЦЕЛЕВАЯ ФОРМА — имя действия КЛЮЧОМ (внесено в код, круг 45)
Alex отверг форму `objcmd: <имя>` («и почему objcmd: set_sensor, а не set_sensor так же как set_var»).
**Правило: имя действия — ключ узла**, как `set_var`. `cmd` и `objcmd:`-обёртка снесены.
```yaml ```yaml
- id: 10398 - id: 10398
objcmd: set_sensor # имя действия (маска 1 %0) set_sensor: # ← КЛЮЧ = имя действия (маска '1 ')
cmd: objcmd 8382 "1 %0" # текст команды — читаемость + обратная сборка target: 8382 # кому адресована команда
target: 8382 # кому адресована команда source: # ← ОТКУДА значение: тело объекта-источника
source: 8472 # ← ОТКУДА берётся значение (поле 3 записи) id: 8472
type: var
name: var1
- id: 10381
set_contour_temp: # маска ',,,'
target: 9339
suffix: ';#h' # хвост поля 2 ПОСЛЕ кавычек — часть текста
source:
id: 10380
type: objstate
object: 9841
- id: 10371
set_sensor:
target: 8700
source: 14.5 # литерал — число, объекта нет
``` ```
У `10371` источник — **литерал**: `source: 14.5`. Одно поле, два типа значения; читатель видит **Три имени действий** (`_OC_OPS` / `_OBJCMD_MASKS`, ключ — маска **до** `%0`):
разницу сразу: `8472` есть в конфиге как объект, `14.5` — нет. `'1 '``set_sensor` (датчик, °C) · `'6,'``set_analog_output` (аналоговый выход, В) ·
`',,,'``set_contour_temp` (целевая t контура, °C).
### 26.4. Шаги правки (~40 мин, риск низкий) - **`cmd` снесён** — производное от `set_*` + `target` + `suffix`, энкодер собирает сам
(`_objcmd_code`). Хранить текст в YAML не нужно.
- **`source` раскрывается телом** (`_operand_body`) — той же формой, что операнды `expr`:
`{id, type, name|object|op|left|right}` либо голое число-литерал. Голый id неразличим на глаз.
- **`suffix`** — хвост после закрывающей кавычки (`;#a`, `;#h`). Regex `"([^"]*)"` его НЕ берёт
(обрезает на кавычке) → второй regex `"([^"]*)"(.*)$`. Часть поля 2, к классу действия не относится.
- **`raw` в `source` — костыль, снесён.** Давал 88 лишних блоков в артефакте (питфолл 94).
1. Декодер `config-to-yml.py:1011``node['source'] = _oc_arg` вместо `node['args'] = [_oc_arg]`. ~1 строка. ### 26.3.1. 🔴 ПИТФОЛЛ 93 — `_oc_mask` переименован, а проверка осталась на старом имени
2. Энкодер `yml-to-config.py:1004` — читать `source`, не `args`; поле 5 по-прежнему производное
(`source ∈ _known_ids``0`, иначе `1`). ~3 строки.
3. Энкодер `emit_action:770` — добавить ветку `objcmd` (та же логика, что в `emit_step`).
4. Цикл добивки 1245–1290 — добавить ветку `objcmd`, чтобы объект не проходил по стечению.
5. Круг 9/9 → перегенерировать артефакт **в целевой файл** (§22.5, питфолл 87).
**Проверка — подмена, не read-back:** поменять `source: 8472``source: 8669` в **копии**, собрать, Маски перешли на базу до `%0` (`'1 '`, `'6,'`, `',,,'`), но строка
убедиться, что изменилась **ровно одна строка** и в ней `8669`. `if _OC_OPS.get(_oc_mask) is None:` продолжала смотреть на **полную** маску (`'1 %0'`).
Ключа нет → `None`**все 6 записей молча уходили в fallback `descr`+`args`**.
Круг при этом **зелёный** — fallback доносит байты верно.
**Урок:** переименовал ключ словаря — грепнуть **все** обращения (`_oc_mask`/`_oc_base`).
`grep -n "_oc_mask" config-to-yml.py` находит и словарь, и обе проверки. Общая ловушка §6
доки `roundtrip-key-verification`: зелёный круг + fallback = правка не видна.
### 26.3.2. 🔴 ПИТФОЛЛ 94 — `_body_index` не видит тела, развёрнутые ВНУТРИ сценария
Тела-источники `objcmd` (`10374`, `10376`, `9146`, `10380`, `8472`) живут **не отдельной секцией**,
а словарём внутри шага сценария. `_body_index` строился только по спискам **верхнего уровня**
с ключом `raw``_raw_of()` возвращал `None`**круг терял 5 объектов** (759 → 754).
Три подловушки были вскрыты последовательно:
| Симптом | Причина | Решение |
|---|---|---|
| `-5` объектов | `_body_index` не обходит `scenarios` | обход `_index_body(scenario)` рекурсивно |
| всё ещё `-5` | у тел в `source` **не было `raw`** | `raw` клался в тело (костыль) |
| `-2` (`10372`/`10373`) | вложенные операнды `expr` (`left`/`right`) не регистрировались | `_reg_inline_body` рекурсивно по `left`/`right` |
| `-2` снова | фильтр `_op in _known_ids` **отсекал именно те id**, что нужны | фильтр убран, дедуп на `_register` |
**Финальное решение — `_inline_bodies` + `_reg_inline_body`:** отдельный индекс развёрнутых тел
(признак — `id` + `type`), строка собирается **по полям** хелперами `_script_value_row` /
`_set_var_target_row`, `raw` в YAML не нужен вовсе.
**Урок:** «объект есть в YAML» ≠ «энкодер его выпустит». Индекс по `raw` — хрупкий: любое тело,
развёрнутое на месте, из него выпадает. Считать `_body_index` по **всем** объектам с `id`.
### 26.3.3. 🔴 ПИТФОЛЛ 95 — фильтр по `_known_ids` в собственном обходе = самострел
`_known_ids` (собирается из YAML) содержит id **вложенных** тел тоже — они видны в структуре.
Проверка `if _op in _known_ids: continue` пропускала ровно те объекты, которые и надо выпустить.
`_known_ids` — это «отличить литерал от ссылки» (питфолл 41), **не** «что уже выпущено».
Для второго есть `_emitted_ids`, и он объявлен ниже по коду — тоже подловушка.
### 26.4. ✅ ВЫПОЛНЕНО (2026-09-18, круг 45)
| # | Шаг | Статус |
|---|---|---|
| 1 | Декодер: `source` вместо `args` | ✅ |
| 2 | Энкодер `emit_step`: чтение `source` | ✅ |
| 3 | Энкодер `emit_action`: ветка objcmd | ✅ (`_objcmd_in`, до `'pickle'`) |
| 4 | Цикл добивки: ветка objcmd | ✅ |
| 5 | Круг 9/9 + артефакт в **целевой файл** | ✅ **759 → 759, чистый** |
| 6 | + Форма «имя — ключ» вместо `objcmd:` | ✅ (сверх плана, требование Alex) |
| 7 | + `suffix` (`;#a`/`;#h`) | ✅ (сверх плана, круг +4/-4 без него) |
| 8 | + Снос `raw`-костыля (88 блоков) | ✅ (сверх плана) |
**Проверка — подмена, не read-back:** поменять `source` у записи в **копии**, собрать, убедиться,
что изменилась **ровно одна строка** и в ней новое значение.
**Инструмент:** правки энкодера внесены идемпотентным скриптом `/tmp/fix_encoder_objcmd.py`
(6 замен с проверкой `count == 1`, печатает OK/SKIP) — не sed, не инлайн-питон в шелле.
### 26.6. ⏳ СЛЕДУЮЩЕЕ — тип 9 в ту же форму («имя действия — ключ»)
Запрос Alex в конце круга 45: привести тип 9 (`relay_commands`) к тому же виду, что `set_var`.
```yaml
- id: 9564
set_relay: # ← ключ = действие
descr: Включить выход 13/9: Рад. ванная 2эт
target: 9464
value: true # true / false / 5.2
```
**Что известно по данным** (45 записей типа 9 в дампе 23-21-20):
поле 3 — `'1'` (22 шт., вкл) · `'0'` (21 шт., выкл) · `'8574'` / `'2782'` (2 шт., сетпойнты, °C).
🔴 **`descr` типа 9 — свободный русский текст, ключом быть не может:** 45 уникальных подписей.
Ключ надо выводить **из значения** (`true`/`false` → вкл/выкл, число → сетпойнт).
`descr` при этом **обязан остаться внутри** — иначе 45 подписей исчезнут из YAML.
**Ждём от Alex:** имя ключа (`set_relay` / `set_output` / `relay_cmd`) и подтверждение, что `descr`
остаётся полем. Форма типа 9 — последний незакрытый пункт формы.
### 26.5. 🔴 ПИТФОЛЛ 92 — форма согласована ≠ в код внесена ### 26.5. 🔴 ПИТФОЛЛ 92 — форма согласована ≠ в код внесена
@@ -20,6 +20,54 @@ related:
> Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит `87e315c`). > Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит `87e315c`).
> Разбор в проекте — [[personal/projects/zont-config-compiler]] §10.1. > Разбор в проекте — [[personal/projects/zont-config-compiler]] §10.1.
### 6. 🔴 ПИТФОЛЛ 93 — переименовал ключ словаря → грепни ВСЕ обращения к нему
Маски `objcmd` перешли с полных (`'1 %0'`) на базы до плейсхолдера (`'1 '`), но проверка
`if _OC_OPS.get(_oc_mask) is None:` осталась смотреть на **старое** имя переменной. Ключа нет →
`None`**все 6 записей молча ушли в fallback `descr`+`args`**. Круг **зелёный**: fallback
доносит байты верно, а новый ключ просто не появляется (ровно §6-сценарий выше).
```bash
# переименовал переменную/ключ — найди КАЖДОЕ обращение, а не только определение
grep -n "_oc_mask\|_oc_base" config-to-yml.py # словарь, обе проверки, обе сборки
```
**Симптом, который надо узнать:** артефакт показывает **старую** форму при зелёном круге.
Это не «правка не сохранилась» — это **fallback**, который её перекрывает.
### 7. 🔴 ПИТФОЛЛ 94 — «объект есть в YAML» ≠ «энкодер его выпустит»
Тела, **развёрнутые внутри** другого объекта (операнды `left`/`right`, `source`), не лежат
отдельной секцией. Индекс тел, построенный по спискам верхнего уровня с ключом `raw`, их
**не видит**`_raw_of()` отдаёт `None` → объекты **теряются из выхода**.
Разбор был последовательным и вскрыл **четыре** подловушки:
| Симптом | Причина | Решение |
|---|---|---|
| `-5` объектов | индекс не обходил `scenarios` | рекурсивный обход вложенного |
| всё ещё `-5` | у развёрнутых тел **нет `raw`** | `raw` клался в тело (костыль, потом снесён) |
| `-2` | вложенные операнды `left`/`right` не регистрировались | рекурсия по `left`/`right` |
| `-2` снова | фильтр `x in _known_ids` отсекал **нужные** id | фильтр убран, дедуп на `_register` |
**Правила:**
- Индекс тел собирать по признаку **`id` + `type`**, а не по наличию `raw`.
- Строку собирать **хелпером по полям**`raw` в промежуточном файле не нужен вовсе.
- **`_known_ids` — НЕ «что уже выпущено»**, а «отличить литерал от ссылки». Для второго есть
отдельное множество (`_emitted_ids`). Перепутать = самострел: пропустишь ровно то, что надо выпустить.
### 8. 🔴 ПИТФОЛЛ 95 — костыли в промежуточном формате видно глазом владельца
Чтобы индекс нашёл развёрнутые тела, в них временно клался `raw`. Круг — зелёный, но артефакт
зарос **88 блоками сырья**, и владелец это увидел сразу.
```
круг зелёный + артефакт зарос костылём = правка не принята
```
**Правило:** `raw`/`_`-хелперы в **выходном** файле — только на время отладки. Если костыль нужен
индексу — значит индекс построен по неверному признаку, чинить индекс, а не засорять артефакт.
## Когда применять ## Когда применять
Любой двусторонний конвертер с тестом байтовой точности: `.txt ⇄ .yml`, сериализатор с Любой двусторонний конвертер с тестом байтовой точности: `.txt ⇄ .yml`, сериализатор с