[2026-09-17] eagle: personal/projects/zont-config-compiler.md personal/tech/roundtrip-key-verification.md
This commit is contained in:
@@ -66,7 +66,23 @@ aliases:
|
||||
- ZONT objcmd args
|
||||
- ZONT три точки входа энкодера
|
||||
- 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 имя действия ключом
|
||||
- ZONT три точки входа энкодера
|
||||
- ZONT _body_index питфолл
|
||||
- ZONT _known_ids питфолл
|
||||
related:
|
||||
- '[[family/tech/zont-api]]'
|
||||
- '[[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`) | — |
|
||||
| 5 | Поле 4 = `2` у `expr` (`10077`–`10085`) — то же производное поле, что поле 5 (§21.5). Имени не дано, энкодер восстанавливает по признаку «второй операнд — литерал» | форма не согласована |
|
||||
| 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`) |
|
||||
| 9 | **Тип 9 (`relay_commands`) в ту же форму** — `set_relay:` с `descr`/`target`/`value` (§26.6) | делать (нужно имя ключа от Alex) |
|
||||
|
||||
> ⚠️ Пункты **7** и **8** добавлены 2026-09-18 — оба согласованы, но не реализованы.
|
||||
> ⚠️ Пункт **8** добавлен 2026-09-18 — согласован, но не реализован.
|
||||
> ✅ Единственный «нерешённый по форме» пункт — 5 (поле 4 у `expr`), и он **не блокирует** круг:
|
||||
> 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
|
||||
@@ -3569,30 +3590,125 @@ next_id = max(все существующие id) + 1 ← НЕ «перв
|
||||
верно. Round-trip доказывает «вывод парсера == вход», про энкодер не говорит ничего (см. док
|
||||
`roundtrip-key-verification`).
|
||||
|
||||
### 26.3. Целевая форма (вариант A, согласована)
|
||||
### 26.3. ✅ ЦЕЛЕВАЯ ФОРМА — имя действия КЛЮЧОМ (внесено в код, круг 45)
|
||||
|
||||
Alex отверг форму `objcmd: <имя>` («и почему objcmd: set_sensor, а не set_sensor так же как set_var»).
|
||||
**Правило: имя действия — ключ узла**, как `set_var`. `cmd` и `objcmd:`-обёртка снесены.
|
||||
|
||||
```yaml
|
||||
- id: 10398
|
||||
objcmd: set_sensor # имя действия (маска 1 %0)
|
||||
cmd: objcmd 8382 "1 %0" # текст команды — читаемость + обратная сборка
|
||||
target: 8382 # кому адресована команда
|
||||
source: 8472 # ← ОТКУДА берётся значение (поле 3 записи)
|
||||
set_sensor: # ← КЛЮЧ = имя действия (маска '1 ')
|
||||
target: 8382 # кому адресована команда
|
||||
source: # ← ОТКУДА значение: тело объекта-источника
|
||||
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`. Одно поле, два типа значения; читатель видит
|
||||
разницу сразу: `8472` есть в конфиге как объект, `14.5` — нет.
|
||||
**Три имени действий** (`_OC_OPS` / `_OBJCMD_MASKS`, ключ — маска **до** `%0`):
|
||||
`'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 строка.
|
||||
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).
|
||||
### 26.3.1. 🔴 ПИТФОЛЛ 93 — `_oc_mask` переименован, а проверка осталась на старом имени
|
||||
|
||||
**Проверка — подмена, не read-back:** поменять `source: 8472` → `source: 8669` в **копии**, собрать,
|
||||
убедиться, что изменилась **ровно одна строка** и в ней `8669`.
|
||||
Маски перешли на базу до `%0` (`'1 '`, `'6,'`, `',,,'`), но строка
|
||||
`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 — форма согласована ≠ в код внесена
|
||||
|
||||
|
||||
@@ -20,6 +20,54 @@ related:
|
||||
> Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит `87e315c`).
|
||||
> Разбор в проекте — [[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`, сериализатор с
|
||||
|
||||
Reference in New Issue
Block a user