diff --git a/personal/projects/zont-config-compiler.md b/personal/projects/zont-config-compiler.md index 1eadf031..e424a28b 100644 --- a/personal/projects/zont-config-compiler.md +++ b/personal/projects/zont-config-compiler.md @@ -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 — форма согласована ≠ в код внесена diff --git a/personal/tech/roundtrip-key-verification.md b/personal/tech/roundtrip-key-verification.md index f18e390c..f507faa9 100644 --- a/personal/tech/roundtrip-key-verification.md +++ b/personal/tech/roundtrip-key-verification.md @@ -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`, сериализатор с