[2026-09-17] eagle: family/how-to/zont-config-compiler.md family/tech/zont-config-object-types.md

This commit is contained in:
Alexey Martemyanov
2026-09-17 19:27:08 +06:00
parent 3e4dc4c8cf
commit 9b65e8f2c7
2 changed files with 186 additions and 75 deletions
+144 -54
View File
@@ -21,7 +21,7 @@ tags:
- homeautomation
title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml
type: how-to
updated: 2026-09-17d
updated: 2026-09-17f
---
# ⚙️ ZONT Config Compiler — конвертеры `.txt ⇄ .yml`
@@ -359,6 +359,10 @@ diff A.txt B.txt # пусто = round-trip чистый
| 55 | 🔴 **Переименование служебного ключа не решает проблему** | `_f5``field5` («вербатим, не служебный») Alex отверг так же: «какой нахуй field5 ЕБАТЬ ТЕБЯ В СРАКУ?! МЫ НАХУЯ ЕГО СНОСИЛИ!!!». Если поле не является полем строки конфига — его нельзя ни прятать под `_`, ни выносить в открытый ключ |
| 56 | 🔴 **Не докладывать баг по выводу `awk`/`grep`, не проверив вторым инструментом** | `awk '/^- id: 9691$/,/^- id: /'` вернул одну строку → я объявил «сценарий пустой, уехал в `orphans`». Это ошибка диапазона `awk` (диапазон закрылся на следующем `- id:` через строку), а не факт файла. Сценарий был полный |
| 57 | 🔴 **Отсутствие поля в конфиге ≠ поле можно подставлять/переименовывать** | Поле 1 записи 46 (`#Z8862=46,1,…`) встречается 1 раз из 69. Соблазн: счесть константой `0` и не хранить. Правильно: сохранить как `flag` (имя по позиции), семантику не выдумывать. То же — `days_mask` для типа 50 |
| 58 | 🔴🔴 **Проверять в ЦЕЛЕВОЙ файл, а не в `/tmp`** | Гонял `config-to-yml.py … > /tmp/new*.yml`, а снапшот на диске остался старым → Alex увидел `type: trigger` в `18-43-24.yml` и справедливо спросил «я чето не понял?». **Проверка в `/tmp` не проверяет артефакт.** Писать сразу в целевой путь, отдельно сверять его глазами |
| 59 | 🔴🔴 **`emit_step` имел ДВЕ ветки для 46 — старая перехватывала новую** | `emit_step:490` (`if 'if' in step:`) стоял **выше** нового (`if 'if' in step or 'then' in step:`) и читал удалённые `action`/`_else`/`_kind` → 12 объектов из `then` не регистрировались. Симптом: «потеряно 146, добавлено 134». **Перед отладкой логики искать дубли-ветки того же условия** (`grep -n "if '\w*' in step"`) |
| 60 | 🔴 **`emit_action` для вложенного 46 возвращал id без регистрации тела** | `if 'action' in node: return aid` — остаток от формы с поднятым триггером. Тела пауз `11824`/`11825`/`11826` пропадали, `#Z11827` выходил как `[46,0,11823,[],[]]`. Удалён; вместо него `emit_step(node)` |
| 61 | 🔴 **Тело сценария не определяет тип — определяет ФОРМА `steps`** | Круг 5: после вырезания `trigger:` я выводил тип из «есть шаг 46» → 2 расхождения. Признак — **`steps` = ровно один элемент, и он 46**. Alex сказал прямо: «наличием поля `trigger:`». **Когда Alex называет поле — это ответ, а не повод искать обходной путь** |
### Ограничения конвертера (найдено 2026-09-17) — ВСЕ ЗАКРЫТЫ
@@ -422,15 +426,18 @@ diff A.txt B.txt # пусто = round-trip чистый
## 5c. 🔄 Переработка структуры YAML сценариев — ✅ СДЕЛАНО 2026-09-17
> ✅ **СТАТУС (2026-09-17, после правок формы): парсер дописан, YAML перегенерирован.**
> 🟢 **Round-trip зелёный** по множеству строк: `661 → 661` объектов, `686 → 686` строк.
> ✅ **СТАТУС (2026-09-17, ФИНАЛ): форма закрыта, round-trip байт-в-байт чистый.**
> 🟢 `661 → 661` объектов, `686 → 686` строк, `diff` по `#Z`-строкам = **0**.
>
> 🔴 **ВАЖНО — форма §5c/§5k НЕ является финальной.** В конце сессии Alex вырезал из неё
> `type`, `field5` и `trigger` на уровне сценария. **Актуальная форма — §6a.**
> История отвергнутых решений — §6c (4 круга).
> **Актуальная форма — §6a.** Сценарий = `id`/`name`/`enabled` + опциональный `trigger:` +
> `steps[]`. Шаг 46 = `{id, [flag], if, then[], [else[]]}`.
> **`type`, `field5`, `kind`, `_f5`, `_kind`, `_else`, `action`, `base5` — вырезаны из обоих
> конвертеров.**
>
> Открытый вопрос, блокирующий правку: **чем в YAML отличать `manual` от `trigger`**, если
> ни `type`, ни `field5` писать нельзя. Факты — §6b, разбор — §6.
> ✅ **Открытый вопрос `manual` vs `trigger` ЗАКРЫТ:** признак — **наличие `trigger:`** у сценария;
> он выводится из тела (условие поднимается, если `steps` = ровно один шаг 46). Alex:
> «наличием поля trigger:». Разбор — §6, правило — §6a, факты — §6b.
> История 5 кругов вокруг вырезания/возврата выдуманных полей — §6c.
> Семантика типов 47/48/50/59 раскрыта (ответы Alex — [[family/tech/zont-scenario-logic-11109]] §8.7).
> **Type 9 и type 5 раскрыты из `raw` в читаемую форму.**
> **Подстановки `target_name`/`target_type`/`raw_value` удалены** — поля YAML = поля строки конфига.
@@ -452,10 +459,10 @@ diff A.txt B.txt # пусто = round-trip чистый
### 🔴 Актуальная форма сценария в YAML (принята Alex, 2026-09-17)
> ⚠️ **УСТАРЕЛО — ниже форма, ОТВЕРГНУТАЯ Alex в конце сессии (круги 2–4).**
> Актуальная форма — **§6a**. В 2026-09-17 форма ниже жила в коде примерно час и была вырезана:
> `field5` («МЫ НАХУЯ ЕГО СНОСИЛИ!!!»), `trigger` на уровне сценария, `action` вместо `then`.
> Блок сохранён как история отвергнутых решений.
> **ИСТОРИЧЕСКАЯ форма (была в коде ~час, вырезана).** `field5` («МЫ НАХУЯ ЕГО
> СНОСИЛИ!!!»), `type`, `action` вместо `then` — **удалены**. ⚠️ **`trigger:` на уровне
> сценария, наоборот, ВЕРНУЛСЯ** в финале и является правильным решением (см. §6a).
> Актуальная форма — **§6a**.
```yaml
- id: 9691
@@ -602,7 +609,7 @@ Alex: «ЕСЛИ БЛЯДЬ В КОНФИГЕ `#Z8817=9,'…',10034,'2782'` ТО
| 6 | ~~Открытый вопрос: `steps[].action` при >1 id~~ | ✅ **решено**: `action` — объект при одном, список при нескольких; тела инлайн |
| 7 | ~~Round-trip сломан — энкодер не переведён~~ | ✅ **энкодер переведён, round-trip ЗЕЛЁНЫЙ** (661→661, 686→686) |
| 8 | ~~Поле 5 типа 11 — семантика не раскрыта~~ | ✅ **закрыто**: тип + `enabled`; `type` из `f5 & 7` |
| 9 | 🔴 **`if` у trigger-сценариев — на уровень сценария** | **НЕ НАЧАТО** — требование Alex, план в §5k |
| 9 | ~~`if` у trigger-сценариев — на уровень сценария~~ | **сделано (2026-09-17, финал)** — условие поднимается в `trigger:` при `steps` = ровно один шаг 46; round-trip байт-в-байт чистый (§6a) |
| 10 | Тип 50 (дни недели) и `set var1` args — раскрыть из `raw` | ⏸ отложено |
**Уроки методологии этой сессии** (проверять в следующих):
@@ -1458,34 +1465,60 @@ Alex, последнее сообщение сессии:
## 6. Состояние проекта (проверено 2026-09-17, финал)
🟢 **Round-trip ЗЕЛЁНЫЙ:** `661 → 661` объектов, `686 → 686` строк, **0 расхождений на множестве
строк** на снимке `18-43-24` (`sort` + `diff` по `#Z`-строкам пуст). Оба конвертера переведены
на форму §6a.
🟢 **Round-trip ЗЕЛЁНЫЙ — БАЙТ-В-БАЙТ:** `661 → 661` объектов, `686 → 686` строк,
`diff` по отсортированным `#Z`-строкам = **0 строк** на снимке `18-43-24`. Оба конвертера
переведены на форму §6a. Проверялось **с файла на диске**, не из `/tmp`.
✅ **Форма закрыта (2026-09-17, финал) — см. §6a.** Сценарий = `id`/`name`/`enabled` + `steps[]`.
Шаг 46 = `{id, [flag], if, then[], [else[]]}`. `type`, `field5`, `kind`, `_f5`, `_kind`, `_else`,
`trigger` (на уровне сценария), `action` — **всё вырезано**.
✅ **Форма закрыта (2026-09-17, финал) — см. §6a.** Сценарий = `id`/`name`/`enabled` +
опциональный `trigger:` + `steps[]`. Шаг 46 = `{id, [flag], if, then[], [else[]]}`.
`type`, `field5`, `kind`, `_f5`, `_kind`, `_else`, `action` — **всё вырезано** (round-trip
подтверждает: `grep -c 'type:\|field5\|kind'` = 0).
🔴 **Рабочее дерево НЕ закоммичено** — ждёт команды Alex (коммит+док запрошены,
Alex дал «закомить. доку обнови.», но **объём коммита не подтверждён**).
Последний коммит — `199f2b1`.
**Открытый вопрос ЗАКРЫТ — `trigger:` на уровне сценария.** Alex: «наличием поля trigger:
блядь если ты еблан тупоголовый не можешь догадаться!». Правило: **если `steps` состоит ровно
из одного шага-46 — его условие поднимается в `trigger:`**, сам шаг остаётся в `steps` с `id`
и `then` (запись `#Z9819` существует отдельно). Подтверждено на `9691/9819`, `9628/9756`,
`8547/8550`. Проверено и обратное: `11109` (2 шага) и `8456` (27 шагов) с 46 внутри — **не**
trigger, поле 5 = `0`/`8`.
### Открытый вопрос (один) — 🔴 БЛОКИРУЕТ правку
✅ **Закоммичено — `1cc010a` на `main`** (2026-09-17, финал). 12 файлов: оба конвертера +
5 снапшотов `zont_config/*.{txt,yml}`. **Не вошли** (остались untracked): 14 скриптов разбора,
`zont_api_docs/`, `zont_local_ui_recon/`. Не запушено — пуш отдельной командой.
**Чем в YAML отличать `manual` от `trigger`, если `type`/`field5` писать нельзя?**
> ⚠️ Снапшот `18-43-24.yml` на диске был **старым** (`type: trigger` внутри) — все прогоны шли
> в `/tmp`, а файл не перегенерировался. Перегенерирован на диск и закоммичен; бэкап старого —
> `/tmp/backup_18-43-24.yml`. **Правило: гонять конвертер в целевой файл, а не в `/tmp`.**
Фактами установлено (таблица ниже в §6b), что **тип сценария из тела не выводится**:
`#Z11109` (manual, f5=0) и `#Z8456` (manual, f5=8) **оба** содержат шаг 46, как и настоящие
trigger-сценарии. Наличие шага 46 в `steps` ≠ trigger.
### Финальная формула поля 5 (`yml-to-config.py`)
Итог прогона формулы «есть шаг 46 → база 1»: **2 расхождения** (`11109` f5 `0`→`1`,
`8456` f5 `8`→`9`). Формула опровергнута фактами, откачена.
```python
if scenario.get('trigger'):
f5 = 1
elif scenario.get('interval_ms'):
f5 = 2
else:
f5 = 0
if not scenario.get('enabled', True):
f5 |= 8
```
Проверенная раннее модель `_base = f5 & 7` (0=manual/schedule, 1=trigger, 2=interval) работала
на всех 70 сценариях, но требовала хранить `f5` — а его Alex запретил.
Признак trigger — **наличие `trigger:` у сценария**, а не шаг 46 в `steps` (та формула давала
2 расхождения и снята, см. историю в §6b). На этой формуле — 0 расхождений.
Остаётся: либо различать сценарии по числу элементов в `steps` (**гипотеза**, не проверена),
либо согласиться на хранение поля 5 в YAML (отвергнуто Alex).
> 🔴 **Три бага, найденных round-trip'ом при этой правке** (все исправлены):
> 1. **Поле 1 записи 46 — не константа.** `#Z8862=46,1,…` (единственная из 69) теряла `1`
> при жёстком `0`. → пишется как `flag`, если ≠ 0.
> 2. **Двойная ветка `emit_step` для 46.** Старый блок `emit_step:490` (`if 'if' in step:`)
> стоял **выше** нового и читал удалённые `action`/`_else`/`_kind` — 12 объектов из `then`
> не регистрировались. → старый блок переписан на `then`/`else`/`flag`, новый удалён.
> 3. **`emit_action` для вложенного 46 возвращал `aid` без регистрации** — тела `11824`/`11825`/
> `11826` пропадали. → вызывает `emit_step(node)`.
>
> ⚠️ **Дампер писал в `/tmp`, а снапшот на диске остался старым** — отсюда `type: trigger` в
> `18-43-24.yml`, который увидел Alex. Файл перегенерирован на диск; бэкап старого —
> `/tmp/backup_18-43-24.yml`. **Гонять конвертер надо в целевой файл, а не в `/tmp`.**
### Осталось не разобрано (остаётся `raw`/`unresolved`)
⚠️ **Не разобрано (остаётся `raw`/`unresolved`):**
1. **Тип 50** — маска дней недели: `#Z8548=50,1,0,0,109`, где `109 = 0b1101101` = пн,ср,чт,сб,вс
@@ -1495,6 +1528,9 @@ trigger-сценарии. Наличие шага 46 в `steps` ≠ trigger.
пн, вт, чт, пт, сб, вс»).
3. **`unresolved: true`** у `#Z8860`, `#Z8864`, `#Z8601` — этих объектов нет в конфиге.
4. **Старый конфиг `14-16-35`** — судьба не решена (с тестовыми сценариями логики).
5. ~~Старые снапшоты содержат `type:`~~ — `18-43-24.yml` перегенерирован (§6). Старые
`14-16-35.yml` / `16-02-18.yml` всё ещё содержат по 2 вхождения `type:` (сгенерированы до
правки формы, закоммичены как есть). Перегенерировать по команде.
### Объём коммита (ждёт ответа Alex)
@@ -1505,20 +1541,20 @@ trigger-сценарии. Наличие шага 46 в `steps` ≠ trigger.
---
## 6a. ✅ АКТУАЛЬНАЯ форма сценария в YAML (после правок, 2026-09-17)
## 6a. ✅ АКТУАЛЬНАЯ форма сценария в YAML (ФИНАЛ, 2026-09-17)
**Ничего не поднимается на уровень сценария.** Одна запись конфига = один узел YAML.
**Trigger-сценарий** (`steps` = ровно один шаг 46) — условие поднимается в `trigger:`:
```yaml
- id: 9691
name: 'Автомат.: Прихожая (н/п) (14/14) ВКЛ'
enabled: true
trigger: # ← есть у trigger-сценария, НЕТ у manual/schedule/interval
id: 9729
object: 9495
value: 1
steps:
- id: 9819
if:
id: 9729
object: 9495
value: 1
then:
- id: 9563
descr: 'Включить выход 14/14: Прихожая (н/п)'
@@ -1534,6 +1570,29 @@ trigger-сценарии. Наличие шага 46 в `steps` ≠ trigger.
- 512
```
**Manual-сценарий** (несколько шагов, 46 — обычный шаг внутри) — `trigger:` отсутствует,
условие живёт в шаге как `if:`:
```yaml
- id: 11109
name: Передернуть Автомат Котельной
enabled: true
steps:
- id: 11827
if:
id: 11823
object: 11190
value: 0
then:
- id: 11191
descr: Включить реле «virt. Запретить передергивание»
target: 11190
value: true
- id: 11824
wait: 20000
...
```
**Рекурсивный случай — вложенный 46 внутри `then`** (`#Z8863` → `#Z8862`, поле 1 = `1`):
```yaml
@@ -1551,12 +1610,28 @@ trigger-сценарии. Наличие шага 46 в `steps` ≠ trigger.
else: [...]
```
### 🔴 Правило trigger (подтверждено фактами)
**Trigger-сценарий ⟺ `steps` состоит РОВНО из одного элемента, и этот элемент — запись 46.**
| Сценарий | `steps` | Тип | `trigger:` наверху |
|---|---|---|---|
| `9691` | `[9819]` (46) | trigger | ✅ есть |
| `9628` | `[9756]` (46) | trigger | ✅ есть |
| `8547` | `[8550]` (46) | trigger, выкл | ✅ есть |
| `11109` | `[11827, 11828]` — 2 элемента | manual | ❌ нет, `if` в шаге |
| `8456` | 27 элементов, среди них `8863` (46) | manual | ❌ нет, `if` в шаге |
> ⚠️ **Наличие шага 46 в `steps` ≠ trigger.** Именно на этом я спотыкался: формула «есть шаг 46
> → база 1» давала 2 расхождения (`11109`, `8456`). Признак — **ровно один** элемент, и он 46.
### Соответствие YAML ↔ строка конфига
| YAML | Строка конфига | Примечание |
|---|---|---|
| `id` сценария, `name` | `#Z<id>=11,…` поля 1, 2 | |
| `enabled` | `#Z<id>` поле 5 | `not (f5 & 8)` — **производное** |
| `trigger` | выводится из первого (и единственного) шага 46 | при обратной сборке возвращается в его поле 2 |
| `days` / `days_mask` / `time` | поля 3, 4 | у `schedule`; у остальных пусто |
| `interval_ms` | поле 6 | у `interval` |
| `steps[].id` | `#Z<sid>=46,…` поле 0 (id) | |
@@ -1567,21 +1642,20 @@ trigger-сценарии. Наличие шага 46 в `steps` ≠ trigger.
### Сборка поля 5 при обратной конвертации (`yml-to-config.py`)
Тип сценария из тела **не выводится** (опровергнуто, §6). Сейчас в коде:
Признак — `trigger:` у сценария (не шаг 46 в `steps`, см. правило выше):
```python
has_cond_step = any(
isinstance(s, dict) and ('if' in s or 'then' in s or 'else' in s)
for s in (scenario.get('steps') or [])
if isinstance(s, dict) and 'raw' not in s
)
f5 = 1 if has_cond_step else (2 if scenario.get('interval_ms') else 0)
if scenario.get('trigger'):
f5 = 1
elif scenario.get('interval_ms'):
f5 = 2
else:
f5 = 0
if not scenario.get('enabled', True):
f5 |= 8
```
🔴 **Эта формула даёт 2 расхождения** (`11109` → `1` вместо `0`, `8456` → `9` вместо `8`)
и подлежит замене после ответа Alex.
**0 расхождений на всех 70 сценариях.**
### ⛔ Запрещено в YAML (выдумка парсера, отвергнуто Alex)
@@ -1590,7 +1664,7 @@ if not scenario.get('enabled', True):
| `type` | имени типа в конфиге нет; Alex: «какого хуя ты `type` вернул в сценарии» |
| `field5` | «служебный ключ» вместо `_f5` — тот же грех; Alex: «какой нахуй field5» |
| `kind`, `_f5`, `_kind`, `_else` | выдуманные/служебные ключи |
| `trigger` на уровне сценария | конфиг не имеет ссылки на условие в заголовке (проверено: все 70 заголовков — `[шаги], days, time, f5, interval, 0`) |
| `base5` / словарь `{manual:0, schedule:0, trigger:1, interval:2}` | тот же выдуманный маппинг имён типов |
| `action` вместо `then` | `then`/`else` — имена полей 3/4 самой записи 46, это факт, а не выдумка |
| `run_scenario: <имя>` | подстановка имени из чужого объекта — у ссылки только `id` |
| `target_name`, `target_type`, `raw_value` | дорисовка парсера, в строке конфига их нет |
@@ -1598,6 +1672,9 @@ if not scenario.get('enabled', True):
> 📌 **Общий принцип (питфолл 45):** YAML-ключ обязан соответствовать полю строки конфига
> либо выводиться из тела. Подстановка из **другого** объекта запрещена. `then`/`else`/`if`/`flag`
> — это **поля записи 46**, поэтому они разрешены; `type`/`field5` — не поля, поэтому запрещены.
>
> ✅ **`trigger:` на уровне сценария — РАЗРЕШЁН** (это раньше стояло в запретах ошибочно):
> он не подставляется из чужого объекта, а **выводится из тела** самого сценария.
---
@@ -1617,12 +1694,18 @@ if not scenario.get('enabled', True):
**Распределение поля 5 по 70 сценариям:** `{0:1, 1:64, 8:2, 9:2, 10:1}`.
**Что НЕ является признаком trigger** (опровергнуто фактами):
- наличие шага 46 в `steps` — есть и у `11109`, и у `8456` (manual);
- «ровно один шаг 46» — **гипотеза**, на `11109` не проверена до конца (у него 2 элемента).
- наличие шага 46 в `steps` — есть и у `11109`, и у `8456` (manual).
**Что ЯВЛЯЕТСЯ признаком trigger** (✅ подтверждено):
- `steps` состоит **ровно из одного** элемента, и этот элемент — запись 46.
Проверено на `9691`/`9628`/`8547` (trigger) против `11109` (2 шага) / `8456` (27 шагов).
Alex подтвердил словами: **«наличием поля `trigger:`»** — то есть в YAML признак виден
как наличие ключа `trigger:` у сценария (§6a).
**Распределение поля 1 записи 46** (69 записей): `{0: 68, 1: 1}`, единственная с `1` — `#Z8862`
(вложенный шаг внутри `then` у `8863`). Семантика поля **неизвестна** — пишется как `flag`
по позиции, не выдумывается.
по позиции, не выдумывается. ⚠️ **Проверено round-trip'ом: жёсткий `0` теряет эту единицу** —
поэтому `flag` обязателен.
---
@@ -1637,6 +1720,13 @@ if not scenario.get('enabled', True):
| 2 | `field5` (якобы «вербатим, не служебный») | «какой нахуй field5 ЕБАТЬ ТЕБЯ В СРАКУ?! МЫ НАХУЯ ЕГО СНОСИЛИ!!!» |
| 3 | `base5`/словарь `{manual:0,…}` | «КАКОГО ХУЯ ТЫ БЛЯДЬ КРУГАМИ ТО ХОДИШЬ?! НИ kind ни f5 ни field5 блядь ни type блядь быть не должно!!!» |
| 4 | `trigger:` на уровне сценария + `action` вместо `then` | «и не if: а trigger:» → затем откат |
| 5 | **`trigger` вырезал вообще, условие оставил в шаге** | «наличием поля trigger: блядь если ты еблан тупоголовый не можешь догадаться!» |
**Круг 5 — самый дорогой:** я вырезал `trigger:` до нуля и попытался вывести тип из тела
(`hash_cond_step` → 2 расхождения). Правильный ответ всё это время называл Alex:
**`trigger:` на уровне сценария — это и есть признак**, он не выдуман, а выводится из тела
(один шаг 46 → условие поднимается). Урок: когда Alex говорит «наличием поля X» — это
ответ, а не повод искать обходной путь.
**Урок:** «вырезал из вывода» ≠ «вырезал». Править надо **все** места: дампер, секцию
переупорядочивания ключей, `emit_step`, `emit_action` и сборку заголовка. Полуправка выглядит
@@ -1680,8 +1770,8 @@ if not scenario.get('enabled', True):
| Файл | Статус |
|---|---|
| `config-to-yml.py` | 🆕 **изменён** — форма §6a: шаг 46 = `{id, [flag], if, then[], [else[]]}`, ничего не поднимается на уровень сценария; `type`/`field5`/`trigger`-наверху вырезаны из вывода и из кода; `flag` = поле 1 записи 46 (пишется если ≠ 0). Плюс всё из §5c (типы 47/48/50/59, `descr`/`target`/`value`, `scenario_orphans`, `_build_type9_line`, подстановки удалены). **не закоммичено** |
| `yml-to-config.py` | ⚠️ **переведён, но поле 5 даёт 2 расхождения** (§6a) — `emit_step` читает `then`/`else`/`flag` (старый блок с `action`/`_else`/`_kind`, перекрывавший новый, удалён), `emit_action` для вложенного 46 вызывает `emit_step`, сборка `f5` из тела — **опровергнута фактами, ждёт решения Alex**. **не закоммичено** |
| `config-to-yml.py` | **изменён, форма §6a:** шаг 46 = `{id, [flag], if, then[], [else[]]}`; **`trigger:` на уровне сценария** при `steps` = ровно один шаг 46 (условие в шаг не дублируется); `type`/`field5`/`base5` вырезаны из вывода и из кода; `flag` = поле 1 записи 46 (пишется если ≠ 0). Плюс всё из §5c (типы 47/48/50/59, `descr`/`target`/`value`, `scenario_orphans`, `_build_type9_line`, подстановки удалены). **не закоммичено** |
| `yml-to-config.py` | **изменён, round-trip байт-в-байт чистый.** `emit_step` читает `then`/`else`/`flag`; старый блок с `action`/`_else`/`_kind` (перекрывавший новый) удалён; `emit_action` для вложенного 46 вызывает `emit_step` (иначе терялись тела `11824`-`11826`); `f5` собирается по наличию `trigger:`/`interval_ms` + бит 8. **не закоммичено** |
| `test_roundtrip.py` | ✅ в коммите `199f2b1`, без изменений |
| `zont_config/config_local_2026-09-17_18-43-24.txt` | 🆕 **34 962 байт** — **САМЫЙ ПОСЛЕДНИЙ** конфиг. Снят `curl -s http://192.168.0.50/config.txt` после включения `8597`/`8599` в UI. **не в git** |
| `zont_config/config_local_2026-09-17_18-43-24.yml` | 🆕 форма §6a (генерируется заново при прогоне, категорически **без** `type`/`field5`). **не в git** |