# Формат формы v8unpack (Form.json + Form.elem.json) и рецепты правок

Распакованная форма обработки 1С = две части:

- **`Form/<ИмяФормы>/Form.json`** — «шапка» формы: корневой элемент формы `["4",…]`,
  служебная панель `["22", …, "ФормаКоманднаяПанель", …]`, тип формы, настройки
  СКД (base64). Дочерние элементы помечены как `"Дочерние элементы отдельно"` —
  их детали лежат в `Form.elem.json`.
- **`Form/<ИмяФормы>/Form.elem.json`** — детали элементов. Это **валидный JSON**,
  верхние ключи:
  - `params` — обычно null
  - `props` — **реквизиты формы** (атрибуты): `[{name,id,raw}]`. `raw[0]=="9"`,
    `raw[1]==["<id>"]`, `raw[3]=="\"Имя\""`, дальше — ОписаниеТипов.
  - `commands` — локальные команды формы (часто `null`, если их нет).
  - `tree` — иерархия элементов `{name,type,child}` БЕЗ raw (только структура).
    Типы: `Table`, `Field`, `Group`, `Button`.
  - `data` — словарь `{путь: {raw, ver}}`. **raw элемента — большой вложенный
    массив-строк.** raw ТАБЛИЦЫ (head `"55"`) содержит ВНУТРИ себя её командные
    панели (head `"22"`) и КНОПКИ (head `"31"`). Колонки таблицы — отдельные
    ключи `data["Таблица/Поле"]` (head поля `"37"`).

**Важно:** физическое размещение в data НЕ отражает логическую иерархию формы
напрямую. Кнопка, логически принадлежащая командной панели, физически сидит в
raw того элемента, к чьей панели она привязана. Имя кнопки = `<Контейнер>ОбщаяКоманда<Имя>`
(автогенерация), но имя — косметика; реальное размещение определяет контейнер
(`'22'`-панель), в котором лежит её `'31'`-массив. Пример: в `ОтчетПоДаннымЗаказа`
кнопка названа `ФормаОбщаяКомандаСвязанныеДокументы`, но физически в панели
ТАБЛИЦЫ `аДокументыКоманднаяПанель` — то есть «как в форме» по имени, а по факту
в панели таблицы.

## Безопасная правка

elem.json — валидный JSON, и **v8unpack принимает Python-переформатированный
файл без потерь** (проверено: load→dump indent=2 ensure_ascii=False → pack →
unpack → diff чистый). Поэтому:

- Правь формы **только программно** (`json.load`/`json.dump`), не текстом —
  иначе легко разбалансировать скобки в тысячах строк вложенных массивов.
- После любой правки — **semantic diff**: загрузи orig и edit, сравни секции,
  убедись, что изменилось РОВНО задуманное. Затем `epf.sh roundtrip`.

## Кодировка кнопки (head "31")

```
["31",
  ["<код>", "<guid элемента>"],   # напр. ["130","02023637-…"] — служебный
  "0","0","0",
  "\"<ИмяКнопки>\"",
  ["1","0"],
  "1",
  ["0", "<UUID-команды>"],        # uuid общей команды (= uuid в НоваяКонфа/CommonCommands/<Имя>.xml)
  ["1", ["<id-реквизита>"]],      # ПРИВЯЗКА ПАРАМЕТРА КОМАНДЫ → реквизит формы с этим id
  ...
]
```

- `02023637-7868-4a5f-8576-835a76e0c9ba` — служебный GUID, встречается у многих
  элементов (не тип, не команда).
- UUID общей команды «Связанные документы» в НоваяКонфа =
  `41f87891-ddd5-4e0f-a4f2-8039fb71981b` (см. `CommonCommands/СвязанныеДокументы.xml`:
  Picture=`CommonPicture.СтруктураПодчиненности`, CommandParameterType — длинный
  список DocumentRef, есть `СчетНаОплатуПокупателю`/`СчетНаОплатуПокупателю2`/
  `ЗаказПокупателя2`).

## РЕЦЕПТ: кнопка общей команды «не появляется»

Корневая причина почти всегда — **привязка параметра `["1",["<id>"]]` указывает
на реквизит формы, которого НЕТ** в `props`. 1С прячет кнопку, у которой не
резолвится параметр. Лечение:

