[2026-09-17] eagle: personal/projects/zont-config-compiler.md personal/tech/roundtrip-key-verification.md
This commit is contained in:
@@ -3,7 +3,7 @@ title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml
|
||||
namespace: personal
|
||||
type: how-to
|
||||
created: '2026-09-17'
|
||||
updated: '2026-09-17d'
|
||||
updated: '2026-09-17e'
|
||||
tags:
|
||||
- personal
|
||||
- zont
|
||||
@@ -154,7 +154,7 @@ python3 test_roundtrip.py zont_config/config_X.txt
|
||||
|
||||
`1, 3, 4, 5, 6, 7, 9, 10, 11, 14, 16, 20, 24, 25, 27, 28, 42, 45, 46, 49, 51, 52, 53, 57`
|
||||
➕ **`0`** (дискретные датчики) и **`36`** (их вложенные конфиги) — **25 типов**.
|
||||
➕ конструкции логики **`47` / `48` / `50` / `59`**.
|
||||
➕ конструкции логики **`47` / `48` / `50` / `59`** (тела 59 — `set var` / `puts` / `storeev` / `objcmd`, §5.10).
|
||||
📌 `README_converters.md` перечисляет **23** — пропускает `0` и `36`.
|
||||
|
||||
---
|
||||
@@ -206,7 +206,8 @@ python3 test_roundtrip.py zont_config/config_X.txt
|
||||
| `45` | пауза | `[45, ms]` | `wait: ms` |
|
||||
| `47` | сравнение | `[47, op, left, value|right]` | `{op, left, value}` |
|
||||
| `48` | группа | `[48, logic, [ids]]` | `{group, children}` |
|
||||
| `59` | мини-скрипт | `[59, '<код>', arg1, arg2, flag]` | `{descr, args}` |
|
||||
| `59` | мини-скрипт | `[59, '<код>', arg1, arg2, flag]` | `{descr, args[, set_var]}` |
|
||||
| — | **тела 59** | `set var<N>` → `set_var` ‖ `puts` ‖ `storeev I/A` ‖ `objcmd` (+`target`/`value`) | §5.10 |
|
||||
| `5` | действие над выходом | `[5, '<descr>', output_ref, value, …]` | `{descr, target, value, params?}` |
|
||||
| `9` | команда (реле/контур/режим) | `[9, '<descr>', target, '<value>']` | `{descr, target, value}` |
|
||||
|
||||
@@ -360,9 +361,11 @@ enabled = not (f5 & 8)
|
||||
| `target_name`, `target_type`, `raw_value` | дорисовка парсера, в строке конфига их нет |
|
||||
| `_step_id`, `_trigger_id`, `_then` | выдуманные служебные ключи |
|
||||
| `group`/`op`/`condition`/`operator` как ключи **сценария** | их в конфиге нет |
|
||||
| `set_var_name` / имя целевого объекта вместо id | **подстановка имени из другого объекта** — у ссылки только `id` (§5.10, §10.1) |
|
||||
|
||||
✅ **РАЗРЕШЕНО:** `trigger:` (выводится из тела — один шаг 46), `if`/`then`/`else`/`flag` — **реальные поля
|
||||
записи 46**, `action` — имя списка у шага с поднятым условием.
|
||||
записи 46**, `action` — имя списка у шага с поднятым условием, `set_var` — **id цели записи** из `args[0]`
|
||||
тела `set var<N>` (§10.1).
|
||||
Служебные `_`-ключи — **только** на нестандартных случаях (`_op` при операторе ≠ 1).
|
||||
|
||||
> 📌 **Общий принцип:** YAML-ключ обязан соответствовать полю строки конфига либо выводиться из тела.
|
||||
@@ -414,7 +417,7 @@ enabled = not (f5 & 8)
|
||||
|
||||
| Тело (`descr`) | Кол-во | Что это | Где живёт значение |
|
||||
|---|---|---|---|
|
||||
| `set var1` | 12 | **запись значения объекту** | `args[0]` = id целевого объекта (`8844`, `8831`, `8429`) |
|
||||
| `set var1` | **10** | **запись значения объекту** | `args[0]` = id целевого объекта (`8829`, `8831`, `8844`…); у `#Z8472` = `0` (цели нет) |
|
||||
| `puts "…"` | 5 | **print log** — вывод текста | args = `0,0,0` (текст в самом `descr`) |
|
||||
| `storeev I "…"` / `storeev A "…"` | 5 | **журнал событий**: `I` = инфо, `A` = alert | args = `0,0,0` |
|
||||
| `objcmd <id> "fmt"` | 3 | команда объекту (в т.ч. с хвостом `;#a` / `;#h`) | `args[0]` = id/операнд |
|
||||
@@ -427,15 +430,17 @@ enabled = not (f5 & 8)
|
||||
`#Z8598=59,'storeev I "инфо событие в пн, ср, чт, пт, сб"',0,0,0` ·
|
||||
`#Z8818=59,'objcmd 8700 "1 %0"',14.5,0,1` · `#Z8821=59,'expr "%0 + %1"',8819,8820,0`
|
||||
|
||||
**В YAML сейчас:** `{id, descr: 'set var1', args: [8829, 0, 0]}` — verbatim, без семантики.
|
||||
`target`/`value` дорисовываются только для тел, начинающихся с `objcmd `.
|
||||
**В YAML:** `{id, descr: 'set var1', args: [8829, 0, 0], set_var: 8829}` — цель записи вынесена
|
||||
ключом `set_var` (§10.1, коммит `87e315c`). `target`/`value` дорисовываются только для тел,
|
||||
начинающихся с `objcmd `.
|
||||
|
||||
> 🔴 **Alarm — отдельного типа НЕТ.** Сигнализация/события выражаются телом `storeev` (журнал
|
||||
> событий) и условием на битовую маску. Не искать «тип alarm» в конфиге.
|
||||
|
||||
> 🔴 **Дыра в читаемости:** 12 записей `set var1` **переиспользуют** один и тот же `descr` — по YAML
|
||||
> не видно, что они разные. А несущие объекты (`#Z8830` → `#Z8829=49,8450,1,0`; маски `#Z8844=50,1,0,0,123`)
|
||||
> лежат в `scenario_orphans` как raw-склад и в теле сценария **не развёрнуты**.
|
||||
> 🔴 **Дыра в читаемости (частично закрыта):** 10 записей `set var1` **переиспользуют** один и тот же
|
||||
> `descr` — по YAML не видно, что они разные. Теперь различаются ключом `set_var` (§10.1). А несущие
|
||||
> объекты (`#Z8830` → `#Z8829=49,8450,1,0`; маски `#Z8844=50,1,0,0,123`) по-прежнему лежат в
|
||||
> `scenario_orphans` как raw-склад и в тело сценария **не развёрнуты** (вариант B не выбирался).
|
||||
|
||||
> ⚠️ **Артефакт парсера:** `#Z8195` (SMS) в `steps` → `raw: *id002` — PyYAML-анкор на секцию
|
||||
> `sms_notifications`. Два анкора в файле (`&id001` sensors, `&id002` SMS) — **законные**, это не
|
||||
@@ -475,6 +480,27 @@ diff A.txt B.txt # пусто = чисто
|
||||
> ⚠️ **`>` затирает целевой файл ещё до старта питона** — пустой `.yml` рядом с непустым `.txt` означает
|
||||
> **падение** конвертера: смотреть stderr.
|
||||
|
||||
### 🔴 Проверка НОВОГО ключа — тест на подмену, а не round-trip
|
||||
|
||||
Round-trip остаётся зелёным, даже если энкодер **полностью игнорирует** новый ключ: он сравнивает
|
||||
то, что положил парсер. Для каждого нового YAML-ключа обязателен один тест **эффекта**:
|
||||
|
||||
```bash
|
||||
cd /tmp && rm -rf zt && mkdir zt && cd zt
|
||||
cp /Users/admin/Automation/HA-ZONT-Modbus/zont_config/<снимок>.yml t.yml
|
||||
# подменить ОДНО значение нового ключа и пересобрать .txt
|
||||
/usr/bin/python3 /Users/admin/Automation/HA-ZONT-Modbus/yml-to-config.py t.yml > out.txt
|
||||
iconv -f cp1251 -t utf-8 out.txt | tr -d '\r' | grep '^#Z<id>=<тип>'
|
||||
# ✅ новое значение на месте ❌ старое = правка не доехала (искать ВТОРУЮ точку входа)
|
||||
```
|
||||
|
||||
Реальный случай 2026-09-17 (`set_var`, §10.1): правка была внесена только в `emit_action`,
|
||||
инлайн-ветка `emit_step` её игнорировала → round-trip 🟢, значение на выходе **старое**.
|
||||
|
||||
> 📌 Правило «проверка — результат, а не факт записи» действует и здесь: `read back` YAML = «записалось»,
|
||||
> подмена + пересборка = «работает». Сравнивать `diff` против оригинала — должно измениться
|
||||
> **ровно ожидаемое число строк**.
|
||||
|
||||
---
|
||||
|
||||
## 7. Питфоллы
|
||||
@@ -490,7 +516,8 @@ diff A.txt B.txt # пусто = чисто
|
||||
| 5 | 🔴 **Гонять конвертер в ЦЕЛЕВОЙ файл, а не в `/tmp`** | Проверка в `/tmp` не проверяет артефакт. Alex видел `type: trigger` в файле, который я не перегенерировал |
|
||||
| 6 | 🔴 **Не отдавать артефакт, не прочитав его самому** | Round-trip «байты сходятся» ≠ «читаемо». Форма `blocks` проходила round-trip, но `8456` превращался в список цифр — выявил Alex |
|
||||
| 7 | 🔴 **Артефакты — в проект, не в `/tmp`** | «качай доки в папку в проекте а не в темп» |
|
||||
| 8 | 🔴 **Бэкапы кода — только git** | «какой нахуй бэкап скриптов — там в гите все» |
|
||||
| 8 | 🔴 **Бэкапы кода — только git. Не создавать копии «на всякий»** | «какой нахуй бэкап скриптов — там в гите все» (2026-09-17: «какой нах бэкап. у нас гит»). Коммит-чекпойнт ДО правки = бэкап; откат = `git checkout <SHA>`. Копии файлов в проект **не плодить**. Исключение — **доки Obsidian перед УДАЛЕНИЕМ** (MCP-удаление необратимо) |
|
||||
| 8a | ⚠️ **`git commit` на отсутствующих изменениях → exit 1** | Если дерево чистое, «коммит ДО» делать нечего — уже закоммичено. Проверить `git log -1`, не считать exit 1 провалом |
|
||||
| 9 | 🔴 **`read_file` возвращает контент с номерами строк — не patch-ить им vault** | Обсидиан — только через obsidian-MCP |
|
||||
| 10 | 🔴 **Коммит до проверки Alex** | Порядок: коммит ДО → правка → заливка → **проверка Alex** → коммит ПОСЛЕ |
|
||||
| 11 | ⚠️ `grep -n "id: N"` по YAML даёт **несколько** совпадений | Объект живёт и в своей секции, и внутри сценария. Номер строки меняется между генерациями |
|
||||
@@ -516,8 +543,9 @@ diff A.txt B.txt # пусто = чисто
|
||||
| 26 | 🔴 **Правка комментария — не правка кода** | Три ответа подряд «готово, зелёный», при том что горячий блок стоял выше моего нового. Мёртвый новый код = ложный зелёный |
|
||||
| 27 | ⚠️ **`>` в шелле затирает `.yml` до старта питона** | Проверять exit-код |
|
||||
| 28 | ⚠️ **Один шаг может принадлежать нескольким сценариям** | Хелпер `_register()` — обновляет запись по id, не добавляет дубль |
|
||||
| 28a | 🔴 **12 разных `set var1` выглядят в YAML одинаково** | `descr` у всех один, различие — только в `args[0]`. Не «оптимизировать» их обратно в одну запись. Смысл теряется: см. §5.10, план §10.1 |
|
||||
| 28a | 🔴 **10 разных `set var1` выглядят в YAML одинаково** | `descr` у всех один, различие — в `set_var` (+ `args[0]`). Не «оптимизировать» их обратно в одну запись. §5.10, §10.1 |
|
||||
| 28b | ⚠️ **Несущие объекты 49/50 живут в `scenario_orphans`, не в теле шага** | `#Z8830` → `#Z8829=49,8450,1,0`. Sweep (§7.4) докидывает их только как raw-склад. Раскрытие = отдельное решение (вариант B §10.1), не «попутная починка» |
|
||||
| 28c | 🔴 **Две точки входа энкодера: правка в одной = потеря правки при зелёном round-trip** | `emit_action` (скрипты из списков/`scenario_orphans`) и инлайн-ветка `emit_step` (`descr`+`args`) — **форк**. Новый ключ вводить в **обе**; проверять тестом на **подмену значения** (§10.1) |
|
||||
|
||||
### 7.3. Проверка гипотез
|
||||
|
||||
@@ -616,10 +644,10 @@ https://lk.zont-online.ru/download/firmwares/H2000_PRO_<HW>__<FW>_<PROFILE>.zip
|
||||
|---|---|
|
||||
| Round-trip | 🟢 **ЗЕЛЁНЫЙ, байт-в-байт** — `661 → 661`, `686 → 686`, `diff` = 0 (проверен на `19-53-21`) |
|
||||
| Форма сценария | ✅ закрыта (§5), оба конвертера переведены |
|
||||
| Тела типа 59 | 📖 таксономия собрана (§5.10) · ⏳ `set_var` не развёрнут — **план §10.1, ждём ответа** |
|
||||
| Коммит | `823fabd` на `main` — «Scenario YAML shape: bare action ids, no anchors» (3 файла, +200/−1000) |
|
||||
| Последний | `b75c51f` — «Drop stale YAML snapshots in old scenario shape» |
|
||||
| Предыдущий | `1cc010a` — **содержит сломанные версии** (анкоры, лишний `if`, `then` вместо `action`); закрыт §9.1 |
|
||||
| Тела типа 59 | ✅ **разобраны (§5.10)** · `set_var` развёрнут (§10.1, коммит `87e315c`); несущие условия 49/50 — нет |
|
||||
| Коммит | `87e315c` — «Type 59: decode set-var target into 'set_var' (parser + encoder, both entry points)» (4 файла) |
|
||||
| Предыдущий | `b75c51f` — «Drop stale YAML snapshots in old scenario shape» |
|
||||
| Ранее | `823fabd` — форма сценария §5; `1cc010a` — **содержит сломанные версии**; закрыт §9.1 |
|
||||
| Документация | ✅ **три дока сведены в один** — `personal/projects/zont-config-compiler.md` (§9.2) |
|
||||
| Push | ❌ **не сделан** |
|
||||
|
||||
@@ -646,6 +674,9 @@ git commit -m "Scenario YAML shape: bare action ids, no anchors"
|
||||
> 📌 **`--amend` не понадобился** — история фиксирует обе итерации формы, откат возможен по SHA.
|
||||
> В коммит вошли **только 3 файла** — 10 скриптов разбора и 2 папки остались untracked намеренно.
|
||||
|
||||
> 🔴 **Бэкап-папка `backups_before_scripts_*` УДАЛЕНА** — Alex: «какой нах бэкап. у нас гит».
|
||||
> Путь отката — только git-история (`b75c51f` → `87e315c` → …). Не воссоздавать (питфолл 8).
|
||||
|
||||
### 9.2. Мерж трёх доков в один (2026-09-17)
|
||||
|
||||
Документация конвертеров жила в **трёх** доках с наложенными слоями правок (§5b→§5c→§5j→§5k→§6→§8)
|
||||
@@ -681,39 +712,67 @@ git commit -m "Scenario YAML shape: bare action ids, no anchors"
|
||||
|
||||
1. **Тип 50** — маска дней недели: `#Z8548=50,1,0,0,109`, где `109 = 0b1101101` = пн, ср, чт, сб, вс.
|
||||
Alex: «это выбор дней недели, то же самое что ставится в значение var1». Сейчас `raw`.
|
||||
2. **`set var1` с `args: [8844, 0, 0]`** — внутри объекта `8844` лежит та же маска дней недели,
|
||||
объект не раскрыт: «что какого-то хуя уехало вовне сценария вообще — только там пн, вт, чт, пт, сб, вс».
|
||||
→ **см. §10.1, план в работе.**
|
||||
2. **Несущие объекты `set var1`** — `set_var` даёт **id цели** (`8844`), но тело цели (`#Z8844=50,1,0,0,123` —
|
||||
та же маска дней недели; `#Z8829=49,8450,1,0` — условие) лежит в `scenario_orphans`, в тело сценария
|
||||
не развёрнуто. Alex: «что какого-то хуя уехало вовне сценария вообще — только там пн, вт, чт, пт, сб, вс».
|
||||
→ **вариант B, §10.1 (не выбирался).**
|
||||
3. **`unresolved: true`** у `#Z8860`, `#Z8864`, `#Z8601` — этих объектов нет в конфиге.
|
||||
4. **Тип 3** (SMS `8195`) — тело лежит якорем в `sms_notifications`, не раскрыто.
|
||||
5. **Тип 11 внутри `steps` другого сценария** — ссылка на сценарий голым `id` (`#Z8456` держит `11109`).
|
||||
6. **Старые снапшоты `14-16-35.yml` / `16-02-18.yml`** — по 2 вхождения старого `type:`, не перегенерированы.
|
||||
|
||||
### 10.1. 📋 ПЛАН: развернуть `set var` / `print log` / alarm (2026-09-17, ОЖИДАЕТ ОТВЕТА)
|
||||
### 10.1. ✅ ВЫПОЛНЕНО: `set_var` в записи 59 (2026-09-17)
|
||||
|
||||
**Запрос Alex:** «Теперь разверни set var, print log и alarm нотификации».
|
||||
|
||||
**Разведка (сделана):** снят живой конфиг `config_local_2026-09-17_19-53-21.txt` (686 строк,
|
||||
661 `#Z`), round-trip 🟢 чистый, таксономия §5.10. Три конструкции = тела **типа 59**.
|
||||
**Итог:** все три — тела **типа 59** (§5.10). Развёрнут `set_var`; `puts`/`storeev` уже
|
||||
самодостаточны (`descr` + args), отдельного типа alarm нет. Сделано **вариантом A**.
|
||||
|
||||
| Шаг | Что | Файл |
|
||||
|---|---|---|
|
||||
| 1 | ✅ **Бэкап** | `zont_config/backups_before_scripts_20260917_195411/` (оба конвертера + `test_roundtrip.py` + txt) |
|
||||
| 2 | В `dump` типа 59: если `descr` начинается с `set var` → добавить `set_var: <args[0]>` (голый id; резолв имени **запрещён** §5.7) | `config-to-yml.py` |
|
||||
| 3 | В `emit_action`: `set_var` → собрать `[59, 'set var<N>', set_var, 0, 0]` | `yml-to-config.py` |
|
||||
| 4 | ⚠️ Развернуть 12 несущих условий (`#Z8829`/`8831`/…/`8847`, типы 49/50) из `scenario_orphans` | оба |
|
||||
| 5 | Round-trip + правка §4/§5.7 + снять п.2 этого списка | док |
|
||||
| Шаг | Что | Файл | Статус |
|
||||
|---|---|---|---|
|
||||
| 1 | Коммит ДО (бэкап не нужен — git, питфолл 8) | `b75c51f` | ✅ |
|
||||
| 2 | `dump` типа 59: `descr` начинается с `set var` **и** `args[0]` — непустой int (не `bool`/`float`) → `set_var: <args[0]>` | `config-to-yml.py` | ✅ |
|
||||
| 3 | `emit_action` **и** инлайн-ветка `emit_step`: `set_var` переопределяет arg 1, тип проверяется (не int → exit 2) | `yml-to-config.py` | ✅ |
|
||||
| 4 | Развернуть несущие условия (`#Z8829`/`8831`/…/`8847`) | — | ❌ не делалось (вариант A) |
|
||||
| 5 | Round-trip + док | док | ✅ |
|
||||
|
||||
**Открытый вопрос (задан Alex, ответа нет):** делать ли шаг 4.
|
||||
**Коммит ПОСЛЕ:** `87e315c` — «Type 59: decode set-var target into 'set_var' (parser + encoder,
|
||||
both entry points)», 4 файла (2 конвертера + снимок `19-53-21` `.txt`/`.yml`).
|
||||
|
||||
- **Вариант A (рекомендован)** — только `set_var` в 59 (шаги 2, 3, 5). Минимум риска,
|
||||
round-trip гарантированно зелёный. Шаг 4 не трогаем.
|
||||
- **Вариант B** — A + развернуть несущие условия 49/50. Полный смысл, но **меняет форму `8456`**
|
||||
(сейчас та байт-совместима через `scenario_orphans`).
|
||||
#### 🔴 Питфолл, поймавший реальный баг: круг «правка в шаге не доезжает»
|
||||
|
||||
**Порядок работ (правило Alex):** коммит ДО → правка → заливка → проверка Alex → коммит ПОСЛЕ.
|
||||
Две точки входа — **разные**. `emit_action` (скрипты в `scenario_orphans`/списках) и
|
||||
**инлайн-ветка `emit_step`** (`if 'descr' in step and 'args' in step`) — форк: правка только в
|
||||
`emit_action` даёт **зелёный round-trip** и **незамеченную потерю правки**.
|
||||
|
||||
**Текущее состояние:** разведка + бэкап сделаны, **правок кода НЕТ**. Ждём «A» или «B».
|
||||
Проверка, которая это вскрыла (проверять не `read back`, а **эффект**):
|
||||
|
||||
```bash
|
||||
cd /tmp && rm -rf zt && mkdir zt && cd zt
|
||||
cp /Users/admin/Automation/HA-ZONT-Modbus/zont_config/config_local_2026-09-17_19-53-21.yml t.yml
|
||||
# подменить ОДИН set_var: 8829 -> 7777 (в блоке args descr: set var1)
|
||||
/usr/bin/python3 /Users/admin/Automation/HA-ZONT-Modbus/yml-to-config.py t.yml > out.txt
|
||||
iconv -f cp1251 -t utf-8 out.txt | tr -d '\r' | grep '^#Z8830=59'
|
||||
# ❌ 8829 = правка не доехала ✅ 7777 = доехала
|
||||
```
|
||||
|
||||
> 📌 **Правило:** round-trip зелёный ≠ правка работает. Round-trip читает то, что положил парсер;
|
||||
> если энкодер проигнорировал ключ — байты сходятся. Один тест на **подмену значения** обязателен
|
||||
> для каждого нового ключа. После починки инлайн-ветки: `#Z8830=59,'set var1',7777,0,0`,
|
||||
> `diff` vs оригинал = **ровно 1 строка**. (Питфолл 25 — тот же корень: объект отображается в
|
||||
> двух местах, править надо **все**.)
|
||||
|
||||
#### Факт по данным
|
||||
|
||||
- `set var1` в конфиге **10**, не 12 (в плане было 12 — ошибка счёта). Формы: 9 с ненулевым
|
||||
`args[0]` → получили `set_var`; `#Z8472=59,'set var1',0,0,0` (arg1 = 0) → **без** `set_var`
|
||||
(запись ничего не пишет, разворачивать нечего).
|
||||
- `set var1` — 0 не влезает в предикат намеренно: `0` = «нет цели», не id.
|
||||
|
||||
**Что НЕ сделано (сознательно):** несущие условия `#Z8830 → #Z8829=49,8450,1,0` и маски
|
||||
`#Z8844=50,1,0,0,123` остались в `scenario_orphans` как raw-склад — разворачивание их в тело
|
||||
сценария меняет форму `8456` (вариант B, Alex не выбрал). Видно по `set_var: 8829` — цель есть,
|
||||
тело цели лежит рядом в `scenario_orphans`.
|
||||
|
||||
---
|
||||
|
||||
@@ -721,18 +780,17 @@ git commit -m "Scenario YAML shape: bare action ids, no anchors"
|
||||
|
||||
| Файл | Статус |
|
||||
|---|---|
|
||||
| `config-to-yml.py` | ✅ форма §5: `trigger:` подъём через `pop('if')`, шаг = `{id, [flag], action}` или `{id, [flag], if, then, [else]}` · ⏳ тип 59 без `set_var` (§10.1) |
|
||||
| `yml-to-config.py` | ✅ `emit_step` читает `then`/`action`/`else`/`flag`; `f5` из `trigger:`/`interval_ms` + бит 8 · ⏳ тип 59 без `set_var` (§10.1) |
|
||||
| `config-to-yml.py` | ✅ форма §5: `trigger:` подъём через `pop('if')`, шаг = `{id, [flag], action}` или `{id, [flag], if, then, [else]}` · ✅ тип 59 даёт `set_var` (§10.1) |
|
||||
| `yml-to-config.py` | ✅ `emit_step` читает `then`/`action`/`else`/`flag`; `f5` из `trigger:`/`interval_ms` + бит 8 · ✅ `set_var` в **обеих** точках (`emit_action` + инлайн `emit_step`) |
|
||||
| `test_roundtrip.py` | ✅ без изменений (в коммите `199f2b1`) |
|
||||
| `zont_config/config_local_2026-09-17_19-53-21.{txt}` | 🆕 **актуальный** снимок с прибора (686 строк, 661 `#Z`) |
|
||||
| `zont_config/config_local_2026-09-17_19-53-21.{txt,yml}` | 🆕 **актуальный** снимок с прибора (686 строк, 661 `#Z`) — в коммите `87e315c` |
|
||||
| `zont_config/config_local_2026-09-17_18-43-24.{txt,yml}` | предыдущий снимок, форма §5 |
|
||||
| `zont_config/backups_before_scripts_20260917_195411/` | 🆕 бэкап **перед** правками типа 59 (оба конвертера + `test_roundtrip.py` + txt) |
|
||||
| `zont_config/config_local_2026-09-17_{17-45-00,16-13-28,16-02-18,14-16-35}.*` | исторические снапшоты |
|
||||
| `zont_config/config_0FA7C33CC89F_…_12-12-28.txt` | боевой конфиг (598 `#Z`, 25 `#S`) |
|
||||
| `zont_config/archive/` | `-2`/`-3`/`-4` — закоммичены (`7ae0e32`) |
|
||||
|
||||
> 📌 **Бэкап кода — только git** (питфолл 8), но **перед правкой типа 59** Alex просил бэкап
|
||||
> рабочей копии — он лежит в `backups_before_scripts_<TS>/`, не в `/tmp`.
|
||||
> 📌 **Бэкап кода — только git** (питфолл 8). Alex 2026-09-17: «какой нах бэкап. у нас гит».
|
||||
> Копии файлов перед правкой **не делать**, рабочий откат = `git checkout <SHA>`.
|
||||
|
||||
**Не относится к конвертерам** (исторический TrueNAS-стек, **декомиссирован**):
|
||||
`INFRASTRUCTURE.md`, `docker-compose.yml`, `docker run.txt`, `modbus_*_bridge.py`,
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
---
|
||||
title: 🔁 Round-trip key verification — проверка нового ключа конвертера
|
||||
namespace: personal
|
||||
type: how-to
|
||||
created: '2026-09-17'
|
||||
updated: '2026-09-17'
|
||||
tags:
|
||||
- personal
|
||||
- how-to
|
||||
- testing
|
||||
- converters
|
||||
- reference
|
||||
related:
|
||||
- '[[personal/projects/zont-config-compiler]]'
|
||||
---
|
||||
|
||||
# 🔁 Round-trip key verification
|
||||
|
||||
> Зеркало навыка `roundtrip-key-verification` (категория `software-development`, Hermes).
|
||||
> Появился из реального бага в ZONT-конвертере 2026-09-17 (коммит `87e315c`).
|
||||
> Разбор в проекте — [[personal/projects/zont-config-compiler]] §10.1.
|
||||
|
||||
## Когда применять
|
||||
|
||||
Любой двусторонний конвертер с тестом байтовой точности: `.txt ⇄ .yml`, сериализатор с
|
||||
decode + encode, dump/undump AST, DB ⇄ ORM.
|
||||
|
||||
**Триггер:** добавил **новый ключ** в декодированный вывод и round-trip зелёный.
|
||||
|
||||
## 🔴 Ловушка
|
||||
|
||||
Байтовый round-trip доказывает, что **вывод парсера == вход**. Про энкодер он не говорит
|
||||
ничего — энкодер получает ровно то, что парсер положил.
|
||||
|
||||
Если энкодер **игнорирует** новый ключ, тест всё равно зелёный:
|
||||
|
||||
```
|
||||
original.txt → parse → YAML(с новым ключом) → encode(ключ игнорируется) → original.txt ✅ diff = 0
|
||||
▲ ключ добавлен ▲ ключ потерян, старое поле доносит байты
|
||||
```
|
||||
|
||||
Потеря невидима: старое поле (`args`) рядом и по-прежнему собирает байты.
|
||||
|
||||
**Реальный случай:** ключ `set_var` добавлен в парсер типа 59, энкодер поправлен **только**
|
||||
в `emit_action`. Вторая точка входа — инлайн-ветка `emit_step` (`'descr' in step and 'args' in step`) —
|
||||
ключ игнорировала. Round-trip 🟢, правка пользователя молча терялась.
|
||||
|
||||
## Обязательный шаг: тест подменой (mutation)
|
||||
|
||||
`read back` («записалось») — **не** проверка. Подменить новое значение и пересобрать; эффект
|
||||
обязан появиться в выходе.
|
||||
|
||||
```bash
|
||||
cd /tmp && rm -rf t && mkdir t && cd t
|
||||
cp <реальный-артефакт>.yml t.yml
|
||||
python3 - <<'EOF'
|
||||
s = open('t.yml', encoding='utf-8').read()
|
||||
old = "...блок со старым значением нового ключа..."
|
||||
new = "...тот же блок, значение заменено на маркер (напр. 7777)..."
|
||||
assert s.count(old) == 1, s.count(old) # assert уникальности — без случайных мультиправок
|
||||
open('t.yml', 'w', encoding='utf-8').write(s.replace(old, new))
|
||||
EOF
|
||||
python3 <проект>/encode.py t.yml > out.txt
|
||||
diff <(iconv -f <вх> -t utf-8 <оригинал> | tr -d '\r' | sort) \
|
||||
<(iconv -f <вых> -t utf-8 out.txt | tr -d '\r' | sort)
|
||||
# ✅ изменилась РОВНО одна строка, в ней маркер
|
||||
# ❌ ноль изменений = энкодер ключ проигнорировал → искать ДРУГУЮ ветку кода
|
||||
```
|
||||
|
||||
Проверять **обе** половины:
|
||||
1. **Парсер** — ключ есть в промежуточном файле, на ожидаемом месте.
|
||||
2. **Энкодер** — подмена доезжает до выхода, `diff` = ровно 1 строка.
|
||||
|
||||
## Найти ВСЕ точки входа
|
||||
|
||||
У конвертеров их обычно две и больше: рекурсивный обход и инлайн/ссылочная ветка, либо
|
||||
проход по реестру типов и явный цикл.
|
||||
|
||||
- `grep -n "def emit_<x>\|def dump_<x>" *.py` — выписать **всех** вызывающих.
|
||||
- Не править одну и полагать, что покрыта другая. Дублированные ветки — норма.
|
||||
- Симптом пропущенной ветки: в тесте подменой остаётся старое значение.
|
||||
|
||||
В **каждой** ветке ставить проверку типа, чтобы плохое значение падало громко (`exit 2`),
|
||||
а не тихо уходило сырым полем.
|
||||
|
||||
## Смежные питфоллы
|
||||
|
||||
- **Проверять тип производного ключа.** Парсер, сохраняющий whole-float как float (`14.5`, `1.0`),
|
||||
даст не-int для ключа-идентификатора. Guard:
|
||||
`isinstance(v, int) and not isinstance(v, bool) and not isinstance(v, float) and v != 0`.
|
||||
`0` = «цели нет», а не валидный id — пропускать, не выдумывать.
|
||||
- **Один объект в двух секциях** → ключ править во **всех** местах отображения, иначе одна
|
||||
проекция показывает новую семантику, другая старую. Round-trip и тут зелёный.
|
||||
- **Считать вхождения по данным**, не по плану: «12 элементов» из плана оказались 10 в конфиге.
|
||||
- **Не резолвить имя из другого объекта.** Производный ключ несёт сырой id; `target_name`
|
||||
вместо `target` — запрещённая подстановка.
|
||||
|
||||
## Что записать после проверки
|
||||
|
||||
1. Новый ключ, где эмитится, какие точки входа поправлены.
|
||||
2. Команду подмены и наблюдённый результат («1 строка изменилась, маркер на месте»).
|
||||
3. Питфолл «две точки входа» — чтобы следующая сессия сразу проверяла обе.
|
||||
Reference in New Issue
Block a user