ZONT док: круги 37-40 (end, орфаны, trigger.step_id, set_var args)

This commit is contained in:
Alexey Martemyanov
2026-09-17 23:10:24 +06:00
parent 665a2147b5
commit ab32dd434c
+157 -55
View File
@@ -3,7 +3,7 @@ title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml
namespace: personal
type: how-to
created: '2026-09-17'
updated: '2026-09-17k38'
updated: '2026-09-17k40'
tags:
- personal
- zont
@@ -34,6 +34,13 @@ aliases:
- ZONT терминатор сценария
- ZONT unresolved = конец сценария
- ZONT from отменён
- ZONT trigger step_id
- ZONT action отменён
- ZONT шаг 46 id шага
- ZONT поле 5 записи 59
- ZONT args выдуманный ключ
- ZONT круг 39
- ZONT круг 40
related:
- '[[family/tech/zont-api]]'
- '[[family/how-to/home-automation]]'
@@ -206,7 +213,7 @@ python3 test_roundtrip.py zont_config/config_X.txt
| 52 | Modbus-регистры | `name`, `register`, `bit_width`, `repeat_period`, `num_vars` | вложены в устройство 51 |
| 53 | Аналоговые выходы | `name`, `min`, `max`, `value`, `offset`, `scale`, `flags`, `address` | `analog_outputs` |
| 57 | MQTT-топики | `topic`, `sensors` | `mqtt_topics` |
| **59** | Мини-скрипт ZONT | `descr`, `args[]` (+ `target`/`value` для `objcmd *`); тела `set var`/`puts`/`storeev` — §5.10 | инлайн в `then`/`else` |
| **59** | Мини-скрипт ZONT | `descr`, `args[]` (+ `target`/`value` для `objcmd *`); тела `set var`/`puts`/`storeev` — §5.10. **Поле 5 = признак литерала** (`1` если поле 2 число), в YAML не пишется — §21.5 | инлайн в `then`/`else`/`steps` |
> 🔴 Для каждого типа указаны только **реально декодируемые** поля; остальное — `raw` и пишется как есть.
@@ -253,41 +260,45 @@ python3 test_roundtrip.py zont_config/config_X.txt
### 5.1. Trigger-сценарий
`steps` состоит **ровно из одного шага типа 46** — условие **переносится** в `trigger:` наверх,
у шага остаётся `action`**голый id** действия (тело живёт в своей секции):
`steps` состоит **ровно из одного шага типа 46** — условие **переносится** в `trigger:` наверх
вместе с **id шага** (`step_id`), у самого шага в `steps` ничего не остаётся: там только **тела
действий** (их id + развёрнутое тело):
```yaml
- id: 9688
name: 'Автомат.: (н/п) (14/12) ВЫКЛ'
enabled: true
- id: 8547
name: 'Тестовый сценарий по времени'
enabled: false
trigger:
id: 9726
object: 9494
value: 0
id: 8548
type: days_mask
days: [mon, wed, thu, sat, sun]
step_id: 8550
steps:
- id: 9816
action: 9560
- id: 8549
log: test
```
Соответствие конфигу:
```text
#Z9688=11,'Автомат.: (н/п) (14/12) ВЫКЛ',[9816],0,0,1,0,0
#Z9816=46,0,9726,[9560],[] ← шаг: условие 9726, действие 9560
#Z9726=49,9494,1,0 ← триггер: объект 9494, значение 0
#Z9560=5,'Выключить выход 14/12: (н/п)',146032,1,0,0,[],0,0,0,256
#Z8547=11,'…',[8550],0,0,9,0,0 ← сценарий; поле 3 = [8550] (id шага 46)
#Z8550=46,0,8548,[8549],[] ← шаг: условие 8548, действие 8549
#Z8548=50,1,0,0,109 ← условие → trigger: (type: days_mask)
#Z8549=59,'puts "test"',0,0,0 ← тело действия → steps[]
```
| YAML | Строка конфига | Поля |
|---|---|---|
| `id`, `name` | `#Z9688=11,…` | 1, 2 |
| `enabled` | `#Z9688` поле 5 | `not (f5 & 8)`**производное** |
| `trigger.id` | `#Z9726=49,…` | 1 (id условия) |
| `trigger.object` | `#Z9726` | 2 (объект под наблюдением) |
| `trigger.value` | `#Z9726` | 4 (значение; поле 3 = оператор) |
| `steps[].id` | `#Z9816=46,…` | 1 (id шага) |
| `steps[].action` | `#Z9816` | 4 (список действий шага) |
| тело действия | `#Z9560=5,…` | своя строка, в свою секцию |
| `id`, `name` | `#Z8547=11,…` | 1, 2 |
| `enabled` | `#Z8547` поле 5 | `not (f5 & 8)`**производное** |
| `trigger.id` | `#Z8548=50,…` | id условия |
| `trigger.type`/`days`/`op`/`time` | `#Z8548` | разбор по полю 2 |
| `trigger.step_id` | `#Z8550=46,…` | id шага 46 (поле 3 сценария `[8550]`) |
| `steps[].id` | `#Z8549=59,…` | id действия (поле 4 шага `[8549]`) |
| тело действия | `#Z8549=59,…` | своя строка, разбор по типу |
> 🔴 **`step_id` — служебный ключ-ссылка, а не выдуманное поле.** Он нужен, чтобы собрать шаг 46
> обратно; энкодер вынимает его из `trigger` и подставляет в поле 3 строки 46. См. §21.3.
### 5.2. Manual / if-конструкция
@@ -322,7 +333,11 @@ python3 test_roundtrip.py zont_config/config_X.txt
| У шага есть свой `if`? | Ключ | Почему |
|---|---|---|
| **да** — условие внутри шага | **`then`** (+ `else` парой) | условие и ветки — единая конструкция if/then/else |
| **нет** — условие поднято в `trigger:` | **`action`** | от шага остался только список действий |
| **нет** — условие поднято в `trigger:` | шага в `steps` **нет вообще** | id шага уезжает в `trigger.step_id`, а `steps` = **тела действий** (§21.3) |
> 🔴 Ключ **`action` ОТМЕНЁН** (круг 39, `8f2edd4`). Был компромиссом: шаг оставался в `steps`, а
> действия писались голыми id, из-за чего орфан `- id: 8550 / action: 8549` читался как обрывок.
> Alex: «какой нахуй action» · «steps блядь - log» · «засунь в триггер».
> 🔴 Это **не противоречие** в требованиях Alex: он говорил про **разные шаги**. «какого хуя там
> then блядь?!» — про шаг без `if`; «ДА БЛЯДЬ! Then конечно!!!» — про шаг с `if`.
@@ -416,13 +431,19 @@ enabled = not (f5 & 8)
| ⛔ **`type: var` / `object: <голый id>` для тела `set var`** | Дублирующая ветка `t == 'var'` вытеснила `set_var` и оставила голый id цели. Alex: **«где блядь `set_var`»**. Форма удалена, `set_var` — единственная; цель-скрипт разворачивается на месте через `target` + `type` (§10.7, круг 29) |
| ⛔ **`tail: [0,0]` у тела `set var`** | выдуманный ключ «на всякий случай»; Alex: **«это че за ебанина?»** → хвост идёт тем же `args`, что у прочих шагов (§15.2) |
| ⛔ **`flag: 1` / `kind` внутри `set_var`** | имя поля 4 я выдумал; Alex: **«ФЛАГ БЛЯДЬ!!!!!!»**. Реальный `flag` — только поле 1 записи 46 (§15.1) |
| ⛔ **`args: [0, 1]` у `set_var`** | **выдумка ото начала**: `0` — поле 4 (всегда ноль), `1` — поле 5 (**производное**: «в поле 2 литерал»). Alex: «да блядь! опять?!» (§21.2.1, `4b99d8e`) |
| ⛔ **`action: <id>` / шаг 46 в `steps`** | **ОТМЕНЕНО** (круг 39): шаг не пишется в `steps`, его id едет в `trigger.step_id`, `steps` = тела действий. Alex: «какой нахуй action» (§21.3) |
| ⛔ **`from: 0` / `value: 0` — поле 2 тела `set var`** | Alex: **«Я СКАЗАЛ СНЕСТИ, А НЕ МЕНЯТЬ НАЗВАНИЯ!»**. Поле 2 **не раскрывается** в YAML; энкодер ставит `0` сам (§17) |
| ⛔ **`raw: [50,1,0,0,109]` в `trigger:`** | расписание — тот же объект, что цель `set_var`; раскрывать `type: days_mask` + `days` (§19) |
| ⛔ **`raw: [59, 'puts "…"', 0,0,0]` в орфанах** | `puts`/`storeev` — разобранные тела; орфан раскрывается как `log:` / `storeenv:` (§19.3) |
| ⛔ **`action: <id>` и вообще ключ `action`** | **ОТМЕНЁН** (круг 39, `8f2edd4`). При поднятом условии шага в `steps` нет вообще: id шага едет в `trigger.step_id`, `steps` = тела действий. См. §5.3, §21.3 |
| ⛔ **`args: [0, 1]` у `set_var` с числовым полем 2** | поле 5 записи 59 — **производное** («поле 2 = литерал, а не id»), энкодер ставит сам. Alex: «да блядь! опять?!» (§21.5) |
| ⛔ **раскрывать поля 4/5 записи 59 в YAML** | у `expr` поле 4 = `2` и у `set` поле 5 = `1`**одно и то же производное поле** («операнд/значение — литерал»). В YAML не пишется, восстановимо по составу (§21.5) |
**РАЗРЕШЕНО:** `trigger:` (выводится из тела — один шаг 46), `if`/`then`/`else`/`flag` — **реальные поля
записи 46**, `action` — имя списка у шага с поднятым условием, `set_var: {id, name, object/operator/value
| days/days_mask}` — **разобранное значение** (§10.1), `log: <текст>` — плоский (§10.3),
**РАЗРЕШЕНО:** `trigger:` (выводится из тела — один шаг 46) с ключом **`step_id`** (id шага 46) и
**телами действий прямо в `steps`**, `if`/`then`/`else`/`flag`**реальные поля записи 46**,
`set_var: {id, name, object/operator/value|days/days_mask}`**разобранное значение** (§10.1),
`log: <текст>` — плоский (§10.3),
`storeenv: {level, text}`**разобранный вызов** журнала событий (§10.2),
**`op: '…'` + `time: 'HH:MM'`** у `type: time_condition` — знак подтверждён на 4/4 условиях (§10.5),
**`param: <имя>`** у `type: condition` при поле 3 = `0` — словарь по типу владельца (§10.6).
@@ -467,9 +488,11 @@ enabled = not (f5 & 8)
| 29 | `param` заведён **словами** по типу владельца | «словами» · «как везде» | ✅ **принято**`param: pressure`, как `event: upper_threshold` (§10.6) |
> 📌 Круги 10–11 разобраны в §10.2, 1218 — в §10.1, 14 — в §10.3, 1924 — в §10.4/§10.5.
> Итог: `storeenv: {level: info|alert, text}` — два ключа, `id` команды снаружи, словами, без `event`;
> `set_var: {id, name, type, object/event|value}` — плоско; `log: <текст>` — плоская строка;
> объект 50 — две формы (`time_condition` / `days_mask`).
Итог: `storeenv: {level: info|alert, text}` — два ключа, `id` команды снаружи, словами, без `event`;
`set_var: {id, name, type, object/event|value}` — плоско; `log: <текст>` — плоская строка;
объект 50 — две формы (`time_condition` / `days_mask`).**`args` у `set_var` — выдумка** (поле 5
производное, §21.2.1). ⛔ **`action` отменён** — шаг 46 не пишется в `steps`, его id едет в
`trigger.step_id` (§21.3).
> 🔴 **Три системных корня кругов 12–18:**
> **(а) ключ-контейнер на один смысл** — `{var1: …}`, `{text: …}`: обёртка ради обёртки, Alex видит
@@ -2972,19 +2995,19 @@ Alex прочитал `git show --stat ba6ef44` (2 файла, 10 строк) и
(а) «ключ переименовать в `end`» и (б) «он и означал конец, поэтому голый id верен, откатывать нечего».
Развести **одним вопросом** («какое слово ставить?»), не выбирать самому — это и есть питфолл 77.
### 20.3. ⏳ Открыто на конец сессии
### 20.3. ⏳ Открыто (обновлено в конце сессии 2026-09-17)
| # | Что | Кто должен решить |
|---|---|---|
| 1 | ✅ **ЗАКРЫТО** — смысл `unresolved` = конец сценария, ключ переименован в **`end`** (§21.1) | — |
| 2 | 🔴 **`steps` триггер-сценария**: убрать `- id: 8550 / action: 8549`, тело действия — прямо в `steps`. **Куда девается id шага `8550`?** Три попытки откачены (§21.3) | **Alex** — назвать форму |
| 3 | ✅ **ЗАКРЫТО формально**`objcmd` разобран: `descr` + `args` + `target` (`10029`/`10033`/`10036`/`10053`) | — |
| 4 | Имя хвоста `args: [0, 1]` у `#Z10069` (§15.2) — открыто с круга 32 | **Alex** |
| 5 | Поле 4 = `2` у `expr` (`10077``10085`) — имя не дано | форма не согласована |
| 6 | Push в Gitea — 11 коммитов не запушены | по команде Alex |
| 2 | **ЗАКРЫТО**`action` выброшен, шаг 46 не пишется в `steps`, его id едет в `trigger.step_id`, `steps` = тела действий (§21.3, `8f2edd4`) | — |
| 3 | ✅ **ЗАКРЫТО**`objcmd` разобран: `descr` + `args` + `target` (`10029`/`10033`/`10036`/`10053`) | — |
| 4 | **ЗАКРЫТО** `args: [0, 1]` у `#Z10069` была выдумкой; поле 5 записи 59 производное, энкодер ставит сам (§21.5, `4b99d8e`) | — |
| 5 | Поле 4 = `2` у `expr` (`10077``10085`) — то же производное поле, что поле 5 (§21.5). Имени не дано, энкодер восстанавливает по признаку «второй операнд — литерал» | форма не согласована |
| 6 | Push в Gitea — **15** коммитов не запушены | по команде Alex |
> 🔴 **Приёмка не выполнена по п.2** — код не трогать, пока форма не названа. Всё остальное в этом доке —
> ✅ закоммичено и проверено на 9 конфигах.
> ✅ Единственный открытый пункт — 5 (поле 4 у `expr`), и он **не блокирует** круг: 9/9 зелёный,
> все открытые ранее вопросы закрыты. Всё в этом доке проверено на 9 конфигах.
---
@@ -3048,30 +3071,65 @@ Alex прочитал `git show --stat ba6ef44` (2 файла, 10 строк) и
| | |
|---|---|
| **HEAD** | **`272f4c6`** — «trigger: id шага 46 переехал в trigger.step, steps = тела действий» |
| **Содержимое `ba6ef44`** (история) | только снятие флага: `config-to-yml.py` (4 точки) + регенерация артефакта. 2 файла, `+6/-7` |
| **HEAD** | **`4b99d8e`** — «set_var: args убран у числового поля 2, поле 5 ставится само» · ✅ **ЗАПУШЕН** |
| **Рабочее дерево** | чистое по конвертерам |
| **Файл на диске** | `config_local_2026-09-17_22-26-26.yml` — 757 объектов; `4171`/`4178`/`4233` = `end: true` |
| **Файл на диске** | `config_local_2026-09-17_22-26-26.yml` — 757 объектов; `trigger.step_id`, `end: true` у терминаторов, `args` только у `objcmd` |
| **Круг** | ✅ **9/9** — потеряно 0, лишнее 0 |
| **Коммиты сессии** | `4c6a6f5` · `806ed2e` · `b770bed` · `ba6ef44` · `0cbbeb8` · `2007771` · `0c4b9a6` · `821659b` · `16ec710` · `375d01a` · `07c076f`**push не делался** |
| **Коммиты сессии** | `4b99d8e` · `8f2edd4` · `272f4c6` · `6d0b6a5` · `4c6a6f5` · `806ed2e` · `b770bed` · `ba6ef44` · `0cbbeb8` · `2007771` · `0c4b9a6` · `821659b` · `16ec710` · `375d01a` · `07c076f` **ЗАПУШЕНЫ** (`d94b809..4b99d8e`) |
| **Незакоммичено в `zont_config/`** | 2 удалённых `.yml`-снапшота (`18-43-24`, `19-53-21`), новый `.txt` |
| **Открыто** | §20.3 — п.1 ✅ закрыт (`end`), п.3 `objcmd` ✅ форма есть (`descr`+`args`+`target`); остались `args`-хвост `#Z10069`, поле 4 `expr`, push |
| **Открыто** | только поле 4 = `2` у `expr` (`10077``10085`) — имя не дано; всё остальное ✅ закрыто |
### 21.3 ✅ ЗАКРЫТО — Круг 39 (ФИНАЛ): `action` выброшен, id шага едет в `trigger.step`
### 21.2.1 ✅ Круг 40 (ФИНАЛ)`set_var`: `args` выдуман, поле 5 производное
**«какой нахуй action»** · **«steps блядь - log»** · **«засунь в триггер»** — целевую форму Alex
назвал. Итог: ключа `action` нет; шаг 46 не пишется в `steps`; его id поднимается в `trigger:`
ключом **`step`**; `steps` содержит **тела действий**.
**«да блядь! опять?!»** на `args: [0, 1]` у `#Z10069` — Alex отверг его в третий раз (круги 32,
36, 40). Причина найдена в данных:
```
#Z10069=59,'set varname',42,0,1 ← поле 5 = 1
#Z10029=59,'objcmd 8700 "1 %0"',14.5,0,1
#Z10077=59,'expr "%0 + %1"',9146,1,2 ← поле 5 = 2
```
| Поле 5 | Сколько | Что значит |
|---|---|---|
| `0` | **80** записей | поле 4 (поле 2) — **id объекта** либо пусто |
| `1` | **2** (`10029`, `10069`) | в поле 2 — **литерал значения** (`14.5`, `42`) |
| `2` | **5** (`10077``10085`) | второй операнд `expr`**литерал** |
**Все 7 записей с полем 5 ≠ 0 — ровно те, где поле 4 не является id.** Значит поле 5 —
**производное**, в YAML не пишется; энкодер ставит его сам по признаку «в поле 2 число».
| Точка | Что |
|---|---|
| `config-to-yml.py` (`~938`) | ветка «поле 2 = число»: `{'name', 'value'}`**без `args`** |
| `yml-to-config.py` (`~688`) | `[59, 'set %s', val, 0, 1]` — поле 5 = `1` ставится само, поле 4 всегда `0` |
Круг ✅ **9/9**, `#Z10069``set_var: {name: varname, value: 42}`.
**Коммит:** **`4b99d8e`** («set_var: args убран у числового поля 2, поле 5 ставится само»).
**Push выполнен:** `d94b809..4b99d8e` (14 коммитов), `origin/main` = HEAD.
🔴 **ПИТФОЛЛ 86 — `args` был выдумкой от начала и до конца.** Ключ рождался трижды и трижды
отвергался; правильный ответ лежал в **данных**: поле 5 — не «хвост», а признак литерала, и
восстанавливается энкодером. Прежде чем давать имя «остатку полей», проверить, не является ли
этот остаток **производным от уже разобранных полей** (ср. питфолл 82).
### 21.3 ✅ ЗАКРЫТО — Круг 39 (ФИНАЛ): `action` выброшен, id шага едет в `trigger.step_id`
**«какой нахуй action»** · **«steps блядь - log»** · **«засунь в триггер»** · **«только пусть
`id: 8548` / `step_id: 8550`»** — целевую форму Alex назвал. Итог: ключа `action` нет; шаг 46 не
пишется в `steps`; его id поднимается в `trigger:` ключом **`step_id`**; `steps` содержит
**тела действий**.
```yaml
- id: 8547
name: тестовый сценарий по времени
enabled: false
trigger:
step: 8550 # id шага 46 — раньше был шаг в steps с action: 8549
id: 8548 # условие шага
id: 8548 # условие шага — ПЕРВЫМ ключом
type: days_mask
days: [mon, wed, thu, sat, sun]
step_id: 8550 # id шага 46 — следом
steps:
- id: 8549 # тело действия, развёрнуто на месте
log: test
@@ -3079,13 +3137,13 @@ Alex прочитал `git show --stat ba6ef44` (2 файла, 10 строк) и
| Точка | Что |
|---|---|
| `config-to-yml.py` (`~1161`) | `scenario['trigger'] = {'step': <id шага>, **<условие>}`; `steps` = `dump_step` по `then` |
| `config-to-yml.py` `_is_body_inline` (`~1240`) | `v.get('step') == obj_id` считается ссылкой — иначе шаг падал в орфаны (**было 70**) |
| `yml-to-config.py` (`~1071`) | из `trigger` вынимается `step` + `flag`, шаг `46` собирается обратно, `steps` → его `then` |
| `config-to-yml.py` (`~1161`) | `scenario['trigger'] = {**<условие>, 'step_id': <id шага>}`условие первым; `steps` = `dump_step` по `then` |
| `config-to-yml.py` `_is_body_inline` (`~1240`) | `v.get('step_id') == obj_id` считается ссылкой — иначе шаг падал в орфаны (**было 70**) |
| `yml-to-config.py` (`~1071`) | из `trigger` вынимается `step_id` + `flag`, шаг `46` собирается обратно, `steps` → его `then` |
Круг ✅ **9/9 чистый**, орфанов **4** (было 6: `8549`/`8554` развернулись).
**Коммит:** **`272f4c6`** («trigger: id шага 46 переехал в trigger.step, steps = тела действий»).
**Коммиты:** `272f4c6` (ключ `step`, id шага первым) → **`8f2edd4`** (`step_id`, условие первым).
#### Откаченные попытки (не возвращаться)
@@ -3095,6 +3153,7 @@ Alex прочитал `git show --stat ba6ef44` (2 файла, 10 строк) и
| 2 | декодер: тело **плоско** в `action` | ключ `action` остался | ⛔ |
| 3 | декодер: `steps` = тела, id шага ключом `step:`**без** правки `_is_body_inline` | **6570 орфанов**: шаги 46 уехали в `raw` | ⛔ |
| 4 | `then:` на шаге вместо `action` | не то: `then` закреплён за шагом со своим `if` | ⛔ |
| 5 | ключ `step:` (без `_id`) | имя отвергнуто: «только пусть `step_id`» | ⛔ |
🔴 **ПИТФОЛЛ 83 — четыре круга на одну форму.** Я правил по одному месту и гонял круг. Правильно:
выяснить форму **одним вопросом** («куда девается id шага 8550?» — ответ Alex: «засунь в триггер»),
@@ -3109,9 +3168,52 @@ Alex прочитал `git show --stat ba6ef44` (2 файла, 10 строк) и
`scenario_orphans` с `raw`. Поднимая id шага в `trigger:`, **сразу** добавить ключ в
`_is_body_inline`.
### 21.4. ✅ Круг 38 (ФИНАЛ) — `action` развёрнут, шаги ушли из `scenario_orphans`**
### 21.5. ✅ Круг 40 — поле 5 записи 59 = производное, `args` у `set_var` убран
**Alex: «да блядь! опять?!»** на `set_var: {name: varname, value: 42, args: [0, 1]}`.
**Строка конфига:** `#Z10069=59,'set varname',42,0,1` — поле 2 = `42` (число), поле 4 = `0`, поле 5 = `1`.
Хвост `(0, 1)` был склеен в выдуманный `args`.
**Проверено по всем 87 записям типа 59 в дампе:**
| Поле 5 | Кол-во | Кто | Смысл |
|---|---|---|---|
| `0` | **80** | все `set <имя>` со ссылкой, `puts`, `storeev`, `objstate`, `expr` с двумя id | поле 4 — **id объекта** либо пусто |
| `1` | **2** | `10029` (`objcmd`, поле 2 = `14.5`), `10069` (`set`, поле 2 = `42`) | поле 2 — **литерал значения** |
| `2` | **5** | `10077``10085` (`expr`, поле 4 = `1`/`-1`) | второй операнд **expr** — литерал |
Поле 5 — **производное**: «в соответствующем поле литерал, а не id». В YAML **не пишется**,
энкодер восстанавливает сам.
| Точка | Что |
|---|---|
| `config-to-yml.py` (`~938`) | ветка «поле 2 = число» больше **не** пишет `args` — только `value` |
| `yml-to-config.py` (`~688`) | вместо разбора `args` возвращает `[59, 'set <имя>', val, 0, 1]` |
```yaml
- id: 10069
set_var:
name: varname
value: 42
```
Круг ✅ **9/9 чистый**; `args` в артефакте — ровно **4**, все законные (`objcmd`: `descr`+`args`+`target`).
**Коммит:** **`4b99d8e`** («set_var: args убран у числового поля 2, поле 5 ставится само»).
🔴 **ПИТФОЛЛ 86 — «хвост полей» ≠ «args».** Прежде чем склеивать поля 3..n в `args`, проверить по
**всем** записям типа, одинаково ли они устроены. Здесь поле 5 имело смысл (признак литерала), и
склейка создала ключ, которого в конфиге нет. Признак ошибки: значение есть **ровно у одной записи**
из десятков (`1` из 87) — это сигнал «производное поле, спросить по данным», а не «хвост».
### 21.4. ✅ Круг 38 — `action` развёрнут, шаги ушли из `scenario_orphans` (ЗАМЕНЁН кругом 39)
**Коммит `6d0b6a5`** («action: тела шагов развёрнуты на месте, шаги ушли из scenario_orphans»):
форма `action:` затем отвергнута (§21.3) и заменена на `trigger.step_id` в `8f2edd4`. Коммит
оставлен в истории — он не ломал круг и не терял данные.
**Актуальная форма trigger-сценария — §21.3.** Ниже — история.
| Точка | Что |
|---|---|