1. `form_inspect.py F --buttons` — увидеть имя кнопки, uuid команды, id привязки.
2. Если реквизита с этим id нет — добавить его, с типом, покрывающим документ
   строки. Тип бери из рабочего образца (та же конфигурация → GUID'ы типов валидны):
   ```
   form_edit.py add-attribute --form F.json --name ТекДанныеДляДокСтруктуры \
       --id 5 --template-form ОтчетПоДаннымЗаказа/Form/Форма/Form.elem.json \
       --template-attr ТекДанныеДляДокСтруктуры
   ```
   Проверь тип: GUID реквизита должны соответствовать типу колонки-ссылки. В кейсе
   аСверка колонка `Счет` = `ДокументСсылка.СчетНаОплатуПокупателю2`
   (см. объектный модуль, `Колонки.Добавить("Счет", Новый ОписаниеТипов(...))`),
   а тип образца = `СчетНаОплатуПокупателю2`(uuid `3080554c…`) + `ЗаказПокупателя2`
   (uuid `d002160c…`) — покрывает, и общая команда такой тип принимает. ОК.
3. Модуль формы должен при активации строки писать текущий документ в реквизит:
   ```
   &НаКлиенте
   Процедура <Таблица>ПриАктивизацииСтроки(Элемент)
       ТекСтрока = Элементы.<Таблица>.ТекущиеДанные;
       ТекДанныеДляДокСтруктуры = ?(ТекСтрока = Неопределено, Неопределено, ТекСтрока.<КолонкаСсылки>);
   КонецПроцедуры
   ```
   (+ при создании — инициализировать первой строкой). Эти строки — в .bsl, с
   маркерами `//+ Бурдуков`.

Картинку кнопка наследует от общей команды (`СтруктураПодчиненности`) — отдельно
картинку ставить не нужно.

## РЕЦЕПТ: сделать таблицу редактируемой

1С: флаг таблицы **`ТолькоПросмотр`** перекрывает поля — если он Истина, вся
таблица readonly. Найти флаг (индекс в raw таблицы `'55'`) **триангуляцией**:

```
form_inspect.py F.json --readonly СписокСчетовПоКонтрагенту \
   --siblings КомиссияКлиентов_unpacked/Form/ФормаСписокЗаказов/Form.elem.json \
             УстановкаЮрЛиц_src/Form/ФормаСписокСПоступлениями/Form.elem.json
```

Кандидат = индекс, уникально равный `"1"` у нашей (readonly) таблицы, тогда как у
редактируемых родственников там `"0"`. В семействе «список счетов» это **индекс 36**
(проверено на аСверка: только у неё `[36]="1"`, у всех родственников `"0"`).
Снять:

```
form_edit.py set-table-readonly --form F.json --table СписокСчетовПоКонтрагенту --value 0
```

⚠ Индекс 36 — для ЭТОГО семейства форм. Для другой структуры — обязательно
перетриангулируй (`--readonly`), не доверяй индексу вслепую. Не трогай флаги,
различающиеся непоследовательно (в кейсе `[41]` тоже отличался, но был `=1` и у
редактируемых таблиц → НЕ readonly, не трогали).

## С нуля от шаблона

Синтезировать v8unpack-JSON руками непрактично. Делай от шаблона:

1. Выбери ближайшую обработку в `ОбработкиЭкспорт/`, скопируй её `.epf` под новым
   именем, распакуй (`epf.sh unpack`).
2. Переименуй в `ExternalDataProcessor.json`: поля `name`/`name2`/синоним
   (и uuid при необходимости — новый GUID, чтобы не конфликтовать).
3. Перепиши объектный модуль `ExternalDataProcessor.obj.bsl` под свою логику
   (маркеры `//+ Бурдуков` на свои правки).
4. Формы `Form/*/` — лишние удали (и из дерева формы в `ExternalDataProcessor.json`),
   нужные правь через `form_*`-инструменты или копируй из подходящих образцов.
5. `epf.sh roundtrip` + открыть в 1С на старте, что не падает.

Реальнее почти всегда — НЕ «с нуля», а доработать существующую обработку
(см. рецепты выше).

## РЕЦЕПТ: добавить колонку ТЧ в форму (поле '37')

После `obj_edit.py add-attr` (реквизит ТЧ, см. `object_metadata.md`) — добавить поле в форму:
1. Поле-колонка = `data["Таблица/ТаблицаИмяРеквизита"]` с raw head `'37'`. Привязка к
   реквизиту: `["3", ["<id Объекта>"], ["0","<ТЧ-binding-uuid>"], ["0","<реквизит-uuid>"]]`
   (Объект→ТЧ→реквизит). Имя поля в raw[6], заголовок — `["1","1",["\"ru\"","\"Заголовок\""]]`.
2. **Клонируй поле-донор** (колонку того же типа): element-id поля = пары `[n,"02023637-…"]`
   (SVC-guid). Каждая колонка владеет тройкой id (поле+подсказка+меню); колонки идут с шагом 3
   (14,17,20,…). Возьми свободную тройку, ремапни её в клоне, смени имя/заголовок/привязку.
3. `data["Таблица/НоваяКолонка"]` = клон; узел `{name,type:"Field"}` в `tree` под таблицей
   (ПОРЯДОК в tree = порядок колонок на форме). Raw таблицы `'55'` список колонок НЕ ведёт.
4. Заполни колонку в модуле (`Колонки.Добавить` + `НоваяСтрока.X = …`, маркеры `//+`).
Пример кода клонирования — в истории кейса СуммаНДС (`examples/`).

## РЕЦЕПТ: привязать событие таблицы (ПриАктивизацииЯчейки / «Связанные документы» по выбранной ячейке)

Симптом: кнопка «Связанные документы» показывает не тот документ (всегда первой строки),
т.к. `ТекДанныеДляДокСтруктуры` обновляется только в `ПриСозданииНаСервере`, а обработчик
активации **не привязан к событию таблицы в форме** (в elem.json события таблицы нет).

Рабочий механизм (образец `ОтчетПоДаннымЗаказа`/`аКорректировкаДокументовОператора`):
- Модуль: `&НаКлиенте Процедура <Таблица>ПриАктивизацииЯчейки(Элемент)` — берёт документ
  АКТИВНОЙ ЯЧЕЙКИ: `ИмяКолонки = СтрЗаменить(Элемент.ТекущийЭлемент.Имя, Элементы.<Таблица>.Имя, "")`,
  затем `ТекДанныеДляДокСтруктуры = Элементы.<Таблица>.ТекущиеДанные[ИмяКолонки]` (в `Попытка`,
  с проверкой `Если ТекущийЭлемент.Имя = "<Таблица>Счет" ИЛИ "…Заказ"`).
- Форма (raw таблицы `'55'`): блок событий формата
  `["1", "<event-guid>", "\"<Таблица>ПриАктивизацииЯчейки\"", "1","0", "<event-guid>","0","1"]`.
  GUID событий (стандартные): ПриАктивизацииЯчейки=`f228b12f-d892-4925-b338-695617357b32`,
  ПриАктивизацииСтроки=`60edb81d-887b-478e-94ee-7fef2b13393d`. Для 2 событий —
  `["2",g1,n1,g2,n2,"1","0",g1,"0","1",g2,"0","1"]`.

⚠ **КЛЮЧЕВОЕ:** блок событий нельзя ВСТАВИТЬ новым элементом — v8unpack его выкинет при
пересборке (схема '55' читает фикс. число элементов). Нужно **ЗАМЕНИТЬ пустой слот событий**
`["0"]` (он стоит вплотную перед командной панелью таблицы `["22",…КоманднаяПанель]`).
Найди слот перебором: замени каждый `["0"]` в raw таблицы на блок событий → `epf.sh roundtrip`
(семантический) → где блок ВЫЖИЛ пересборку и изменился ТОЛЬКО этот слот, там и слот событий
(обычно последний `["0"]` перед панелью). Кейс СуммаНДС/аСверка: слот был `ot[58]=["0"]`.
1С-приёмку (реально ли срабатывает событие) проверяет пользователь; если нет — привязать
событие в Конфигураторе (таблица → событие ПриАктивизацииЯчейки → одноимённый обработчик).

**ПриАктивизацииЯчейки vs ПриАктивизацииСтроки (важно).** Если «Связанные документы» должны
показывать документ ВСЕЙ СТРОКИ (а не той ячейки, что кликнули) — бери **ПриАктивизацииСтроки**
(`60edb81d-887b-478e-94ee-7fef2b13393d`), как на главной форме аСверки (`аДокументыПриАктивизацииСтроки`):
обработчик без условий по колонке, `ТекДанныеДляДокСтруктуры = Элементы.<Таблица>.ТекущиеДанные.<КолонкаДок>`.
ПриАктивизацииЯчейки обновляет реквизит лишь при клике по нужной ячейке → при клике по другой колонке
реквизит застревает (на [0]-строке из ПриСозданииНаСервере) → кнопка открывает «левый» документ.
**Перепривязка существующего блока (НЕ вставка):** заменить ОБА вхождения event-GUID
(`f228b12f…`↔`60edb81d…`) + строку имени обработчика в raw таблицы, переименовать процедуру в .bsl —
это правка значений на месте, `roundtrip` блок сохраняет (проверено, кейс аСверка/список счетов 2026-06-11).

## РЕЦЕПТ: «Только просмотр» бывает ПОЛЕ-УРОВНЕВЫМ (флаг [14] колонки '37')

Таблица может быть readonly не из-за табличного флага, а из-за того, что КОЛОНКИ
помечены «Только просмотр». Флаг — **индекс [14] в raw поля-колонки `'37'`**
(1=только просмотр, 0=редактируемо). Найти надёжно: сравнить ЗАВЕДОМО редактируемое
поле (напр. поле-фильтр, куда юзер вводит) с колонкой сетки — отличие ровно в [14].
Снять со всех колонок таблицы:
```
form_edit.py set-fields-readonly --form F.json --table аДокументы --value 0   [--column X]
```
⚠ Таблица может быть вложена (в группе/странице) → колонки в data лежат как
`"Группа/.../Таблица/ТаблицаКолонка"`; тул ищет по сегменту `/<table>/`. Вычисляемые
итоги (ИтогоXXX в отдельной группе) — оставляй readonly. Кейс аСверка: сетка `аДокументы`
в форме `Форма` — 7 колонок были [14]=1, сняли. (Табличный флаг [36] — отдельный
аспект; если таблица всё ещё readonly после [36]=0 — смотри поле-уровневый [14].)

## Тип поля колонки: сделать «Поле флажка» (булево → кликабельный флажок)

Булева колонка ТЧ по умолчанию может рендериться как **надпись** («Нет/Да» текстом),
а не как кликабельный флажок. Тип отображения колонки (`ВидПоля`) кодируется в её
`data[".../<кКолонка>"].raw` в ДВУХ местах:

- `raw[5]`  — `"2"` для надписи/ввода, **`"3"` для ПолеФлажка**
- `raw[39]` — блок настроек типа поля:
  - надпись/ввод: начинается с `"36"` (длинный блок ~66 элементов)
  - **ПолеФлажка: `["11", "0", ["3","4",["0"]], ["3","4",["0"]], "0", ["1","0"], ["3","4",["0"]], ["7","3","0","1","100"], "0","0","0","2","0"]`** (13 элементов, generic — без GUID/привязок)

Рецепт (python, как в §3): берёшь `raw` колонки, ставишь `raw[5]="3"` и заменяешь
`raw[39]` на блок флажка выше. Привязку к реквизиту (`raw[11]`, uuid булева) и имя
(`raw[6]`) НЕ трогаешь — блок флажка их не содержит, годится для любого булева поля.
Эталон живого флажка: `аПроверкаОшибокДляДекларацииV2.epf` (колонки `аДокументыNМетка`).
Грабли: флаг `raw[39][65]` (в «36»-блоке) — НЕ про флажок (встречается и у строковых
полей); не путать. Выверено на `УстановкаЮрЛицаИДоговораГостиницы` (колонка кМетка).

## Отбор СПИСКОМ (мультивыбор ссылок) из большого справочника

Задача: поле отбора по ссылке (Контрагент и т.п.) с выбором НЕСКОЛЬКИХ значений —
F4/«…» открывает каталог, выбранные показываются флажками в выпадающем (контрол
«ввод множественных значений», платформа 8.3.23+). Кейс DEVACC-747 (ОтчетПоДаннымЗаказа).

**Ключевое:** мультивыбор даёт связка «реквизит формы `СписокЗначений` + флаги ПОЛЯ».
Просто перепривязать поле на `СписокЗначений` НЕ хватает — поле унаследует одиночный
контрол ref-поля и нарисуется как «…». НЕ путать с баунд-списком (`ОтборСтавкаНДССчет`,
`ОтборДоговорКонтрагента`): там `РежимВыбораИзСписка=Истина`+фиксированный `СписокВыбора`
(годится для перечислений/малых наборов); для НЕограниченного справочника нужен именно
множественный ВВОД, эталон — `СписокКонтрагентов`.

**Эталон (живой мультивыбор ссылок):** `УстановкаЮрЛицаИДоговораГостиницы.epf` →
форма `Форма` → реквизит/поле `СписокКонтрагентов` (тип `СправочникСсылка.Контрагенты`),
а также `СписокОрганизаций`.

**Реквизит формы** (`props[]`, raw head `'9'`): тип `СписокЗначений` —
Pattern `4772b3b4-f4a3-49c0-a1a5-8cb5961511a3`; `ElementType` = тип-ссылка справочника
(`["0","1","ElementType",["#","f5c65050-3bbb-11d5-b988-0050bae0a95d",["Pattern",["#",<GUID-ref-типа>]]]]`).
GUID ref-типа бери из объектного реквизита того же справочника или соседнего форм-реквизита
(напр. Контрагенты = `9f6206b2-1ed6-423c-9b08-fd4978930c49`). Чистые флаги реквизита:
`raw[9]=["0","0"]`, `raw[11]="0"` (у баунд-`ОтборДоговорКонтрагента` иначе — `["0","1",["0"]]`/`"1"` —
НЕ копируй его как образец мультивыбора-ссылок).

**🛑 ГЛАВНОЕ (DEVACC-747, выстрадано): реквизит формы `СписокЗначений` для мультивыбора-ссылок
НЕЛЬЗЯ надёжно создать ручной правкой v8unpack.** Платформа хранит у такого реквизита скрытую
бинарную привязку типа, которой НЕТ в выгружаемом v8unpack-JSON. Ручной реквизит → JSON совпадает
с рабочим байт-в-байт, а поле всё равно получает кнопку выбора `ChoiceButton` («…») и диалог
«выберите тип данных». Round-trip v8unpack это НЕ ловит (сверяет только JSON). Структура самого
ПОЛЯ (флаги `[4]/[5]`, `[26]`, `[39][..]`) тут НИ ПРИ ЧЁМ — рабочее и нерабочее поля бывают
структурно идентичны. Дело в РЕКВИЗИТЕ.
**Рецепт:** реквизит формы `СписокЗначений`+ElementType создаёт ПЛАТФОРМА — Конфигуратор (попроси
юзера добавить поле-отбор как в рабочей обработке) ИЛИ XML-сборка (`<Type>v8:ValueListType</Type>`
+`<Settings xsi:type="v8:TypeDescription"/>`, поле `<DropListButton>true`+`<ExtendedEditMultipleValues>true`
без `<ChoiceButton>`) через `/build` в базе `ib_u<uid>` с АКТУАЛЬНОЙ конфигурацией (НЕ в старой —
заморозит type-id, сломает другие поля; НЕ в пустой — не разрешит типы).
**⚠️ НЕ ВЕРНО (DEVACC-747, опровергнуто позже — repack БЕЗОПАСЕН; см. поправку в Changelog 29.06: «фильтр не работает» = код EnumRef-vs-СписокЗначений, не repack):** ~~v8unpack-REPACK РУШИТ привязку ВСЕХ `СписокЗначений`-полей
формы.** Симптом: после repack мультивыбор-фильтры (статус, ставка НДС, договор, контрагент) перестают
фильтровать (поле показывает значение, но связанный `СписокЗначений` пуст). Round-trip ЧИСТЫЙ и выгрузка
в XML идентичны — НЕ ловят (привязка не в JSON/XML). Доказано: правка, добавлявшая ТОЛЬКО обработчик колонки
(фильтры не трогались), сломала отбор по статусу — виноват сам repack. ⇒ **Файл с любым платформенным
`СписокЗначений`-полем (мультивыбор-фильтр) НЕЛЬЗЯ пересобирать v8unpack'ом ВООБЩЕ — даже ради правки модуля
или соседнего поля.** Все правки такого файла — ТОЛЬКО в Конфигураторе (дать юзеру инструкцию: форм-событие
через дизайнер + код модуля) или платформенной XML-сборкой (`/build` в базе с актуальной конфигурацией).
(Историческая заметка ниже про «после создания платформой v8unpack-правки безопасны» — НЕВЕРНА, оставлена
как предупреждение.)

~~**ПОСЛЕ создания платформой v8unpack-правки БЕЗОПАСНЫ и round-trip'ятся faithful:**~~ перенос поля в
дереве, удаление лишнего поля/реквизита (`props`+`data`+`tree`), проводка события
`OnChange`→обработчик (слот после блока `'36'`: `["1","<eventGUID>","\"<Проц>\"","1","0","<eventGUID>","0","1"]`,
default пустой `["0","1","0"]`), правка модуля (`bsl`).
⚠️ `<eventGUID>` — это **тип события** (НЕ произвольный!): `ПриИзменении`/`OnChange` поля ввода =
`fe115cc8-9e33-4684-a166-bd5136fe7a9f` (этот же GUID у всех OnChange-событий формы — общий тип, не
конфликтует; различает обработчики имя процедуры). Случайный GUID → при выгрузке `<Event name="<guid>">`
вместо `<Event name="OnChange">` и событие НЕ сработает. Бери GUID из существующего события того же типа
в ЭТОЙ форме (grep по elem.json: 8-эл запись `["1","<guid>","\"…ПриИзменении\"",…]`); если нет —
проверь выгрузкой XML, что вышло именно `name="OnChange"`. Выверено DEVACC-747 (`аДокументыСтатусСчетаПриИзменении`). Проверено: выгрузка→сборка→выгрузка платформенного
реквизита даёт байт-идентичное поле. Верифицируй итог выгрузкой `.epf→XML` (1cv8
`DumpExternalDataProcessorOrReportToFiles`): у поля должно быть `DropListButton`+`ExtendedEditMultipleValues`
и НЕ быть `ChoiceButton`.

---
(ИСТОРИЧЕСКОЕ, до развязки DEVACC-747 — оставлено как описание структуры, НЕ как рецепт создания):
поле **`СписокКонтрагентов` в `КомиссияКлиентов.epf`** (узел `data["Отбор/СписокКонтрагентов"]`).
Его отличие от одиночно-стилевого поля — ЛИШНИЙ БЛОК отображения списком в начале:
- `raw[4] = "1"` (у одиночного `"0"`)
- `raw[5] = ["0", ["0", ["\"B\"", "1"], "0"]]` (у одиночного здесь сразу имя — структура сдвинута на 1!)
Из-за этого сдвига `raw[6]` у такого поля = `"2"`, а ИМЯ лежит в `raw[7]`, datapath в `raw[12]`
(а не `[6]`/`[11]`). Поле `len≈60` (одиночное `≈59`).
🚫 НЕ бери за образец `ОтборКлиент` (`АннуляцияЗаказов`) или `СписокКонтрагентов`
(`УстановкаЮрЛицаИДоговораГостиницы`) — они БЕЗ этого блока (`raw[4]="0"`, имя в `raw[6]`),
рендерятся ОДИНОЧНЫМ полем «…»+«x» и/или просят выбрать тип. Проверено DEVACC-747:
правка отдельных флагов `raw[26]/[39][..]` НЕ даёт ▼-мультивыбор — нужен именно блок `[4]/[5]`.
**Надёжный способ — ПЕРЕНОС ПОЛЯ ЦЕЛИКОМ из `КомиссияКлиентов`** (copy.deepcopy), заменив СТРУКТУРНО
(не строковым replace — ломается на экранировании `\"`):
- `raw[1][0]` собственный id → свой; имя `raw[7]='"<Имя>"'`; datapath `raw[12]=["1",["<id-реквизита>"]]`;
- собственные id контекстного меню/подсказки (`["121",G]`/`["122",G]`) → свои;
- слот события сразу ПОСЛЕ блока `'36'` (`raw[i36+1]`, по умолчанию `["0","1","0"]`) → свой обработчик
  `ПриИзменении` `["1","<eventGUID>","\"<Проц>\"","1","0","<eventGUID>","0","1"]`, если он нужен.
Сравнение полей — по `data[...]`-узлам (искать field по суффиксу ключа пути, НЕ по `raw[6]`).
Реквизит формы (`СписокЗначений`+ElementType) — см. выше; он одинаков у всех, дело именно в ПОЛЕ.

**Грабли round-trip:** для поля, привязанного к реквизиту ФОРМЫ, v8unpack генерирует
производный ключ `data/<путь>/ПутьКДанным=<имя реквизита>` — у прежней ОБЪЕКТНОЙ привязки
его не было. После перепривязки добавь его вручную, иначе re-unpack даёт расхождение.

**Рецепт «одиночный объектный отбор → списочный»:** объектный реквизит (`Объект.X`,
одиночная ссылка) НЕ может быть `СписокЗначений` → заведи реквизит ФОРМЫ `X` типа
`СписокЗначений`+ElementType, **перенеси ПОЛЕ целиком из `КомиссияКлиентов`** (эталон ▼-мультивыбора
выше — НЕ правь флаги старого одиночного поля, перенос блока `[4]/[5]` обязателен), добавь
`ПутьКДанным`. В .bsl: `ПодготовитьX()` (нормализация в список, терпит и одиночную
ссылку для миграции сохранённых настроек), `&X` в запросе `= &X` → `В (&X)` + флаг
`&ЕстьОтборX`, очистка `X = Новый СписокЗначений`. ⚠️ Старый объектный реквизит того же имени
**ПЕРЕИМЕНУЙ** (`obj_edit retype`, напр. `X`→`XУстар`) — тёзка объектного ref и форменного
списка может давать составной тип выбора; сирота с другим именем безвреден (удаление объектных
реквизитов в v8unpack рискованнее переименования).

## РЕЦЕПТ: добавить НОВУЮ кнопку в командную панель ('22'-узел) — грабли со счётчиком

Панель (raw head `'22'`, напр. `<Имя>КоманднаяПанель`) хранит кнопки как плоский
список пар `маркер a9f3b1ac-f51b-431e-b102-55a69acdecad` + `узел '31'` (кнопка),
и это можно клонировать/вставлять структурно (в отличие от форм-реквизита
`СписокЗначений`, см. ниже). **НО** в заголовке самой панели есть **счётчик
элементов — индекс `[21]`** (число кнопок, строкой), который v8unpack НЕ
пересчитывает сам — его надо инкрементировать вручную при вставке.

Симптом без фикса: round-trip v8unpack (unpack→pack→unpack, сравнение JSON)
**остаётся чистым** (значит сама вставка структурно валидна для v8unpack), но
загрузка получившегося `.epf` в РЕАЛЬНОЙ 1С (`onec-build-agent /dump-epf`)
падает: `422 {"error":"dump failed", "log":"...Ошибка формата потока"}`. Это НЕ
общий баг «v8unpack repack всегда ломает» (DEVACC-776 про другое — про то, что
пересборка САМОГО v8unpack ненадёжна как финальный артефакт вообще; тут же
проблема куда точнее локализована и лечится).

**Диагностика/фикс:** сравнить count-поле `panel[21]` со ЖИВЫМ образцом такой
же панели с бОльшим числом кнопок (напр. `ОтчетПоДаннымЗаказа.epf`,
`аДокументыКоманднаяПанель`, 11 кнопок → `panel[21]=='11'`) — оно РАВНО числу
кнопок в панели, а не что-то другое. После вставки нового маркер+кнопка:
```python
panel[21] = str(int(panel[21]) + 1)
```
Выверено (DEVACC-757, 27.07.2026, `СписокРеализаций.epf`): без инкремента —
`Ошибка формата потока`; с инкрементом — грузится чисто, кнопка и её `BackColor`
(`<BackColor>#C8E9C7</BackColor>`) видны в XML-дампе платформы.

⚠ Финальный артефакт для НОВОЙ кнопки — как и для новых колонок — надёжнее
получать через цикл `/dump-epf` (проверить XML: кнопка + `CommandName` +
`BackColor` на месте) → **пересобрать через `/build`** (platform-native, а не
доверять v8unpack-пересборке как финалу), а не только через `epf.sh roundtrip`
(тот ловит только внутреннюю согласованность v8unpack, не формат потока
реальной 1С).

## РЕЦЕПТ: цвет кнопки (зелёная=исполнительная, жёлтая=«Заполнить» и т.п.)

Цвет — поле `raw[19]` кнопки `'31'`: `["3","4",["0"]]` = без цвета (авто);
`["3","0",["<BGR-int>"]]` = свой цвет. BGR — то же число, что и в XML-пути
(`<BackColor>#RRGGBB</BackColor>`, см. `xml_build_from_scratch.md`).

⚠ **НЕ бери цвет с первого попавшегося донора-обработки — юзер держит СВОЙ
цветовой стандарт по ВСЕМ обработкам, единичный образец может оказаться
исключением.** Проверено на кейсе DEVACC-755: кнопка `кнПоставитьЮрЛицо` из
`УстановкаЮрЛицаИДоговораГостиницы.epf` дала `#70AD47`→`4697456` — юзер это
отверг («не такой зелёный, как в других обработках»). Массовый скан ВСЕХ `.epf`
из `ОбработкиЭкспорт/` (не одной обработки!) показал реальный стандарт:
**зелёный `#C8E9C7` → BGR-int `13101512`** (14 вхождений на исполнительных
кнопках — `аДокументыПроставитьЛПГост`, `ФормаОбработатьПроводки`,
`СоздатьСтруктуру`, `аДокументыПровестиСчет` и др.), **жёлтый `#FFE100` →
`57855`** (кнЗаполнить/«Заполнить») подтвердился и остался единственным
кандидатом. Перед покраской кнопки по цветовому смыслу («исполнительная»,
«Заполнить» и т.п.) — сначала грепни BGR-значения `raw[19]` по ВСЕМ `.epf` юзера
(скрипт: распаковать все, рекурсивно собрать `node[19]` у `'31'`-кнопок, частоты
по значению), бери самое частое для нужной семантики, а не первое найденное.

## ГРАБЛИ: удаление элемента с «Дочерние элементы отдельно» — v8unpack ломается на пустой группе

Если элемент физически хранится **отдельным data-ключом** (не встроен в raw родителя,
как кнопки командной панели таблицы — см. выше), а его контейнер-группа помечена
`"Дочерние элементы отдельно"` — при удалении ЭТОГО ОДНОГО элемента (data-ключ + узел
`tree`) v8unpack **не выдаёт ошибку**, но пересобранный файл **теряет почти ВСЕ
остальные data-ключи** (немая массовая порча, обнаруживается только round-trip'ом:
`data` orig vs repacked расходится почти во всём). Причина — опустевшая
«отдельная» группа (0 детей) ломает внутренний энкодер v8unpack.

**Лечение:** не оставлять пустую группу-обёртку — удалять **всю ветку целиком**
(саму группу-контейнер + её data-ключи + её узел в `tree`), а не только лист.
Кейс: кнопка `ФормаКомандаЗаполнитьДокументы` сидела одна в `Группа1/ФормаГруппа1`
(отдельным data-ключом) — удаление только кнопки калечило файл; удаление ВСЕЙ ветки
`Группа1` (родитель тоже был единственным ребёнком своего родителя) собралось чисто.

⚠ **`tree` на верхнем уровне — ПЛОСКИЙ СПИСОК, не dict с `child`.** Рекурсивный обход
`tree`, который фильтрует только `node['child']` у dict-узлов, НЕ уберёт узел, если тот
лежит прямо в списке верхнего уровня (или любом промежуточном списке между dict-узлами,
т.к. `tree` = чередование list/dict). Фильтруй явно на каждом уровне списка:
`e['tree'] = [n for n in e['tree'] if n.get('name') != 'Имя']`, и то же самое для
`node['child']` внутри рекурсии — не полагайся, что один "универсальный" рекурсивный
фильтр отработает оба случая (list-верх и dict-с-child).

**Диагностика:** если `epf.sh roundtrip` после удаления элемента формы вдруг показывает
расхождение почти по ВСЕМ ключам (а не по одному ожидаемому) — почти наверняка это
опустевшая «отдельная» группа, а не баг твоей правки. Проверить гипотезу быстро:
`v8unpack -B <dir> <out.epf>` напрямую (без диффа) — если пакует без явной ошибки, но
`unpack` результата теряет ключи — это оно.
