[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 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`, сериализатор с
|
||||||
|
|||||||
Reference in New Issue
Block a user