[2026-09-17] eagle: family/how-to/zont-config-compiler.md family/tech/zont-scenario-logic-11109.md work/projects/cpm-m3-telemetry-privacy-triage.md

This commit is contained in:
Alexey Martemyanov
2026-09-17 15:33:05 +06:00
parent d0dfc989e6
commit 4eae38bab8
3 changed files with 214 additions and 172 deletions
+121 -95
View File
@@ -21,7 +21,7 @@ tags:
- homeautomation - homeautomation
title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml title: ⚙️ ZONT Config Compiler — конвертеры .txt ⇄ .yml
type: how-to type: how-to
updated: 2026-09-17i updated: 2026-09-17j
--- ---
# ⚙️ ZONT Config Compiler — конвертеры `.txt ⇄ .yml` # ⚙️ ZONT Config Compiler — конвертеры `.txt ⇄ .yml`
@@ -146,12 +146,11 @@ updated: 2026-09-17i
### Как это выглядит в YAML ### Как это выглядит в YAML
> ⚠️ **ТЕКУЩАЯ форма (коммит `199f2b1`).** Переделывается по **§5c** — там одна вложенная форма > **НОВАЯ форма (2026-09-17, §5c).** Все сценарии — единая вложенная структура `blocks[].if/then/else`
> `blocks[].if/then` для всех сценариев вместо плоского дубля и отдельных секций. Ниже — как есть > плюс блок `trigger:`. Плоские `when`/`then` и секции `steps`/`delays`/`scenario_conditions` убраны.
> сейчас, чтобы читать работающий код; целевая форма — в §5c. > **Форма `11109` ниже — устаревшая**, оставлена для истории; актуальная — §5c.
Одношаговые сценарии (64 из 65) сохраняют плоский вид `when`/`then` — старые YAML не ломаются. **Устаревшая форма (коммит `199f2b1`)** плоский `when`/`then` для 1-шаговых + `steps` + `extra_links`:
Многошаговые получают список `steps`, а хвостовые задержки — `extra_links`:
```yaml ```yaml
scenarios: scenarios:
@@ -384,20 +383,12 @@ diff A.txt B.txt # пусто = round-trip чистый
--- ---
## 5c. 🔄 Переработка структуры YAML сценариев — 🟢 СОГЛАСОВАНО, парсер дописывается (2026-09-17) ## 5c. 🔄 Переработка структуры YAML сценариев — СДЕЛАНО 2026-09-17
> ✅ **СТАТУС (обновлено 2026-09-17, вечер): Alex дал «дописывай парсер».** Семантика типов > ✅ **СТАТУС (2026-09-17, ночь): парсер и энкодер дописаны, round-trip чистый на 6 конфигах.**
> 47/48/50/59 **раскрыта** (ответы Alex — [[family/tech/zont-scenario-logic-11109]] §8.7), > Семантика типов 47/48/50/59 раскрыта (ответы Alex — [[family/tech/zont-scenario-logic-11109]] §8.7),
> таблица операторов и логики подтверждена фактами конфига. Работа по §5c разрешена. > операторы и логика подтверждены фактами конфига. Код реализован.
> > **Не закоммичено** — ждёт команды Alex.
> ⚠️ **Но остались 8 непрозрачных элементов** конкретно в сценарии `8456`
> (`#8458`, `#8463`, `#8465`, `#8467`, `#8486`, `#8488`, `#9500`, `#8457`/`#8470`).
> Alex спросил «всё ли кристально прозрачно?» — ответ: **нет**. Вопросы заданы, ответа ещё нет
> (полный список — [[family/tech/zont-scenario-logic-11109]] §8.10).
>
> 📌 **Решение по стратегии парсера:** сохранять байт-в-байт всё, что не понято (скрипты `59`
> как сырые строки, суффиксы `;#a`/`;#h`/`;#p`, отсутствующие id — как есть), без «умного» перевода.
> Тогда round-trip чистый, а непонятное не портится.
**Задача Alex (исходная):** «переписать блок парсинга/сборки сценариев чтобы он составлял синтаксис как у Home Assistant automations вместо текущей разбросанной структуры. с опциональными айдишниками у операторов». **Задача Alex (исходная):** «переписать блок парсинга/сборки сценариев чтобы он составлял синтаксис как у Home Assistant automations вместо текущей разбросанной структуры. с опциональными айдишниками у операторов».
@@ -411,58 +402,96 @@ Alex отклонил:
**Правило:** в ZONT **нет триггеров** — роль триггера играет само изменение реле, а сценарий читается **Правило:** в ZONT **нет триггеров** — роль триггера играет само изменение реле, а сценарий читается
как вложенные инструкции «если … то …». Структура YAML должна повторять ZONT, а не HA. как вложенные инструкции «если … то …». Структура YAML должна повторять ZONT, а не HA.
### Утверждённая форма (предложена, ждёт финального ОК по двум вопросам) ### ✅ Реализованная форма
```yaml ```yaml
scenarios: scenarios:
- id: 11109 - id: 8456
name: 'Передернуть Автомат Котельной' name: Простой тестовый сценарий
enabled: false trigger: {type: schedule} # manual | trigger | schedule | interval
_raw_trigger_kind: 8 # исходное поле 5
blocks: blocks:
- id: 11827 # шаг (46) - id: 8506 # шаг (46)
if: if:
id: 11823 # условие (49), id опционален id: 8496
relay: 11190 group: and # and | or | not
operator: equals # equals | not_equals children:
value: 0 - {id: 8493, op: '<', left: 8492, value: 3}
- {id: 8495, op: '>=', left: 8472, right: 8494}
then: then:
- {id: 11191, action: relay_on, relay: 11190} - id: 8505
- {id: 11030, action: relay_off, relay: 11028} kind: 1
- {id: 11824, action: wait, ms: 20000} if: {id: 8501, group: or, children: [...]}
- {id: 11029, action: relay_on, relay: 11028} then:
- {id: 11825, action: wait, ms: 60000} - {id: 8502, script: 'puts "then-text"', args: [0, 0, 0]}
- {id: 11192, action: relay_off, relay: 11190} else:
- {id: 11826, action: wait, ms: 0} - {id: 8504, script: 'storeev A "alert"', args: [0, 0, 0]}
tail: # хвостовые задержки из поля 2 extra_links: [8457, 8458, ..., 8507] # прочие ссылки поля 2, в исходном порядке
- {id: 11828, action: wait, ms: 0} _raw_links: [8457, ..., 8506, 8507] # точный порядок поля 2 (для байт-tочности)
``` ```
**Что меняется по сравнению с текущим YAML:** **Маппинг типов на YAML:**
| Сейчас (разбросано) | Станет | | Тип | В YAML | Пример |
|---|---| |---|---|---|
| `scenarios[].when` / `.then` — плоский дубль для 1-шаговых | убрано: **одна форма** `blocks[].if/.then` для всех 65 | | `11` | `trigger: {type, …}` + `blocks[]` | `trigger: {type: schedule, days: [mon,…], time: '13:26'}` |
| `scenarios[].steps[]` + отдельные `delays` / `scenario_conditions` / `scenario_steps` | одна вложенная структура `blocks` | | `46` | `blocks[].{if, then, else}` | `{id, kind?, if, then: […], else?: […]}` |
| `wait` лежит отдельной секцией верхнего уровня | `wait`**внутри** `then`, по месту срабатывания | | `47` | `{op, left, value}` или `{op, left, right}` | `{id: 8493, op: '<', left: 8492, value: 3}` |
| `extra_links: [11828]` — голые id | `tail:` с блоками `wait` | | `48` | `{group, children}` | `{id: 8496, group: and, children: […]}` |
| `49` | `{condition: {object, operator, value}}` | старая форма, поддержана |
| `59` | `{script: <код дословно>, args: […]}` | `{id: 8502, script: 'puts "then-text"'}` |
| `50`, неизвестные | `raw: […]` | `{id: 8548, raw: [50, 1, 0, 0, 109]}` |
**Имена ключей и нулевые паузы — решено, не переспрашивать:** **Операторы type 47 (порядок UI `<, >, =, <=, >=`):** `0`=`<`, `1`=`>`, `2`=`=`, `3`=`<=`, `4`=`>=`.
- Ключи: **`blocks` / `if` / `then`** — ближе к формулировке Alex «вложенные инструкции если … то …». **Логика type 48:** `0`=`and`, `1`=`or`, `2`=`not`.
- Нулевые паузы (`ms: 0`) — **остаются как `wait`**. Честно отражают конфиг, нужны для round-trip.
- `id` у операторов — **опциональные**: пишутся, когда есть в конфиге; отсутствуют, когда нет.
**План работ:** **Триггер (поле 5 типа 11):**
1. `config-to-yml.py` — блок type 11: генерировать `blocks[].if/then`; `wait` (45) встраивать в `then`; `tail` для хвостовых
2. `yml-to-config.py` — обратный разбор `blocks`; **поддержать старые формы** (`when`/`then`, `steps`) для совместимости
3. `test_roundtrip.py` — все 4 конфига должны остаться чистыми
4. Доки — обновить §2.1 этой заметки
**Обязано сохраниться байт-в-байт (риск round-trip):** | Поле 5 | `trigger.type` | Доп. поля |
|---|---|---|
| `0`/`1` | `manual` | `enabled: bool` |
| `8` | `schedule` | `days[]`, `days_mask`, `time`, `time_raw` (поле 4 = маска дней, бит 0=ПН; поле 6 = `(час<<8)\|мин`) |
| `9` | `trigger` | — |
| `10` | `interval` | `interval_ms` |
### Реализация (код)
**`config-to-yml.py`** — рекурсивные хелперы `dump_step()` / `dump_condition()` / `dump_leaf()` /
`dump_action()`; сборщики `scenario_scripts` / `scenario_conditions` / `scenario_raw_objects`;
`_raw_links` для точного порядка поля 2; **fixed-point sweep** операндов (`left`/`right`/`args`) —
без него теряются скрипты второго порядка (напр. `8459`/`8460` через `8462`); неизвестные типы →
`raw_objects` вместо падения (там же `KNOWN_TYPES += 47,48,50,59`).
**`yml-to-config.py`** — обратные `emit_step()` / `emit_condition()` / `emit_action()`; поддержка
старых форм (`steps`/`when`/`then`) через `_scenario_from_legacy()`; восстановление порядка ссылок
из `_raw_links`; новые секции `scenario_scripts`/`scenario_conditions`/`scenario_raw_objects`/`raw_objects`
в `TYPE_ORDER`.
### 🔴 Три питфолла реализации (round-trip ловил каждый)
| # | Проблема | Причина | Решение |
|---|---|---|---|
| 1 | Объекты не попадали в вывод | `result_lines` в энкодере — **фильтрованное подмножество**: объект должен быть и в `lines`, и в `TYPE_ORDER` | Добавить секции в `TYPE_ORDER` |
| 2 | Операнды терялись | `left`/`right`/`args`**не рёбра дерева**, обход их не видит | Fixed-point sweep: собирать референсы из `raw`-тел, докидывать, повторять |
| 3 | exit 4 «Duplicate ID» | Секции эмитились дважды (явный цикл + `TYPE_ORDER`) | Убрать явный цикл; типы 5/9/3/0/36 не регистрировать в `raw_objects` (у них свои секции) |
**Обязано сохраниться байт-в-байт (сохранено):**
- порядок объектов в файле (45 идёт **после** 11, но **до** 46) - порядок объектов в файле (45 идёт **после** 11, но **до** 46)
- `extra_links` — хвостовые задержки в поле 2 сценария - порядок ссылок поля 2 сценария`_raw_links`
- число полей (7 vs 8) и `_raw_*`-поля (`_raw_field_count`, `_raw_field3`, `_raw_divider`) — **не удалять** - число полей (7 vs 8) и `_raw_*`-поля (`_raw_field_count`, `_raw_trigger_kind`, `_raw_trigger_params`) — **не удалять**
**Бэкап:** коммит `199f2b1` уже есть — откат возможен в любой момент, отдельных копий не делать. ### ✅ Round-trip — 6/6 конфигов чисто
```bash
cd /Users/admin/Automation/HA-ZONT-Modbus
python3 test_roundtrip.py zont_config/config_local_2026-09-17_14-16-35.txt # ✅ 685/685
python3 test_roundtrip.py zont_config/config_0FA7C33CC89F_…_12-12-28.txt # ✅
for f in zont_config/archive/*.txt; do python3 test_roundtrip.py "$f"; done # ✅ -2, -3, -4
python3 test_roundtrip.py zont_local_ui_recon/config_live_192.168.0.50.txt # ✅
```
**Бэкапы кода:** `/tmp/config-to-yml.py.bak`, `/tmp/yml-to-config.py.bak` (сняты перед правкой).
**Откат:** git (`199f2b1` — последний коммит).
--- ---
@@ -617,67 +646,64 @@ https://lk.zont-online.ru/download/firmwares/H2000_PRO_<HW>__<FW>_<PROFILE>.zip
--- ---
## 5i. 🔴 Конструктор логики ZONT (типы 47/48/50/59) — НЕ ПОДДЕРЖАН ## 5i. 🔴 Конструктор логики ZONT (типы 47/48/50/59) — ПОДДЕРЖАН 2026-09-17
> **Найдено 2026-09-17.** Alex создал в UI тестовые сценарии («все доступные триггеры и варианты > **Найдено 2026-09-17.** Alex создал в UI тестовые сценарии («все доступные триггеры и варианты
> логики»). Разбор локального конфига `config_local_2026-09-17_14-16-35.txt` (660 `#Z`) вскрыл > логики»). Разбор локального конфига `config_local_2026-09-17_14-16-35.txt` (660 `#Z`) вскрыл
> **визуальный конструктор логики** со скриптовым движком. > **визуальный конструктор логики** со скриптовым движком.
>
**Конвертер на этом конфиге падает:** > ✅ **Статус: поддержан.** Парсер и энкодер дописаны (§5c), round-trip чистый. Ранее падал с
> `Ошибка: Условие 8496: не type 49` (exit 2) — **больше не падает**.
```bash
$ python3 config-to-yml.py zont_config/config_local_2026-09-17_14-16-35.txt > /tmp/new.yml
Ошибка: Условие 8496: не type 49
EXIT=2
```
**Две ключевые находки:** **Две ключевые находки:**
1. 🔴 **Поле 5 типа 11 — НЕ `enabled`, а ТИП ЗАПУСКА:** `0`/`1` ручной, `8` расписание, 1. 🔴 **Поле 5 типа 11 — НЕ `enabled`, а ТИП ЗАПУСКА:** `0`/`1` ручной, `8` расписание,
`9` триггер, `10` интервал. Поле 6 — параметр (для интервала — мс). **Текущий конвертер `9` триггер, `10` интервал. Поле 6 — параметр (для интервала — мс; для расписания — `(час<<8)|мин`,
теряет эту информацию** (сводит к булеву `enabled`). поле 4 — маска дней недели, бит 0=ПН).
2. **Новые типы:** `47` (лист условия), `48` (группа И/ИЛИ), `50` (запуск по id), 2. **Новые типы:** `47` (лист условия: `op, left, value|right`), `48` (группа `and`/`or`/`not`),
`59` (мини-скрипт: `objcmd`/`objstate`/`expr`/`set`/`puts`/`storeev`). `50` (объект-триггер, хранится как `raw`), `59` (мини-скрипт: `objcmd`/`objstate`/`expr`/`set`/
`puts`/`storeev`; код и args сохраняются **дословно**).
**Что нужно сделать** (семантика получена — [[family/tech/zont-scenario-logic-11109]] §8.7; **Операторы type 47 (порядок UI `<, >, =, <=, >=`):** `0`=`<`, `1`=`>`, `2`=`=`, `3`=`<=`, `4`=`>=`.
работа разрешена Alex'ом 2026-09-17 вечером, «дописывай парсер»):
парсер 47/48/50/59 с рекурсивным деревом условий; поле 5 хранить как тип запуска (плюс маска дней
и время для расписания); принимать непустое поле 4 шага; допускать не-46 типы в поле 2.
> 📌 **Стратегия:** всё непонятое (скрипты `59` дословно, суффиксы `;#a`/`;#h`/`;#p`, отсутствующие id) > 📌 **Стратегия (реализована):** всё непонятое (скрипты `59` дословно, суффиксы `;#a`/`;#h`/`;#p`,
> сохранять байт-в-байт, без «умного» перевода — round-trip останется чистым. 8 открытых вопросов > отсутствующие id, `type 50`) сохраняется байт-в-байт через `raw` / `unresolved` — без «умного»
> по сценарию `8456` — [[family/tech/zont-scenario-logic-11109]] §8.10. > перевода. Round-trip остаётся чистым, а непонятное не портится.
➡️ **Полный разбор — [[family/tech/zont-scenario-logic-11109]] §8** ➡️ **Полный разбор — [[family/tech/zont-scenario-logic-11109]] §8**
--- ---
## 6. Состояние проекта (проверено 2026-09-17, вечер) ## 6. Состояние проекта (проверено 2026-09-17, ночь)
**Конвертеры закоммичены — `199f2b1`.** Рабочее дерево чистое (после этого коммита). **Конвертеры переписаны под сценарный конструктор (§5c).** Рабочее дерево — **не закоммичено**,
ждёт команды Alex. Последний коммит — `199f2b1`.
⚠️ **Но появился конфиг, который конвертер НЕ разбирает** — с конструктором логики (§5i). **Round-trip чистый на 6/6 конфигах** (включая новый локальный с 660 `#Z` и legacy 7-поле `-2`).
| Файл | Статус | | Файл | Статус |
|---|---| |---|---|
| `config-to-yml.py`, `yml-to-config.py` | ✅ **изменены и закоммичены** (`199f2b1`, §5b) | | `config-to-yml.py` | 🆕 **изменён** — парсер типов 47/48/50/59, `blocks/if/then/else`, триггер, raw-фоллбэк (§5c). **не закоммичено** |
| `test_roundtrip.py` | **новый файл**, в коммите `199f2b1` | | `yml-to-config.py` | 🆕 **изменён** — обратные `emit_*`, `_raw_links`, новые секции в `TYPE_ORDER` (§5c). **не закоммичено** |
| `zont_config/config_0FA7C33CC89F_…_2026-09-17_12-12-28.txt` | 32 689 байт — свежий конфиг с контроллера, **не в git** (`untracked`) | | `test_roundtrip.py` | ✅ в коммите `199f2b1`, без изменений |
| одноимённый `.yml` | **пересобран, 80 472 байта / 4320 строк / 24 секции**, YAML валиден. **не в git** | | `zont_config/config_local_2026-09-17_14-16-35.txt` | 🆕 **34 907 байт, 660 `#Z`, 25 `#S`** — конфиг с **тестовыми сценариями** Alex. Снят с `http://192.168.0.50/config.txt`. **не в git** |
| `zont_config/archive/` | **не в git** (`untracked`) — лежит в `.gitignore`-нейтральном состоянии | | `zont_config/config_0FA7C33CC89F_…_2026-09-17_12-12-28.txt` | 32 689 байт — свежий боевой конфиг, **не в git** |
| `zont_config/H2000_PRO_config_actual-{2,3,4}.txt` / `.yml` |**перемещены в `archive/` и закоммичены** (`7ae0e32`) | | `zont_config/archive/` | `-2`/`-3`/`-4` **закоммичены** (`7ae0e32`) |
| `zont_api_docs/` | ✅ **новая папка** — локальная копия доки облачного API ZONT (`zont_api_docs.html` 250 KB, `zont_api_docs.txt` 99 KB, `convert.py`). **не в git**. Исследование — [[family/tech/zont-api]] | | `zont_local_ui_recon/config_live_192.168.0.50.txt` | живой конфиг для разведки WS-интерфейса, **не в git** |
| `zont_config/config_local_2026-09-17_14-16-35.txt` | 🆕 **34 907 байт, 660 `#Z`, 25 `#S`** — конфиг с **тестовыми сценариями** Alex (все виды триггеров/логики). Снят с `http://192.168.0.50/config.txt`. **не в git** | | `zont_api_docs/` | ✅ локальная копия доки облачного API (`zont_api_docs.html`, `.txt`, `convert.py`). **не в git** |
| `dump_new_types.py`, `probe_types.py`, `trace_scenarios.py` | 🆕 **новые скрипты разбора** сценарных типов 47/48/50/59. Разбор — [[family/tech/zont-scenario-logic-11109]] §8.8. **не в git** | | `read_scenarios.py` | 🆕 **читаемый дамп сценария** — дерево если/то/иначе с именами. Токенайзер `split_top()`. **не в git** |
| `read_scenarios.py` | 🆕 **читаемый дамп сценария** — дерево если/то/иначе с именами объектов. Токенайзер `split_top()` учитывает кавычки/`\`/`[]`. **не в git** | | `dump_new_types.py`, `probe_types.py`, `trace_scenarios.py` | 🆕 скрипты разбора сценарных типов. Разбор — [[family/tech/zont-scenario-logic-11109]] §8.8. **не в git** |
| `probe_sched.py`, `verify_answers.py`, `check_ops.py`, `audit_8456.py`, `audit2.py` | 🆕 **проверочные скрипты** — расписание, ответы Alex против конфига, операторы, аудит 8456. **не в git** | | `probe_sched.py`, `verify_answers.py`, `check_ops.py`, `audit_8456.py`, `audit2.py`, `chk_extra.py` | 🆕 проверочные скрипты. **не в git** |
| `~/rasputin-tmp/zont-util/` | ✅ **новая папка на Mac** (вне репо) — настроечная утилита `H1000 Programmator` 2.8.5, драйвер USB, прошивка `.enc`, скрипт распаковки `extract.py`. Разбор — §5g-2, [[family/tech/zont-api]] §10 | | `~/rasputin-tmp/zont-util/` | ✅ настроечная утилита `H1000 Programmator` 2.8.5, прошивка `.enc`, `extract.py`. Разбор — §5g-2 |
| `~/rasputin-tmp/zont-{auth-probe,recon,recon2,ws-probe}.js` | ✅ скрипты разведки локального WS-интерфейса. **не в git** | | `~/rasputin-tmp/zont-{auth-probe,recon,recon2,ws-probe}.js` | ✅ скрипты разведки локального WS. **не в git** |
Не запушено: `origin/main..HEAD` = 3 коммита (`199f2b1`, `7ae0e32`, `12ba22b`). **Бэкапы кода перед правкой §5c:** `/tmp/config-to-yml.py.bak`, `/tmp/yml-to-config.py.bak`.
Не запушено: `origin/main..HEAD` = 3 коммита (`199f2b1`, `7ae0e32`, `12ba22b`). Плюс **новые правки
§5c — незакоммичены**.
> ✅ **Пустой `.yml` (0 байт) больше не актуален** — причина была в падении на сценарии `11109`, > ✅ **Пустой `.yml` (0 байт) больше не актуален** — причина была в падении на сценарии `11109`,
> которое исправлено (§5b). Файл пересобран, 598 объектов. > исправлено (§5b).
**Что НЕ в git и почему:** `.txt` свежего конфига и его `.yml` — рабочие артефакты конвертации, **Что НЕ в git и почему:** `.txt` свежего конфига и его `.yml` — рабочие артефакты конвертации,
Alex их не добавлял. Не коммитить без команды. Alex их не добавлял. Не коммитить без команды.
+20 -17
View File
@@ -19,7 +19,7 @@ tags:
- homeautomation - homeautomation
title: 🔁 ZONT — сценарная логика и конструктор (11/46/47/48/49/50/59/45) title: 🔁 ZONT — сценарная логика и конструктор (11/46/47/48/49/50/59/45)
type: tech type: tech
updated: '2026-09-17e' updated: '2026-09-17f'
--- ---
# 🔁 ZONT — сценарная логика и конструктор логики # 🔁 ZONT — сценарная логика и конструктор логики
@@ -441,10 +441,15 @@ python3 dump_new_types.py # все сценарные объекты
> внутри кавычек (`objcmd 9102 "6,%0";#a` разваливалось). Новый `split_top()` учитывает > внутри кавычек (`objcmd 9102 "6,%0";#a` разваливалось). Новый `split_top()` учитывает
> кавычки, экранирование `\` и вложенные `[]` — теперь скрипты печатаются как есть. > кавычки, экранирование `\` и вложенные `[]` — теперь скрипты печатаются как есть.
### 8.10. ⏳ НЕПРОЗРАЧНЫЕ ЭЛЕМЕНТЫ СЦЕНАРИЯ 8456 (вопросы заданы Alex 2026-09-17, ответа нет) ### 8.10. ⏳ НЕПРОЗРАЧНЫЕ ЭЛЕМЕНТЫ СЦЕНАРИЯ 8456 (✅ разблокированы raw-стратегией)
Alex спросил: «а по всем действиям в "Простой тестовый сценарий" — тебе всё кристально прозрачно?» Alex спросил: «а по всем действиям в "Простой тестовый сценарий" — тебе всё кристально прозрачно?»
Ответ — **нет**. Ниже ровно то, что не выводится из конфига. **Не строить гипотезы — ждать ответа.** Ответ был**нет**. Ниже ровно то, что не выводится из конфига.
> ✅ **Разблокировано 2026-09-17.** Alex решил: «допиливаешь парсер с известными типами, а неизвестные
> — пусть в yml будут raw values, и тогда я доразберу». После этого конвертер дописан, round-trip
> чистый, **эти вопросы больше не блокируют работу** (§8.9). Семантику доразбирает Alex в YAML
> через `raw`-поля. **Гипотезы по-прежнему не строить** — открытые пункты остаются открытыми.
| # | Элемент | Что неясно | | # | Элемент | Что неясно |
|---|---|---| |---|---|---|
@@ -472,23 +477,21 @@ Alex спросил: «а по всем действиям в "Простой т
(type 59 как сырые строки с `arg1..arg3`, отсутствующие id — как есть). Round-trip останется (type 59 как сырые строки с `arg1..arg3`, отсутствующие id — как есть). Round-trip останется
байт-точным независимо от того, понята семантика или нет. байт-точным независимо от того, понята семантика или нет.
### 8.9. Состояние: конвертер на новом конфиге ПАДАЕТ ### 8.9. Состояние: конвертер новый конфиг РАЗБИРАЕТ (2026-09-17)
```bash > ✅ **ИСПРАВЛЕНО.** Все пять пунктов ниже реализованы в конвертерах (§5c в
$ python3 config-to-yml.py zont_config/config_local_2026-09-17_14-16-35.txt > /tmp/new.yml > [[family/how-to/zont-config-compiler]]). Round-trip чистый на 6/6 конфигах.
Ошибка: Условие 8496: не type 49 > Ранее падал с `Ошибка: Условие 8496: не type 49` (exit 2) — **больше не падает**.
EXIT=2
```
**Причина:** `config-to-yml.py` в блоке сценариев требует `cond[0] == 49` и **Что было нужно — ВСЁ СДЕЛАНО:**
`step[4] == []`. Новые типы (47/48/50/59) и непустое поле 4 не поддерживаются.
**Что нужно (после ответов Alex на §8.7):** | # | Было нужно | Статус |
1. Парсер типов **47/48/50/59** + рекурсивная сборка дерева условий |---|---|---|
2. Поле 5 типа 11 — хранить как **тип запуска** (0/1/8/9/10) + поле 6 (параметр), не как `enabled` | 1 | Парсер типов **47/48/50/59** + рекурсивное дерево условий | ✅ `dump_condition`/`dump_leaf`/`dump_action` |
3. Поле 4 шага — принимать непустые списки | 2 | Поле 5 типа 11 — **тип запуска** (0/1/8/9/10), не `enabled` | ✅ `trigger: {type, days, time, interval_ms}` |
4. Ссылки в поле 2 — допускать не-46 типы (45, 11, 3, 5, 9) | 3 | Поле 4 шага — принимать непустые списки | ✅ `blocks[].else` |
5. Round-trip всех 5 конфигов (боевой + 3 архива + новый локальный) | 4 | Ссылки в поле 2 — допускать не-46 типы (45, 11, 3, 5, 9) | ✅ `extra_links` + `_raw_links` |
| 5 | Round-trip всех 5 конфигов | ✅ **6/6** (боевой + 3 архива + новый локальный + live) |
**Счётчики нового конфига (для сверки):** `660 #Z`, `25 #S`, байт — 34 907. **Счётчики нового конфига (для сверки):** `660 #Z`, `25 #S`, байт — 34 907.
@@ -1,94 +1,107 @@
# Tech Design 2 / Privacy Triage: Apple — CPM M3 Telemetry # TD: Pixel Definition: CPM M3 Telemetry (macOS/iOS)
Status: **DRAFT — privacy review required** **Author:** Alex M
**Name:** Alex M · **Email:** amartemyanov@duckduckgo.com · **Objective:** O-E **Reviewer:** TBD
**PR:** [Add PR link] **Stakeholders:** Michal, Konrad, Russell (from the first CPM Tech Design; confirm for M3)
**Project:** https://app.asana.com/1/137249556945/project/1163321984198618/task/1216761517055116?focus=true **Project:** [CPM: root-cause the stuck embedded extension messaging (macOS/iOS)](https://app.asana.com/1/137249556945/task/1216761517055116)
**Baseline:** [[cpm-extension-health-pixels-privacy-triage]] **Previous Tech Design:** [CPM breakage pixels](https://app.asana.com/1/137249556945/task/1218005985662983)
**Scope:** Current implementation in `codex/cpm-failure-telemetry`, reviewed 2026-09-17. iOS 18.4+ / macOS 15.4+. Describes implemented collection, not the proposed reduced design.
## 1. Purpose and approach ## Background & Requirements
The first design detects CPM initialization failures, persistent messaging failures and recovery. M3 adds facts that help distinguish background-process failure, context-load errors and tab wiring problems. The first CPM pixels detect initialization failure, a stuck messaging episode and recovery. They do not explain whether a failure coincides with background-process termination, a context-load error or incorrect tab wiring.
An in-memory recorder observes extension lifecycle, WebKit process callbacks and critical memory pressure. When an existing initialization-failure or stuck pixel fires, it attaches a best-effort snapshot. There are no new JavaScript probes, page-content reads or periodic telemetry uploads. The delegate proxy forwards WebKit callbacks and is remotely switchable; this switch disables the proxy, **not all M3 collection**. Forced background-process termination can reproduce a persistent CPM outage. M3 adds native-side diagnostics to investigate whether the same failure occurs in production on iOS and macOS, without relying on the extension's messaging channel.
A separate new pixel reports extension reload operations that fail before the replacement context becomes active. Graveyard mitigation and A/B cohorts are outside M3. ## Problem Statement
## 2. Pixels and frequency Distinguish background-process/context failures from tab-local problems and identify failed extension reload operations. Collect the diagnostic state alongside existing failures, without page content, URLs or persistent identifiers.
Names below are base names; PixelKit adds frequency/platform suffixes. iOS uses its standard platform/form-factor suffixes; macOS keeps its standard naming policy. These are not new explicit `platform` / `form_factor` payload fields. Standard metadata includes `appVersion`; macOS schemas also declare `pixelSource` and `channel`. M3 provides evidence for the production-validation milestone; it does not implement graveyard mitigation or an A/B experiment.
| Pixel | Change / trigger | Frequency | ## Recommended Approach
Keep the existing CPM failure/stuck detection. A shared in-memory recorder collects lifecycle events, critical-memory-pressure notifications and current WebKit/tab state. When a failure or stuck pixel fires, attach a best-effort snapshot. No additional JavaScript probes or periodic telemetry uploads are introduced.
A forwarding delegate proxy observes background-process termination and responsiveness callbacks. The original delegate owns the proxy; the proxy references it weakly. A remote flag enables/disables proxy installation, not the entire diagnostics recorder.
| Pixel | Trigger | Frequency |
|---|---|---| |---|---|---|
| `debug_web_extension_cpm_initialization_failed_after_<reason>` | Add the snapshot when an eligible finished navigation fails the existing CPM response check. | Daily | | `debug_web_extension_cpm_initialization_failed_after_<reason>` | Existing initialization failure; add diagnostic parameters. | Daily |
| `debug_web_extension_cpm_messaging_stuck_<reason>` | Add the snapshot when a later eligible navigation confirms the failure episode. | Daily + count; once per stuck episode | | `debug_web_extension_cpm_messaging_stuck_<reason>` | Existing failure confirmed by a later eligible navigation; add diagnostic parameters. | Daily + count, once per episode |
| `debug_web_extension_reload_failed` | **New:** an extension context unload/load operation fails during reload; not limited to CPM. | Daily + count | | `debug_web_extension_reload_failed` | **New:** extension unload/load fails during a reload operation. Applies to all supported extension types. | Daily + count |
For both CPM families, `<reason>` is `session_restoration`, `tab_crash`, `extension_reload` or `other`. The stuck reason comes from the initial failure. This includes the `stuck_extension_reload` variant omitted from the first note's table. `<reason>`: `session_restoration`, `tab_crash`, `extension_reload`, `other`. PixelKit applies standard frequency/platform suffixes. Standard metadata includes `appVersion`; macOS also declares `pixelSource` and `channel`.
Existing recovery pixels and `debug_web_extension_cpm_messaging_extension_reload_failed` gain **no** diagnostic parameters. The latter means CPM failed after a successful extension reload; the new pixel instead means the reload operation itself failed. Recovery pixels remain unchanged. The new reload-failure pixel reports a failed reload operation, unlike the existing `cpm_messaging_extension_reload_failed`, which reports CPM failure **after a successful reload**.
## 3. What is actually sent ### Failure / stuck parameters
### CPM failure / stuck snapshot Unavailable optional facts are omitted. Boolean values are `true` / `false`.
Booleans are strings `true\|false`. Unavailable optional facts are omitted, not sent as false. | Parameter | What is collected |
| Parameter | Collected fact / transmitted values |
|---|---| |---|---|
| `extension_context_loaded` | Whether the tracked context is loaded. Boolean. | | `extension_context_loaded` | Context is loaded — boolean. |
| `background_view_alive` | Whether the recorder's current background WKWebView still exists. Boolean. | | `background_view_alive` | Current background WKWebView exists — boolean. |
| `background_web_process_alive` | Whether its current process identifier is nonzero. Boolean; **PID is not sent**. | | `background_web_process_alive` | Background process identifier is nonzero — boolean; PID is not sent. |
| `background_web_process_responsive` | WebKit's process-responsiveness verdict via SPI. Boolean. | | `background_web_process_responsive` | WebKit's process-responsiveness verdict — boolean. |
| `network_process_restarted` | Current network PID differs from the value captured at context load. Boolean; not a restart count or proof of causality. | | `network_process_restarted` | Network PID differs from the value at context load — boolean; PIDs are not sent. |
| `tab_known_to_webkit` | Failing tab is present in the context's open-tabs set. Boolean. | | `tab_known_to_webkit` | Failing tab is in WebKit's open-tabs set — boolean. |
| `tab_controller_matches_context` | Failing tab and context use the same extension controller. Boolean. | | `tab_controller_matches_context` | Tab and context use the same extension controller — boolean. |
| `tab_has_extension_user_scripts` | Tab's registered user scripts include the expected extension world. Boolean; does not prove execution. | | `tab_has_extension_user_scripts` | Registered scripts include the expected extension world — boolean, not proof of execution. |
| `native_handler_registered` | Autoconsent native-message handler is registered. Boolean; does not prove a reply was delivered. | | `native_handler_registered` | Autoconsent native-message handler is registered — boolean, not proof of message delivery. |
| `memory_pressure_critical` | Age of last observed critical-pressure notification: `none\|under_1_min\|under_5_min\|under_30_min\|over_30_min`. No memory size is sent. | | `memory_pressure_critical` | Age of last observed critical pressure: `none`, `under_1_min`, `under_5_min`, `under_30_min`, `over_30_min`. |
| `background_view_create_count` | Views created since context load: `0\|1\|2\|3_to_5\|over_5`. | | `background_view_create_count` | Creations since context load: `0`, `1`, `2`, `3_to_5`, `over_5`. |
| `background_view_leaked_count` | Tracked live views beyond the expected current view; same count buckets. Indicates retained views, not a proven leak. | | `background_view_leaked_count` | Retained views beyond the expected current view; same buckets. Not proof of a leak. |
| `extension_context_errors` | `none`, or accumulated distinct error descriptors since context load. Context errors map to names; other/underlying errors include **domain and numeric code**, not localized text. No explicit payload cap. | | `extension_context_errors` | `none` or accumulated error descriptors: mapped context-error names, with raw domain/code for other or underlying errors. No localized text; no explicit payload cap. |
| `background_events` | Recent lifecycle sequence, described below. Omitted when empty. | | `background_events` | Recent lifecycle sequence with relative ages in whole seconds. |
The recorder retains at most 40 lifecycle events in memory. A snapshot takes the latest 12; serialization removes oldest entries until the value fits 255 characters. Format: `event_name@-seconds`, relative to snapshot time, rounded to whole seconds. The recorder keeps 40 events in memory; the pixel includes at most the last 12 and 255 characters, dropping oldest entries first. Format: `event_name@-seconds`, relative to snapshot time.
Event names: `load`, `view`, `dealloc`, `unresponsive`, `responsive`, `proxy_on`, `proxy_off`, `died_<reason>`, `error_<descriptor>`. Death reasons: `memory_limit\|cpu_limit\|requested_by_client\|crash\|shared_crash_limit\|unknown`. Error timeline entries represent background-load errors. Names are lowercased, character-filtered and limited to 64 characters; **filtering is not an error-domain allowlist**. Events: `load`, `view`, `dealloc`, `unresponsive`, `responsive`, `proxy_on`, `proxy_off`, `died_<reason>`, `error_<descriptor>`. Termination reasons: `memory_limit`, `cpu_limit`, `requested_by_client`, `crash`, `shared_crash_limit`, `unknown`. Error events represent background-load failures. Event labels are lowercased, character-filtered and capped at 64 characters.
Example: `view@-60,died_crash@-42,error_background_failed_to_load@-41`. These are internally generated event labels, not identifiers supplied by a website. Example: `view@-60,died_crash@-42,error_background_failed_to_load@-41`.
### New reload-failure pixel ### Reload-failure parameters
| Parameter | Allowed values | | Parameter | Values |
|---|---| |---|---|
| `extension_type` | `embedded\|darkMode\|adBlocking\|searchToken\|unknown` | | `extension_type` | `embedded`, `darkMode`, `adBlocking`, `searchToken`, `unknown` |
| `reload_trigger` | `data_clearing\|scriptlet_update\|explicit` | | `reload_trigger` | `data_clearing`, `scriptlet_update`, `explicit` |
| `reload_phase` | `unload\|load\|lightweight_load\|full_load\|fallback_load` | | `reload_phase` | `unload`, `load`, `lightweight_load`, `full_load`, `fallback_load` |
| `reload_error` | `already_loaded\|not_loaded\|base_url_in_use\|no_background_content\|background_failed_to_load\|unknown\|other` | | `reload_error` | `already_loaded`, `not_loaded`, `base_url_in_use`, `no_background_content`, `background_failed_to_load`, `unknown`, `other` |
Unlike `extension_context_errors`, `reload_error` is an allowlisted category: it includes no raw domain, numeric code, underlying error or message. These are allowlisted enums; this pixel does not send raw error domains, codes or messages.
## 4. Privacy review questions ## Testing
**Browsing data or identifiers?** No intentional collection of URLs, domains visited, search queries, titles, page content, message payloads, user IDs, tab/document/context IDs, extension UUIDs or process IDs in these new payloads. Internal object/ID comparisons emit booleans. Error domains are error namespaces, not visited website domains, but are currently not allowlisted. - Unit tests: parameter serialization, buckets, timeline limits, attachment to failure/stuck pixels, reload-error mapping, delegate forwarding, ownership and runtime flag changes.
- Integration validation on iOS/macOS: force background-process termination, check the emitted diagnostics, test proxy on/off and extension reload failures. Confirm unknown SPI values are omitted.
- Privacy validation: inspect actual payloads and run both platform schema validators. Confirm URLs, identifiers and error messages are absent. Review exact-second timelines and error-domain/code values before release.
**Could events be linked or unusually specific?** No explicit correlation identifier is added. However, exact-second event sequences and arbitrary error-domain/code combinations can be high-cardinality. This draft does **not** assert that linkability is impossible. ## Additional Considerations (if applicable)
**Sensitive user actions?** `reload_trigger=data_clearing` reveals that a failing reload followed data clearing. It does not identify cleared sites or data. This requires explicit triage consideration. **Privacy**
**Temporary or permanent?** Intended for diagnosis and the M4 validation gate. Current schemas do not implement an expiry/removal deadline; agree one before rollout. No change to the baseline health pixels' intended lifetime. New pixel and additional parameters require Privacy Triage. No intentional collection of browsing URLs, search queries, page content, message payloads, tab/context IDs or PIDs in these payloads. Exact-second timelines and unrestricted error-domain/code combinations can be high-cardinality; bucketing/allowlisting needs a decision. `data_clearing` discloses the operation preceding a failed reload.
**Self-service eligible?** Not asserted. Exact seconds conflict with the repository's coarse-bucketing guidance; error descriptors, the data-clearing trigger and retention/removal policy need a privacy decision. Current local diagnostics logs separately include raw errors with `privacy: .public`; remove/redact these before production. Pixel approval does not approve those logs.
## 5. Boundaries and release gates **Security**
**Local logs are separate from pixel payloads.** Current recorder code also logs process IDs and raw localized/underlying errors with `privacy: .public`. These logs may expose paths or URLs and must be removed/redacted before production; pixel approval does not approve them. Uses guarded private WebKit selectors for process/script facts. Validate supported OS versions; missing facts must not block pixel delivery.
**What these pixels cannot establish:** no denominator of all process deaths; deaths followed by normal recovery may produce no diagnostic pixel. A failing tab does not prove an all-tabs outage. The rolling timeline may lose the original trigger, and deferred snapshots can observe newer state. **Site Breakage**
**Observer effect:** the findings note records that installing the delegate proxy changed the forced-process-death reproduction. Validate the current proxy on/off on both platforms before treating M3 as an unaffected production baseline. No intended page-behavior changes. However, the investigation recorded a changed reproduction with the delegate proxy installed; validate the current proxy on/off before treating observations as an unaffected baseline.
**Approval requested:** necessity and format of the snapshot fields, coarse replacement or explicit exception for exact-second timelines, allowlisting/bounding of error descriptors, the data-clearing trigger, and a diagnostic sunset date. **Experimentation**
Implementation references: `CPMMessagingDiagnostics.swift`, `CPMMessagingDiagnosticsRecorder.swift`, `CPMBackgroundWebViewDelegateProxy.swift`, `WebExtensionPixelFiring.swift`; iOS/macOS pixel schemas. Investigation: [[cpm-web-extension-breakage-findings]]. Milestones: [[cpm-web-extension-breakage-validation]]. None in M3. Graveyard mitigation and A/B testing remain separate milestones.
**Operational**
Update the existing CPM Grafana board for the new fields and reload failures. Agree a diagnostic removal/review date; none is enforced currently. Failure-only snapshots do not provide a denominator of all process deaths or prove an all-tabs outage. Rolling timelines can lose the trigger; deferred snapshots can observe newer state.
**Localization / Internationalization**
None.