Compare commits

...

545 Commits

Author SHA1 Message Date
Nick Shirokov 3d36c20269 fix(epf-build,epf-dump,web-publish): надёжный резолв пути к платформе 1С
Распространение фикса резолва (см. предыдущий коммит по db-*) на
оставшиеся навыки с тем же дублированным блоком:
- epf-build, epf-dump (ps1+py): резолв 1cv8.exe — реестр .v8-project.json
  → числовая сортировка версий → glob Program Files [+ (x86)] с заметкой.
- web-publish (ps1+py): резолв bin-каталога (для wsap24.dll) — те же
  приоритеты; v8path из реестра уже есть нужный bin-каталог.

Чинит лексикографический выбор версии и узкую область поиска; py
деградирует без падения вне Windows. Версии: epf-* 1.0→1.1,
web-publish 1.2→1.3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:35:48 +03:00
Nick Shirokov e507e6bfba fix(db): надёжный резолв 1cv8.exe во всех навыках группы
Новая лестница приоритетов поиска платформы (ps1 + py, 18 файлов):
1) -V8Path; 2) v8path из .v8-project.json (скрипт сам ищет файл вверх от
cwd — пин-версия соблюдается даже без -V8Path); 3) glob по Program Files
[+ (x86)] с ЧИСЛОВОЙ сортировкой версий и заметкой «Auto-selected
platform X.Y.Z: <путь>».

Исправляет: лексикографический выбор версии (8.3.9 вместо 8.3.27);
тихий выбор максимальной версии (риск подъёма формата базы) — теперь
реестр в приоритете; узкую область поиска (добавлен x86). Python-порт
деградирует без падения вне Windows (glob пуст → чистый exit, не
трейсбэк). Версии: cf-семейство 1.0→1.1, load-xml/load-git 1.4→1.5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 14:23:51 +03:00
Nick Shirokov 293e8e7a55 chore(db): убрать boilerplate-разделы из SKILL.md группы
Удалены не несущие нагрузки разделы «Коды возврата» (универсальные 0/1)
и generic «прочитай лог / покажи результат». Сохранены только смысловые
следующие шаги (предложить db-update, регистрация в реестре, занятость
базы, предупреждения, Partial-режим). Правки только в SKILL.md, EOL/CRLF
сохранён, версии скриптов не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:57:48 +03:00
Nick Shirokov 6787e97a72 feat(db-load-dt): загрузка ИБ из DT-файла (RestoreIB), opt-in
Новый навык пакетной загрузки всей информационной базы из .dt через
конфигуратор /RestoreIB (с опц. -JobsCount, -UnlockCode/UC). Операция
необратима (полная перезапись базы) → disable-model-invocation: true,
плюс инструкция: сначала предложить db-dump-dt как точку отката, затем
подтверждение. db-update после не нужен. PS1 + py-порт.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:57:48 +03:00
Nick Shirokov 5ad2e4b5fe feat(db-dump-dt): выгрузка ИБ в DT-файл (DumpIB)
Новый навык пакетной выгрузки всей информационной базы (конфигурация +
данные) в .dt через конфигуратор /DumpIB. Авто-режим разрешён (бэкап).
PS1 + py-порт в стиле db-dump-cf.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 13:57:35 +03:00
Nick Shirokov b9c7af02de fix(meta-edit): убрать лишний -Path в авто-вызове валидаторов (py-порт)
argparse-конструкция add_argument("-XPath", "-Path") объявляет два АЛИАСА
одного аргумента, а py-порты edit-навыков понимали её как «передать оба
флага» и звали валидатор как `-XPath -Path <путь>`. argparse трактовал
`-Path` как опцию, а не значение → "error: expected one argument", и
авто-валидация тихо падала (exit code не пробрасывается, правка проходила).

Убран лишний -Path в 5 py-местах:
- meta-edit.py, cf-edit.py, subsystem-edit.py, interface-edit.py (авто-вызов)
- meta-validate.py (рекурсивный batch-вызов, строка 34 — тоже был сломан)

ps1-порты корректны (один флаг), не трогались. Версии подняты парно
(ps1+py) для синхронности. Проверка «скрипт не найден → skip» уже была.

Проверено: одиночная валидация, batch-режим, сквозной meta-edit→валидация.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 11:55:42 +03:00
Nick Shirokov 58ea52cb63 merge: хуки support-guard + суфлёр (опт-ин) в support-state-format
Консолидация: §1B (в навыках) + §1A/суфлёр (хуки, экспериментальные, опт-ин).
2026-06-21 11:22:14 +03:00
Nick Shirokov 6e458bf0b8 chore(hooks): сделать хуки опт-ин (экспериментально) + доки
- plugin.json: убран ключ hooks → плагин больше НЕ подключает хуки
  автоматически. Базовая защита поддержки (§1B в навыках) остаётся
  on-by-default; хуки — опциональный слой поверх (перехват правок мимо
  навыков + суфлёр), включается вручную.
- hooks/README.md: помечено «экспериментально, по умолчанию выключено»;
  раздел установки переписан под ручное включение.
- docs/v8-project-guide.md: добавлен флаг skillSuggester (глоб. + по базе)
  и секция про опциональные хуки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:40:46 +03:00
Nick Shirokov 63fd91c3ca fix(role-compile): выровнять версию py-порта (1.6→1.7) с ps
Дуал-порт версии разъехались ранее (ps бампнули, py — нет); выравниваю.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:29:41 +03:00
Nick Shirokov a991458ef2 feat(mutators): предметная диагностика support-guard (§1B) по причине отказа
Синхронизация §1B с улучшенным текстом хука (ветка feat/support-guard-hooks):
вместо общего списка всех вариантов — текст под конкретную причину
(capability-off / locked / not-removed) с подставленным реальным путём и
точными командами support-edit. Понятно модели вне контекста.

- Все 16 навыков-мутаторов (оба рантайма): Assert-EditAllowed /
  assert_edit_allowed строят сообщение по $code/code причины отказа.
- Терминология «редактирование» (как у платформы 1С).
- Версии заголовков подняты в обоих рантаймах.
- EOL/BOM каждого файла сохранены; deny-тесты 16/16 на PS и PY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 20:18:21 +03:00
Nick Shirokov 4ec2420af6 feat(hooks): суфлёр различает чтение/правку + убран триггер на поиск
- Подсказка зависит от действия: Read → info-навык (понять структуру),
  Edit|Write|MultiEdit → мутатор (meta-edit/form-edit/…). Throttle теперь
  по (сессия, группа, действие) — отдельно read- и write-подсказка.
- Убран триггер на Grep|Glob (группа search): *-info помогают ПОНЯТЬ
  найденный объект, а не НАЙТИ по содержимому → подсказка вводила в
  заблуждение. Суфлёр только на файловых инструментах.
- cfe-подсказка ведёт и на cf-info (читает свойства/состав расширения),
  и на cfe-diff (специфика); правка — cfe-borrow/cfe-patch-method.
- README обновлён.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:54:06 +03:00
Nick Shirokov ba0880a5c5 style(hooks): «правка» → «редактирование» в текстах гарда и README
Единообразие с термином платформы 1С («редактирование объекта
метаданных запрещено»).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:39:00 +03:00
Nick Shirokov 378b19b59f refactor(hooks): предметная диагностика гарда по причине + README для читателя
- support-guard: вместо общего списка всех вариантов — текст под конкретную
  причину отказа (decideSupport.code: capability-off | locked | not-removed),
  с подставленным реальным путём и точными командами support-edit. Понятно
  модели вне контекста: что за состояние и что именно сделать.
- support-state: decideSupport возвращает code (дискриминатор причины).
- README: переписан для читателя — убраны отсылки к внутренней реализации
  (§-нумерация, декодер, разбор common/); назначение, установка, настройка
  (.v8-project.json), что делать при отказе, проверка.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 19:36:55 +03:00
Nick Shirokov ebd620d262 feat(hooks): §1A гард поддержки + суфлёр навыков (node-хуки Claude Code)
Харнес-слой поверх пола §1B: ловит правки мимо навыков-мутаторов.

- support-guard.mjs (PreToolUse Edit|Write|MultiEdit) — §1A: блокирует
  сырую правку объекта поставщика «на замке» / read-only конфы; реакция
  deny|warn|off из .v8-project.json editingAllowedCheck, идентично §1B.
- skill-suggester.mjs (PostToolUse Read|Grep|Glob|Edit|Write|MultiEdit) —
  ненавязчивая подсказка профильного навыка, throttle 1×/сессия/группа,
  не блокирует; флаг skillSuggester (on|off).
- common/: support-state.mjs (порт декодера bin 1:1 из Assert-EditAllowed),
  project.mjs (реакция из .v8-project.json), object-class.mjs (карта
  путь→навык с различением cf/cfe и mxl/скд по нюху корня).
- test/run.mjs: 38 standalone-тестов на корпусе cfsrc + синтетике.
- plugin.json: hooks → ./hooks/hooks.json (авто-загрузка в плагине).

§1C (грубый Bash-гейт) отброшен — дублирует §1B, формат bin заморожен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 18:39:05 +03:00
Nick Shirokov 07ea676326 feat(db-load-xml,db-load-git): исключать файлы состояния поддержки из частичной загрузки
Партиал-загрузка ParentConfigurations.bin платформой не принимается
(мутный отказ «редактирование объекта метаданных запрещено», пустое имя
= корень; смена поддержки требует полной загрузки). Чтобы не падать
невнятно, оба навыка теперь исключают служебные файлы поддержки из
partial-списка с явной подсказкой.

- db-load-xml (Mode Partial): фильтрует -Files и -ListFile — убирает
  ParentConfigurations.bin и ConfigDumpInfo.xml, грузит остальное;
  если после фильтра пусто — подсказка использовать -Mode Full.
- db-load-git: ParentConfigurations.bin из git-diff отбрасывается явно
  (раньше — неявно, через несуществующий производный xml) и о смене
  поддержки печатается предупреждение; ConfigDumpInfo.xml как и прежде
  пропускается.

Не блокируем быструю частичную загрузку объектов (bin всё равно partial
не применяется — ничего «быстрого» не теряем); смену поддержки честно
направляем на полную загрузку. Оба порта, v1.3→v1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 17:10:02 +03:00
Nick Shirokov de04a8dc7a feat(support-edit): навык переключения состояния поддержки + ссылка из диагностики гарда
Замыкает петлю issue #23: после отказа support-guard модель может
легитимно включить редактирование. support-edit правит правила поддержки
в Ext/ParentConfigurations.bin выгрузки (только XML; в ИБ — полной загрузкой):
  -Path <путь> -Set editable|off-support|locked   (пообъектно или корень)
  -Path <путь> -Capability on|off                  (возможность изменения)
Path-based, симметрично гарду — модель берёт путь прямо из отказа.

Реализация: regex-замена in-place по разобранному формату bin (round-trip
байт-в-байт; парсер подтверждён consumed=100% на корпусе acc/erp/K=7).
При выключенной возможности (G=1) пообъектный -Set отказывает с подсказкой
«сначала -Capability on» (явные шаги). Включение возможности ставит всё на
замок (вендорские флаги «не редактировать» в выгрузке недоступны — массовой
разблокировки нет; non-goal). Оба порта дают байт-в-байт идентичный bin.

Диагностика support-guard (32 файла, 16 мутаторов × 2 порта) теперь печатает
готовую команду support-edit под конкретный отказ.

Тесты: фикстуры g0/g1 + кейсы set-editable/off-support/предусловие/capability
со снапшотами bin; зелёные на PowerShell и Python. Все 16 мутаторов +
support-edit зелёные на обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 16:45:43 +03:00
Nick Shirokov acbd6be46c test(support-guard): committed deny-тесты на 13 навыков-мутаторов + фикс help-add
Регрессионная защита гарда: по одному expectError-кейсу guard-deny на
каждый из 13 размноженных мутаторов (раньше committed-тесты были только
у 3 пилотных). Ловит случайное удаление/поломку guard-вызова в будущем.

Фикстуры on-support (рукотворный bin: корень/объект f1=0, плюс элемент
f1=0 для edit-existing навыков — форма 4444, макет 5555, подсистема 6666).
Структура под конвенцию каждого навбыка: owner/root для add/compile/edit
конфигурации; плоский Locked/Ext для help-add (EPF-стиль).

Заодно исправлен пред-существующий баг help-add Detect-FormatVersion
(v1.5→v1.6, оба порта): Substring(0, byteLength) падал на кириллическом
Configuration.xml (байт>символов). Теперь Substring по длине строки;
фикстура help-add кириллическая — регрессия фикса покрыта тестом.

Все 13 guard-кейсов зелёные на PowerShell и Python; deny через exit≠0 +
stderr "support-guard". Существующие кейсы не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:45:50 +03:00
Nick Shirokov b17dd5d04c feat(mutators): размножить support-guard на остальные навыки-мутаторы
Контракт Assert-EditAllowed (дословная копия из meta-edit, оба порта)
добавлен в 13 навыков-мутаторов. Всем единый вызов require=editable —
«кого проверять» решает не навык, а walk-up по файловой системе от пути
цели:
  - целевой файл имеет root-uuid → его f1 (правка существующего элемента);
  - иначе ближайший вверх <dir>.xml с uuid → f1 владельца (добавление
    дочернего: форма/реквизит/макет к объекту);
  - иначе корень Configuration.xml → f1 корня (новый объект верхнего уровня).

Это воспроизводит семантику поддержки 1С автоматически, поэтому один и
тот же навык проверяет разное по состоянию дампа: skd-compile/mxl-compile/
form-compile в существующий макет/форму → f1 этого элемента (modify), в
несуществующий → f1 владельца. Проверено обоими сценариями.

Навыки: form-edit, form-add, form-compile, skd-edit, skd-compile, cf-edit,
subsystem-edit, subsystem-compile, interface-edit, template-add, help-add,
mxl-compile, role-compile. Диагностика через [Console]::Error.WriteLine +
exit 1 (как в эталоне). Версии подняты в обоих портах.

Проверено: deny на обоих портах (крафт-фикстуры на копиях, корпус не
тронут); все 16 навыков-мутаторов зелёные на PowerShell и Python
(264 кейса). BOM сохранён везде.

Follow-up (в upload-плане): пред-существующий баг help-add
Detect-FormatVersion (Substring на кириллице, до гарда, не связан);
авторезолв пути в meta-edit; per-skill committed deny-тесты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 15:27:38 +03:00
Nick Shirokov 2136245b69 feat(meta-edit,meta-compile,meta-remove): support-guard перед правкой объектов на поддержке
Пилот энфорсмента issue #23: перед записью навыки-мутаторы проверяют
состояние поддержки (Ext/ParentConfigurations.bin) и блокируют опасную
правочку. Триггер — наличие bin (конфиг на поддержке); реакция из
.v8-project.json editingAllowedCheck (deny|warn|off, по умолчанию deny).

Assert-EditAllowed (нативная копия в каждом навыке, оба порта):
walk-up резолвит uuid цели (объект / владелец / корень — по пути) и
корень конфигурации, затем G-vs-f1 и консервативная свёртка min(f1).
Два режима: require-editable (f1≥1, G≠1) для правок/добавлений;
require-removed (f1=2) для удаления.
- meta-edit (v1.7): editable на редактируемом объекте;
- meta-compile (v1.13): editable на корне (добавление нового объекта);
- meta-remove (v1.2): removed на удаляемом объекте.

Диагностика через [Console]::Error.WriteLine + exit 1 (не Write-Error:
под ErrorActionPreference=Stop тот бросает и был бы проглочен catch'ем).

Тесты: малая on-support фикстура с рукотворным bin (root/Locked f1=0,
Removed f1=2); guard-deny кейсы (expectError) — оба рантайма зелёные,
старые кейсы не сломаны (конфиги без bin → allow). Поле editingAllowedCheck
задокументировано в docs/v8-project-guide.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:57:14 +03:00
Nick Shirokov 489b8389aa feat(form-info,skd-info,role-info,subsystem-info,mxl-info): строка состояния поддержки
Размножение per-object паттерна состояния поддержки из meta-info на
остальные info-навыки. Унифицированный helper Get-SupportStatusForPath
(нативная копия в каждом скрипте): walk-up от пути цели — берёт uuid
ближайшего метафайла элемента (форма/макет/роль/подсистема, либо сам
целевой .xml) и корень конфигурации с ParentConfigurations.bin, затем
G-vs-f1 и консервативная свёртка min(f1) по блокам поставщиков.

Резолв точный на уровне элемента: форма с индивидуально включённым
редактированием (Валюты.ФормаСписка f1=1) показывает «редактируется»,
а форма того же объекта на замке (ФормаЭлемента f1=0) — «на замке».

form-info v1.4, skd-info v1.7, role-info/subsystem-info/mxl-info v1.1.
Проверено на корпусе (acc G=1 → read-only, erp → снято) — оба порта
байт-в-байт. Тесты: все 7 info-навыков зелёные на PowerShell и Python.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 14:14:47 +03:00
Nick Shirokov 96660f4d9d feat(cf-info,meta-info): вывод состояния поддержки из ParentConfigurations.bin
Декодер Ext/ParentConfigurations.bin (нативная копия в каждом скрипте,
без общего модуля — по конвенции автономности навыков; single-pass,
не падает на битом/пустом файле).

cf-info (v1.3): блок «Поддержка:» в overview/full — на поддержке /
возможность изменения вкл-выкл / счётчики на замке/редактируется/снято
(только при G=0) / снята полностью / расширение (CFE). При G=1 показывает
read-only без вводящих в заблуждение счётчиков; тяжёлый скан 7.4МБ
пропускается, когда не нужен. При K>1 перечисляет конфигурации поставщика.

meta-info (v1.3): строка «Поддержка:» под заголовком объекта — walk-up к
корню конфигурации, статус с учётом G-vs-f1 и консервативной свёртки
min(f1) по блокам поставщиков. Для на-замке/read-only — короткое
последствие+действие (cfe-*), для остальных терсно.

Проверено на корпусе и размеченных дампах (acc G=1, erp снято,
ЧастьОбъектов, Корень, НесколькоПоддержек K=7 мультивендор, CFE) —
оба порта байт-в-байт. Формат: docs/1c-support-state-spec.md.

Тесты: cf-info 7/7, meta-info 17/17 на PowerShell и Python.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:57:31 +03:00
Nick Shirokov 75a97d51ea docs(support-state): распознавание расширения (CFE) как безопасного пути
У расширения нет Ext/ParentConfigurations.bin, поэтому правки его
исходников не ограничиваются состоянием поддержки базовой конфигурации.
Расширение распознаётся позитивно — по <ConfigurationExtensionPurpose>
в Configuration.xml, что отделяет его от случая полностью снятой
поддержки (там bin тоже почти пуст) и даёт корректную диагностику.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:58:58 +03:00
Nick Shirokov 8817f37bf0 docs(support-state): спецификация формата Ext/ParentConfigurations.bin
Описание состояния поддержки конфигурации 1С из XML-выгрузки:
глобальная «возможность изменения» (G), список конфигураций
поставщика (K блоков) и пообъектные правила (f1: на замке /
редактируется с сохранением поддержки / снят с поддержки).

Грамматика контейнера подтверждена на образцах одиночной и
множественной поддержки. Добавлено правило свёртки при конфликте
поставщиков (консервативно: любой f1=0 → замок) и алгоритм
статической проверки редактируемости объекта по пути файла.

Основа для guardrail-хука безопасной доработки типовых
конфигураций (issue #23).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 13:13:32 +03:00
Nick Shirokov ae82412377 feat(switch): добавить платформу Yandex Code Assistant (#22)
Code Assistant ищет навыки в .codeassistant/skills/ (приоритетнее .agents/).
Добавлен отдельный ключ codeassistant в реестр switch.py, пункт в
интерактивное меню, строка в таблицу README и две записи в матрицу
build-ports (powershell + python).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:07:24 +03:00
Nick Shirokov a38bb55bca chore(tests): refresh form-info/rich-form снапшот (пред-существующий несвежий)
Снапшот form-info отставал от давних сертифицированных фич form-compile (есть в
собственных снапшотах form-compile): <ShowTitle> у группы + полный AdditionSource
+ companion-панели у табличных дополнений (SearchString/ViewStatus/SearchControl)
+ каскадная перенумерация id. Контент не теряется — только добавления и сдвиг id.
Не связано с правками этой сессии (фейл воспроизводился и на пред-сессионном
компиляторе). Полный регресс: 427/427.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:53:01 +03:00
Nick Shirokov 1c21bef26c feat(form-compile): drop-on-miss warn для enum ориентации/behavior + выравнивание портов
Нераспознанное значение ориентации (group/columnGroup/page) и behavior теперь даёт
WARN с авторским набором допустимых значений — раньше промах по карте молча не
эмитил тег (тихая потеря). pass-through-ключи не трогаем (там verbatim, потери нет).

Заодно выровнен регистр enum-резолва ориентации между портами: py был
case-sensitive у columnGroup/page, ps1 (switch) — нет; теперь оба нечувствительны.

v1.172. Регресс form-compile 43/43 (ps1+py), form-validate 10/10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:16:28 +03:00
Nick Shirokov 09d7097476 feat(form-compile): авторские references + индекс, корректность наборов значений
Каскад инструкции: ядро SKILL.md покрывает большинство задач, для редких/нишевых
конструкций — 12 тематических файлов в references/ (по индексу в SKILL.md).
Контракт references: только «как собрать DSL для задачи» — без механики эмиссии,
синонимов, авторезолва и forgiving (это тихая помощь модели); ссылки только внутри
навыка; область строго по элементу-владельцу.

Корректность ядра (наборы значений выверены по корпусу + доменно, forgiving/legacy
исключены из авторских):
- group расцеплён: ориентация (vertical/horizontalIfPossible/alwaysHorizontal) +
  отдельный ключ behavior (collapsible/popup) — popup-группы стали выразимы;
- titleLocation: полный набор none/left/right/top/bottom/auto;
- commandBarLocation += Bottom; searchStringLocation += Bottom/CommandBar/PullFromTop;
- общее свойство tooltip; events: null → авто-имя обработчика; правило уникальности имён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:16:22 +03:00
Nick Shirokov 717f3d8cc5 docs(form-decompile): сжать раздел «Что получаешь» + актуализировать ring3
Раздел разросся; ужал до сути (черновик, не обратим, теряет молча).
Исправлен устаревший факт: CommandInterface и ConditionalAppearance (без
scope) теперь поддерживаются (эмитятся), не валят скрипт. Падение — только
CA со scope / design-time диаграммы-планировщики / неизвестный элемент /
не-Form root. Убраны детали реализации (disable-model-invocation —
во фронтматтере) и нишевый абзац про GroupList.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 11:53:52 +03:00
Nick Shirokov e8b8d32e0d feat(form-decompile): Python-зеркало декомпилятора (порт ps1→py)
Полный порт form-decompile.ps1 v0.147 → .py (structure 1:1, как form-compile).
Двухпортовость декомпилятора замыкает dual-port для всего form-пайплайна.

Паритет ps1↔py: 1733/1733 байт-в-байт на list-iter.txt (все закрытые
кластеры), 0 расхождений, 0 крашей. Полный корпус 17k — в процессе.

Учтённые PowerShell-семантики (иначе тихие расхождения):
- `-eq`/`-ne` регистронезависимы → _ps_ieq на сравнениях заголовок↔авто-имя
  (title-суппресс: "Check date" == "check date")
- одноэлементный @() разворачивается при return без `,`-оператора
  (Build-DLInputParameters → inputParameters: объект при 1, массив при 2+)
- truthiness одноэлементного массива (@("") → falsy → дроп FunctionalOptions)
- .NET XmlDocument НЕ нормализует CRLF в InnerText (ET — нормализует):
  \r\n→&#13;\n внутри корня (не в прологе/эпилоге)
- порядок ключей .NET Hashtable (цвета) захвачен из PS 5.1, не из литерала
- [decimal] сохраняет масштаб vs [double] (Decimal в сериализаторе)

WS-стратегия: два читателя на одном ET-дереве (_text сворачивает
whitespace-only→"" как PreserveWhitespace=false; _text_ws — сырой для Resolve-WS).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 21:28:14 +03:00
Nick Shirokov 2a8d594f66 fix(form-decompile,form-compile): DataSet field TypeLink/Folder/пустой dataPath
Поле набора динсписка (settings.fields[]) — три подвида, терявшиеся при раундтрипе
(форма ИнвентаризацияНМА/ФормаПодбораДокументовЗатрат: 18 diff-строк → 0):

1. inputParameters[].typeLink {field, linkItem} — связь по типу (dcscor:TypeLink,
   субконто с типом-от-счёта). Декомпилятор склеивал InnerText в строку
   ("СчётДт"+"1"="СчётДт1") → компилятор писал xs:string. Структурный захват + эмит.
2. folder: true — поле-папка (DataSetFieldFolder, группировка СубконтоДт над
   СубконтоДт1/2/3; без <field>). Ловился только NestedDataSet; компилятор хардкодил
   DataSetFieldField + всегда <field>.
3. пустой dataPath: "" — поле с <dcssch:dataPath/> + <field> (≠ дефолт dataPath==field).
   Декомпилятор дропал → компилятор реконструировал dataPath=field. Has-Child вместо
   $dp -and; явный dataPath (вкл. "") побеждает fallback (self-closing при "").

Зеркало py (ps1==py байт-в-байт), регресс 43/43 (ps+py), широкий прогон list-top:
match 25→26, TOTAL 445→427, 0 регрессий. Декомпилятор v0.147 / компилятор v1.171.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:28:40 +03:00
Nick Shirokov 06331a9b80 fix(form-decompile,form-compile): dataParameters мульти-value + SpellCheckingOnTextInput
(1) dcsset:dataParameters — параметр с НЕСКОЛЬКИМИ <dcscor:value> (valueListAllowed,
напр. два DesignTimeValue Перечисление.X) — декомпилятор читал ОДНО (SelectSingleNode),
2-е/3-е дропались. Фикс: SelectNodes → массив (декомпилятор) + ветка массива в
Emit-DataParameters (компилятор ps1+py, отдельный <dcscor:value> на каждое значение по типу).
(2) SpellCheckingOnTextInput (input) → GENERIC_SCALARS (обе стороны+py).

Формы Организации/ФормаСписка (dataParameters мульти-DesignTimeValue) + ВводАдреса
(SpellChecking) → match. ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:07:14 +03:00
Nick Shirokov 9ec5857e22 fix(form-compile): GUID.GUID значение → xr:DesignTimeRef (Normalize-ChoiceValue)
Значение параметра выбора (choiceParameters app:value) вида "GUID.GUID" (raw-ссылка по
метаданным.значение, оба GUID) эмитилось как xs:string: Normalize-ChoiceValue не
распознавал raw-GUID-ссылку → xs:string. Тот же класс, что choiceList DesignTimeRef-GUID
(commit 2d326c99), но другой потребитель.

Универсальный фикс: ветка GUID.GUID → xr:DesignTimeRef в Normalize-ChoiceValue (всегда
ссылка, не строка; named-ссылки Enum.X.Y детектятся ниже). Закрывает choiceParameters
и любой др. потребитель Normalize-ChoiceValue; choiceList не затронут (там явный
valueType побеждает Normalize). Зеркало py.

Форма НастройкиПрямыхВыплатФСС/ФормаЗаписи → match. ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:31:56 +03:00
Nick Shirokov b5e8e1df7a feat(form-decompile,form-compile): батч простых хвостов — generic-скаляры, form Scale, CommandBar HL Auto, CheckBox FooterDataPath
Хвост из указанных форм (по 1 в корпусе, кроме CheckBox FooterDataPath):
(1) GENERIC_SCALARS (обе стороны+py): AutoCorrectionOnTextInput (input) /
    CommandUniqueness (button bool) / AllowInputEmptyMultipleValues (input bool) /
    BehaviorOnHorizontalCompression (table).
(2) Форменный <Scale> (масштаб формы) → KNOWN_FORM_PROPS.
(3) CommandBar>HorizontalLocation: компилятор через Get-HLocation скипал Auto
    (умолчание дополнений), но CommandBar хранит его фактически (декомпилятор ловит
    только при наличии) → эмит фактический, включая Auto. Зеркало py.
(4) CheckBoxField>FooterDataPath/FooterText — общие cell-свойства колонки, не ловились
    у check (как раньше расширяли на picField). Захват + эмит (ps1+py).

Выборка 9 форм: match 7/9 (остаток 2 — InputField>MultipleValuesFont структурный font
[отложен] + app:item>Value DesignTimeRef-GUID). ps1==py байт-в-байт. Регресс 43/43. Spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:25:37 +03:00
Nick Shirokov 95d8ece309 fix(form-decompile,form-compile): use:false на группе фильтра (FilterItemGroup)
Группа условий фильтра <dcsset:item xsi:type="dcsset:FilterItemGroup"> может нести
<dcsset:use>false</dcsset:use> (группа отключена, в т.ч. пустая OrGroup без детей).
Декомпилятор ловил group/items/presentation/viewMode/userSettingID, но НЕ use →
терялось; компилятор не эмитил.

Декомпилятор: захват use:false на группе. Компилятор: emit <dcsset:use>false</dcsset:use>
перед <groupType> (порядок исходника). Зеркало py. Корпус: 6 форм.

Форма ДокументооборотСКонтролирующимиОрганами/ПоказСообщений → match. ps1==py
байт-в-байт. Регресс 43/43. Spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:11:38 +03:00
Nick Shirokov 26c804391a fix(form-decompile): фильтр right xs:string "1"/дата — явный valueType (авто-детект дал бы число/дату)
Значение фильтра <dcsset:right xsi:type="xs:string">1</dcsset:right> терялось как тип:
декомпилятор исключал xs:-типы из захвата valueType (расчёт на авто-детект компилятора),
но компилятор авто-детектит строку "1" как xs:decimal (число) → xs:string-ность терялась.

Принцип (подтверждён пользователем): когда авто-вывод типа компилятором дал бы ДРУГОЙ
тип, чем фактический — декомпилятор должен указать valueType явно. Фикс: при xs:string +
значение-строка матчит числовой/дату-паттерн (что компилятор детектит иначе) →
фиксируем valueType="xs:string". Компилятор honors явный тип.

Корпус: 8 значений в 3 формах. Форма ЭлектронныйЗаказЗаявка/ТитулГрузоотправителя →
match. Декомпилятор-only. ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:05:41 +03:00
Nick Shirokov 4855f79403 fix(form-decompile,form-compile): значения параметров дин-списка — ent:-тип, multi-value, dataParameters в partial-дескрипторе
Форма ПомощникРасчетаНалогаУСН теряла значения параметров дин-списка (3 бага):
(1) ent: системное перечисление в значении (ent:AccumulationRecordType=Expense) →
    компилятор понижал до xs:string. Фикс: ветка ^ent: в Emit-DLValue/emit_dl_value
    (value несёт тот же xsi:type, что valueType).
(2) Параметр с valueListAllowed + НЕСКОЛЬКО <dcssch:value> — декомпилятор читал ОДНО
    (SelectSingleNode), 2-е/3-е дропались. Фикс: SelectNodes → массив (компилятор уже
    эмитит array через Emit-DLValue по каждому).
(3) ListSettings с <dcsset:dataParameters> ронялся в канон-fallback (Get-ListSettingsShape
    unknown top-level → $null) → компилятор додумывал полный канон (лишние userSettingID/
    itemsUserSettingID). Фикс: dataParameters → дескриптор + case в partial-пути (ps1+py),
    контент из settings.dataParameters.

Форма → match (8 diff → 0). Корпус: ent:/multi-value по 1 форме (редко). ps1==py
байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 17:01:52 +03:00
Nick Shirokov 4434493446 fix(form-decompile): whitespace-контент input ML (footerText/warningOnEdit/inputHint/nonselectedPictureText)
FooterText с whitespace-контентом (`<v8:content>\n</v8:content>` — blank-footer, оба
языка) схлопывался в пустой `<v8:content/>`: декомпилятор читал через Get-LangText,
PreserveWhitespace=false стрипал → "" → компилятор эмитил self-closing. Тот же
whitespace-ML корень, что у Attribute>Title / choiceList presentation / Column>Title.

Фикс: input ML-чтения footerText (3) / warningOnEdit (4) / inputHint (1) /
nonselectedPictureText (2) → Get-LangTextWS (восстанавливает значимый пробел/перенос
из WS-дока; безопасный суперсет — для непустого контента идентичен). Многострочный
`\n`-контент раундтрипится (Esc-Xml сохраняет перенос, тег спанит строки как оригинал).

Корпус: 2 формы (НастройкиОтправкиЭДО acc+erp). Проверка 25 форм с FooterText/
WarningOnEdit/NonselectedPictureText: match 25/25, 0 dec-fail. Декомпилятор-only.
ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:48:49 +03:00
Nick Shirokov 47e980f932 feat(form-decompile,form-compile): itemsUserSettingPresentation в дескрипторе ListSettings
ListSettings может нести items-уровневую подпись <dcsset:itemsUserSettingPresentation>
(рядом с itemsViewMode/itemsUserSettingID). Get-ListSettingsShape ронял её в канон-fallback
(unknown top-level element → return $null) → терялась. Аналог container-level
userSettingPresentation (commit 66817312), но items-уровень.

Декомпилятор: захват itemsUserSettingPresentation в дескриптор (Get-PresByType — форма
по xsi:type). Компилятор: новый case в потреблении дескриптора (Emit-USPresentation /
emit_us_presentation). Зеркало py.

Корпус 8.3.24: 2 формы (ОплатаПлатежнойКартой/ФормаПлатежиПоРеестрам, …): match 0→2,
TOTAL→0. ps1==py байт-в-байт. Регресс 43/43. Spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:39:00 +03:00
Nick Shirokov 6cfc504509 feat(form-decompile,form-compile): DisplayImportance форменного AutoCommandBar
Форменная командная панель (<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">) может
нести DisplayImportance="Low"/"VeryLow" (адаптивная важность). Декомпилятор не захватывал
этот атрибут, маркер autoCmdBar создавался только при halign/autofill-false/children →
DisplayImportance терялся; компилятор не эмитил.

Декомпилятор: захват $acb DisplayImportance + расширен гейт маркера (DI тоже триггерит
сохранение autoCmdBar-элемента). Компилятор: DI-Attr на тег форменного AutoCommandBar
(обе ветки open/self-closing). Зеркало py. Корпус: 11 форменных ACB с DisplayImportance.

Выборка 11 форм (ПерепискаСКонтролирующимиОрганами/ФормаГрупповойОтправки, …):
match 11/11, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:21:44 +03:00
Nick Shirokov 831c80d9f0 feat(form-decompile,form-compile): батч — form Enabled, PictureField EnableDrag, UsualGroup CurrentRowUse, Column FillCheck
Четыре «расширить существующее на другой тип» свойства из свежего iter-прогона:
(1) Форменное <Enabled>false</Enabled> (доступность всей формы, 6 форм) → KNOWN_FORM_PROPS
    (декомпилятор; компилятор авто-PascalCase Emit-Properties уже эмитит).
(2) PictureField>EnableDrag (4) — как PictureDecoration: декомпилятор ловил generic-ом,
    но Emit-Layout не эмитит EnableDrag → явный emit в Emit-PictureField (после Emit-Layout).
(3) UsualGroup>CurrentRowUse (7) — как Pages: захват в обработчике UsualGroup + Emit-Group
    (после Representation).
(4) Column>FillCheck (6) — как у реквизита: захват в Decompile-AttrColumn + Emit-AttrColumn
    (после Type; bool true→ShowError / строка verbatim, синоним fillChecking).

Зеркало py (2/3/4; декомпилятор ps1-only). Выборка 18 форм: match 18/18, TOTAL→0.
ps1==py байт-в-байт. Регресс 43/43. Spec обновлён (enabled/group currentRowUse/column fillCheck).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 16:10:20 +03:00
Nick Shirokov db341f2351 fix(form-decompile,form-compile): UUID-ссылка (N/M:GUID) в Save/UseAlways теряла/получала префикс
Поле-ссылка по UUID (1/0:GUID) обрабатывается как путь-с-точкой: компилятор НЕ
реинъектит префикс "имя." (платформа хранит её без префикса). Два места рассогласованы:

(1) Save (декомпилятор) — продолжение фикса 2abaa28f: снимали префикс "имя." и у
UUID-остатка (1/0:GUID без точки матчил [^.]+$), компилятор не возвращал → потеря.
Добавлен guard `$matches[1] -notmatch '^\d+/\d+'` → UUID-путь храним полным.

(2) UseAlways (компилятор ps1+py) — реинъектил "имя." к UUID-полю без префикса
(1/0:GUID → Объект.1/0:GUID), оригинал хранит без префикса. Добавлен guard
`-notmatch '^\d+/\d+'` (зеркало правила Save-компилятора). Корпус: 1 форма
(ПланВнутреннихПотреблений/ФормаДокумента, useAlways UUID no-prefix).

Форма → match. ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:47:52 +03:00
Nick Shirokov 2abaa28f16 fix(form-decompile): Save Field — многоуровневый путь теряет префикс реквизита
Реквизит с <Save><Field>имя.Settings.Filter</Field> (напр. SettingsComposer):
декомпилятор снимал префикс "имя." ВСЕГДА (regex `(.+)`) → "Settings.Filter", но
компилятор реинъектит префикс ТОЛЬКО для полей без точки (dot-правило: путь с точкой =
полный, как есть). Рассогласование → префикс реквизита терялся при раундтрипе.

Фикс (декомпилятор): снимаем префикс "имя." только когда остаток — простое под-поле
без точки (`([^.]+)$`); многоуровневый путь "имя.X.Y" храним ПОЛНЫМ → компилятор
по dot-правилу эмитит как есть. Period-кейс (одноуровневые EndDate/StartDate/Variant)
не затронут.

Корпус 8.3.24: 366 многоуровневых Save-полей в 89 формах. Выборка 40 форм: match 40/40,
0 регрессий (включая Period). Декомпилятор-only. Регресс 43/43. Spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:30:26 +03:00
Nick Shirokov 03720d93ed feat(form-decompile,form-compile): AutoShowOpen/ClearButtonMode (input) + EnableDrag на PictureDecoration
(1) AutoShowOpenButtonMode (input, enum Auto/Always/FilledOnly, 14) +
AutoShowClearButtonMode (3) — листовые скаляры → GENERIC_SCALARS (обе стороны + py).

(2) PictureDecoration>EnableDrag (7) — декомпилятор ловил generic-ом (Add-CommonProps),
но EnableDrag эмитился ТОЛЬКО в Emit-Table/SpreadSheet (Emit-Layout его не выводит) →
PictureDecoration терял. Добавлен явный emit в Emit-PictureDecoration (после Emit-Layout).
Generic-перенос enableDrag в Emit-Layout отклонён: сдвигает позицию в сертифицированных
Table/SpreadSheet-снэпшотах (EnableDrag может быть XDTO-позиционно-чувствителен, как
HeaderHeight/CurrentRowUse) — точечный фикс безопаснее.

Выборка 22 формы: match 19 (целевые AutoShow*/PictureDecoration>EnableDrag закрыты;
остаток 3 — SpellCheckingOnTextInput + value). ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:22:10 +03:00
Nick Shirokov 90d2649a5f fix(form-compile): companion наследовал DisplayImportance владельца (PowerShell dynamic scope)
Emit-Companion / Emit-CompanionPanel вызывали DI-Attr $el, но $el НЕ их параметр —
PowerShell брал его из родительского скоупа (эмитируемого элемента). Поэтому
авто-генерируемые companion (ExtendedTooltip/ContextMenu/AutoCommandBar с name="@")
наследовали DisplayImportance владельца (CheckBoxField/UsualGroup/Table), которого
в оригинале у них нет → ложный ADDED. Корпус: ExtendedTooltip/ContextMenu НИКОГДА не
несут DisplayImportance, AutoCommandBar — только element-level (11), не companion.

Фикс: DI-Attr от СОБСТВЕННОГО объекта компаньона ($content / $panel), не от ambient
$el. Python не имел dynamic-scope-бага (di_attr на companion не эмитил вовсе), но для
паритета добавлен di_attr(content/panel) — оба рантайма теперь идентичны (companion
без собственного DI → пусто).

Выборка 19 форм (СостоянияОригиналовПервичныхДокументов acc+erp, + формы с
DisplayImportance-владельцами): match 18, ADDED DisplayImportance исчез. ps1==py
байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 15:04:33 +03:00
Nick Shirokov e1fb40189c feat(form-decompile,form-compile): Popup>CommandSource + InputField multiple-value/itemWidth скаляры
(1) Popup>CommandSource — источник команд попапа (Form/FormCommandPanelGlobalCommands/
Item.X) не ловился/эмитился (был только у ButtonGroup/CommandBar). Добавлен в
обработчик Popup (декомпилятор, с тем же id-ссылка guard) + Emit-Popup (после Title/
ToolTip, перед компаньоном). Зеркало py.

(2) Листовые скаляры в GENERIC_SCALARS (обе стороны + py): ItemWidth (radio/check,
22), ShowCheckBoxesInDropList (input bool, 7), MultipleValueDataPath /
MultipleValuePresentDataPath (input, по 10) + хвост множественного выбора
MultipleValuesTextColor/BackColor (цвет — текст-контент) / MultipleValuePictureShape /
MultipleValuePictureDataPath (input, по 1).

Выборка 41 форма: match 35 (целевые категории ItemWidth/ShowCheckBoxes/
MultipleValue*/Popup>CommandSource закрыты; Контрагенты/ФормаВыбора → match).
ps1==py байт-в-байт. Регресс 43/43. Spec обновлён (commandSource +popup).
Остаток 6 форм — отдельные кластеры (MultipleValuesFont структурный, CA-whitespace
value, DataSet field, DisplayImportance на companion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:57:00 +03:00
Nick Shirokov 2d326c99a5 feat(form-decompile,form-compile): choiceList значение — DesignTimeRef по GUID + nil
Значение элемента <ChoiceList> (InputField/RadioButtonField):
(1) <Value xsi:type="xr:DesignTimeRef">GUID.GUID</Value> — ссылка по метаданным-GUID
(raw, не по имени) эмитилась как xs:string: декомпилятор исключал DesignTimeRef из
valueType (расчёт на авто-детект компилятора), но Normalize-ChoiceValue детектит только
named-ссылки (Enum.X.Y), GUID.GUID → xs:string. Фикс: декомпилятор сохраняет
valueType="xr:DesignTimeRef" при значении-GUID (по префиксу GUID); named-ссылки
по-прежнему авто-детектятся.
(2) <Value xsi:nil="true"/> — nil-значение варианта эмитилось как typed-empty xs:string
(Convert-TypedValue пустого nil-узла → ""). Фикс: декомпилятор ставит valueType="nil",
компилятор эмитит <Value xsi:nil="true"/>.

Зеркало py. Выборка 15 форм (ИндексацияЗаработка/ФормаДокумента, РассылкиОтчетов, …):
match 13→15 целевых (остаток 2 формы — отдельный кластер dcsset:left булев-литерал).
ps1==py байт-в-байт. Регресс 43/43. Spec обновлён (choiceList valueType nil/DesignTimeRef).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:41:17 +03:00
Nick Shirokov 8465bbc82e fix(form-compile,form-decompile): ManualQuery=false при наличии QueryText (отклонение эвристики)
Компилятор форсил <ManualQuery>true</ManualQuery> всегда при наличии query (hasQuery →
true). Но платформа изредка хранит QueryText при ManualQuery=false (корпус: 16 форм
query+mainTable+manualQuery=false, против 2447 query+manualQuery=true) — список с
сохранённым авто-запросом, но не в «ручном» режиме.

Декомпилятор: фиксирует manualQuery ТОЛЬКО при отклонении от эвристики hasQuery
(query есть, но ManualQuery=false → settings.manualQuery=false). Компилятор: явный ключ
manualQuery (в т.ч. false) ПОБЕЖДАЕТ эвристику; различает present-false от absent
(раньше $st.manualQuery -eq $true трактовал явный false как absent → forced true). Зеркало py.

Выборка 16 форм (ОснованияЛьготПоИмущественнымНалогам/ФормаВыбора, … acc+erp):
match 0→16, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43. Spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:25:55 +03:00
Nick Shirokov 8eedca4c22 feat(form-compile,form-decompile): typed-empty значение параметра дин-списка (xs:string vs nil) + SettingsStorage
(1) Пустое значение schema-параметра дин-списка: компилятор ВСЕГДА эмитил
<dcssch:value xsi:nil="true"/>, но платформа часть пустых строковых параметров пишет
типизированным пустым <dcssch:value xsi:type="xs:string"/> (корпус: 27 typed-empty,
все xs:string; 255 nil). Решается ФОРМОЙ value, не valueType: декомпилятор различает
(<value xsi:type="xs:string"/> → value:"", <value xsi:nil/> → ключ опущен/null —
Convert-TypedValue пустого xs:string даёт ""). Компилятор: при value:"" (явная пустая
строка, тип отсутствует или string) → typed-empty xs:string, НЕ nil. Ветка ПЕРЕД vla-nil
(решение не зависит от valueListAllowed). Зеркало py.

(2) SettingsStorage — форменное свойство (ссылка на хранилище настроек, корпус 11) →
KNOWN_FORM_PROPS (декомпилятор; компилятор авто-PascalCase Emit-Properties уже эмитит).

Выборка 17 форм: match 13→15 (типовая МашиночитаемыеДоверенности — 18 typed-empty,
была вся в nil → match). ps1==py байт-в-байт. Регресс 43/43. Spec обновлён.
Остаток 2 формы (другие value-подвиды): DesignTimeValue в dcscor-контексте дропнут;
пустой LocalStringType self-closing vs пара — отдельные находки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:18:37 +03:00
Nick Shirokov 670a574249 feat(form-decompile,form-compile): оформление заголовка + CurrentRowUse на Pages
Контейнер вкладок <Pages> может нести оформление заголовка (TitleFont/TitleTextColor/
TitleBackColor/…) и <CurrentRowUse>. Декомпилятор оформление УЖЕ захватывал (через
Add-CommonProps→Add-Appearance), но Emit-Pages не вызывал Emit-Appearance → терялось.
CurrentRowUse не ловился у Pages (только Table).

Компилятор: Emit-Appearance (профиль field, как у Page) после Emit-Layout +
CurrentRowUse после PagesRepresentation (порядок XSD). Декомпилятор: захват
currentRowUse в обработчике Pages. Зеркало py. currentRowUse → allowlist (ps1+py).

Корпус 8.3.24: Pages title-appearance ~5, CurrentRowUse ~3. Выборка 8 форм
(КлиентБанк/ЗагрузкаВыписки, Контрагенты/ФормаНовогоЭлемента, … acc+erp):
match 0→8, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43. Spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:48:19 +03:00
Nick Shirokov e3ae9c27d1 feat(form-decompile,form-compile): пустой/whitespace right фильтра + GetInvisibleFieldPresentations (кластер Attribute>right)
(1) Пустой <dcsset:right xsi:type="xs:string"/> ≠ отсутствие <right>: декомпилятор
схлопывал оба в shorthand-маркер `_`, а компилятор для shorthand `_` не эмитит right
вовсе → пустой right терялся (Get-FilterValueWithType маппит наличие пустого/nil right
в '_', отсутствие → $null — РАЗЛИЧИМЫ). Фикс (декомпилятор): при value='_' с реально
присутствующим <right> форсим объектную форму {value:"_"} — компилятор эмитит
self-closing right (ветка `_` уже была). Заодно whitespace-/пробельные значения,
рвущие shorthand-парсинг (split по пробелам), уходят в объектную форму.

(2) Whitespace-only <right>   </right> (9 пробелов): PreserveWhitespace=false стрипал
в '' → '_' → self-closing. Восстанавливаем реальные пробелы из WS-дока (Resolve-WS,
как у whitespace-заголовков) → объектная форма value="   ".

(3) GetInvisibleFieldPresentations — Settings-скаляр дин-списка (после MainTable;
дефолт true, корпус 20/20 = false → эмит отклонения). Захват/эмит факт. значения,
зеркало py.

Выборка 14 форм (ДоговорыКонтрагентов, ЕдиницыГенерирующие×2, РаботаСНоменклатурой,
ПравилаИнтеграции, … acc+erp): match 0→14, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43.
Spec обновлён (getInvisibleFieldPresentations). (1)/(2) — декомпилятор-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:37:39 +03:00
Nick Shirokov 85ae72739f feat(form-decompile,form-compile): FooterText/FooterDataPath на PictureField
PictureField (поле картинки в таблице) может нести <FooterText> (ML-текст подвала
колонки) и <FooterDataPath> — общие cell-свойства колонки, уже поддержанные у
input/labelField, но не у PictureField. Декомпилятор не захватывал, компилятор не
эмитил → терялось (форма УчётныеЗаписиДокументооборота: двуязычный FooterText
«Доступность ЭП»/«Digital signature availability»).

Декомпилятор: захват footerDataPath/footerText в обработчике PictureField (зеркало
input/labelField). Компилятор: эмиссия после Emit-Layout (как у input). Зеркало py.
Ключи уже в allowlist (общие cell-props).

Корпус 8.3.24: PictureField FooterText = 4 формы. Выборка 4 формы
(УчётныеЗаписиДокументооборота acc+erp, ЗаказМатериаловВПроизводство,
СчётФактураВыданный): match 0→4, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:16:04 +03:00
Nick Shirokov 668173121d feat(form-decompile,form-compile): userSettingPresentation контейнера ListSettings (filter/order/CA)
Контейнер настроек компоновщика (<dcsset:filter>/<order>/<conditionalAppearance>)
может нести собственный <dcsset:userSettingPresentation> — кастомную подпись
пользовательской настройки (после userSettingID). Декомпилятор кодировал контейнер
только как блок-мету "vu"/"u"/"v" (viewMode/userSettingID), теряя presentation;
компилятор не эмитил.

Дескриптор listSettings[tag] теперь — строка-код "vu" ИЛИ объект
{ meta:"vu", presentation:<текст/{ru,en}> }. Декомпилятор: Get-PresByType сохраняет
форму по xsi:type (ru-only LocalString ≠ xs:string). Компилятор: новый параметр
blockUserSettingPresentation в Emit-Filter/Order/ConditionalAppearance (+ в гейт
hasBlockMeta — иначе контейнер только-с-presentation, без items/viewMode/userSettingID,
не эмитился). Зеркало py.

Корпус 8.3.24: 6 контейнеров-presentation в 6 формах. Выборка 6 форм
(ОтветственныеЗаАктуализацию/ЗаПодписание acc+erp, ПравилаФормированияРезервов,
СтавкиНДСНоменклатуры): match 0→6, TOTAL→0. ps1==py байт-в-байт. Регресс 43/43.
Spec обновлён. Cert: раундтрип (формат платформы, позиция как в оригинале).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 13:03:16 +03:00
Nick Shirokov 9af86b7810 feat(form-decompile,form-compile): ColumnGroup HeaderDataPath + HeaderFormat
Группа колонок таблицы может нести динамический заголовок из данных
(<HeaderDataPath>путь</HeaderDataPath>) и формат заголовка (<HeaderFormat>, ML-текст).
Декомпилятор не захватывал, компилятор не эмитил → теряло (14 строк на форме
БольничныйЛист/ФормаПодробнееОРасчете, 2 ColumnGroup'ы).

Ключи на columnGroup: headerDataPath (path-скаляр), headerFormat (ML — строка/{ru,en}).
Эмиссия в общем cell-блоке Emit-Layout: headerDataPath перед HeaderHorizontalAlign,
headerFormat после (порядок XSD, рядом с уже сертифицированным HeaderHorizontalAlign).
Добавлены в allowlist knownKeys (ps1+py).

Корпус 8.3.24: HeaderDataPath/HeaderFormat = по 2 (обе в этой форме — редкий край).
Форма → match (TOTAL 14→0). Зеркало py байт-в-байт (сверено нормализованным diff).
Регресс 43/43 (ps1+py). Spec обновлён (раздел columnGroup). Cert: раундтрип +
смежность с сертиф. HeaderHorizontalAlign в том же эмит-блоке.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:48:07 +03:00
Nick Shirokov dc56ef6899 fix(form-compile): MobileDeviceCommandBarContent с пустым значением (PS array-unwrap + self-closing)
12 форм корпуса несут MobileDeviceCommandBarContent с одним ПУСТЫМ item
(<xr:Value xsi:type="xs:string"/>, не имя). Декомпилятор захватывал
mobileCommandBarContent: [""], но компилятор не эмитил блок:

(1) PS-ловушка: гейт `if ($def.mobileCommandBarContent -and ...)` — одноэлементный
массив @("") в boolean-контексте разворачивается в "" → falsy → блок пропущен.
Фикс: $null-проверка вместо truthy ($null -ne ... -and Count -gt 0).
(2) Пустое значение → самозакрывающийся <xr:Value xsi:type="xs:string"/> (зеркало платформы).

Python не имел unwrap-ловушки ([""] truthy), но self-closing добавлен для байт-паритета
(+ is not None гейт для единообразия).

Выборка 9 форм (РасширенныйВводКонтактнойИнформации, ХранилищеВариантовОтчетов×3,
ФормаНастроекОтчета, ИнтерфейсДокументовЭДО, ПользовательскиеМакетыПечати, …):
match 0→9, TOTAL→0. Регресс 43/43 (ps1+py). Блок именованных значений (148 форм) уже
был сертифицирован; пустой — тот же блок с пустым значением (формат платформы).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:12:51 +03:00
Nick Shirokov a41a6d822b fix(form-decompile): whitespace-заголовок колонки реквизита (Get-LangTextWS)
Продолжение систематической чистки whitespace-ML: колонка реквизита (ValueTable,
Decompile-AttrColumn) с whitespace-only <Title> (<v8:content> </v8:content>) теряла
пробел через Get-LangText → "" → компилятор эмитил пустой <Title/>. Тот же фикс
Get-LangText → Get-LangTextWS (декомпилятор-only, безопасный суперсет — пробел
восстанавливается только когда контент-узел есть, но пуст).

Корпус 8.3.24: 4 whitespace-заголовка колонок в 4 формах. Выборка 4 формы: match 0→4,
TOTAL→0. Регресс не затронут (декомпилятор-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:07:52 +03:00
Nick Shirokov 8631026259 fix(form-decompile,form-compile): whitespace-presentation choiceList + SpecialTextInputMode (кластер RadioButtonField>Presentation)
(1) Presentation элемента <ChoiceList> (переключатель / поле ввода) с whitespace-only
контентом (<v8:content> </v8:content>, пробел) терялась: Decompile-ChoiceList читал
presentation через Get-LangText → "" (суппресс-маркер) → компилятор эмитил пустой
<Presentation/>. Фикс: Get-LangText → Get-LangTextWS (тот же корень, что был у
Attribute>Title; декомпилятор-only — Emit-ChoicePresentation " " уже умеет, IsNullOrEmpty
пропускает пробел в контент-ветку). Кластер снова раздут harness-мис-атрибуцией:
одна реальная whitespace-потеря на форму, generic строки-обёртки (<Presentation>/
<v8:item>/<v8:lang>ru) сыпались ложным LOST под соседними элементами.

(2) SpecialTextInputMode (Email/PhoneNumber — моб. спец-режим ввода input) → GENERIC_SCALARS
(компилятор ps1+py, декомпилятор ps1), листовой enum pass-through.

Выборка 3 формы (Новости/ФормаПросмотраНовостейРабочийСтол, МастерПереходаВОблако,
ПеремещениеОС/ФормаДокумента): match 0→3, TOTAL→0. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 12:02:12 +03:00
Nick Shirokov e127dfcf3d fix(form-decompile): whitespace-заголовок реквизита (Get-LangTextWS) — кластер Attribute>Title
Реквизит формы с whitespace-only <Title> (<v8:content> </v8:content>, одиночный
пробел) терялся: декомпилятор читал заголовок через Get-LangText, а PreserveWhitespace=false
стрипал пробел → "" (= суппресс-маркер «нет заголовка») → компилятор не эмитил Title.

Фикс: Get-LangText → Get-LangTextWS (существующий хелпер, восстанавливает значимый
пробел — как уже сделано для UsualGroup Title/ToolTip). Декомпилятор-only: компилятор
" "-заголовок уже умеет (прецедент групп). Для непустого/мультиязык-контента поведение
не меняется (Get-LangTextWS == Get-LangText).

Корпус 8.3.24: 13 whitespace-заголовков реквизита в 10 формах. Кластер Attribute>Title
(impact 42) был раздут harness-мис-атрибуцией: одна реальная потеря на форму, а
generic строки-обёртки (<Title>/<v8:item>/<v8:lang>ru) сыпались ложным LOST под
соседними реквизитами. Выборка 5 форм: match 0→5, TOTAL→0. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:55:06 +03:00
Nick Shirokov ed2339a4bc feat(form-compile): значение v8:Type «Неопределено» — локальный xmlns на теге (фильтр + параметр дин-списка)
Значение типа v8:Type (на практике всегда <prefix>:Undefined — тип «Неопределено»
из namespace http://v8.1c.ru/8.2/data/types, префикс авто d6p1/d8p1/dN…) эмитилось
без объявления namespace → битый QName; а в параметре дин-списка компилятор вообще
ронял v8:Type → xs:string.

Корпус 8.3.24: 11 тегов (6 <dcsset:right> фильтра + 5 <dcssch:value> параметра),
значение всегда prefix:Undefined, ns всегда data/types. Топ ROOT-пробел нового
baseline (Attribute>value 48 LOST + 44 ADDED).

Фикс: хелпер Get-ValueTypeNsAttr / _value_type_ns_attr (объявляет xmlns:<pref> для
не-стандартного префикса при valueType v8:Type) в обе ветки Emit-FilterItem
(скаляр + массив op `in`) + новая ветка v8:Type в Emit-DLValue / emit_dl_value.

Выборка 7 форм (Взаимодействия acc/erp, ЖурналОпераций×3, ДокументЭДОБЗК, ЧекиККМ):
match 0→6, TOTAL→0. Зеркало py байт-в-байт, регресс 43/43 (ps1+py). Раундтрип
восстанавливает точные исходные байты платформы (её собственный формат — cert не нужен).
Spec обновлён (раздел filter).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 11:39:30 +03:00
Nick Shirokov 71ecec6594 fix(form-decompile): ссылки на члена формы по id (N:uuid) — игнорировать с предупреждением, не verbatim
groupList/customSettingsFolder/userSettingsGroup/commandSource в форме "N:uuid" — ссылка на
член формы по id. Наш компилятор переназначает id → verbatim указал бы НЕ ТУДА (тихая порча).
Резолв N→имя ненадёжен: N не всегда соответствует named-элементу (форма ДенежныеДокументы:
GroupList=2:uuid, но элемента id=2 НЕТ; в конфигураторе список пустой — платформа сама не
разрешает эту dangling-ссылку; uuid константный для всех форм). «Страна id=2» в бэклоге —
совпадение.

Решение: декомпилятор захватывает только ИМЯ-форму; "N:uuid" опускает с предупреждением
(stderr) — задаётся вручную через form-edit. Результат идентичен (dangling → пустой список),
но без мусорной ссылки. Имя-форма (GroupList 8, CSF 18, UserSettingsGroup 3127, CommandSource
5116 в корпусе) round-трипится как есть.

Гард `^\d+:[0-9a-fA-F]{8}-` в 4 точках захвата. Harness стрипает "N:uuid"-форму (намеренное
непокрытие). Выборка 118 форм: match 113/118, ref-тег потерь 0, регрессий 0. Отменяет
verbatim-захват CustomSettingsFolder из dd32d2a6 для id-формы (имя остаётся).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 23:08:12 +03:00
Nick Shirokov dd32d2a6ca feat(form-decompile): CustomSettingsFolder — группа пользовательских настроек компоновщика
Форменное свойство формы отчёта со СКД <CustomSettingsFolder> — имя группы, куда
генерируются пользовательские настройки компоновщика (1С: «Группа пользовательских
настроек»). Декомпилятор не ловил → терялось (23 формы, напр. ИсторияРазмераПриложения).

Декомпилятор-only: +CustomSettingsFolder в KNOWN_FORM_PROPS. Компилятор уже эмитит
(emit_properties авто-PascalCase). Значение: имя группы (18) или N:<GUID> ссылка по id (5,
verbatim — как уже принятый GroupList). Ключ customSettingsFolder.

Выборка 23 формы: match 23/23, CustomSettingsFolder-потерь 0. Валидация раундтрипом
(decompiler-only). Регресс не затронут (только новый захват).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:30:15 +03:00
Nick Shirokov 227423ee1f feat(form-decompile,form-compile): KeyType/KeyField набора динамического списка (запросный список)
Запросный динамический список (без MainTable) задаёт ключ набора: <KeyType>
(FieldValue/RowKey/RowNumber) + <KeyField>* (0+ полей) — после Parameter*, до MainTable.
Декомпилятор не ловил → терялось (21 форма, напр. ВыборПрисоединенногоФайла).

DSL: settings.keyType (строка-enum) + settings.keyFields (массив). Взаимоисключающи с
mainTable (запросный список vs таблично-ориентированный — 1С: KeyField+MainTable ломает
пути данных списка). Декомпилятор: захват KeyType + всех KeyField; компилятор (ps1+py):
эмит после Emit-DLParameters, до MainTable (позиция из корпус-сигнатур).

Выборка 22 формы: match 17/22, KeyType/KeyField-потерь 0 (остаток — др. кластеры:
CheckBox ItemWidth, order-use, SearchControlAddition, empty-right), регрессий 0.
Регресс 43/43, ps1==py. Cert: corpus round-trip (запросные списки — 22 shipped-формы
грузятся; синтетический кейс = полный query-based список с Table, непропорционально
для verbatim 2-тег; MainTable+KeyField несовместимы → к dynamic-list-form не добавить).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:25:19 +03:00
Nick Shirokov c0487c51b7 feat(form-decompile,form-compile): Button Parameter (параметр команды кнопки)
Кнопка команды может нести <Parameter> (после CommandName) — параметр команды:
- xr:MDObjectRef (20 в корпусе 8.3.24): ссылка на объект метаданных, напр.
  DocumentJournal.Взаимодействия (команда ShowInList «Показать в списке»);
- v8:TypeDescription (16): описание типа <v8:Type>cfg:DocumentRef.X</v8:Type>
  (команда CreateByParameter «Создать по параметру»).
Декомпилятор не ловил → терялось (форма ЭлектронноеПисьмоИсходящее и др.).

DSL: ключ button.parameter (синоним «параметр»), дизамбигуация по форме значения —
строка → MDObjectRef (verbatim), объект {type} → TypeDescription (грамматика типа,
переиспользует Emit-Type с tag=Parameter). Декомпилятор: MDObjectRef → строка,
TypeDescription → {type} (Decompile-Type). Позиция: после CommandName.

Выборка 16 форм с Button Parameter: match 16/16, 0 потерь (оба вида). Кейс commands
(+кнопка с параметр:{type:CatalogRef} через рус-синоним) сертифицирован загрузкой в 1С —
позиция Parameter и синоним подтверждены. MDObjectRef-вариант: та же позиция эмиссии +
corpus round-trip (ShowInList требует list-контекст, синтетически не воспроизвести).
Регресс 43/43, ps1==py. parameter в knownKeys allowlist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:07:41 +03:00
Nick Shirokov 934462f4d2 fix(form-compile): Save Field — не префиксовать ссылку вида N/M без двоеточия
Поле <Save><Field> вида N/M (ссылка на элемент/колонку, напр. 5/0, 5/1) без двоеточия
получало лишний префикс имени реквизита (ДатаОкончанияПериода.5/0) — условие «оставить
как есть» ловило только N/M: с двоеточием (^\d+/\d+:). Форма ОтражениеДокументовВМеждународномУчете.

Фикс: ^\d+/\d+: → ^\d+/\d+ (bare N/M тоже как есть). Корпус 8.3.24: N/M bare 10, N/M: 746
(уже обрабатывался). Декомпилятор симметричен (strip префикса имя. иначе как есть) — не менялся.
Форма → match. Снэпшоты не затронуты (N/M в кейсах не встречается), регресс 43/43 (ps1+py).
Валидация раундтрипом shipped-формы (грузится в 1С; N/M — платформенная ссылка, эмитим verbatim).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:45:49 +03:00
Nick Shirokov 092f30a663 fix(form-decompile): точное число пробелов в whitespace-only <v8:content> декораций-распорок
LabelDecoration-распорки/отступы несут whitespace-only Title (<v8:content>   </v8:content>,
N пробелов) — у части (10/23 в корпусе 8.3.24) нет Width/stretch, и число пробелов = реальная
ширина выравнивания (не рудимент). PreserveWhitespace=false стрипал whitespace-only content в ""
→ Get-LangTextWS восстанавливал ОДИН пробел → терялось число (оригинал 3 пробела, regen 1).

Фикс (декомпилятор-only): второй XmlDocument с PreserveWhitespace=true (основной парс не трогаем,
нулевой риск); Resolve-WS навигацией по индекс-пути элементов (структура обоих документов идентична)
достаёт точную строку пробелов; Get-LangTextWS восстанавливает её вместо одиночного пробела.
Компилятор не менялся — эмитит content verbatim (esc_xml пробелы не трогает).

Выборка 34 формы с multi-whitespace content: LabelDecoration-потерь 0, match 30/34 (остаток —
др. контексты <v8:content> под Attribute + несвязанные кластеры), регрессий 0. Валидация
раундтрипом (decompiler-only, кейс не нужен).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:39:42 +03:00
Nick Shirokov 03b2b5a64e feat(form-decompile,form-compile): ToolTip companion ExtendedTooltip (подсказка расширенной подсказки)
Companion <ExtendedTooltip> (это LabelDecoration) может нести собственный <ToolTip> —
реальный текст подсказки (ML), а не пустой Title. Декомпилятор ловил Title/layout/flags/
events компаньона, но НЕ его ToolTip → реальный двуязычный текст молча терялся (форма
ВводОстатков/ФормаТовары: расширенная подсказка ЕдиницаИзмеренияТНВЭД).

DSL: extendedTooltip.tooltip (ML-текст). Декомпилятор: захват <lf:ToolTip> компаньона
(Get-LangText). Компилятор (ps1+py): tooltip в companionStructKeys + эмит <ToolTip> после
Title (порядок схемы LabelDecoration). ≠ элементного tooltip обычной подсказки —
скоупится вложенностью (могут сосуществовать).

Редкое (1 форма в rt-iter), но реальная потеря контента. Форма ВводОстатков → match.
Кейс input-fields (ОбычноеПоле: элементный tooltip + extendedTooltip с text+tooltip+events)
сертифицирован загрузкой в 1С — оба tooltip сосуществуют. Регресс 43/43, ps1==py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:26:15 +03:00
Nick Shirokov abcd5be2b0 feat(form-decompile,form-compile): presentation элемента CA/фильтра — сохранение формы xs:string vs LocalStringType
Топ-кластер нового baseline (~190 impact). <dcsset:presentation> элемента условного
оформления и групп/сравнений фильтра: платформа хранит ru-only текст и как xs:string
(плоский), и как LocalStringType (мультиязык-обёртка с одним ru). Декомпилятор схлопывал
ru-only LocalStringType в строку (Get-MLText) → компилятор писал xs:string → mismatch.
Плюс компилятор-баг: filter-item presentation эмитился через Emit-MLText (всегда мультиязык
БЕЗ xsi:type), даже для плоской строки.

Фикс:
- Декомпилятор: Get-PresByType — ветвь по xsi:type, сохраняет {lang:text} объект для
  LocalStringType (даже один ru) vs плоскую строку для xs:string. Применён к presentation
  элемента CA (Build-ConditionalAppearance) и фильтра (group + comparison, Build-FilterItem).
- Компилятор (ps1+py): filter-item presentation через by-form Emit-USPresentation/
  emit_us_presentation (строка→xs:string, объект→LocalStringType с xsi:type). CA-item
  presentation компилятор уже эмитил by-form — не трогаем.

Выборка 45 форм с LocalStringType-presentation: presentation-потерь 0, match 27→33,
TOTAL 127→63, регрессий 0 (сверка с baseline). Кейс dynamic-list-form (+CA presentation
{ru} ru-only + filter presentation объект/строка) сертифицирован загрузкой в 1С. Регресс
43/43, ps1==py (общий снэпшот на обоих рантаймах).

baseline после кластера ListSettings/DataSet (A+B+C): match 1869→1975, TOTAL 3495→2557.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:55:00 +03:00
Nick Shirokov aa39973ae6 feat(form-decompile,form-compile): поле DataSet динсписка — useRestriction/attributeUseRestriction/inputParameters (остаток кластера C)
Обычное поле набора <Field DataSetFieldField> может нести ограничения использования и
связь по параметрам выбора:
- <dcssch:useRestriction> {field?,condition?,group?,order?} (54 формы) — где поле НЕ
  использовать (отбор/группировка/порядок/как поле);
- <dcssch:attributeUseRestriction> та же структура (18) — ограничения для реквизитов поля;
- <dcssch:inputParameters> (6) — связь по параметрам выбора (как у параметра дин-списка).

DSL: settings.fields[].useRestriction / attributeUseRestriction (объект {field,condition,
group,order} bool | флаг-строка "#noField #noFilter #noGroup #noOrder" | массив) +
inputParameters. Общие хелперы Get-RestrictList/Emit-RestrictBlock (ps1) и parse_restrict/
emit_restrict_block (py); inputParameters переиспользует Emit-DLInputParameters. Декомпилятор
Build-RestrictObj + Build-DLInputParameters.

Порядок детей поля (из корпус-сигнатур, подтверждён загрузкой в 1С): dataPath, field,
title, useRestriction, attributeUseRestriction, presentationExpression, valueType,
appearance, inputParameters.

Выборка 69 форм с field-props: field-property потерь 0 (match 36→39, TOTAL 396→150,
cascade LOST 111→12). Кейс dynamic-list-form (+useRestriction/attributeUseRestriction
на поле Code) сертифицирован загрузкой в 1С. Регресс 43/43, ps1==py байт-в-байт.

Кластер C (DataSet динсписка) закрыт: Field valueType + CalculatedField + field
presentationExpression/appearance + field useRestriction/attributeUseRestriction/
inputParameters. Остаток (BACKLOG): нюансы пустого value (xs:string vs nil;
LocalStringType self-closing) + отдельный InputField multiple-value кластер.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 20:15:32 +03:00
Nick Shirokov dab122c166 feat(form-decompile,form-compile): свойства поля DataSet динсписка — presentationExpression + appearance
Обычное поле набора <Field DataSetFieldField> может нести:
- <dcssch:presentationExpression> (выражение представления поля, 30 форм) — строка;
- <dcssch:appearance> (формат/оформление поля, ~9 форм) — dcscor:item SettingsParameterValue
  (тот же блок, что в условном оформлении: параметр→значение с типизацией).

DSL: settings.fields[].presentationExpression (строка) + fields[].appearance (объект
{параметр:значение}). Декомпилятор: захват presentationExpression + appearance через
существующий Get-SettingsAppearance. Компилятор (ps1+py): presentationExpression перед
valueType, appearance после valueType (порядок исходника, подтверждён корпус-сигнатурами);
appearance переиспользует Emit-AppearanceValue/emit_appearance_value.

Выборка 36 форм с field pres/appearance: match 33/36, 0 потерь pres/appearance (остаток
3 формы — несвязанные нюансы пустого value параметра / пустого LocalStringType). Кейс
dynamic-list-form (+явное поле Code с presentationExpression+appearance Формат/ЦветТекста)
сертифицирован загрузкой в 1С. Регресс 43/43, ps1==py (общий снэпшот на обоих рантаймах).

Остаток field-свойств (BACKLOG): useRestriction/attributeUseRestriction/inputParameters/
order на обычном <Field> + 2 раскрытых нюанса (пустой xs:string value vs nil; пустой
LocalStringType self-closing vs пара).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:52:19 +03:00
Nick Shirokov 73b07a6fbc feat(form-decompile,form-compile): вычисляемые поля + valueType поля DataSet динсписка (кластер C)
Две части пробела DataSet динамического списка:

1. Field valueType (63 формы/54). Поле набора <Field DataSetFieldField> может нести
   <dcssch:valueType> (тип значения; кастомные/вычисляемые поля). Декомпилятор ловил
   field/dataPath/title/nested, теперь и valueType (переиспользует Decompile-Type);
   компилятор эмитит после title через существующий emit_dl_value_type. 0 потерь на выборке.

2. CalculatedField (6 форм, редкое). Новый ключ settings.calculatedFields — зеркало skd:
   shorthand "Имя [Заголовок]: тип = Выражение #noField #noFilter #noGroup #noOrder"
   (порт Parse-CalcShorthand) или объект. Форм-специфика: dcssch:-теги, presentationExpression,
   orderExpression* (структура {expression,orderType,autoOrder} в namespace dcscommon с
   локальным xmlns), useRestriction{field,condition,group,order}. Эмиттер форм-специфичный
   (skd использует dcscom:-префикс и не имеет pres/orderExpression). Позиция в DataSet —
   после Field*, до Parameter*. Декомпилятор Build-CalcField (объектная форма для точного
   round-trip). Выборка calc-форм: calc-теги ушли из диффов (3/6 match, остаток — отдельный
   field appearance/presentationExpression).

Кейс dynamic-list-form (+grouping +calculatedFields: shorthand с флагами + объект с
presentationExpression/orderExpression/valueType) сертифицирован загрузкой в 1С (порядок
детей CalculatedField подтверждён платформой). Регресс 43/43 (ps1+py).

ps1==py байт-в-байт (сверено на кейсе). Фикс по пути: ps1 Parse-CalcShorthand — `\b` в
generator-heredoc превратился в backspace 0x08 → #-флаги не парсились (py был верен);
поймано прямой сверкой вывода ps1 vs py.

C-остаток (в BACKLOG): свойства обычного <Field> — presentationExpression + appearance
(формат/цвет поля) — отдельный подкластер.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:38:14 +03:00
Nick Shirokov 43d36119cb feat(form-decompile,form-compile): группировка строк динамического списка (StructureItemGroup в ListSettings, кластер B)
Структура группировок дин-списка (`<dcsset:item StructureItemGroup>` → groupItems →
GroupItemField, вложенность через дочерний item) — переиспользована модель/реализация
из skd (Emit-GroupItems/Get-GroupFields), но плоская: группировка списка всегда
линейная цепочка одно-польных уровней над неявными деталями (без children/selection/
order/details — корпус подтверждает).

DSL — новый ключ `settings.grouping` (forgiving-синонимы `structure`/`группировка`):
- шорткат "A > B > C" (вложенные уровни, внешний→внутренний) или массив;
- элемент уровня — строка (имя поля) или объект {field, groupType?, periodAdditionType?,
  periodAdditionBegin?, periodAdditionEnd?} для нестандартного поля (ключи = теги
  исходника; periodAddition с авто-детектом ISO-дата/dcscor:Field).
Корпус 8.3.24 (29 форм/34 уровня): groupType Items 33 / Hierarchy 1, periodAddition нет.

Компилятор (ps1+py): Emit-ListGrouping + рекурсивная цепочка StructureItemGroup в
позиции после conditionalAppearance, до itemsViewMode. Оба пути — shape-дескриптор
(round-trip) и канонический (авторинг). Декомпилятор: Build-ListGrouping (линейная
цепочка; bail→$null на ветвлении/мультиполе/доп.содержимом = честный LOST, не порча);
Get-ListSettingsShape распознаёт `item`→`structure` (раньше → $null/канон-fallback,
из-за чего терялась группировка и додумывался itemsUserSettingID).

Выборка 17 форм с группировкой: match 0→10, TOTAL 75→25 (остаток — др. кластеры:
presentation xs:string, order-item use). Широкая (cat-a 102): match 82→84, TOTAL
280→256, ноль регрессий. Кейс dynamic-list-form (+grouping "Description > Code")
сертифицирован загрузкой в 1С. Регресс 43/43 (ps1+py).

Фикс по пути: PS-ловушка — одноэлементный массив разворачивался при return из
Parse-ListGrouping → строка → индексация давала char → пустой <field>. Unary comma ,@().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:09:48 +03:00
Nick Shirokov 081d3a8a2f feat(form-decompile,form-compile): динсписок DataSet/ListSettings — TypeId-verbatim + DataSetFieldNestedDataSet + ListSettings self-closing (механика кластера A)
Три механических фикса доминирующего кластера встроенных DCS-настроек динсписка
(152/690 дифф-форм rt-iter). Таргет-выборка 102 формы: TOTAL 657→280, match 0→82,
ноль регрессий.

1. TypeId-verbatim. Тип параметра/реквизита, заданный глобальным стабильным GUID
   (<v8:TypeId>, не <v8:Type>) — платформа так сериализует типы, чьё имя в контексте
   недоступно (определяемые/характеристики). Декомпилятор не ловил → параметр терял
   valueType. Маркер 'typeid:<GUID>' в грамматике типа: Decompile-Type ловит <v8:TypeId>,
   Emit-SingleType разворачивает обратно (как роль-по-GUID; GUID глобально стабилен →
   безопасно). Форма ОстаткиПартийСАТУРН/ФормаОстатков → match.

2. DataSetFieldNestedDataSet. Компилятор хардкодил xsi:type="DataSetFieldField" для
   всех полей набора → терял поле-вложенный набор (реквизит табличной части объекта).
   Маркер fields[].nested: декомпилятор ловит ...NestedDataSet, компилятор зеркалит.

3. ListSettings self-closing. Пустой дескриптор listSettings:{} эмитился парой
   <ListSettings></ListSettings>, оригинал — self-closing <ListSettings/> (70 форм
   корпуса). Зеркалим self-closing при пустом эмите (отслеживание буфера).

Остаток кластера (вне A, отдельные задачи): структура группировок списка
(dcsset:item StructureItemGroup), KeyField/CalculatedField/Field-valueType DataSet.

spec: fields.nested, listSettings:{} → self-closing, тип-токен typeid:<GUID>
(раундтрип, не для авторинга). Регресс form-compile 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 17:16:43 +03:00
Nick Shirokov c383cc4ffe feat(form-decompile,form-compile): батч скаляров — форм. ChildItemsWidth/VerticalAlign/HorizontalSpacing + check/radio EqualItemsWidth/ItemTitleHeight
Раундтрип терял несколько pass-through скаляров:
- форм-уровень <ChildItemsWidth> (36 форм), <VerticalAlign> (26), <HorizontalSpacing> (25) —
  обрабатывались только как элементные генерики, не на форм-уровне;
- <EqualItemsWidth> (check/radio, 28: false 23/true 5) и <ItemTitleHeight> (radio) — чистый
  двусторонний пробел (не ловились вовсе).

decompile: форм-уровневые → KNOWN_FORM_PROPS; элементные → GENERIC_SCALARS (зеркало компилятора).
compile (ps1+py): форм-уровневые через generic Emit-Properties (авто-PascalCase); элементные —
в genericScalars (Emit-Layout→Emit-GenericScalars, покрывает check/radio).

Верификация: таргет-раундтрип 129 форм с этими тегами → 5 категорий закрыты (0 LOST; остаток —
др. категории Button>Parameter/valueType/content). Регресс form-compile 43/43 (ps1+py); 1С-cert
groups (форм-уровень) + radio-auto-enum (element). spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 16:21:59 +03:00
Nick Shirokov 01e5de8acf feat(form-decompile,form-compile): userSettingPresentation — плоская строка xs:string vs мультиязычный LocalStringType
Раундтрип ломал кастомную подпись пользовательской настройки в элементах настроек компоновки
(filter/order/conditionalAppearance/dataParameters): <dcsset:userSettingPresentation xsi:type="xs:string">
эмитился как мультиязычный <v8:item> блок (без нужного xsi:type) → 182 строки diff на 13 формах.

Корпус (acc+erp 8.3.24): 26 xs:string (плоская строка) vs 7 v8:LocalStringType (мультиязычный).
Компилятор всегда звал Emit-MLText (мультиязычная форма без xsi:type) — ломал ОБА случая.

compile (ps1+py): выделенный Emit-USPresentation/emit_us_presentation — строка → xsi:type="xs:string",
объект {ru,en} → xsi:type="v8:LocalStringType". Заменены 4 call-site (filter item/CA/dataParameters).
decompile: Get-PresText (строка ИЛИ объект) уже стоял в filter/group/order; добавлен в dataParameters
(был Get-MLText, ронял xs:string).

Верификация: таргет-раундтрип 13 форм с xs:string-подписью → match (182→0); регресс form-compile
43/43 (ps1+py); 1С-cert dynamic-list-form (оба типа подписи — xs:string и LocalStringType — грузятся). spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 15:42:27 +03:00
Nick Shirokov 8542b45719 fix(form-compile): FixedArray для одноэлементного значения параметра выбора (PS unwrap)
Раундтрип терял FixedArray у choiceParameter со значением-списком из ОДНОГО элемента
(напр. ВводОстатковВнеоборотныхАктивов/ФормаРедактированияСтрокиНМА: 9 из 10 FixedArray
эмитились как скаляр → 45 строк LOST). Корень — классический PowerShell unwrap: Get-ElProp
возвращает 1-элементный массив, но PS разворачивает его на RETURN функции (и при биндинге
параметра), так что $isArray=false → значение эмитилось как одиночный <Value> вместо
<Value xsi:type="v8:FixedArray"> с одним <v8:Value>.

Фикс (только ps1): в Emit-ChoiceParameters значение читается ПРЯМЫМ member/индексер-доступом
(не через Get-ElProp — его return разворачивает), массив-ность вычисляется до биндинга и
передаётся в Emit-ChoiceParamValue явным флагом -isArray (foreach по развёрнутому скаляру = 1
итерация → корректный 1-элементный FixedArray). PY не затронут (Python не разворачивает списки).

Системный артефакт: во многом раздувал кластер app:item>Value в раундтрипе (PS-харнесс).
Верификация: таргет-форма → match (45→0; FixedArray 1→10); регресс form-compile 43/43 (ps1+py);
1С-cert input-fields (1-элементный массив-choiceParameter → FixedArray, грузится).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 15:26:55 +03:00
Nick Shirokov 819b7fa126 feat(form-decompile,form-compile): dataParameters динсписка + nil-значение схема-параметра (valueListAllowed)
Раундтрип терял две вещи в настройках динамического списка реквизита (форма ВыборПодписантовПечатныхФорм):
1. <dcsset:dataParameters> — значения параметров запроса в ListSettings (список SettingsParameterValue
   {use, parameter, value?}). Не захватывались/не эмитились вовсе.
2. <dcssch:value xsi:nil/> у схема-параметра при valueListAllowed=true: компилятор по умолчанию его
   пропускал (if valueListAllowed → return), но платформа пишет не всегда (корпус 27 с / 47 без).

dataParameters: грамматика портирована из skd-compile (Emit-DataParameters + Parse-DataParamShorthand +
Test/Emit-EmptyValue) — консистентно с СКД (shorthand "Имя @off" / объект). Form-нюанс: значение
опционально (use=false плейсхолдер без value-узла, в отличие от skd-settings). compile эмитит после
filter (XSD-порядок). decompile: Build-FormDataParameters (объект с полным valueType / shorthand).

nil-значение: декомпилятор ставит явный маркер value:null при valueListAllowed+nil-тег; компилятор
эмитит nil при valueListAllowed + явный value (различает absent от null через Has-DLProp/value_explicit).

Корпус: dataParameters в 9 формах (17 items, все use=false, 4 со значениями DesignTimeValue/ent:/decimal).
Верификация: таргет-раундтрип формы → match (22→0); 9 dataParameters-форм — категория закрыта (остаток —
др. категории filter userSettingPresentation/MultipleValue*). Регресс form-compile 43/43 (ps1+py,
PY-паритет снэпшота); 1С-cert dynamic-list-form (vla-nil + dataParameters грузятся). spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 15:00:19 +03:00
Nick Shirokov b794560492 feat(form-decompile,form-compile): оператор фильтра Like (подобно) + рус. синоним в shorthand
Раундтрип ломал отбор с comparisonType=Like: декомпилятор выдавал сырой токен Like в
short-form ("Поле Like %x%"), а парсер компилятора его не знал → весь текст уходил в поле,
op сбрасывался в Equal, значение терялось (напр. РегламентированноеУведомление.../ФормаСвДобытВалют:
"КодВалют Like %/ %" → поле="КодВалют Like %/ %", потеря Like + %/ %).

Корпус (acc+erp 8.3.24): из 15 comparisonType недоставал только Like (8 шт.) — добавлен Like/NotLike.
По просьбе — рус. синоним оператора: подобно/неподобно (forgiving-ввод, как ПОДОБНО в конфигураторе).

decompile (filterOpMap): Like→like, NotLike→notLike (каноничный токен short-form).
compile (ps1+py): comparisonTypes + Parse-FilterShorthand opPatterns += like/notLike + подобно/неподобно.
PY доведён до регистронезависимости PS (re.IGNORECASE на op-парсинге + CI-лукап comparisonType),
чтобы Like/LIKE/ПОДОБНО резолвились одинаково в обоих портах.

Верификация: таргет-раундтрип 4 форм с Like → match (было 10→0); регресс form-compile 43/43
(ps1+py); 1С-cert dynamic-list-form (фильтры like и подобно → <comparisonType>Like, грузятся). spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:28:57 +03:00
Nick Shirokov 3340d48898 feat(form-decompile,form-compile): формат динамического заголовка группы/страницы (Format при titleDataPath)
Раундтрип терял <Format> на UsualGroup/Page — формат значения пути к данным заголовка
(<TitleDataPath>): мультиязычный формат вида БЛ=; БИ=* / BF=; BT=* (напр. УчетныеЗаписиЭДО/
УчетнаяЗапись — 4 группы/страницы, 40 строк diff с каскадом структурных v8:item-обёрток).

Корпус (acc+erp 8.3.24): <Format>-блоки — LabelField 1784, InputField 1480 (уже обрабатывались),
UsualGroup 13, Page 10 (пробел). Механизм формата уже был у полей (Add-FormatProps / Emit-MLText);
подключён к группе/странице.

decompile: Add-FormatProps в ветках UsualGroup/Page (захват format/editFormat).
compile (ps1+py): Emit-MLText <Format>/<EditFormat> в Emit-Group/Emit-Page (рядом с titleDataPath).

Верификация: таргет-раундтрип 11 форм с group/page Format → категория закрыта (0 Format LOST;
целевая форма match, было 40→0); остаток — др. категория ListSettings + 1 ring3 (SpreadsheetDocument).
Регресс form-compile 43/43 (ps1+py); 1С-cert кейса groups (группа с Format+TitleDataPath грузится). spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:17:26 +03:00
Nick Shirokov cb3dda0d53 feat(form-decompile,form-compile): ролевой доступ колонки реквизита (View/Edit xr-флаг)
Раундтрип терял <View>/<Edit> на колонках реквизита (колонка ValueTable/ValueTree с <Type>):
ролевой доступ вида <Edit><xr:Common>false</xr:Common><xr:Value name="Role.X">true</xr:Value></Edit>
(напр. НастройкиУчетаЗарплаты/ФормаДополнительныхДанных — колонки РайонныйКоэффициент/Ссылка).
Механизм xr-флага уже был у самого реквизита (View/Edit) и userVisible — у колонок не подключён.

decompile (Decompile-AttrColumn): захват view/edit через Decompile-XrFlag (bool | {common,roles}).
compile (Emit-AttrColumn, ps1+py): эмиссия <View>/<Edit> через Emit-XrFlag после FunctionalOptions.
(Колонки идут своим путём Decompile-AttrColumn, не через GENERIC_SCALARS — коллизии ключа edit нет.)

Корпус (acc+erp 8.3.24): из 282591 колонок — 14 с <View>, 21 с <Edit> (редко, но реально).
Верификация: таргет-раундтрип 72 форм с колоночными View/Edit → категория закрыта (0 LOST;
остаток — другие категории content/right). Регресс form-compile 43/43 (ps1+py); 1С-cert кейса
table (колоночные View common-only и Edit с ролью грузятся в платформу). spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 14:06:19 +03:00
Nick Shirokov e007233c91 feat(form-decompile,form-compile): Table refreshRequest + форм. collapseItemsByImportanceVariant/groupList (pass-through)
Раундтрип терял три скаляра (чистые двусторонние пробелы — не ловились ни декомпилятором,
ни компилятором):
- Table <RefreshRequest> (запрос обновления дин-списка; корпус acc+erp 8.3.24 — 33, всегда PullFromTop)
- форм-уровень <CollapseItemsByImportanceVariant> (сворачивание по важности; 27: DontUse 26/Use 1)
- форм-уровень <GroupList> (ссылка на группу списка, значение 2:<GUID>/имя; 30)

Все три — pass-through (захват «как есть», зеркало платформы). RefreshRequest — в Emit-Table
рядом с currentRowUse; форм-уровневые — через KNOWN_FORM_PROPS (decompile) + generic Emit-Properties
(авто-PascalCase, compile ps1+py).

Верификация: таргет-раундтрип 68 форм с этими тегами → три категории закрыты (0 LOST; остаток —
другие категории valueType/HorizontalSpacing). Регресс form-compile 43/43 (ps1+py); 1С-cert кейса
dynamic-list-form (все три тега, включая GroupList с GUID, грузятся в платформу). spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:43:55 +03:00
Nick Shirokov 569234b448 fix(form-compile): cfg-префикс для голых конфигурационных типов ConstantsSet/ReportObject
Раундтрип терял префикс: оригинал <v8:Type>cfg:ConstantsSet</v8:Type>, regen — голый
<v8:Type>ConstantsSet</v8:Type> (напр. ПанельАдминистрированияБП/НастройкиРегистровУчета,
реквизит НаборКонстант). cfg:-regex компилятора требует точку (ConstantsSet.X), а голая
форма без .Имя уходила в default без префикса.

Корпус (acc+erp 8.3.24) — голые cfg-типы (без точки): DynamicList 5205 (уже обрабатывался),
ConstantsSet 103, ReportObject 10. Блок DynamicList расширен на все три. Дотированные формы
ConstantsSet.X/ReportObject.X по-прежнему ловит общий cfg:-regex. Декомпилятор не трогали —
он уже отдаёт голую форму (Decompile-Type снимает cfg:).

compile (ps1+py). Верификация: таргет-раундтрип формы → match (было 2 → 0); регресс
form-compile 43/43 (ps1+py); 1С-cert кейса attributes-types (реквизиты ConstantsSet/ReportObject
грузятся в платформу). spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 13:04:54 +03:00
Nick Shirokov d0361561ca fix(form-decompile): восстановление значимого пробела в мультиязычном заголовке (декорация-разделитель)
PreserveWhitespace=false стрипает <v8:content> </v8:content> → "" (неотличимо от суппресса).
Хелпер Get-LangTextWS восстанавливал пробел только для одиночной (ru-only) строки; для
мультиязычной мапы {ru:" ", en:" "} (напр. БольничныйЛист: LabelDecoration «Пробел1» с ru+en
пробелами) оба значения терялись → regen давал <v8:content/> вместо <v8:content> </v8:content>.

Get-LangTextWS расширен на map-случай (восстанавливает " " в каждом языке, где content-узел
есть, но текст пуст — платформа не эмитит пустой content). Get-MLFormattedValue (заголовок
LabelDecoration) зарефакторен на переиспользование Get-LangTextWS вместо собственного inline
single-string восстановления. Компилятор (Emit-MLItems) пробел уже эмитит корректно — правка
только в декомпиляторе.

Версия 1.0x → 0.103 (декомпилятор — draft, продолжаем 0.xx-схему).
Верификация: таргет-роундтрип формы → match (было 4 → 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:57:35 +03:00
Nick Shirokov 75b77caf9b feat(form-decompile,form-compile): InputField availableTypes + typeDomainEnabled (ограничение доступных типов)
Раундтрип терял блок ограничения типов у поля ввода на составном/характеристика-типе:
<TypeDomainEnabled> + <AvailableTypes> (напр. ЯндексМаркетВитринаФулфилмент/РегламентноеЗадание —
поле ИмяПользователя с AvailableTypes=xs:string Length=0 Variable).

Корпус (acc+erp 8.3.24): AvailableTypes встречается в 18 местах, ВСЕ — на InputField (полное
покрытие scope). Содержимое — стандартный 1С type-description (<v8:Type>+qualifiers), тот же
формат, что у реквизитов → переиспользуем существующий механизм типов:
- decompile: Decompile-Type на узле <AvailableTypes> → компактная DSL-строка (одиночная/мультитип "a | b")
- compile (ps1+py): Emit-Type с tag='AvailableTypes' (сам разбирает мультитип); TypeDomainEnabled — bool pass-through

availableTypes — формат типа реквизита (§Типы): одиночный или составной через "|".

Верификация: таргет-раундтрип всех 17 форм с AvailableTypes → 15 match, остаток 6 строк в 2 формах —
ДРУГИЕ категории (ExtendedTooltip/Value), не AvailableTypes; целевая форма match (было 8 → 0).
Регресс form-compile 43/43 (ps1+py); 1С-cert кейса input-fields (поле с мультитип AvailableTypes
грузится в платформу). spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:40:16 +03:00
Nick Shirokov da1d8c9297 feat(form-decompile,form-compile): суппресс-маркер title:"" для команд (подавление авто-вывода заголовка)
Раундтрип додумывал <Title> команды из имени, когда в оригинале заголовка нет
(напр. ФормаРабочегоМеста_320х320: 126 строк ADDED — «Задания выбрать», «Сканирование
далее» и т.п. на ~30 командах-кнопках с одной картинкой).

Корпус (acc+erp 8.3.24): из 92040 команд форм 99.87% имеют <Title> (захват штатный),
лишь 121 (0.13%) — без заголовка. Значит авто-вывод как дефолт верен (помощь модели),
а для редкого хвоста нужен суппресс-маркер — ровно как уже сделано для элементов
(Emit-Title) и формы (autoTitle).

compile (ps1+py): Emit-Commands зеркалит Emit-Title — ключ title есть+непустой → эмитим;
есть+"" → суппресс (не эмитим, не додумываем); отсутствует → авто-вывод из имени.
decompile: нет <Title> у команды → title:"" (иначе компилятор додумает).

Верификация: таргет-раундтрип формы → match (было 126 → 0); регресс form-compile 43/43
(ps1+py); 1С-cert кейса commands (команда с title:"" грузится без <Title>). spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:22:52 +03:00
Nick Shirokov 6377cdfba5 feat(form-decompile,form-compile): Table headerHeight/footerHeight/currentRowUse + форм. conversationsRepresentation (pass-through)
Раундтрип терял 4 свойства (категория Table-скаляры + форм-уровень):
- Table <HeaderHeight>/<FooterHeight> (высота шапки/подвала в строках; ~35/~6 форм)
- Table <CurrentRowUse> (использование текущей строки; ≠ одноимённое свойство команды,
  у которой свой путь захвата/эмиссии). Значения: DontUse/Use/SelectionPresentation/
  SelectionPresentationAndChoice/Choice
- форм-уровень <ConversationsRepresentation> (Auto/Show/DontShow; редкое)

Все три Table-свойства были явно отложены в Emit-Table (комментарий о «строгом Table-XSD»).
Корпусные данные показывают, что 1С эмитит те же теги в РАЗНЫХ позициях у разных форм →
загрузчик толерантен к порядку детей Table (как и существующий компилятор с ранним DataPath).
Размещены pass-through в Emit-Table (height-теги рядом с UseAlternationRowColor, CurrentRowUse
у блока дин-списка); форм-уровень — generic Emit-Properties (авто-PascalCase).

decompile (ps1): захват headerHeight/footerHeight/currentRowUse на Table; ConversationsRepresentation
в KNOWN_FORM_PROPS. compile (ps1+py): эмиссия в emit_table.

Верификация: таргет-раундтрип 4 форм → match (TOTAL diff lines 0); регресс form-compile 43/43
(ps1+py); 1С-cert кейса table (форма с тремя тегами грузится в платформу). spec обновлён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 11:09:15 +03:00
Nick Shirokov 6e4fdb443a feat(form-decompile,form-compile): батч скаляров (IncompleteChoiceMode/EqualColumnsWidth/ChildrenAlign/ImageScale/Zoomable/Shape/PictureLocation) + форменные ShowCloseButton/HorizontalAlign/ChildrenAlign/ShowTitle + PictureDecoration NonselectedPictureText + ShowTitle factual
Из топа list-iter2 (формы из корпуса):
- Generic-скаляры (input/radio/group/picDecoration/button через Emit-Layout): IncompleteChoiceMode,
  EqualColumnsWidth(bool), ChildrenAlign, ImageScale, Zoomable(bool), Shape, PictureLocation.
- Форменные свойства → KNOWN_FORM_PROPS (декомпилятор) + авто-PascalCase Emit-Properties:
  ShowCloseButton, HorizontalAlign, ChildrenAlign, ShowTitle.
- PictureDecoration NonselectedPictureText (ML, как у picField; после Title).
- ShowTitle factual у UsualGroup/Page/ColumnGroup — раньше ловили/эмитили только false,
  явный <ShowTitle>true> терялся (8989 в корпусе); теперь true/false при наличии.

⚠️ Table HeaderHeight/FooterHeight/CurrentRowUse НЕ добавлены: строгий Table-XSD требует точной
позиции тегов (Representation→…→HeaderHeight→Footer→…→CurrentRowUse→RowFilter), generic-позиция
ломает загрузку (XDTO exception) — отдельная задача по позициям в Emit-Table.

Зеркало py. Выборка 82 формы: 0 корневых утечек батч-тегов (остаток — CFE-форма с потерянным
контейнером расширения, не связано). Кейсы table/radio-tumbler-strings/groups/element-appearance/
input-fields расширены и сертифицированы в 1С (явный ShowTitle=true, форменные props, picDecoration
NPT, button Shape/PictureLocation). Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 22:09:32 +03:00
Nick Shirokov 330447c95f feat(form-decompile,form-compile): системные перечисления в choiceList + footerDataPath/footerText/editMode на labelField/radio + AdditionalColumns empty self-closing
Четыре находки из rt-iter2 (формы Хозрасчётный/ФормаСчёта, СпецификацииНоменклатуры):

1. **Системные перечисления в choiceList** (ent: namespace, напр. ent:AccountType=ВидСчёта):
   значение несёт xsi:type="ent:AccountType", компилятор эмитил xs:string (терял тип).
   Per-item ключ `valueType` (как у фильтра): декомпилятор сохраняет не-примитивный
   не-DesignTimeRef xsi:type, компилятор эмитит его вместо авто-детекта.
2. **footerDataPath/footerText на LabelField** — были только у InputField, на поле-надписи
   (колонка таблицы) терялись. Добавлены (decompile + compile, позиция по корпусу).
3. **editMode у RadioButtonField** — не ловился/не эмитился (форма ВидСчёта). Добавлен.
4. **Пустая AdditionalColumns** (table-ref без колонок) — компилятор эмитил пустую пару
   <AdditionalColumns table="X"></...>, платформа — self-closing. Фикс: self-closing при
   пустых columns.

Зеркало py. Обе формы → match. Кейсы radio-tumbler-strings (+ent:AccountType +editMode),
picture-field (labelField-колонка +footerDataPath/footerText), additional-columns
(+пустая группа) сертифицированы в 1С. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:32:04 +03:00
Nick Shirokov bdd38691aa feat(form-compile): роль по GUID в xr-флаге — без префикса Role. (заимствованные/расширение)
Роль в xr-флаге (userVisible/view/edit/use, объектная форма {common,roles}) может
ссылаться по GUID (<xr:Value name="<guid>"> без префикса Role. — заимствованная роль
или расширение), а не по имени. Emit-XrFlag всегда добавлял Role. → Role.<guid>
(форма Сотрудники/ВыплатыУчётЗатрат: 4 ADDED Role.GUID + 4 LOST). Фикс: ключ-роль,
совпадающий с GUID-паттерном, эмитится как есть (без Role.). Декомпилятор GUID и так
сохранял верно (нет префикса для снятия). Зеркало py.

Форма Сотрудники → match. Кейс attributes-types (+edit с ролью по GUID) сертифицирован
в 1С (платформа принимает неизвестную GUID-роль). Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:10:14 +03:00
Nick Shirokov 2ab1b5bc14 feat(form-compile): пустое значение фильтра "_" → self-closing <dcsset:right> (а не литеральный _)
Маркер пустого значения "_" (Get-FilterValue для пустого <dcsset:right xsi:type="X"/>)
эмитился компилятором как литеральный `_` (<dcsset:right xsi:type="dcscor:Field">_</...>)
в объектной форме фильтра (когда форсится valueType/userSettingPresentation). Платформа
хранит self-closing пустой тег (напр. сравнение с незаданным полем dcscor:Field, или
пустой xs:string). Фикс: value=="_" → <dcsset:right xsi:type="$vt"/> (vt из valueType
или xs:string). Зеркало py.

Формы ОтправкиОтчетности/ФормаЭлемента (dcscor:Field) → match; чинит и Новости
(xs:string пустой в object-форме). Кейс input-fields (+CA фильтр с пустым dcscor:Field)
сертифицирован в 1С. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 21:02:17 +03:00
Nick Shirokov 9900f8f656 feat(form-decompile,form-compile): события ExtendedTooltip-компаньона (переиспользование механизма событий элемента)
ExtendedTooltip (companion = LabelDecoration) может нести <Events> (напр.
URLProcessing у hyperlink-подсказки — ОбработкаНавигационнойСсылки). Декомпилятор
ловил own-content (layout/оформление/флаги/hyperlink), но НЕ события → весь блок
Events LOST (топ list-iter: ExtendedTooltip>Events 492 impact). Был отложенный хвост
кластера ExtendedTooltip own-content.

Переиспользован механизм событий элемента: декомпилятор Get-Events → etObj['events'];
компилятор Emit-Events (typeKey 'label', после Title) + 'events' в companionStructKeys
(чтобы text+events шёл структурной веткой). Зеркало py. Round-trip формы
НастройкиИнтеграцииМаркетплейс/ФормаЭлемента бит-в-бит (3 ExtendedTooltip с URLProcessing).
Кейс input-fields (extendedTooltip +hyperlink +events URLProcessing) сертифицирован
в 1С. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 20:46:20 +03:00
Nick Shirokov 32273d2be8 feat(form-decompile,form-compile): эмиссия оформления Page/Popup + ColumnGroup HeaderPicture + whitespace Title/ToolTip
Декомпилятор ловил эти свойства (Add-CommonProps/Add-Appearance), но эмиттеры
компилятора их не выводили → LOST. Четыре подфикса (по выбору пользователя из топа
list-iter):

1. Page>BackColor/TitleTextColor/TitleFont (193+): Emit-Page не звал Emit-Appearance.
   Добавлен (profile field, после ShowTitle перед компаньоном — порядок корпуса).
2. Popup>TitleTextColor/TitleFont (133/127): Emit-Popup не звал Emit-Appearance. Добавлен.
3. ColumnGroup>HeaderPicture (144): Emit-ColumnGroup не звал Emit-ColumnPics. Добавлен
   (после ShowInHeader/Layout перед оформлением — порядок корпуса).
4. UsualGroup Title/ToolTip с whitespace-контентом: Add-CommonProps читал title/tooltip
   через Get-LangText (PreserveWhitespace=false стрипал <v8:content> </> → "" →
   компилятор подавлял). Новый Get-LangTextWS восстанавливает " " (как Get-MLFormattedValue).
   1-пробельные tooltip'ы теперь матчатся; редкий N-пробельный → косметика числа пробелов.

Зеркало py. Выборка 40 форм с этими категориями: целевые потери 0 (match 21,
остаток — несвязанный хвост). Кейсы pages (+backColor/titleTextColor/titleFont),
column-group (+headerPicture), button-group (popup +titleTextColor/titleFont)
сертифицированы в 1С. Регресс 43/43 (ps1+py).

Раскрыто (отдельно): MultipleValuesBackColor (input) — другой appearance-ключ.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 20:04:09 +03:00
Nick Shirokov c96bcc3566 feat(form-decompile,form-compile): дата в фильтре = StandardBeginningDate Custom (голая ISO-дата как шорткат)
Дата-значение фильтра платформой почти всегда хранится как StandardBeginningDate
Custom, а не xs:dateTime (корпус 8.3.24: 268 SBD-Custom vs 2 xs:dateTime в
dcsset:right). Добавлен естественный шорткат: голая ISO-дата без valueType →
компилятор выводит SBD Custom+date. Работает и в shorthand-строке фильтра
("ДатаЗаказа > 2020-01-01T00:00:00").

Компилятор: Emit-FilterItem ловит дату без valueType → SBD Custom (раньше → xs:dateTime);
Parse-FilterShorthand больше не ставит valueType=xs:dateTime для даты (оставляет
вывод компилятору). Декомпилятор: SBD Custom+date → голая дата без valueType
(компилятор восстановит), именованный вариант → строка+valueType, SED/нетипичное →
объект+valueType. Escape для плоского xs:dateTime — явный valueType. Зеркало py.

Кейс input-fields (+date-фильтр голой датой объектно и shorthand-строкой, +именованный
вариант) сертифицирован в 1С, round-trip подтверждён (голые даты без valueType
возвращаются). Регресс 43/43, SBD-корпус (40 форм) без регрессий (match 28).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:52:59 +03:00
Nick Shirokov 89bb58a101 feat(form-decompile,form-compile): StandardBeginningDate — короткая форма значения (строка-вариант)
Добавлена короткая запись значения фильтра StandardBeginningDate (как в skd для
StandardPeriod): именованный вариант → просто строка "BeginningOfThisDay" (без даты);
Custom → объект {variant:"Custom", date:"…"} (нужна дата). Компилятор Emit-FilterItem
принимает обе формы (строка → variant-only; объект → variant+date); декомпилятор
эмитит строку для именованных вариантов, объект для Custom.

Зеркало py. Кейс input-fields: именованный вариант переведён на короткую строку
(snapshot байт-в-байт = объектной форме). Round-trip: Custom→объект,
BeginningOfThisDay→строка. Регресс 43/43, SBD-корпус (40 форм) без регрессий.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:31:45 +03:00
Nick Shirokov 3ce71d436f feat(form-decompile,form-compile): StandardBeginningDate в значении фильтра — структурно {variant, date?}
Значение фильтра типа v8:StandardBeginningDate (стандартная дата начала) серилизуется
структурно: <v8:variant xsi:type="v8:StandardBeginningDateVariant">Custom</v8:variant>
+ <v8:date>… (Custom несёт дату; именованные варианты — без). Компилятор эмитил
плоскую склейку InnerText (Custom3999-12-31T23:59:59), декомпилятор брал
сцепленный текст. Корпус 8.3.24: 307 случаев (Custom 280 с датой, BeginningOfThisDay
23, …Week 3, …Year 1; StandardEndDate/StandardPeriod как значение фильтра не
встречаются, но обработаны симметрично).

Не «забытый порт» — skd-decompile тоже не структурирует SBD в filter right (только в
dataParameters). DSL: value = {variant, date?} + valueType="v8:StandardBeginningDate".
Декомпилятор Get-FilterValueWithType читает variant/date; компилятор Emit-FilterItem
эмитит структурно (variant xsi:type выводится из valueType). Зеркало py.

Форма УправлениеОбменом (ДатаЗакрытия = SBD, op Equal): SBD-потерь 0 (остаток diff —
несвязанный TitleFont/FooterText). Кейс input-fields (+CA фильтр SBD Custom+date и
именованный вариант) сертифицирован в 1С, round-trip декомпиляции подтверждён.
Регресс 43/43.

ОТДЕЛЬНАЯ НАХОДКА (не в этом коммите): операторы Filled/NotFilled несут
тип-зависимый плейсхолдер <dcsset:right> (пустой xs:string для строк, SBD с дефолт.
датой для дат), который декомпилятор дропает как беззначный — нужен отдельный фикс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:25:24 +03:00
Nick Shirokov 459d3feb69 feat(form-decompile,form-compile): appearance Текст/Заголовок/Формат — xs:string/Field/typed LocalString (категория картинок→текст оформления)
В значениях параметров оформления (dcsset:appearance) компилятор форсил
мультиязычный LocalStringType по имени ключа (Текст/Заголовок/Формат), теряя
плоскую xs:string и dcscor:Field. Корпус 8.3.24 (Текст/Заголовок/Формат):
xs:string 823, LocalString одноязычный 658, многоязычный 188, dcscor:Field 105.
Контекст (форменный CA vs Settings дин-списка) НЕ детерминирует (целевая форма
СправкаРасчётПостоянныхИВременныхРазниц — форменный CA с xs:string).

Решение (scoped-различие по форме значения, в этом конкретном контексте — не общая
конвенция DSL): голая строка → плоский xs:string (нелокализ. литерал; "" →
самозакрывающийся тег); объект {ru,en} → LocalStringType; объект {field:"путь"} →
dcscor:Field. Декомпилятор перестаёт схлопывать одноязычный LocalString здесь
(всегда объект-карта языков → различим от xs:string).

Заодно пред-существующий баг: LocalString-значение параметра несёт
xsi:type="v8:LocalStringType" на теге dcscor:value (846 случаев) — Emit-MLText
эмитил голый тег; добавлен опц. параметр xsiType (ps+py).

Зеркало py (байт-в-байт). Выборка 57 CA-форм (xs:string/LocalString/Field, вкл.
целевую): appearance-потерь 0, целевая → match (match 26→40). Кейс input-fields
(+CA Текст: плоский/multilang/пустой/field) сертифицирован в 1С. Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 18:00:00 +03:00
Nick Shirokov b781f97832 feat(form-decompile,form-compile): string(N,fixed) — AllowedLength=Fixed для строк фикс. длины
Колонки/реквизиты строк фиксированной длины (ИНН/КПП/коды) несут
<v8:AllowedLength>Fixed</v8:AllowedLength>, но DSL выражал только Variable:
компилятор хардкодил Variable, декомпилятор не читал AllowedLength → Fixed терялся
(форма ЭлектроннаяТранспортнаяНакладная/ТитулПеревозчика*: 3 LOST Fixed + 3 ADDED
Variable — мультимножественный учёт тех же колонок).

Корпус 8.3.24: AllowedLength ВСЕГДА присутствует в StringQualifiers (Variable
443127 + Fixed 2687, ABSENT=0) → always-эмиссия Variable верна. Fixed (2687)
всегда с длиной > 0 (12/10/3/1/36…); при Length=0 — всегда Variable.

Грамматика `string(N,fixed)` (по аналогии с `decimal(D,F,nonneg)`). Variable —
дефолт (опускаем суффикс); `variable` принимается forgiving. Emit-SingleType
(ps1+py) эмитит Fixed при суффиксе; декомпилятор Decompile-Type читает AllowedLength
(Fixed → суффикс, Variable/Length=0 → плоский string(N)). Общий путь типов
(реквизиты/колонки/valueType/составные).

Выборка 46 форм с Fixed (вкл. указанную): 0 потерь AllowedLength, целевая форма →
match. Round-trip декомпиляции снэпшота: string(12,fixed)/string(9,fixed) читаются
обратно. Кейс attributes-types (+ValueTable с Fixed-колонками ИНН/Код) сертифицирован
в 1С. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 17:29:09 +03:00
Nick Shirokov b2eaa9e129 feat(form-decompile,form-compile): InputField MultiLine факт. значение + Shortcut как generic-скаляр
Две находки из корпусного хвоста (свериться помог category-forms.py против rt-24):

- **MultiLine факт. значение** (190 LOST в выборке rt-24): компилятор эмитил и
  декомпилятор ловил только `true`, терялся явный `<MultiLine>false>` (425 в корпусе
  8.3.24; absent 204694, true 5183). Теперь захват/эмиссия true/false при наличии,
  отсутствие = дефолт (как PasswordMode). Эвристика autoMaxWidth (multiLineDefault)
  не затронута — продолжает срабатывать только на true.
- **Shortcut → generic-скаляр** (94 LOST на 86 формах): эмитился/ловился только у
  PictureField (инлайн), терялся на InputField (169), UsualGroup (41),
  RadioButtonField (39), Page (22), Table (3), CheckBoxField (1). Перенёс в
  GENERIC_SCALARS (любой элемент через Emit-Layout/Add-GenericScalars), убрал инлайн
  PictureField. Команда — отдельный путь (§7), не трогаю.

Заодно подтверждено покрытие (category-forms против rt-24 = 0): ControlRepresentation
(1464), ShapeRepresentation (877), PasswordMode (689) — уже generic/факт. значение.
AllowedLength (625) — это cascade типов полей дин-списка (отдельный кластер, не трогаем).

Зеркало py (байт-в-байт). Выборка 73 формы (MultiLine=false + Shortcut на 6 типах
элементов): 0 потерь. Кейсы input-fields (+multiLine:false +shortcut на input),
groups (+shortcut на group), picture-field (shortcut через generic) сертифицированы
в 1С. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:47:20 +03:00
Nick Shirokov c4f600a36b feat(form-decompile,form-compile): WarningOnEdit на check/radio/labelField (не только input)
`<WarningOnEdit>` (мультиязычный текст предупреждения при редактировании)
встречается на InputField (576), CheckBoxField (119), RadioButtonField (54),
LabelField (1) по корпусу 8.3.24, но компилятор эмитил и декомпилятор ловил его
только у InputField → терялся на check/radio/labelField.

Расширил эмиссию (Emit-Check/Emit-Radio/Emit-LabelField, после Emit-Layout перед
Format) + захват в декомпиляторе (инлайн SelectSingleNode+Get-LangText в трёх
обработчиках, как у InputField). Парный enum `warningOnEditRepresentation`
(Show/DontShow) уже был generic-скаляром на любом поле — не трогаю. 1С толерантна
к позиции тега внутри поля (сертифицировано загрузкой).

Зеркало py (байт-в-байт). Выборка 46 форм с WarningOnEdit на check/radio:
0 потерь WarningOnEdit. Кейсы input-fields (+check multilang, +labelField) и
radio-tumbler-strings (+radio) сертифицированы в 1С. Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 16:04:15 +03:00
Nick Shirokov 606ac10fdb feat(form-decompile,form-compile): встроенные картинки (xr:Abs) + TransparentPixel + Page Picture (категория картинок)
Закрыты картиночные потери из ROOT-секции корпуса: Page>Picture (702),
PictureField>ValuesPicture (abs), Button>Picture (abs), а также RowsPicture
и командные/popup-картинки. ColumnGroup>HeaderPicture уже ловился (Add-CommonProps),
бэклог-числа были от старого прогона v0.62.

Две системные дыры + один полный пробел (по корпусу 36707 форм):
- xr:Abs (встроенная картинка) игнорировался везде, кроме PictureDecoration:
  Get-PictureRef и Button/Popup/Command picture брали только <xr:Ref>. Теперь
  src с префиксом "abs:" → <xr:Abs> (как у PictureDecoration). ~358 ValuesPicture
  + ~153 Button + хвост.
- xr:TransparentPixel ловился только у PictureDecoration. Теперь — в объектной
  форме картинки-ссылки {src, loadTransparent?, transparentPixel:{x,y}} у
  ValuesPicture/HeaderPicture/FooterPicture/RowsPicture/Page и у командных картинок.
- Page>Picture не поддерживался ни компилятором, ни декомпилятором. Новый ключ
  picture на Page (конвенция ValuesPicture: дефолт LoadTransparent=false, по корпусу
  416/286). Позиция XSD: после Title/ToolTip/флагов, перед Group/ShowTitle.

Компилятор: Emit-PictureRef + Emit-CommandPicture расширены abs/transparentPixel;
Emit-Page эмитит picture; RowsPicture переведён на Emit-PictureRef (был кастомный
блок без abs/TP). Декомпилятор: Get-PictureRef ловит xr:Abs/TransparentPixel;
новый хелпер Set-CommandPicture (Button/Popup/Command — abs + TP через объектную
форму при наличии TP, иначе скаляр); Page и RowsPicture через Get-PictureRef.
Зеркало py (байт-в-байт). abs валидируется раундтрипом на корпусе (нужен встроенный
бинарь — в синтет-кейсы не кладём); transparentPixel + Page picture сертифицированы
загрузкой в 1С (кейсы picture-field/pages/commands). Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 13:35:13 +03:00
Nick Shirokov 73f1e6ec26 docs(form-dsl-spec): group — авторский набор значений vs раундтрип-forgiving
По замечанию: код forgiving принимает широкий набор ориентаций (нужно для
раундтрипа — Horizontal/AlwaysVertical встречаются в корпусе), но для АВТОРИНГА
spec должен предлагать только доступные в конфигураторе значения, иначе модель
разметит форму недоступным значением. Помечены явно: страница/группа —
vertical/horizontalIfPossible/alwaysHorizontal; horizontal/alwaysVertical —
только раундтрип-совместимость, не для авторинга.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:54:59 +03:00
Nick Shirokov a3395d4abe feat(form-decompile,form-compile): HorizontalIfPossible в ориентации страницы/группы
Страница (Page) и обычная группа (UsualGroup) теряли <Group>HorizontalIfPossible
</Group> — orientation-карта содержала Horizontal/Vertical/AlwaysHorizontal/
AlwaysVertical, но не HorizontalIfPossible (ROOT Page>Group 359 на 189 формах).

Доступные значения (по конфигуратору + корпусу): страница/обычная группа —
Vertical/HorizontalIfPossible/AlwaysHorizontal (+ Horizontal реально встречается:
1288 форм на странице — XML-enum шире UI-дропдауна, оставлен forgiving); группа
колонок таблицы — Vertical/Horizontal/InCell (уже обрабатывалось, не трогаем).
InCell на странице/группе не добавляем — в корпусе не встречается. Коэрция не
делается: фактическое значение сохраняется как есть (верность раундтрипа).

Добавлен horizontalIfPossible в Emit-Page + Emit-Group switch (ps1+py) и в gmap
декомпилятора (Page + UsualGroup). Таргет-верификация (выборка 50 из 189): 0
остатка, 29 стали match, 0 регрессов. Кейс pages пере-сертифицирован в 1С
(HorizontalIfPossible грузится). Регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:49:45 +03:00
Nick Shirokov dd9af25e67 feat(form-decompile,form-compile): AutoSaveUserSettings дин-списка (settings.autoSaveUserSettings)
Свойство <AutoSaveUserSettings> внутри <Settings xsi:type="DynamicList"> (после
MainTable) — авто-сохранение пользовательских настроек дин-списка. В корпусе
только false (дефолт true → платформа эмитит отклонение). Не обрабатывалось →
терялось (ROOT Attribute>AutoSaveUserSettings 292 на 178 формах).

Ключ settings.autoSaveUserSettings (bool); декомпилятор захватывает факт. значение,
компилятор эмитит после MainTable при наличии ключа. Зеркало py.

Таргет-верификация (выборка 50 из 178): 0 остатка, 27 стали match, 0 регрессов.
Кейс dynamic-list-form расширен (+autoSaveUserSettings), сертифицирован в 1С.
Регресс 43/43 (ps1+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 11:36:27 +03:00
Nick Shirokov 6d9c41507c docs(form-dsl-spec): excludedCommands — общее свойство поля (не только таблица)
Дополнение к фиксу CommandSet: ключ excludedCommands теперь работает на любом
поле (input/label/check/spreadsheet/html/formatted/picField + таблица + форма),
а не только на таблице. Уточнил описание и значения по типам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:42:52 +03:00
Nick Shirokov d8689b3674 feat(form-decompile,form-compile): CommandSet (отключённые команды) — общее свойство поля
Ранее excludedCommands обрабатывался только для Table-элемента и форм-уровня.
Обычные поля (InputField/LabelField/CheckBoxField/SpreadSheetDocumentField/HTML/
Formatted/Picture) идут через Emit-SimpleField и др. — CommandSet там терялся
(кластер SpreadSheetDocumentField>CommandSet, baseline impact ~1443).

Централизовал: захват в Add-CommonProps (декомпилятор, общий для всех полей),
эмит в Emit-Layout (компилятор ps1+py), убрал дубль из Table-эмиттера. CommandSet —
дочерний элемент базового FormField в схеме, позиция фиксирована независимо от
подтипа → ранняя (после TitleLocation, перед скалярами/Height), как у spreadsheet.

Таргет-верификация (новый цикл category-forms.py): 43 формы корпуса с CommandSet →
после фикса 0 остатка (CommandSet + ExcludedCommand cascade), 26 стали match.
Кейс table пере-сертифицирован в 1С (ранняя позиция грузится), ps1==py, регресс 43/43.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 21:41:43 +03:00
Nick Shirokov d5d19710cb feat(form-decompile,form-compile): корпусный хвост — 8 generic-скаляров + PasswordMode факт. значение
Из ROOT-секции корпусного прогона 8.3.24 (rt-24): дешёвые широкие generic-скаляры
(pass-through, обе стороны + py), не обрабатывались ранее:
- ControlRepresentation (свёртка группы, 1464) · ShapeRepresentation (форма кнопки/
  попапа, 1023) · AutoAddIncomplete (516) · MarkNegatives (433) · InitialListView
  (нач. позиция списка, 246) · ChoiceListHeight (143) · ThreeState (флажок, 119) ·
  ScrollOnCompress (прокрутка страницы, 104).

PasswordMode на InputField: эмитился только при true → терялись 349/504 явных false
в корпусе (ROOT 689). Теперь факт. значение (как на LabelField/markIncomplete);
декомпилятор захватывает true/false при наличии тега.

Проверка: 15 корпусных форм с этими тегами — 0 расхождений по тегам. Регресс 43/43
(ps1+py). Совокупный ожидаемый impact ~4700 TOTAL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:42:07 +03:00
Nick Shirokov dd226697bf feat(form-decompile,form-compile): GanttChart design-time Settings из ring-3 (Фаза 3 кластера Chart-Settings)
GanttChart Settings (<Settings xsi:type="d4p1:GanttChart">) = вложенный
<d4p1:chart> (полный Chart-блок) + gantt-специфика (points/series/timeScale/
drawEmpty/…) — ТОТ ЖЕ d4p1-неймспейс, что и Chart. Переиспользован генерик-движок
Chart как есть (рекурсивный захват/эмит).

Изменения: параметризован xsi:type Settings (выводится из типа реквизита:
d5p1:GanttChart → d4p1:GanttChart); elseif декомпилятора и guard точек/осей
расширены на d4p1:(Gantt)?Chart. Зеркало py (ctype-параметр).

Все 16 форм Ганта корпуса 8.3.24 — раундтрип БАЙТ-В-БАЙТ (0 diff, 0 ring3).
Кейс chart-gantt-settings (полная диаграмма Ганта из эталона) сертифицирован
загрузкой в 1С. Регресс 43/43 (ps1+py).

Кластер Chart-Settings закрыт (Planner + Chart + GanttChart). Остаток —
только редкие диаграммы с точками/осями (типизир. значения/d4p1-ML) → честный
fail-ring3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 17:42:54 +03:00
Nick Shirokov 1333e09ff8 feat(form-decompile,form-compile): Chart design-time Settings из ring-3 (Фаза 2 кластера Chart-Settings)
Реквизит-диаграмма несёт <Settings xsi:type="d4p1:Chart"> — встроенный конфиг
(~110-130 версионно-вариативных полей: тип/серии/легенда/заголовок/шкалы/цвета/
оси, глубокая вложенность с повторяющимися именами). Корпус 8.3.24: 5 форм.

Подход (с пользователем): ГЕНЕРИК-движок. Ключ chart на реквизите; рекурсивный
захват/эмит поддерева d4p1, ключи = локальные имена тегов, порядок ключей =
порядок эмиссии → раундтрип ЛЮБОЙ версии/набора полей бит-в-бит (платформа
добавляет поля, не переставляет). Структуры распознаются по форме узла
(line {width,gap,style} / border {width,style} / font {kind} / ML / области
{left,right,top,bottom} / серии-массивы); малые name-set'ы: ML-поля, серии,
attrs-узлы (gaugeQualityBands). Расширяемость: любое из ~127 свойств — по
каноничному имени.

Авторинг с нуля: декомпиль рабочей диаграммы как шаблон + правка ядра
(chartType/серии/легенда/цвета). Default-fill через merge НЕ делаем — конфликт
с байт-точностью неполных форм (см. docs/form-dsl-spec.md).

Результат: 4 из 5 форм корпуса — байт-в-байт (включая версионно-вариативные).
5-я (точки/оси realPointData/realDataItems с типизир. значениями xsi:type,
xsi:nil, ML с префиксом d4p1:) → честный fail-ring3 (редкий вариант, не
поддержан генериком). Снят fail-ring3 для d4p1:Chart (GanttChart — Фаза 3).
Заодно фикс: d5p1:Dendrogram отсутствовал в specialTypeNs (ps1+py).

Декомпилятор ps1-only (генерик-рекурсия); компилятор зеркало py (ps1==py
байт-в-байт). Кейс chart-settings (полная диаграмма из эталона
ПроверкаКонтрагента) сертифицирован загрузкой в 1С. Регресс 42/42.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:01:15 +03:00
Nick Shirokov f534add7b4 feat(form-decompile,form-compile): измерения планировщика (dimensions) — Planner Phase 1
Расширение Phase 1 кластера Chart-Settings: реквизит pl:Planner теперь несёт
измерения планировщика (<pl:dimension> — «Измерения» в конфигураторе) с элементами.

DSL planner.dimensions[]: объект разреза (value — ссылка xr:DesignTimeRef или nil,
text-заголовок, цвета, font) + elements[] (элементы измерения, РЕКУРСИВНЫ — могут
нести вложенные elements, как показывает UI колонкой «Элементы»; поле
showOnlySubordinatesAreas). Тип value авто-выводится: ссылочный вид →
xsi:type="xr:DesignTimeRef", иначе xs:string. Пустой текст → самозакрывающийся
<pl:text/> (как в выгрузке). Общие хелперы Emit/Get-PlannerValue/Text применены
и к элементам расписания (items).

Раундтрип бит-в-бит: синтетика upload/epf/Диаграммы (items + 2 dimensions +
вложенные elements + period). Зеркало py (ps1==py байт-в-байт). Кейс chart-fields
расширен измерением (nil-разрез + xs:string-элемент + showOnlySubordinatesAreas),
сертифицирован загрузкой в 1С. Регресс 41/41 (ps1+py).

Ограничение: item.dimensionValues (привязка элемента расписания к элементам
измерений) пока всегда пустой.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:57:54 +03:00
Nick Shirokov f064d53eb2 feat(form-decompile,form-compile): Planner design-time Settings из ring-3 (Фаза 1 кластера Chart-Settings)
Реквизит planner-типа несёт <Settings xsi:type="pl:Planner"> — встроенный конфиг
поля-планировщика (элементы расписания + оформление/поведение + шкала времени).
Раньше декомпилятор делал fail-ring3 (третий вид Settings после TypeDescription/
DynamicList). Корпус 8.3.24: Planner Settings = 1 реальная форма (КонтактныеЛица/
ФормаЛиды), всё chart-семейство = 38 форм. Решение (с пользователем): структурный
DSL ради возможности модели СОЗДАВАТЬ дашборды/планировщики, не только раундтрипить.

DSL: ключ planner:{…} на реквизите (docs/form-dsl-spec.md):
- items[] (элементы расписания) + appearance/поведение-скаляры + timeScale
  (placement/levels[]/colors) + period;
- цвета verbatim, шрифт {kind:AutoFont}/ref, граница {width,style}, ML-форматы;
- компилятор подставляет дефолты для пропущенных ключей (краткий авторинг),
  декомпилятор — полный захват (раундтрип бит-в-бит).

Снят fail-ring3 для pl:Planner (Chart/GanttChart остаются — Фазы 2/3).
Заодно фикс: d5p1:Dendrogram отсутствовал в specialTypeNs (эмитился без
xmlns-префикса) — добавлен в карту (ps1+py).

Раундтрип бит-в-бит: синтетика upload/epf/Диаграммы (с items+period) +
реальная ФормаЛиды (без items/period, иные значения скаляров). Зеркало py
(ps1==py байт-в-байт). Кейс chart-fields расширен (+planner +dendrogram),
сертифицирован загрузкой в 1С. Регресс 41/41 (ps1+py).

Ограничение Phase 1: dimensions/item.dimensionValues пока всегда пустые.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:35:10 +03:00
Nick Shirokov 29f288fe1d feat(form-decompile,form-compile): поля диаграмм из ring-3 (Chart/Gantt/GraphicalSchema/Planner/Period/Dendrogram)
Снят fast-fail на 6 chart-полях. Все — простые скелеты (как document/gauge),
кроме GanttChart с вложенной <Table> (ключ ganttTable, переиспользует
Decompile/Emit-Element — устранена коллизия тип-ключа table). Типы реквизитов
уже в special-type ns-карте (d5p1:Chart/GanttChart/FlowchartContextType/
GeographicalSchema, pl:Planner) + v8:StandardPeriod. Edit/WarningOnEditRepresentation
у GraphicalSchema — через готовые GENERIC_SCALARS.

Guard: реквизит с design-time конфигом диаграммы/планировщика
(<Settings xsi:type="d4p1:GanttChart"/"pl:Planner"/…> — третий вид Settings
после TypeDescription/DynamicList) → честный fail-ring3 (не теряем молча).
Planner несёт Settings всегда; Chart/Gantt — при настройке. Поля без Settings
(диаграмма из кода/график-схема/период/дендрограмма/гео) роундтрипятся полностью.

Выборка 2.17: ring3 4→0 (весь ring3 разобран!). Кейс chart-fields
(chart+graphicalSchema+period+ganttChart с ganttTable) сертифицирован в 1С.
Зеркало py байт-в-байт. Регресс 41/41 (ps1+py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:40:48 +03:00
Nick Shirokov 5ebc02d0b3 feat(form-decompile,form-compile): ConditionalAppearance формы из ring-3 (переиспользование DCS-грамматики)
Снят fast-fail на ConditionalAppearance (1304 формы, 4%). Структура — та же
DCS-грамматика, что settings.conditionalAppearance дин-списка, поэтому
переиспользованы Build-ConditionalAppearance (декомпилятор) и
Emit-ConditionalAppearance (компилятор) как есть.

Отличия от настроек списка: тег-обёртка <ConditionalAppearance> (без dcsset:,
параметр wrapTag) + нет блок-мета viewMode/userSettingID + размещение (последний
child <Attributes>, не отдельный Form-child). Форменный ключ conditionalAppearance
(selection/filter/appearance/presentation). Scope в формах не встречается
(0/6186) → fail-ring3 только при scope.

Заодно фикс: мультиязык-presentation элемента CA → xsi:type="v8:LocalStringType"
(был голый <dcsset:presentation>; чинит и settings CA path).

Выборка 2.17: ring3 7→4 (остаток — только chart-семейство), match 211→214,
CA-формы бит-в-бит. Зеркало py байт-в-байт, кейс input-fields
(+conditionalAppearance: selection+filter+appearance+presentation)
сертифицирован загрузкой в 1С. Регресс 40/40 (ps1+py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:08:14 +03:00
Nick Shirokov ca96a7413a fix(form-decompile): whitespace-заголовок декорации-разделителя (пробел в Title)
PreserveWhitespace=false у XmlDocument стрипает значимый пробел в
<v8:content> </v8:content> → декомпилятор читал "" (неотличимо от суппресс-
маркера title:"") → компилятор не эмитил Title → LOST (надписи-разделители
"РазделительТумблеров" и т.п., 487 в корпусе).

Фикс в Get-MLFormattedValue (декорации label/picture): пустой текст ПРИ наличии
узла <v8:content> → исходно был пробел (платформа не эмитит пустой Title) →
восстанавливаем " ". Компилятор уже эмитит пробел корректно (" " truthy в
Emit-DecorationTitle). Decompiler-only.

Выборка 2.17: match 209→211, TOTAL 12→0 (остаток — только GroupList,
документированный не-покрываемый). Валидировано раунтрипом (форма
ЗадачаИсполнителя/ЗадачиПоПредметуБП → match).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:47:34 +03:00
Nick Shirokov 2ff99d1b77 fix(form-compile): составной тип в shorthand параметра дин-списка (dcssch:valueType)
Parse-DLParamShorthand брал тип regex'ом (\S+) — один токен без пробелов.
Составной тип (CatalogRef.X | CatalogRef.Y, с пробелами вокруг |) не матчился
→ вся строка уходила в name → компилятор эмитил <dcssch:name>Имя: TYPE | TYPE</…>
и ТЕРЯЛ <dcssch:valueType>.

Фикс: тип = ([^=]+?) (допускает пробелы/|, исключает '='-разделитель значения);
составной резолвится по частям (per-part Resolve-TypeStr, rejoin ' | ').
Emit-DLValueType уже split'ил по |, эмиссия корректна. Зеркало py.

Выборка 2.17: TOTAL 38→12 (составной тип у дин-списков Task/прочих восстановлен).
Кейс dynamic-list-parameters (+составной параметр DocumentRef | CatalogRef → два
TypeSet) сертифицирован загрузкой в 1С. Регресс 40/40 (ps1+py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:42:51 +03:00
Nick Shirokov e16b23968e feat(form-decompile,form-compile): хвост раскрытых CommandInterface-форм (TOTAL 95→38)
Добивка длинного хвоста, раскрытого при выводе CommandInterface из ring-3.

Generic-скаляры (обе стороны,+py): titleDataPath (Page/Group динамический
заголовок, 1723; парный к footerDataPath), extendedEdit (input, 3400),
maxRowsCount/autoMaxRowsCount/heightControlVariant (Table высота),
editTextUpdate (input enum, 739). ML-текст: warningOnEdit (input, 641),
footerText (input, 269), nonselectedPictureText (picField, 265).

Фикс markIncomplete → фактическое значение (AutoMarkIncomplete true/false;
раньше только true, 1170 false терялись на input+table).

Выборка 2.17: TOTAL 95→38, match 197→209, diff 6→4. Остаток (отложено в
BACKLOG): dcssch:valueType/name манглинг типов полей дин-списка (отдельная
подсистема), LabelDecoration title-пробел, GroupList (не покрываем).
Кейсы input-fields/table/pages расширены и сертифицированы в 1С. Регресс 40/40.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:25:42 +03:00
Nick Shirokov d7dedd4843 feat(form-decompile,form-compile): CommandInterface — командный интерфейс формы из ring-3
Снят fast-fail на CommandInterface (4388 форм, 13.5% корпуса — крупнейший
оставшийся триггер ring-3).

Форменный ключ commandInterface = панели commandBar + navigationPanel, списки
переопределений авто-расстановки (платформа эмитит только отклонения). Элемент:
command (verbatim; "0"=пустой), type (Auto опускаем/Added), defaultVisible,
visible (тот же xr-flag, что userVisible/use — bool или {common,roles}),
group (CommandGroup verbatim), index, attribute. Порядок тегов Item:
Command,Type,Attribute,CommandGroup,Index,DefaultVisible,Visible.

Две формы записи панели: плоский массив (декомпилятор эмитит её) + древовидная
{группа:[команды]} как входной сахар (алиасы important/goTo/seeAlso→
FormNavigationPanel*, important/createBasedOn→FormCommandBar*; иной ключ verbatim;
group из ключа, элементы не дублируют). Голый элемент → строка-shorthand.
Переиспользует Decompile-XrFlag/Emit-XrFlag.

Выборка 2.17: ring3 37→7, match 181→197; сам CommandInterface роундтрипится
бит-в-бит (0 CI-diff, остаток TOTAL — несвязанный хвост раскрытых форм). Зеркало
py байт-в-байт, кейс commands (+commandInterface tree-форма) сертифицирован
загрузкой в 1С. Регресс 40/40 (ps1+py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 21:00:22 +03:00
Nick Shirokov ef036c7cf1 feat(form-decompile,form-compile): хвост report-форм 2.17 (TOTAL 23→0)
Добивка длинного хвоста, раскрытого при выводе спец-полей из ring-3.

Generic-скаляры (обе стороны, +py): ItemHeight (radio), DropListWidth (input).
Форменное VariantAppearance → KNOWN_FORM_PROPS. Targeted: PasswordMode на
LabelField (факт. значение, ≠ input if-true), ChoiceButtonPicture (input, через
Emit/Get-PictureRef), TransparentPixel (под-элемент <xr:TransparentPixel x y> в
<Picture> PictureDecoration → ключ transparentPixel:{x,y}, 1162 в корпусе).

Компилятор-баг ChoiceParameters без значения: платформа эмитит
<app:value xsi:nil="true"/> (13 в корпусе), компилятор додумывал пустую
FormChoiceListDesTimeValue. Теперь по наличию ключа value (hashtable shorthand
vs PSCustomObject — для PS; dict — для py).

Выборка 2.17: TOTAL 23→0, match 170→181, diff 13→2 (остаток — только GroupList,
документированный не-покрываемый: декомпилятор намеренно опускает во избежание
тихой порчи ссылки). Регресс 40/40 (ps1+py). Кейс input-fields расширен
(value-less choiceParameter → nil) и сертифицирован загрузкой в 1С.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:59:12 +03:00
Nick Shirokov b5769ce373 feat(form-decompile,form-compile): report-form свойства + 3 элементных скаляра (раскрыто из-под ring-3)
Закрытие спец-полей раскрыло пробелы в формах Report-объектов (доминируют
среди SpreadSheet-форм). 6 форменных скаляров report-формы → KNOWN_FORM_PROPS
(decompiler-only; компилятор уже эмитит через Emit-Properties): ReportResult/
DetailsData (имена реквизитов), ReportFormType (Main/Settings/Variant),
AutoShowState (Auto/DontShow/ShowOnComposition), ReportResultViewMode/
ViewModeApplicationOnSetReportResult (всегда Auto).

+ 3 элементных generic-скаляра (обе стороны, +py): HorizontalSpacing (группа,
3592 в корпусе), RepresentationInContextMenu (кнопка, 1673),
SettingsNamedItemDetailedRepresentation (таблица, 778).

Выборка 2.17: TOTAL 101→23, match 166→170, diff 17→13 (ring3 37, 0 крашей).
Кейсы groups/commands/table расширены и сертифицированы загрузкой в 1С.
Регресс 40/40 (ps1+py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:38:31 +03:00
Nick Shirokov 7905615091 feat(form-decompile,form-compile): спец-поля документ/датчик из ring-3 (SpreadSheet/HTML/Text/Formatted/ProgressBar/TrackBar)
Кластер ring-3: 6 листовых полей-«документ/датчик» больше не валят
декомпилятор в fast-fail. По корпусу SpreadSheetDocumentField — 21% форм
(самый массовый триггер ring-3), обгоняет CommandInterface.

Декомпилятор: +ELEMENT_KEY, общий Decompile-SimpleField (скелет поля) +
Add-GaugeScalars (числовые min/max/шаги без xsi:type — ≠ типизированных
input). Типоспец. enum/bool скаляры — через GENERIC_SCALARS.
enableDrag/enableStartDrag — фактическое значение (платформа эмитит явный
false на SS; заодно покрыло не-дин-список таблицы). Форменные ScalingMode/
VerticalSpacing → KNOWN_FORM_PROPS.

Компилятор(+py байт-в-байт): тип-ключи spreadsheet/html/textDoc/formattedDoc/
progressBar/trackBar + синонимы (XML-имя/рус), общий Emit-SimpleField,
GENERIC_SCALARS (output/protection/scrollbars/viewScalingMode/show*/…).

Спец-типы реквизитов с локальным xmlns на <v8:Type> (mxl:SpreadsheetDocument
7387, fd:FormattedDocument, d5p1:TextDocument/Chart/GanttChart/Flowchart/Geo/
DataAnalysis, pdfdoc:PDFDocument, pl:Planner) — резолв по полному значению
типа (префикс d5p1 неоднозначен).

Выборка 2.17: ring3 61→37 (−24 формы), match 156→166, 0 dec/compile-fail.
Кейс special-fields (все 6 типов + спец-типы) сертифицирован загрузкой в 1С.
Регресс 40/40 (ps1+py).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 19:25:12 +03:00
Nick Shirokov 24e7116b7d fix(form-compile): эмитить пустой <Attributes/> у форм без реквизитов
Компилятор при отсутствии реквизитов не эмитил <Attributes> вовсе (предполагал
толерантность 1С — не проверено). Корпус: 100% форм (17033) имеют <Attributes>,
162 — пустой <Attributes/>, 0 без него. Платформа эмитит ВСЕГДА.

Emit-Attributes теперь эмитит <Attributes/> при пустом списке. Зеркало py. Кейс
minimal (форма только с заголовком) → снапшот +<Attributes/>, сертифицирован
загрузкой в 1С (форма с пустым Attributes грузится). Форма РазблокированиеРеквизитов
→ match. Раундтрип: match 156→157, with diff 3→2 (остаток — 2 GroupList,
осознанно непокрыты).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 17:15:23 +03:00
Nick Shirokov 1ed23b2a08 feat(form-decompile,form-compile): частичная/минимальная форма ListSettings (дескриптор settings.listSettings)
Раундтрип TOTAL 21→0, match 153→156. Компилятор всегда эмитил ПОЛНЫЙ каноничный
скелет <ListSettings> (filter+order+conditionalAppearance+itemsViewMode+
itemsUserSettingID), а ~7% форм имеют частичный (напр. только <filter> с
userSettingID) → лишние контейнеры = ADDED.

- Декомпилятор: Get-ListSettingsShape фиксирует «форму» скелета в
  settings.listSettings (ordered-карта present top-level: filter/order/
  conditionalAppearance → блок-мета 'v'/'u'/'vu'/''; itemsViewMode/
  itemsUserSettingID → true). Дескриптор пишется ТОЛЬКО для не-каноничных форм
  ($null для полного канона и неподдержанных top-level item/dataParameters/…).
- Компилятор: при наличии дескриптора эмитит ТОЛЬКО указанные части (контент из
  settings.filter/order/CA, блок-мета из дескриптора); иначе — полный канон
  (без изменений). Аддитивно, дескриптор-gated → 93% канон-форм не затронуты.

Зеркало py. Формы ОстаткиАлкогольнойПродукцииЕГАИС (filter-only) и
ОстаткиПартийЗЕРНО (filter+order) → ListSettings бит-в-бит. Регресс 39/39
(канон-путь). Партиал-путь — harness + provenance (подмножество канона).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 16:51:14 +03:00
Nick Shirokov cc8b283f1b feat(form-decompile,form-compile): DisplayImportance + MinValue/MaxValue + verticalSpacing + rowsPicture LoadTransparent + autoMaxWidth/RowPictureDataPath fixes
Раундтрип TOTAL 25→21, match 146→153. Батч из 6 хвостовых находок:

- DisplayImportance: атрибут открывающего тега ЛЮБОГО элемента (адаптивная
  важность VeryHigh/High/Usual/Low/VeryLow). Хелпер DI-Attr/di_attr внедрён в
  открывающие теги; декомпилятор захватывает в диспетчере (атрибут узла).
- MinValue/MaxValue (input): типизированные (xsi:type). Тип сохраняется через
  тип JSON-значения: число → xs:decimal, строка → xs:string.
- verticalSpacing: generic-скаляр группы (<VerticalSpacing>).
- rowsPicture: объектная форма {src, loadTransparent} — компилятор хардкодил
  LoadTransparent=false, теперь факт. значение (792 false / 327 true в корпусе).
- autoMaxWidth: суппресс multiLineDefault-эвристики (декомпилятор фиксирует факт.
  значение; multiLine-input без тега → autoMaxWidth:true).
- RowPictureDataPath: снят гейт hasMainTable у реинъекции smart-default (дин-список
  без mainTable тоже несёт <RowPictureDataPath>; ""-маркер ловит реальное отсутствие).

Зеркало py (компилятор). Кейсы input-fields/groups расширены и сертифицированы
загрузкой в 1С. Регресс 39/39 в обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 15:55:51 +03:00
Nick Shirokov bc81faf892 feat(form-decompile,form-compile): суппресс авто-вывода (MainAttribute/SavedData/AutoTitle) + AutoFillAvailableFields + SaveWindowSettings
Раундтрип TOTAL 40→25, match 138→146. Три класса «компилятор додумывает на
main/titled формах» (декомпилятор не давал суппресс-маркера) + два непокрытых свойства.

- MainAttribute: эвристика 11b.3 (нет явного main + ровно 1 объектный реквизит
  → помечает main). Декомпилятор зеркалит условие → ставит main:false (компилятор
  уже исключает такие кандидаты). Объектные реквизиты часто НЕ main (DynamicList
  1207 без, RecordSet 226 без, и т.д.). Decompiler-only.
- SavedData: эвристика $mainSaved (main + Catalog/Document/ChartOf*/ExchangePlan/
  BusinessProcess/Task Object + RecordManager → SavedData=true). Часто отсутствует
  (DocumentObject 332 без = 23%). Компилятор: явный savedData:false побеждает;
  декомпилятор ставит savedData:false для main-реквизита saved-типа без <SavedData>.
- AutoTitle: компилятор инъектит false при наличии title (~95% форм). Редкие 5%
  (Title есть, AutoTitle нет) → декомпилятор ставит autoTitle:"", компилятор
  пропускает пустую строку в Emit-Properties (общий ""-суппресс).
- AutoFillAvailableFields: свойство <Settings> дин-списка (дефолт true, эмит только
  отклонение false; ключ settings.autoFillAvailableFields).
- SaveWindowSettings: форменный bool (KNOWN_FORM_PROPS + auto-PascalCase).

Зеркало py (компилятор). Кейс dynamic-list-form +saveWindowSettings (сертифицирован).
Формы ЗадачаИсполнителя/Дополнительно, БизнесСеть/*, АнализПравДоступа → чисто.
Регресс 39/39 в обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:07:38 +03:00
Nick Shirokov 1a601a4d0c docs(form-decompile): GroupList — задокументировано как осознанно непокрытое
Форменное свойство <GroupList>id:uuid</GroupList> ссылается на члена формы по
оригинальному id + непрозрачному uuid. Компилятор раздаёт id заново (глобальный
счётчик по порядку эмиссии) → оригинальный id не воспроизводится; uuid нигде
больше в форме нет. Корректный раундтрип требовал бы имя-резолва с предпроходом
присвоения id (свойство эмитится до реквизитов) — рефактор ради редкого (30 на
корпус) свойства неясной семантики. Verbatim прошёл бы harness, но молча испортил
бы ссылку при загрузке. Решение: честный LOST, документируем ограничение.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:11:42 +03:00
Nick Shirokov 8aad132cc3 feat(form-decompile,form-compile): Command AssociatedTableElementId (используемая таблица)
Свойство команды «Используемая таблица» — ссылка по ИМЕНИ элемента-таблицы
(<AssociatedTableElementId xsi:type="xs:string">Имя</…>, 240 в корпусе, всегда
xs:string). Команда работает в контексте этой таблицы (текущая строка).

- Ключ table; forgiving-синонимы associatedTableElementId (XML-тег) и
  ИспользуемаяТаблица (рус., регистро-/пробело-незав.) — команды идут отдельным
  путём Emit-Commands, не через PROP_SYNONYMS слой Emit-Element.
- CurrentRowUse («использование текущей строки») уже поддерживался.

Зеркало py. Кейс table расширен (table + currentRowUse), сертифицирован
загрузкой в 1С. Регресс 39/39 в обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:58:04 +03:00
Nick Shirokov fd9539dd20 docs(form-dsl-spec): уточнение Table autofill — вспомогательные таблицы динамического списка
<Autofill>true> у таблицы появляется для вспомогательных таблиц динамического
списка (отборы/параметры/настройки, привязанные к КомпоновщикНастроек), где
состав колонок генерится платформой (ChildItems пуст). В палитре свойств не
показывается — внутренний флаг конструктора. Уточнено по домен-экспертизе.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:43:01 +03:00
Nick Shirokov 6807b07681 feat(form-decompile,form-compile): Table + PictureField leaf-скаляры (autofill/multipleChoice/searchOnInput/markIncomplete/hyperlink/shortcut)
Раундтрип TOTAL 61→57, match 130→135. Захват+эмит «как есть»:

- Table: autofill (<Autofill> — СВОЁ свойство таблицы, ≠ AutoCommandBar autofill
  = tableAutofill; редко, 270 в корпусе, всегда true = автогенерация колонок,
  ChildItems пуст у 93%), multipleChoice, searchOnInput (Auto/Use/DontUse),
  markIncomplete (<AutoMarkIncomplete>, общий ключ с input).
- PictureField: hyperlink (<Hyperlink> — кликабельная картинка), shortcut (<Shortcut>).

Зеркало py. Кейсы table + picture-field расширены, сертифицированы загрузкой
в 1С. Регресс 39/39 в обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:28:37 +03:00
Nick Shirokov 8bff8d7e05 fix(form-compile,form-decompile): Table <Height> vs <HeightInTableRows> — развод свойств
DSL зря сводил оба тега в height, а компилятор для таблицы всегда эмитил
<HeightInTableRows> → таблица с <Height>5</Height> регенерилась как
<HeightInTableRows>5</HeightInTableRows> (форма БизнесСеть/ВыборОрганизации).
Это РАЗНЫЕ свойства (высота элемента vs высота в строках), сосуществуют в 237
таблицах корпуса (Height-only 1242, HeightInTableRows-only 755).

- Развёл: height → <Height> (generic Emit-Layout, как у всех элементов),
  новый heightInTableRows → <HeightInTableRows>.
- Декомпилятор: Add-Layout ловит <Height> → height, Table-блок
  <HeightInTableRows> → heightInTableRows.

Зеркало py. Кейс table расширен (оба тега), сертифицирован в 1С (платформа
принимает оба). Регресс 39/39 в обоих рантаймах. Раундтрип TOTAL 68→61.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 11:12:56 +03:00
Nick Shirokov 4916f5bf7c fix(form-compile,form-decompile): UseAlways маркер "~" (query-поля дин-списка) — двойной префикс
Компилятор-баг: поле "~Список.Остановлен" (декомпилятор хранил verbatim) не
матчило проверку префикса ^Список\. → добавлялся ещё префикс →
"Список.~Список.Остановлен" (8+ форм выборки: ЗаявкаСотрудника*, Банки,
ОбеспечениеПроизводственныхПроцессов…). "~" — легитимный маркер query-полей
динамического списка (2234/17266 = 13% корпуса).

- Компилятор: префикс ИмяРеквизита. ставится ПОСЛЕ "~" (~Остановлен →
  ~Список.Остановлен); полная форма ~Список.X — verbatim (forgiving ввод).
- Декомпилятор: компактит ~Список.X → ~X (единообразно с короткими именами;
  компилятор разворачивает обратно).

Зеркало py. Кейс dynamic-list-form расширен (~Артикул + Список.Code +
Description), сертифицирован загрузкой в 1С. Регресс 39/39 в обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:44:36 +03:00
Nick Shirokov 7882c1cc2b feat(form-decompile,form-compile): InputField choice-скаляры verbatim + Button Check + FixingInTable + слой рус.синонимов ключей
Раундтрип TOTAL 121→86 (−35), match 114→124.

- InputField choice-кнопки: захват/эмит «как есть» (ChoiceButton/ClearButton/
  DropListButton/SpinButton эмитились только при true, теряя явный false;
  ChoiceButton=true сидел под лишним StartChoice-гейтом). Убран дефолт
  choiceButton=true у ref-полей в from-object — вывод не изменился.
- Новые InputField-скаляры: choiceListButton/quickChoice/autoChoiceIncomplete
  (bool), choiceForm/choiceHistoryOnInput/choiceFoldersAndItems/footerDataPath (value).
- Button checked → <Check>true</Check> (пометка toggle-кнопки). Ключ checked,
  не check (check — тип-ключ CheckBoxField, был бы конфликт диспетчера типов).
- fixingInTable — в generic-скаляры (input/labelField/колонки).
- Общий слой русских синонимов ключей-свойств ($propSynonyms/PROP_SYNONYMS):
  Пометка/Заголовок/Ширина/КнопкаВыбора/ФормаВыбора/… → канон. Нормализация
  в Emit-Element рядом с тип-синонимами; case/space-insensitive; англ. побеждает.

Зеркало py (контент байт-в-байт). Кейсы input-fields/table/synonyms расширены
и сертифицированы загрузкой в 1С. Регресс 39/39 зелёный в обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 22:34:15 +03:00
Nick Shirokov 4a68a977dc feat(form-decompile,form-compile): collapsedTitle — заголовок свёрнутой группы
Ключ collapsedTitle у группы → <CollapsedRepresentationTitle> (мультиязычный
текст, как title/inputHint). Заголовок свёрнутого представления у collapsible/
popup групп. 172 файла в корпусе, не обрабатывался.

Зеркало py (байт-в-байт). Кейс groups сертифицирован в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:51:03 +03:00
Nick Shirokov ab9b4c6197 fix(form-decompile): ExtendedTooltip own-content — formatted мультиязычный Title
Регресс из ExtendedTooltip own-content коммита: при own-layout (Width/Height/…) +
форматированный мультиязычный Title объектная форма теряла текст и formatted.

Причины: (1) merge брал $textVal['text'] на плоском мультиязычном {ru,en} (без
ключа text) → null-текст → пустой <Title>; (2) formatted не захватывался явно, а
компилятор не re-детектит markup на мультиязычном dict → formatted="false".

Фикс: разводим обёртку {text,formatted} от мультиязычного {ru,en} по наличию
ключа 'text'; formatted берём ЯВНО из атрибута <Title formatted>.

Форма erp/МобильноеПриложениеЗаказыКлиентов/ФормаГлавногоУзла → round-trip match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:36:41 +03:00
Nick Shirokov f7d5e2fd00 fix(form-decompile,form-compile): PictureDecoration имя-как-Ref + <xr:Abs> + порядок Title декораций
Баг: Emit-PictureDecoration брал $el.picture (тип-ключ = имя элемента) фолбэком
источника картинки → при отсутствии src писал <xr:Ref>ИмяДекорации>. Фикс:
источник картинки — ТОЛЬКО src.

<xr:Abs> (встроенная картинка, 131 в корпусе): декомпилятор ловил лишь xr:Ref →
теперь src:"abs:Имя" → <xr:Abs>Имя</xr:Abs> (префикс abs:, иначе <xr:Ref>).

Порядок: LabelDecoration эмитил Title перед own-content, а платформа — layout-first
(корпус 16970 vs 44). Переставил флаги/hyperlink/layout/оформление ПЕРЕД Title (как
ExtendedTooltip) — заодно убирает шум атрибуции харнесса на многострочном Title
(Height «уезжал» на родительскую группу; контент был корректен, ломалась line-
атрибуция). Форма МобильноеПриложениеПредприниматель → round-trip match.

Зеркало py (байт-в-байт). Снэпшоты events/element-appearance/additional-columns
обновлены (только порядок) и пере-сертифицированы в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:27:37 +03:00
Nick Shirokov 8915e99ac8 feat(form-decompile,form-compile): MobileDeviceCommandBarContent (состав моб-панели)
Форменное свойство <MobileDeviceCommandBarContent> = список имён командных
панелей/кнопок для командной панели мобильного устройства. DSL:
mobileCommandBarContent: ["Имя1", "Имя2", …] (массив строк).

Константы подтверждены по корпусу (161 файл): Presentation пустой,
CheckState=0, Value xsi:type=xs:string всегда — варьируется только имя-Value.
Компилятор ставит константы, эмитит перед AutoCommandBar; декомпилятор
собирает список имён.

Зеркало py (байт-в-байт). Форма BusinessProcesses/Задание/ФормаСписка →
round-trip match. Кейс commands сертифицирован в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 21:11:18 +03:00
Nick Shirokov ea43522b5a feat(form-decompile,form-compile): ExtendedTooltip own-content (объектная форма)
ExtendedTooltip — это LabelDecoration: может нести own-content (layout/оформление/
флаги/hyperlink) вместо/вместе с текстом. Объектная форма extendedTooltip:
{ text?, formatted?, width?, autoMaxWidth?, maxWidth?, height?, horizontalStretch?,
verticalAlign?, titleHeight?, hyperlink?, visible?, enabled?, textColor?, font?, … }.
Дизамбигуация от текст-формы (строка/ML/{text,formatted}) — по наличию структурного
ключа. Переиспользует Emit-Layout/Emit-Appearance/Emit-CommonFlags + Emit-GenericScalars.

Порядок: own-content ПЕРЕД Title (в корпусе layout-first 582 vs 10) — заодно убирает
шум атрибуции харнесса на многострочном контенте. Декомпилятор собирает объект
(Add-Layout/Add-GenericScalars/Add-Appearance/флаги/hyperlink), текст → .text;
текст-only остаётся строкой (обратная совместимость).

Форма WildberriesПереходРВБ: TOTAL 35→2 (остаток — отдельный rowsPicture
LoadTransparent). Зеркало py (байт-в-байт), кейс input-fields (own-content+текст и
width-only) сертифицирован в 1С. Регресс 39/39 ps1+py. Хвост: События компаньона
(нужно имя-обработчик) — отложено.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 20:55:17 +03:00
Nick Shirokov fb3bce5811 feat(form-decompile,form-compile): пачка простых скаляров (generic pass-through + точечные)
Generic pass-through таблица простых скаляров элемента (captured/emitted «как
есть»): verticalAlign, throughAlign, enableContentChange, pictureSize, titleHeight,
childItemsWidth, showLeftMargin, cellHyperlink, viewMode, verticalScrollBar,
rowInputMode, mask, createButton. Один Emit-GenericScalars в Emit-Layout (покрывает
все элементы) + Add-GenericScalars в пост-обработке декомпилятора (skip-if-present —
специфичная обработка побеждает).

Точечно: Command>modifiesSavedData (bool), Page>showTitle:false, фикс InputField>
textEdit (компилятор эмитил, декомпилятор не ловил).

Баг-фикс: <FillChecking> в схеме НЕТ (0 в корпусе) — реальный тег <FillCheck>
(значение ShowError). Ключ fillCheck (синоним fillChecking, forgiving bool→ShowError).

TOTAL 408→246 (−162), match 81→107. Зеркало py (байт-в-байт). Кейсы groups/commands/
attributes-types/pages сертифицированы в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 19:50:42 +03:00
Nick Shirokov 8ac0dfefd0 feat(form-decompile,form-compile): valueType — пустой Settings (список без ограничения типа)
Пустой <Settings xsi:type="v8:TypeDescription"/> у ValueList (список без
ограничения типа) — частый случай (в корпусе пустых 1893 vs непустых 864).
Три состояния valueType: нет ключа → нет Settings; "" → пустой <Settings…/>;
тип → с типом. Компилятор эмитит по присутствию ключа (включая ""); декомпилятор
для пустого Settings пишет маркер "". (ValueList без Settings вовсе — 58%, без
ключа valueType.)

Зеркало py. Кейс attributes-types: + СписокЛюбой с valueType:"" (пустой Settings),
сертифицирован в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:52:46 +03:00
Nick Shirokov 9f672044ff feat(form-decompile,form-compile): valueType — тип значений ValueList (Settings TypeDescription)
Ключ valueType у реквизита формы: <Settings xsi:type="v8:TypeDescription"> —
уточнение типа значений у реквизита типа ValueList (СписокЗначений). В корпусе
341/341 именно на ValueList. Грамматика значения = как у type (включая составной
"A | B" и квалификаторы string/decimal/date).

Forgiving-синонимы: typeDescription (1С «ОписаниеТипов» / зеркало XML xsi:type),
описаниеТипов, типЗначений — канон valueType (декомпилятор пишет его). ≠ дин-список
Settings (xsi:type="DynamicList", отдельный ключ settings).

Emit-Type параметризован тегом-обёрткой (Type / Settings), Decompile-Type работает
на любом type-узле. Зеркало py (байт-в-байт). Кейс attributes-types (составной
string(50)|decimal(10,2)) сертифицирован в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:23:16 +03:00
Nick Shirokov 7a2c257721 feat(form-compile): forgiving платформенные v8-типы (StandardPeriod/StandardBeginningDate/UUID)
Помогаем модели правильно вывести тип: принимаем англ. без префикса и рус.имя,
приводим к каноничному v8:X (эмитится verbatim):
- StandardPeriod / СтандартныйПериод → v8:StandardPeriod
- StandardBeginningDate / СтандартнаяДатаНачала → v8:StandardBeginningDate
- UUID / УникальныйИдентификатор → v8:UUID
- СписокЗначений → ValueList (v8:ValueListType)

Раньше bare StandardPeriod падал в "Unrecognized bare type" и эмитился без
префикса (невалидно). Input-сахар (декомпилятор пишет каноничный v8:). Зеркало
py. Кейс attributes-types использует рус. СтандартныйПериод (вывод тот же,
сертифицирован). Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 18:07:14 +03:00
Nick Shirokov 5a67c56e92 feat(form-decompile,form-compile): Save — сохранение значения реквизита в польз. настройках
Ключ save на реквизите формы (<Save><Field>…):
- true → <Field>имя</Field> (голое имя, 93% случаев)
- строка/массив строк → под-поля с авто-префиксом "имя." (путь с точкой,
  UUID 1/0:guid, или совпадающее с именем — берутся как есть)
- нет ключа или false → не эмитим

Гипотеза подтверждена на корпусе: Field=имя в 383/410 (93%); gating формовыми
SaveDataInSettings/AutoSaveDataInSettings НЕ требуется (51/160 форм с Save без них).
Период-кейс (голое имя Период + Период.EndDate/StartDate/Variant) round-trip
бит-в-бит. Переиспользует механику нормализации useAlways. ≠ savedData (отдельное,
уже было). Декомпилятор: один Field=имя → save:true, иначе массив со снятым
префиксом. Зеркало py (байт-в-байт), кейс attributes-types сертифицирован в 1С.
Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:59:55 +03:00
Nick Shirokov 09c3dcd988 feat(form-decompile,form-compile): HorizontalLocation у CommandBar (хвост дополнений)
Свойство horizontalLocation у элемента cmdBar (<CommandBar>): auto (дефолт,
не эмитим) / left / right / center, forgiving + рус.синонимы. Переиспользует
Get-HLocation от дополнений (+ добавлен center). Только у <CommandBar> в корпусе
(104 шт, в осн. Right), не у ButtonGroup/Popup/AutoCommandBar.

Компилятор (ps1+py байт-в-байт) + декомпилятор (захват <HorizontalLocation>).
Форма с CommandBar HorizontalLocation → round-trip match. Кейс commands расширен,
сертифицирован в 1С. Регресс 39/39 ps1+py. Закрывает остаток CommandBar>HorizontalLocation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:22:39 +03:00
Nick Shirokov d06c2c49bb feat(form-decompile,form-compile): дополнения командной панели таблицы (Search/ViewStatus/SearchControl)
Дополнения встроенного поиска таблицы как тип-элементы — обе позиции:

(1) Кастомные (в AutoCommandBar/ChildItems) → элементы в commandBar:
    { "searchString": "Имя", "source": "Список", "width": 15, ... }
    Полный набор свойств поля (Emit-Layout/Appearance/CommonFlags/tooltip);
    source дефолт = родительская таблица; horizontalLocation auto/left/right.

(2) Стандартные (авто-генерация на уровне таблицы) → per-table карта
    отклонений additions: { viewStatus: { horizontalLocation: "left" } }.

Тип-как-ключ searchString/viewStatus/searchControl, forgiving-синонимы
(XML-тег, <Type>, рус.имя, имя «Вид» из конфигуратора). Декомпилятор разводит
по позиции (ChildItems → commandBar.children; прямые дети <Table> → карта
additions, только deviations); убран из COMPANION_TAGS, +ELEMENT_KEY.

Хвост: CommandBarLocation авто-вывод для дин-список-таблицы — суппресс-маркер
"" (компилятор инжектит None, верно по корпусу 203≈213; декомпилятор инвертирует:
нет тега → "", None → опускает, иначе → захват).

Зеркало py (байт-в-байт). Синтет-фикстура (upload/epf/ДополненияКП) — perfect
round-trip LOST 0/ADDED 0. Кейс dynamic-list-form расширен (кастомное+override),
сертифицирован в 1С. Регресс 39/39 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:04:52 +03:00
Nick Shirokov 01b598ce8f test(form): регресс-кейсы на уникальность имён + позитивный гард неймспейсов
Негативные кейсы (ожидают [ERROR] + exit 1):
- form-compile: дубль имени элемента / дубль имени команды;
- form-edit: добавление элемента с уже существующим в форме именем;
- form-validate: форма с дублирующимся именем элемента.

Позитивный гард (компилируется без ошибок):
- form-compile: имя реквизита == имя элемента — легально, раздельные неймспейсы;
  защищает emit_element от случайного слияния пулов имён.

Дополнительно прогнано на 38 781 реальной форме выгрузок ERP/ACC/УНФ —
ноль ложных срабатываний новой проверки.

Co-authored-by: brake71 <8448482+brake71@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:32:09 +03:00
Nick Shirokov 41e4714773 fix(form): уникальность имён во всех коллекциях форм + префикс колонок субконто ЧПС
Проверка уникальности имён элементов форм (основа — PR #21 от brake71),
портированная на актуальную ветку и расширенная на все именованные коллекции.

Корень проблемы: генератор формы счёта ПланаСчетов строил колонки таблицы
субконто с «голыми» именами (Валютный, ТолькоОбороты, ВидСубконто), из-за чего
флаг субконто сталкивался с одноимённым признаком учёта счёта → невалидный для
1С XML (форма не открывалась). Теперь имена колонок префиксуются именем таблицы
(ВидыСубконтоВалютный) — как делает generic-путь табчастей и типовая 1С.

- form-compile: fail-fast проверка уникальности в едином emit_element + по
  реквизитам, колонкам (в пределах реквизита), параметрам и командам. Хелпер
  вместо копипаста; проверка после нормализации синонимов.
- form-validate: проверка имён симметрично существующим id-пулам (элементы,
  реквизиты, колонки, команды) + новый блок параметров.
- form-edit: дедуп внутри JSON-определения и против существующих в форме —
  для элементов (рекурсивно), реквизитов (+колонки) и команд; WARN→ERROR.

Каждая коллекция — свой неймспейс (имя реквизита и имя элемента могут совпадать
легально). PS1 и PY — зеркальны. Версии: form-compile 1.74, form-validate 1.7,
form-edit 1.1. Все тест-сеты зелёные на обоих рантаймах.

Co-authored-by: brake71 <8448482+brake71@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:16:12 +03:00
Nick Shirokov ad89929efd fix(form-compile): актуализация allowlist knownKeys (оформление + autoCmdBar)
Allowlist дрейфовал — element-level appearance-ключи (11 канонических +
12 рус.синонимов: textColor/цветтекста/font/шрифт/border/рамка/title*/footer*)
читаются через Get-AppearanceValue и НЕ переименовываются, поэтому сыпали
ложный warning «unknown key» для валидных свойств закрытого кластера Appearance.

Решение самоподдерживающееся: union allowlist с самими структурами
appearance — ps1 foreach по appearanceSpec/appearanceSynonyms.Keys после
литерала; py доп. проверка `not in APPEARANCE_SPEC/APPEARANCE_SYNONYMS`.
Не дрейфует при добавлении новых ключей/синонимов. + статический autoCmdBar.

Проверено: appearance-ключи (canonical + рус.синонимы) не предупреждают,
реальные опечатки (textColorr/bogusKey) по-прежнему ловятся — оба рантайма.
Регресс 36/36 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 23:10:32 +03:00
Nick Shirokov 929d1676bb feat(form-decompile,form-compile): формат и формат редактирования (Format/EditFormat)
Ключи format/editFormat на InputField/LabelField/CheckBoxField (1408/958
файлов в корпусе). LocalStringType как inputHint/title: строка или {ru,en} —
переиспользование Emit-MLText (компилятор) и Get-LangText (декомпилятор).
Декомпилятор: хелпер Add-FormatProps в 3 ветках. Зеркало py.

Round-trip чистый (0 остатка на 30 формах с Format/EditFormat). Кейс
input-fields: format/editFormat строкой на input/check + мультиязык-объект
на labelField; снэпшот сертифицирован загрузкой в 1С.

Заодно добавлены забытые ключи (format/editFormat/choiceParameters/
choiceParameterLinks/typeLink) в allowlist knownKeys (ps1+py), чтобы не
сыпать ложный warning «unknown key».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:53:29 +03:00
Nick Shirokov d0bb26b068 feat(form-decompile): пустой <Presentation/> в choiceList → presentation:"" (суппресс-маркер)
Декомпилятор фиксирует пустой <Presentation/> элемента choiceList как
presentation:"" вместо опускания ключа. Раньше пустота терялась →
компилятор додумывал presentation из значения (Title-FromName для
DesignTimeRef) → LOST <Presentation/> + ADDED непустой <Presentation>.

Паттерн B (суппресс-маркер ""): компилятор УЖЕ обрабатывал presentation:""
корректно (hasPres=true → авто-вывод пропускается; Emit-ChoicePresentation
на "" → пустой <Presentation/>). Правка только в декомпиляторе.

Presentation ушёл из diff на Билеты/КлассификаторПАТСАТУРН/ОстаткиПартийСАТУРН
(подтверждено round-trip). Decompiler-only → регресс через harness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:32:25 +03:00
Nick Shirokov f9ae24a678 fix(form-compile): пустая строка-значение self-closing + Enum.X.EmptyRef без EnumValue
Два общих бага value (всплыли на полном прогоне 2.17, чинят и choiceList,
и параметры выбора):

- Пустое строковое значение эмитилось <Value xsi:type="xs:string"></Value>
  вместо самозакрывающегося <Value xsi:type="xs:string"/>. Введён хелпер
  Get-ChoiceValueTag (3 места: choiceList scalar + choiceParam scalar +
  FixedArray inner). Форма АктивныеПользователи теперь round-trip match.
- Enum.X.EmptyRef нормализатор ломал вставкой .EnumValue. → EnumValue.EmptyRef
  (EmptyRef — пустая ссылка перечисления, не значение). Фикс в Normalize-
  ChoiceValue (Enum-ветка): EmptyRef сохраняется как есть.

Зеркало py. Кейсы: input-fields (пустая строка в choiceList), radio-auto-enum
(Enum.X.EmptyRef) — оба сертифицированы загрузкой в 1С. Регресс 36/36 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:27:04 +03:00
Nick Shirokov 339c70b457 feat(form-compile): xs:dateTime в значениях параметров выбора / choiceList
Авто-детект ISO-datetime (^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$) в
Normalize-ChoiceValue → xs:dateTime вместо xs:string. Без изменения DSL:
декомпилятор уже отдаёт строку, компилятор распознаёт формат. Заодно
исправляет choiceList со значениями-датами (та же FormChoiceListDesTimeValue).

Round-trip бит-в-бит на 3 формах с датами в параметрах выбора. Кейс
input-fields: Отбор.Дата в объектной и короткой формах; снэпшот
сертифицирован загрузкой в 1С. Закрывает микро-хвост кластера ChoiceParameters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:11:34 +03:00
Nick Shirokov 65f63fbc48 feat(form-compile): короткая форма для параметров выбора / связей / связи по типу
Входной сахар (декомпилятор по-прежнему пишет объектную модель):
- choiceParameters: ["Отбор.Х=true", "Отбор.Вид=Enum.A, Enum.B"] — name=value,
  запятые → массив, литералы коэрсятся (true/false → bool, число → number,
  остальное → строка/ref через Normalize-ChoiceValue).
- choiceParameterLinks: ["Отбор.Орг=ОбычноеПоле", "Отбор.Тип=Поле:DontChange"] —
  name=dataPath, опц. хвост :Clear/:DontChange (дефолт Clear).
- typeLink: "ПолеПодсказка" или "ПолеПодсказка#0".

Парсеры на входе каждого эмиттера (строка → объект), далее общий путь.
Байт-в-байт идентично объектной форме (проверено ps1+py). Кейс input-fields:
поле ПолеСвязиКратко на короткой форме рядом с объектным; снэпшот
сертифицирован загрузкой в 1С. Версия компилятора синхронизирована (py 1.67→1.69).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:05:30 +03:00
Nick Shirokov 0397d8f37e feat(form-decompile,form-compile): параметры выбора / связи параметров выбора / связь по типу (кластер ChoiceParameters)
Покрытие трёх конструкций поля ввода (InputField), массовых в корпусе
(ChoiceParameters 844 / ChoiceParameterLinks 685 / TypeLink 84 файлов):

- choiceParameters: [{name, value}] — параметры выбора. value через общий
  Normalize-ChoiceValue (bool/число/строка/ref-путь + синонимы Перечисление./
  Справочник.); массив значений → v8:FixedArray. Presentation всегда пустой.
- choiceParameterLinks: [{name, dataPath, valueChange?}] — связи параметров
  выбора. valueChange дефолт Clear (опускается декомпилятором), forgiving
  Clear/DontChange + рус.синонимы. DataPath xsi:type=xs:string.
- typeLink: {dataPath, linkItem} — связь по типу. linkItem дефолт 0.

Декомпилятор: регистрация app namespace; инверсные хелперы (FixedArray →
массив, дефолт Clear опускается). Компилятор ps1 + зеркало py (байт-в-байт).
Spec: секция в #### input. Тест-кейс input-fields расширен полем со всеми
тремя конструкциями; round-trip бит-в-бит на 3 реальных формах; снэпшот
сертифицирован загрузкой в 1С 8.3.24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:59:26 +03:00
Nick Shirokov d88859580f feat(form-compile): оформление PictureDecoration — un-defer (хвост Appearance закрыт)
Ранее отложил PictureDecoration, ошибочно решив, что падение загрузки = порядок тегов (XSD
расщепляет appearance вокруг <Title>). Разбор остатка показал: падало из-за НЕВАЛИДНОЙ ссылки
на картинку в тесте (picture:"Картинка" → <xr:Ref>Картинка</xr:Ref>, такой CommonPicture нет),
а не из-за оформления. Проверка с валидной StdPicture.Print + appearance → грузится чисто.

Ключевой факт: 1С ТОЛЕРАНТНА к порядку оформления внутри элемента. LabelDecoration в корпусе
тоже расщепляет appearance вокруг Title (TextColor/Font до Title — тысячи раз; BackColor/Border
после), но компилятор эмитит contiguous-после-Title — и LabelDecoration сертифицировался. Значит
профиль decoration валиден и для PictureDecoration.

Разведён 12-й эмиттер (pictureDecoration, профиль decoration), PS+Python. Кейс element-appearance
расширен PictureDecoration (StdPicture.Print, чистое имя через src). Сертификация загрузкой —
чисто. Регресс 36/36 ps+py. Harness: остаток appearance LOST = 0 (был PictureDecoration 2),
TOTAL 1146→1144; весь кластер Appearance 1326→1144 (−182). Версия form-compile v1.67.
2026-06-07 21:20:23 +03:00
Nick Shirokov d5d525aa27 feat(form-compile): оформление групп/таблиц/picField/calendar + декод \u-комментариев (хвост Appearance)
Развёл Emit-Appearance ещё в 5 эмиттеров: UsualGroup/ColumnGroup/Table/PictureField/CalendarField
(профиль field; декомпилятор их уже захватывал в Add-CommonProps — теперь компилятор эмитит).
Порядок собственного оформления по корпусу: группа TitleTextColor/TitleFont/BackColor; таблица
BackColor/BorderColor — укладываются в field-профиль. Зеркало PS+Python.

PictureDecoration НЕ разведён намеренно: его XSD расщепляет оформление вокруг <Title>
(TextColor/Font до Title, Border после) + позиция <Picture> — отдельный мелкий кластер (2 строки).

Сертификация загрузкой в 1С 8.3.24: element-appearance (+ группа с BackColor),
dynamic-list-parameters (+ Table backColor/borderColor, колонка titleTextColor/border) — чисто.
Регресс 36/36 ps+py. Harness 1177→1146 (−31; весь кластер Appearance 1326→1146 = −180),
остаток appearance LOST = PictureDecoration 2, ADDED-регрессий 0.

Попутно: декодированы garbled \u-escape в КОММЕНТАРИЯХ form-compile.py (артефакт Edit-инструмента,
переэкранирующего кириллицу под ASCII-конвенцию файла; в комментариях \u не интерпретируется).
Строковые литералы (имена компаньонов) остаются \u-escaped — там escape функционален.
Версия form-compile v1.66.
2026-06-07 21:03:18 +03:00
Nick Shirokov 1888952a41 feat(form-decompile,form-compile): оформление элементов — цвета/шрифты/граница (кластер Appearance)
Прямые свойства оформления элемента: <TextColor>/<BackColor>/<BorderColor> + header/footer
(<TitleTextColor>/<FooterBackColor>/…), <Font>, <Border>. Раньше терялись при декомпиляции и
не эмитились. Выборка 2.17: LabelDecoration>TextColor 49, InputField>Border/TextColor, Button*3,
LabelField header/footer (колонки).

DSL: ключи англ. camelCase 1:1 с тегами (textColor/backColor/borderColor/titleTextColor/…/
font/border) + приём рус. синонимов (ЦветТекста/ЦветФона/ЦветРамки/Шрифт/Рамка/…Заголовка/…Подвала).
- Цвет — verbatim-строка: style:/web:/win:/#RRGGBB (компилятор не валидирует; win: валиден —
  win:MenuBar/ButtonText/…; sys: в выгрузках не встречается; несуществующее имя → ошибка загрузки).
- Шрифт — строка "style:X" → <Font ref kind=StyleItem/> (минимальная форма); объект → только
  заданные атрибуты (ref/faceName/height/bold/italic/underline/strikeout/kind/scale), дефолты не
  досочиняются. Декомпилятор: чистый style-ref → строка, иначе объект (точный набор атрибутов).
- Граница — строка/{ref} → <Border ref="style:X"/>; {width,style} → явная (ControlBorderType:
  Single/Double/Underline/DoubleUnderline/Overline/Embossed/Indented/WithoutBorder).

Порядок тегов в XML — XSD-профиль по базовому типу (field/decoration/button), компилятор
расставляет сам; вставка перед компаньонами. Декомпилятор — захват в Add-CommonProps (все типы).
Компилятор разведён в 6 эмиттеров (input/check/radio/label/labelField/button) + зеркало Python.

Валидация: round-trip фикстура ПроверкаПользовательскихНастроек — 10/10 LabelField (стили рамки,
header/footer, Absolute/style шрифты) бит-в-бит. Сертификация загрузкой в 1С 8.3.24 (кейс
element-appearance: decoration/field/button, цвета hex/web/style, шрифты, границы) — чисто.
Регресс 36/36 ps+py. Harness sample-2.17 TOTAL 1326→1177 (−149), 0 ADDED-регрессий.
Версии: form-compile v1.65, form-decompile v0.47. Попутно: декод garbled \u-комментариев в .py.

Остаток (декомпилятор захватывает, компилятор пока не эмитит — не регресс): UsualGroup 9, Table 9,
PictureDecoration 2 — отдельным шагом.
2026-06-07 20:42:53 +03:00
Nick Shirokov ff93d169a8 test(form-compile): сертификация снэпшота dynamic-list-parameters в 1С + фикс путей колонок
verify-snapshots: при ManualQuery поля дин-списка = псевдонимы запроса (Код/Наименование),
а не авто-пути mainTable (Code/Description). Колонки кейса ссылались на Список.Code/Description
→ "Неверный путь к данным" при загрузке. Поправил пути под псевдонимы запроса, перегенерил снэпшот.
Форма (ручной запрос + полная грамматика параметров: availableValues/use/denyIncompleteValues)
загружается в платформу 8.3.24 чисто. Сами schema-параметры платформа приняла без замечаний.
2026-06-07 19:32:26 +03:00
Nick Shirokov b692a81ea5 feat(form-decompile,form-compile): schema-параметры динамического списка (кластер DynamicList Settings>Parameters)
Захват/эмиссия <Parameter> (DataCompositionSchemaParameter) внутри <Settings xsi:type="DynamicList"> —
та же сущность, что параметры СКД, но обёртка <Parameter> + дети dcssch:. Ранее теряли при
декомпиляции и не умели эмитить (компилятор обрывал на Field*→MainTable). Корпус acc+erp 8.3.24:
123 формы, 406 параметров.

DSL: ключ settings.parameters переиспользует грамматику параметров СКД ОДИН-В-ОДИН (shorthand
"Имя [Заголовок]: Тип = Значение @valueList @hidden" + объектная форма; ключи value/type/
availableValues/inputParameters/use/denyIncompleteValues/expression/availableAsField). Новых
сущностей не вводим — модель переносит знание skd-параметров напрямую.

Контекстные дефолты дин-списка (паттерн «умный дефолт у всегда-эмитируемого тега», для модели
невидимы — просто опускает ключ):
- useRestriction: эмитим ВСЕГДА, дефолт true (в СКД дефолт false); false → объект useRestriction:false;
- title: авто из имени через Title-FromName (camelCase-split с сохранением аббревиатур); явный
  заголовок — только при отклонении от авто-вывода;
- value: пустое всегда xsi:nil, даже при известном типе (в отличие от типизированного пустого в СКД).

Канон. порядок детей по корпусу: name, title, valueType, value, useRestriction, expression,
availableValue*, valueListAllowed, availableAsField, inputParameters, denyIncompleteValues, use.

Декомпилятор PS-only (зеркало в py отложено, как и весь form-decompile); компилятор зеркалён в py.
Валидация: round-trip бит-в-бит на ФормаОстатков (субсет) + ПроверкаПользовательскихНастроек
(полная грамматика: availableValues/inputParameters/denyIncompleteValues/use). Регресс 35/35 ps+py.
Тест-кейс dynamic-list-parameters (+снэпшот). spec обновлён. Версии: form-compile v1.64, form-decompile v0.46.
2026-06-07 19:29:47 +03:00
Nick Shirokov bc53ee2a14 feat(form-decompile,form-compile): картинки заголовка/подвала колонок + объектная модель картинки-ссылки (кластер column-pictures)
Декомпилятор терял картинки колонок таблиц формы:
- HeaderPicture/FooterPicture (общие для любого поля-колонки: input/check/labelField/picField)
- ValuesPicture: захват флага LoadTransparent (раньше брался только Ref)
- EditMode у PictureField

Единый формат "картинка-ссылка" (headerPicture/footerPicture/valuesPicture):
скаляр (Ref, loadTransparent=false — частый случай по корпусу ~64%) ИЛИ
объект {src, loadTransparent:true} для отклонения. Платформа всегда эмитит
<xr:LoadTransparent>, дефолт DSL=false. Флаг на каждой картинке (на одном
поле их бывает несколько).

Компилятор: HeaderPicture эмитится сразу после <EditMode> (порядок XDTO
строгий — иначе LoadConfigFromFiles падает с XDTO-исключением).

Forgiving-объект для <Picture> кнопки/попапа/команды: общий хелпер
Emit-CommandPicture принимает скаляр ИЛИ {src, loadTransparent}, чтобы
модель могла описать картинку объектно по аналогии с headerPicture.
Полярность кнопки сохранена (дефолт true). Декомпилятор не трогали —
объект только на вход, раундтрип без изменений.

Раундтрип sample-2.17: TOTAL 1582→1457, dec-fail/compile-fail 0.
Снапшот picture-field пересертифицирован в 1С. Регресс 34/34 ps+python.
Декомпилятор v0.45, компилятор v1.63.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:16:38 +03:00
Nick Shirokov 0636ac6877 fix(form-decompile): направление группы — '' при отсутствии <Group> (кластер UsualGroup/ColumnGroup>Group)
Декомпилятор при отсутствии <Group> подставлял дефолт (vertical у UsualGroup,
horizontal у ColumnGroup), и компилятор его эмитил → <Group> додумывался там,
где в оригинале тега нет (ADDED: ColumnGroup>Group=103, UsualGroup>Group=65).

«Опустить дефолт» не подходит: на корпусе Vertical у UsualGroup в двух ролях —
явный <Group>Vertical</Group> (51205, 36%, хранить) и опускаемый дефолт (10%,
тега нет). 1C сериализует «Группировку», только если задана в конфигураторе,
даже Vertical. По значению неразличимы → нужен отдельный маркер «тега не было».

Ключ group/columnGroup — тип-дискриминатор, опустить нельзя. Поэтому нет <Group>
→ значение '' (тип сохраняется, направление не эмитим). Компилятор уже опускает
<Group> при пустом/нераспознанном значении — правка только в декомпиляторе.
spec для group/columnGroup обновлён.

TOTAL diff lines выборки 2.17: 1750 → 1582 (−168); match 23 → 31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:46:52 +03:00
Nick Shirokov cfce486004 feat(form-decompile,form-compile): заголовок реквизита — суппресс-маркер "" + omit авто-вывода (кластер Attribute>Title)
Компилятор для не-main реквизита БЕЗ ключа title додумывал <Title> из имени,
хотя платформа реквизит без синонима хранит без <Title>. На корпусе (295609
реквизитов): 22% без <Title> — всем додумывался заголовок (ADDED Attribute>Title
= 170 в выборке).

Компилятор (ps1+py): эмиссия Title реквизита приведена к логике Emit-Title —
нет ключа → авто-вывод (кроме main); title "" → подавить (раньше "" был falsy
и уходил в авто-вывод — это и был баг); непустой → как есть.

Декомпилятор (ps1): нет <Title> → title:"" (суппресс-маркер); ru-only заголовок,
равный авто-выводу из имени → опускаем ключ (компилятор воспроизведёт, 35% =
103908 реквизитов корпуса); иначе → явный. Скопировано точное зеркало
Title-FromName для сверки.

Регресс: attributes-types.json — реквизит с title:"" (подавление) рядом с
авто-выводом + снэпшот. spec §реквизиты обновлён.

TOTAL diff lines выборки 2.17: 2255 → 1750 (−505); cascade ADDED 292 → 33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:33:02 +03:00
Nick Shirokov 2367eaa353 fix(form-compile): Page с направлением раскладки → не путать с UsualGroup
У Page/Pages ключ `group` означает направление раскладки детей
(<Group>Horizontal</Group>), а не тип UsualGroup. Диспетчер типа элемента
проверял `group` раньше `page`/`pages`, поэтому страница с горизонтальной
раскладкой компилировалась как <UsualGroup name="horizontal"> вместо <Page>.

Переставил `pages`/`page` перед `group` в TYPE_KEYS (ps1 + py зеркало) —
реальная UsualGroup ключа page/pages не несёт, конфликта нет. Emit-Page уже
корректно эмитит <Group> из ключа group.

Регресс: добавил group:horizontal на страницу в кейс pages.json + снэпшот.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 16:02:16 +03:00
Nick Shirokov 8448a28a29 feat(form-decompile,form-compile): loadTransparent картинки команд/кнопок/попапов (захват явного false)
Прозрачность картинки (<Picture><xr:LoadTransparent>) у Command/Button/Popup компилятор
хардкодил true, а в корпусе значение смешано (Command true 11410/false 8066;
Popup true 3142/false 2828). Явный false терялся.

Теперь компилятор эмитит loadTransparent факт. значение (дефолт true — платформа всегда
пишет тег внутри Picture; false при явном loadTransparent:false). Декомпилятор фиксирует
ТОЛЬКО отклонение false (true опускается — додумывается дефолтом, без шума в DSL).
Свойство плоское рядом с picture — консистентно с PictureDecoration(src)/PictureField(valuesPicture).

TOTAL diff lines выборки 2.17: 2489 → 2415 (-74). Command/Button/Popup LoadTransparent
residual → 0. Остаток (отдельный хвост): PictureField (HeaderPicture/valuesPicture),
CheckBoxField-cascade, Table rowsPicture — другие картиночные объекты. Снапшот button-group
(popup loadTransparent:false) сертифицирован в 1С (8.3.24). Регресс form-compile 34/34
зелёный на ps + python. decompile v0.42, compile v1.60.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:46:40 +03:00
Nick Shirokov 4430ebc42e fix(form-compile,form-decompile): Popup-с-картинкой → PictureDecoration (дизамбигуация типа) + Button DataPath
Корень: тип-детекция элемента видела ключ 'picture' РАНЬШЕ 'popup'/'button' в списке
типов. Но 'picture' это и тип (PictureDecoration), и свойство-иконка у popup/button/cmdBar
(оба строковые → не развязать по значению). Попап «{ popup, picture, representation }»
ошибочно компилировался как <PictureDecoration name="StdPicture.X"> → терялся весь попап
+ каскад. Решение: понизить приоритет picture/picField (в конец TYPE_KEYS) — тип-ключ
владельца выигрывает.

+ Button DataPath: кнопки общих команд несут <DataPath> (Объект.Ref, Items.X.CurrentData.Поле,
2706 в корпусе) — привязка команды к контексту. Не захватывался. Добавлен ключ path у button.

TOTAL diff lines выборки 2.17: 2727 → 2489 (-238). Снапшот button-group (+popup с картинкой)
сертифицирован в 1С (8.3.24). Регресс form-compile 34/34 зелёный на ps + python.
decompile v0.41, compile v1.59.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:28:21 +03:00
Nick Shirokov 5112dbec9e feat(form-decompile,form-compile): HorizontalStretch/VerticalStretch — захват явного false
Растягивание (<HorizontalStretch>/<VerticalStretch>) платформа эмитит явным значением
(false 38145 / true 25002 для HS; на Input/Label/Picture/Group/CommandBar). Декомпилятор/
компилятор работали только с true → явный false терялся.

Теперь захват и эмиссия фактического значения (true И false); отсутствие = дефолт
(не эмитим). Бэк-совместимо: true как раньше, +false. Раньше декомпилятор писал ключ
лишь при true — теперь и при false.

TOTAL diff lines выборки 2.17: 2912 → 2727 (-185), match 20 → 22. Stretch residual
92 → 1 (остаток — на companion ExtendedTooltip, отдельный кластер). Снапшот input-fields
(+stretch false) сертифицирован в 1С (8.3.24). Регресс form-compile 34/34 зелёный
на ps + python. decompile v0.40, compile v1.58.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 15:11:12 +03:00
Nick Shirokov 701d56b075 feat(form-decompile,form-compile): размазанный блок скаляров InputField + общие cell-свойства полей
Партия простых скаляров полей, которые не захватывались (платформа эмитит явное
не-дефолтное значение):
  - InputField-специфичные: Wrap, OpenButton, ListChoiceMode, ExtendedEditMultipleValues,
    ChooseType, ChoiceButtonRepresentation — в Emit-Input / InputField-кейс.
  - Общие cell-свойства поля-колонки (Input/Label/Picture/CheckBox/ColumnGroup):
    ShowInHeader, ShowInFooter, AutoCellHeight, FooterHorizontalAlign, HeaderHorizontalAlign —
    вынесены в общий Emit-CommonElementProps / Add-Layout (захват «как есть»).
    ColumnGroup: собственная эмиссия ShowInHeader убрана (общий путь покрывает) —
    устранён двойной эмит.

TOTAL diff lines выборки 2.17: 3179 → 2912 (-267), match 17 → 20 (+3 чистых формы).
Все обработанные скаляры residual → 0. Снапшот input-fields (+скаляры) сертифицирован
в 1С (8.3.24). Регресс form-compile 34/34 зелёный на ps + python.
decompile v0.39, compile v1.57.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:58:17 +03:00
Nick Shirokov 27f5da4829 feat(form-decompile,form-compile): поведение группы — ключ behavior (usual/collapsible/popup)
<Behavior> у UsualGroup: дефолт Авто (не эмитится), явные Обычное/Свертываемая/Всплывающая
эмитятся. Платформа пишет тег только при не-Авто (чистое правило эмиссии). Раньше
декомпилятор мапил только Collapsible (в group:'collapsible'), теряя явный Usual (28069)
и PopUp (141).

Развязаны group (направление) и behavior:
  - group: horizontal/vertical/alwaysHorizontal/alwaysVertical (направление);
  - behavior: usual/collapsible/popup → <Behavior>; отсутствие = Авто.
Legacy group:'collapsible' принимается (= vertical + behavior collapsible) — старые входы целы.

Collapsed обобщён: эмитится при collapsed:true независимо от behavior (в XML <Collapsed>
изредка есть и у PopUp/Usual — round-trip ловит фактическое наличие).

TOTAL diff lines выборки 2.17: 3347 → 3179 (-168). UsualGroup>Behavior residual → 0.
Снапшот groups (behavior usual + collapsible/collapsed) сертифицирован в 1С (8.3.24).
Регресс form-compile 34/34 зелёный на ps + python. decompile v0.38, compile v1.56.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:18:40 +03:00
Nick Shirokov 786bdf97d9 feat(form-decompile,form-compile): AdditionalColumns — доп. колонки табличных частей объекта
<Columns><AdditionalColumns table="Объект.ТабЧасть"><Column>…</AdditionalColumns></Columns>
у главного реквизита-объекта (3654 формы, 10187 блоков) — форма-определённые доп.
колонки табличных частей. Декомпилятор читал только прямые <Column> (SelectNodes lf:Column),
теряя AdditionalColumns целиком (часто весь <Columns> блок объекта).

Ключ реквизита additionalColumns: [{ table, columns: [<col>] }]; <col> — та же грамматика,
что у columns (name/type/title/functionalOptions). Общие хелперы Emit/Decompile-AttrColumn
(переиспользуются прямыми колонками и AdditionalColumns). Порядок схемы: прямые <Column>
сначала, затем AdditionalColumns-группы.

TOTAL diff lines выборки 2.17: 3695 → 3347 (-348). Attribute>Columns/AdditionalColumns
residual → 0. Новый кейс additional-columns (DataProcessor с табчастью + форма) сертифицирован
в 1С (8.3.24). Регресс form-compile 34/34 зелёный на ps + python.
decompile v0.37, compile v1.55.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:47:28 +03:00
Nick Shirokov 6056a4a5af feat(form-decompile,form-compile): UseAlways — поля реквизита, всегда читаемые (две формы DSL)
<UseAlways><Field>ИмяРеквизита.Поле</Field>…> у Attribute (5189: дин-список 3575 +
ValueTable ~788 + прочие) — не захватывался. Свойство «поля, всегда читаемые из БД».

DSL — две формы (сливаются компилятором):
  - на реквизите: useAlways: ["Поле1","Поле2"] (короткие имена; forgiving с/без префикса);
  - на колонке ValueTable: useAlways: true (columns[*]).
Компилятор собирает <Field>ИмяРеквизита.X</Field> из обоих источников (dedupe),
порядок схемы: после FillChecking, до FunctionalOptions/Columns/Settings.

Дин-список: колонки в XML не эмитятся, но если заданы в DSL с useAlways — формируют
UseAlways-массив (Columns подавляются при наличии settings).

Декомпилятор по контексту: ValueTable (есть columns) → useAlways:true на совпавшей
колонке; дин-список/прочие → массив useAlways на реквизите. Префикс «Имя.» снимается.

TOTAL diff lines выборки 2.17: 3869 → 3695 (-174), match 14 → 17 (+3 чистых формы).
Attribute>UseAlways residual → 0. Снапшот table (обе формы + merge) сертифицирован
в 1С (8.3.24). Регресс form-compile 33/33 зелёный на ps + python.
decompile v0.36, compile v1.54.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:29:39 +03:00
Nick Shirokov fdbfa3b643 feat(form-decompile,form-compile): функциональные опции + фиксы round-trip типов (ValueList/UUID/платформенные)
1) Функциональные опции (<FunctionalOptions><Item>FunctionalOption.X</Item>…>) у
   Attribute (4391) / Command (2385) / Column (1272) — не захватывались. Ключ
   functionalOptions (массив имён; forgiving "X"/"FunctionalOption.X"; GUID-опции
   расширений — как есть). Общий хелпер Emit/Decompile-FunctionalOptions (+py).
   Порядок: атрибут после FillChecking; команда после Action; колонка после Type.

2) ValueList round-trip баг: Decompile-Type switch без break → общий case
   ^(v8|v8ui|cfg): перетирал специфичный v8:ValueListType → выдавал «ValueListType»
   (голый), компилятор эмитил <v8:Type>ValueListType</v8:Type> без префикса.
   Добавлены break во все cases.

3) Платформенные типы без friendly-шортката (v8:UUID 3132, v8:StandardPeriod 233,
   v8:Null, v8:StandardBeginningDate, v8ui:VerticalAlign …) теряли префикс
   (декомпилятор снимал v8:, компилятор эмитил голый). Теперь декомпилятор оставляет
   префикс для не-friendly v8:/v8ui: типов (friendly — ValueTable/ValueTree/ValueList/
   TypeDescription/FormattedString/Picture/Color/Font — шорткат), компилятор эмитит
   токены с префиксом (v8:/v8ui:/xs:/dcs*:) verbatim. Покрыт весь хвост.

TOTAL diff lines выборки 2.17: 4068 → 3869 (-199). FunctionalOptions/ValueListType/UUID
residual → 0. Снапшот attributes-types (+ValueList, +v8:UUID) сертифицирован в 1С (8.3.24).
Регресс form-compile 33/33 зелёный на ps + python. decompile v0.35, compile v1.53.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 13:11:14 +03:00
Nick Shirokov 9b77f06aba feat(form-decompile,form-compile): наборы типов TypeSet (определяемый тип, характеристика, любая ссылка)
<v8:TypeSet> (набор типов) не поддерживался: Decompile-Type итерировал только v8:Type
→ тип колонки/реквизита/параметра с TypeSet терялся (компилятор эмитил пустой <Type/>).

Покрыто (9282 вхождения в корпусе):
  - DefinedType.X (6515) — определяемый тип (синоним ОпределяемыйТип.X)
  - Characteristic.X (216) — характеристика (синоним Характеристика.X)
  - AnyRef (268) / AnyIBRef (207) — любая ссылка / любая ссылка ИБ
  - голый ref-вид без .Имя: CatalogRef/DocumentRef/EnumRef/ExchangePlanRef/TaskRef/
    BusinessProcessRef/ChartOf*Ref — «любая ссылка вида»

Развязка с обычным типом — по наличию точки: CatalogRef.Валюты → <v8:Type>,
CatalogRef (голый) → <v8:TypeSet>. DefinedType/Characteristic/голый ref никогда не
бывают v8:Type (проверено: 0). Составной тип через " | " роутит каждую часть
независимо (в т.ч. смешанный Type+TypeSet).

Emit-SingleType (+py) детектит и эмитит <v8:TypeSet>; Decompile-Type снимает cfg:-префикс.
TOTAL diff lines выборки 2.17: 4443 → 4068 (-375), match 13 → 14. Снапшот table
(колонки AnyRef/CatalogRef) сертифицирован в 1С (8.3.24). Регресс form-compile 33/33
зелёный на ps + python. decompile v0.34, compile v1.52.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 22:59:40 +03:00
Nick Shirokov daf7f1526a feat(form-decompile,form-compile): choiceList у InputField (переиспользование радио)
ChoiceList (<ChoiceList>) встречается на RadioButtonField (уже было) и InputField
(2142 в корпусе) — не захватывался у InputField. Логику вынесли в общие хелперы
Emit-ChoiceList / Decompile-ChoiceList (PS1) и emit_choice_list (PY), подключили к
обоим полям. Грамматика та же: [ { value, presentation?/title? } ] (+ рус. синонимы),
авто-вывод presentation. Порядок в InputField: после input-свойств/InputHint, до companions.

TOTAL diff lines выборки 2.17: 5149 → 4443 (-706). InputField>ChoiceList закрыт
(остаток ~5 — другое: app:value в app-неймспейсе = списки выбора параметров/настроек;
ChoiceListButton = отдельное input-свойство). Снапшот input-fields сертифицирован в 1С
(8.3.24). Регресс form-compile 33/33 зелёный на ps + python. decompile v0.33, compile v1.51.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 22:33:32 +03:00
Nick Shirokov 7eb825b3a7 feat(form-decompile,form-compile): commandSource у ButtonGroup/CommandBar
ButtonGroup/CommandBar несут <CommandSource> (источник команд группы): Form (2478),
FormCommandPanelGlobalCommands (1267), Item.<ИмяЭлемента> (команды конкретного
элемента-таблицы). Декомпилятор не захватывал → LOST в форменных/элементных панелях.

Добавлен ключ commandSource (эмитится «как есть», после Title до Representation/Autofill).
Декомпилятор захватывает у ButtonGroup и CommandBar.

TOTAL diff lines выборки 2.17: 5189 → 5149 (-40). ButtonGroup/CommandBar CommandSource
LOST → 0. Снапшот button-group (группа глобальных команд с commandSource) сертифицирован
в 1С (8.3.24). Регресс form-compile 33/33 зелёный на ps + python.
decompile v0.32, compile v1.50.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 22:18:41 +03:00
Nick Shirokov c998139c89 feat(form-decompile): форменный AutoCommandBar — маркер autofill для голого корня
Корень формы (id=-1) при голом <AutoCommandBar/> (autofill=true по умолчанию) терялся:
эвристика B3 (Compute-MainAcbAutofill) при наличии cmdBar-элемента в форме додумывает
Autofill=false → ADDED. Тот же приём, что у таблицы: декомпилятор зеркалит условие
эвристики (Test-AnyCmdBar) и при голом корне + наличии cmdBar пишет отклонение
autoCmdBar:{ autofill: true }. Компилятор не менялся (Compute-MainAcbAutofill уже
возвращает mainAcbDef.autofill; при true без детей корень эмитится голым).

TOTAL diff lines выборки 2.17: 5249 → 5189 (-60). AutoCommandBar ADDED 18 → 0
(голый корень закрыт). Регресс form-compile 33/33 зелёный на ps + python.
decompile v0.31.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 22:08:01 +03:00
Nick Shirokov 7b945e786d feat(form-decompile,form-compile): дин-список AutoCommandBar — маркер отклонения вместо ADDED
Хвост кластера командных панелей: компилятор-эвристика додумывает Autofill=false
дин-список-таблицам (подавляет панель, чтобы не дублировать КП формы), но ~15%
таблиц имеют голый <AutoCommandBar/> (autofill=true по умолчанию — панель оставлена
при таблице). Раньше эвристика их перетирала → ADDED <Autofill>false>.

Решение (B): эвристика держит дефолт false (оптимальный — 85% дин-списков; контекст-
сигналы проверены по корпусу, ни один не даёт >50% «голых» → улучшать дефолт нечем),
а декомпилятор фиксирует ОТКЛОНЕНИЕ маркером commandBar:{ autofill: true }. commandBar
имеет приоритет над эвристикой.

Компилятор: Emit-CompanionPanel больше не пишет <Autofill>true</Autofill> (платформа
его не эмитит — true это дефолт); только <Autofill>false</Autofill>. autofill:true или
отсутствие → тег опускается, при пустой панели → self-closing <AutoCommandBar/>.

TOTAL diff lines выборки 2.17: 5293 → 5249 (-44). Table>AutoCommandBar ADDED 22 → 0
(полностью закрыт). Остаток AutoCommandBar 18 — форменная панель (heuristic B3, корень
формы) — отдельный пункт. Регресс form-compile 33/33 зелёный на ps + python.
decompile v0.30, compile v1.49.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:51:26 +03:00
Nick Shirokov a1a22d1ffe test(form-validate): обновить устаревшие снапшоты (table-form, compiled-form)
Снапшоты отстали от компилятора (table additions с AdditionSource +
ExtendedTooltip companion таблицы; CheckBoxType у CheckBoxField — из прошлых
правок form-compile) — перегенерированы под текущий вывод. Не связано с
логикой form-validate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:32:39 +03:00
Nick Shirokov 7c765137db feat(form-decompile,form-compile): контент командных панелей таблицы — commandBar/contextMenu (companion tier 2)
Крупнейший левередж-кластер. Companion-панели элемента (AutoCommandBar/ContextMenu)
теперь несут контент как СВОЙСТВА:
  - commandBar  → <AutoCommandBar> (командная панель)
  - contextMenu → <ContextMenu>   (контекстное меню)

Значение: массив = shorthand для { children }; объект { autofill?, horizontalAlign?,
children[] }. children — обычная грамматика button/buttonGroup/popup.

Forgiving-синонимы (commandBar ← autoCommandBar/AutoCommandBar/autoCmdBar/cmdBar/
КоманднаяПанель; contextMenu ← ContextMenu/КонтекстноеМеню). Разведение «тип-элемент
vs панель-свойство» — по ТИПУ значения: строка = элемент-тип в дереве (cmdBar:"Имя"),
объект/массив = companion-панель этого элемента. Тип-синонимы применяются только к
строковому значению. Механизм общий (любой элемент), декомпилятор захватывает в
Decompile-Element, компилятор — Emit-CompanionPanel.

Новый ключ кнопки commandName — глобальная команда «как есть» (CommonCommand.X,
Catalog.X.Command.Y) без обёртки Form. (раньше попадала в command и ошибочно
оборачивалась в Form.Command.). stdCommand/command без изменений.

Декомпилятор: для дин-список-таблицы пустой AutoCommandBar(autofill=false) не пишет
commandBar (восстановит heuristic) — без шума. tableAutofill остаётся shorthand,
commandBar имеет приоритет.

TOTAL diff lines выборки 2.17: 7560 → 5293 (-2267), match 11 → 13,
cascade LOST 3414 → 1805. Table>ContextMenu/Table>AutoCommandBar ушли из топа impact.
Снапшот table сертифицирован в 1С (8.3.24); регресс form-compile 33/33 зелёный
на ps + python. decompile v0.29, compile v1.48.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 21:30:08 +03:00
Nick Shirokov d484a5b7ec feat(form-decompile,form-compile): доступ по ролям — userVisible/view/edit/use (единый xr-механизм)
Кластер «доступ по ролям»: единый role-adjustable boolean платформы
(xr:Common + 0..N xr:Value name="Role.X") для четырёх владельцев одним
грамматиком значения:
  - элемент   → userVisible  (<UserVisible>)
  - реквизит  → view, edit    (<View>/<Edit>)
  - команда   → use           (<Use>)

Значение DSL: скаляр false/true → голый <xr:Common>; объект
{ common, roles:{ Имя: bool } } → пер-ролевые исключения (три-state как в
конфигураторе: роль не указана → наследует common; указана → явный bool).
Имя роли forgiving: без префикса / Role. / Роль. → нормализуется в Role.
Отсутствие ключа = полный доступ (платформа тег не пишет) — дефолт не эмитим.

Декомпилятор инвертирует: голый Common → скаляр, есть Value → объект.
Компилятор: общий хелпер Emit-XrFlag / emit_xr_flag (ps1+py).
Порядок схемы: View → Edit после MainAttribute; Use после ToolTip до Action.

Раньше: userVisible умел только голый false (компилятор), декомпилятор не
захватывал ничего; view/edit/use не умел никто.

TOTAL diff lines выборки 2.17: 7911 → 7560 (-351), match 9 → 11.
Снапшоты attributes-types/commands сертифицированы в 1С (8.3.24);
регресс form-compile 33/33 зелёный на ps + python.
decompile v0.28, compile v1.47.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 20:41:04 +03:00
Nick Shirokov f34993b805 feat(form-compile): авто-генерация пустого ExtendedTooltip для Table/CommandBar/Popup (companion presence)
Платформа всегда ставит companion <ExtendedTooltip> на Table/CommandBar/Popup (подтверждено
корпусом: 13174/6086/15681 — 100%), но компилятор его не генерил (не было call-site в
Emit-Table/CommandBar/Popup) → пустой companion терялся при раундтрипе (вскрыто фиксом метрики).

Добавлена эмиссия ExtendedTooltip в Emit-Table (после AutoCommandBar), Emit-CommandBar и
Emit-Popup (перед ChildItems) — с контентом el.extendedTooltip (единый механизм). Зеркало
в form-compile.py идентично.

Валидация: ExtendedTooltip LOST 437→276 — Table(117→1)/CommandBar(46→0) закрыты, Popup
13→7; сертификация в 1С PASS; регресс 33/33 ps+py (снэпшоты перегенерированы — +ExtendedTooltip
и каскад перенумерации id); py==ps1; harness 8144→7911, match 8→9.

Остаток ExtendedTooltip LOST (Button 213 + ButtonGroup 34) — НЕ ExtendedTooltip: это cascade
от непокрытого контента AutoCommandBar (кастомные кнопки/ButtonGroup командной панели не
генерятся компилятором) — отдельный кластер. CheckBoxField 7 — ExtendedTooltip с own-layout
(<Width>). В BACKLOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:13:32 +03:00
Nick Shirokov b147e491ee feat(form-decompile,form-compile): единая ML-text форма для заголовков декораций (Label/Picture) — переиспользование Resolve-MLFormatted
Заголовок декорации — formatted-aware текст (как extendedTooltip). Раньше LabelDecoration
нёс formatted отдельным sibling-ключом, PictureDecoration терял атрибут formatted вовсе
(эмитил голый <Title> через generic Emit-Title). Теперь оба идут через общий
Emit-DecorationTitle → Resolve-MLFormatted (та же единая ML-text форма, что у extendedTooltip):
- title декорации: строка (formatted авто-детектится по разметке) / {ru,en} / {text, formatted}.
- атрибут <Title formatted="…"> эмитится ВСЕГДА (специфика декораций); для обычных элементов
  Emit-Title остаётся без formatted (formatted — только у декораций, подтверждено корпусом:
  LabelDecoration 6568 + PictureDecoration 2, прочие 0).
- back-compat: sibling-ключ formatted принимается как override авто-детекта; компилятор-вывод
  LabelDecoration не изменился.

Декомпилятор (v0.27): декорации захватывают title через Get-MLFormattedValue (гибрид);
sibling formatted больше не выводится (форматированные обычно становятся просто строкой
с markup внутри). Компилятор (ps1+py v1.45): Emit-DecorationTitle для Label+Picture.

Валидация: LabelDecoration formatted round-trip CLEAN; PictureDecoration Title-formatted
закрыт (29→0); регресс 33/33 ps+py; py==ps1; harness 8202→8144. Spec обновлён.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:43:08 +03:00
Nick Shirokov 684cd17d5f feat(form-decompile,form-compile): контент расширенной подсказки extendedTooltip + единая ML-text форма с formatted (кластер companion-content tier 1)
Companion <ExtendedTooltip> несёт <Title> (текст расширенной подсказки) — декомпилятор
пропускал companion целиком, текст терялся. Теперь companion = свойство родителя:
ключ extendedTooltip на элементе (синоним extTooltip).

Единая форма ML-текста (для title/tooltip/extendedTooltip):
- строка → ru; {ru,en} → многоязычно; {text, formatted: true} → форматированный.
- formatted: текст несётся RAW (1С inline-разметка <b>/<color>/<link>/</> — часть строки,
  round-trip через XML-экранирование, спаны не моделируем).
- Гибрид: флаг formatted авто-детектится по известной разметке/</> (детектор идентичен в
  ps1+py+декомпиляторе); явный {text,formatted} — только когда авто-детект неверен (~2%
  корпуса: formatted без разметки / литеральные <…>-плейсхолдеры). Авто-детект подтверждён
  данными (98% верно, мисматч 1003 из 53612).

Декомпилятор (v0.26): извлекает Title из companion <ExtendedTooltip> на родителя (гибрид).
Компилятор (ps1+py v1.44): Emit-Companion с опциональным контентом; 14 call-site'ов
ExtendedTooltip передают el.extendedTooltip; синоним extTooltip→extendedTooltip; whitelist.

Валидация: content-bearing round-trip CLEAN (Банки/ФормаЭлемента byte-identical, formatted=true
round-trip); регресс 33/33 ps+py; py==ps1 идентичны; harness 8368→8202.

Хвосты (в BACKLOG): пустое присутствие companion (Table/CommandBar/Popup не генерят
ExtendedTooltip — ~437, вскрыто фиксом метрики) и ExtendedTooltip с own-layout (<Width>).
Spec: extendedTooltip + раздел ML-text/formatted/markup-словарь.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 18:16:32 +03:00
Nick Shirokov e905d5f576 feat(form-decompile,form-compile): табличные скаляры ChoiceMode/SelectionMode/RowSelectionMode/Vertical-HorizontalLines + дегейт UseAlternationRowColor/InitialTreeView/RowPictureDataPath/RowsPicture (кластер Table scalars)
Свойства таблицы, терявшиеся на раундтрипе. Часть была захвачена в декомпиляторе под
gate динсписка (<UpdateOnDataChange>) → терялась на обычных ValueTable-таблицах.

Новые скаляры (захват + эмиссия, все типы таблиц):
- choiceMode (компилятор уже эмитил — добавлен захват), selectionMode (SingleRow/…),
  rowSelectionMode (Row/…), verticalLines/horizontalLines (явное false).
Дегейт (вынесены из блока динсписка в общую обработку Table — ловятся на ЛЮБОЙ таблице):
- useAlternationRowColor, initialTreeView, rowsPicture — захват/эмиссия без gate.
- rowPictureDataPath — инверсия умного дефолта DefaultPicture осталась дин-список-only;
  обычные таблицы захватывают/эмитят литерал.

Зеркало form-compile.py идентично (py==ps1 проверено).

Валидация: все 7 целевых — 0 LOST / 0 ADDED; round-trip на ValueTable-формах
(АдреснаяКнига и др.); регресс 33/33 ps+py; harness 8448→8368 (на честной метрике
после фикса атрибуции), 0 fail. Остаток по таблицам — companion-контент
(ExtendedTooltip/AutoCommandBar/ContextMenu) и цвета/шрифты — отдельные кластеры.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 17:14:32 +03:00
Nick Shirokov 1b56e7a735 feat(form-decompile,form-compile): честно-табличные свойства ChangeRowSet/Order, AutoInsertNewRow, EnableDrag, RowFilter (кластер Table group A)
Свойства редактируемых (ValueTable) таблиц формы, терявшиеся при раундтрипе:
- ChangeRowSet/ChangeRowOrder — теперь эмитятся явным значением, включая false
  (платформа пишет <ChangeRowSet>false</ChangeRowSet> на ValueTable; раньше компилятор
  эмитил только true → false терялся). Декомпилятор захватывает фактическое значение.
- AutoInsertNewRow — новый ключ (автодобавление строки), захват/эмиссия при true.
- EnableDrag — декомпилятор теперь захватывает (компилятор уже эмитил).
- RowFilter — nil-плейсхолдер <RowFilter xsi:nil="true"/> (в корпусе ВСЕГДА nil, 0 с
  контентом). DSL-ключ rowFilter: null; компилятор эмитит nil при наличии ключа.

Зеркало в form-compile.py идентично (py==ps1 проверено на ValueTable-формах).

Валидация: все четыре — 0 LOST / 0 ADDED (полностью закрыты); round-trip CLEAN на
ValueTable-формах (БанкиУниверсальногоОбмена, БанковскиеСчета); регресс 33/33 ps+py;
harness 7971→7774 (−197), 0 fail. Вывод байт-идентичен реальным формам платформы.

Spec: changeRowSet/changeRowOrder/autoInsertNewRow/enableDrag/rowFilter в table-секции.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:42:26 +03:00
Nick Shirokov a9e1ab64c8 feat(form-decompile,form-compile): общие свойства элемента DefaultItem/FileDragMode/EnableStartDrag/SkipOnInput (кластер generic element props)
Эти свойства — общие для любого типа элемента (таблица, поле, надпись, картинка,
кнопка), а не специфичны для таблицы. Раньше обрабатывались только в дин-список-блоке
Table → терялись на PictureDecoration/PictureField/LabelField/InputField/Button.

Перенесены в общий Emit-Layout/Add-Layout (универсальны — 17 вызовов компилятора,
один вызов декомпилятора на каждый элемент):
- DefaultItem (элемент по умолчанию), EnableStartDrag, FileDragMode — захват при наличии.
- SkipOnInput — теперь эмитится явное значение, включая false (раньше только true);
  декомпилятор захватывает фактическое значение.
- Вынесены в helper Emit-CommonElementProps; убраны дубли из дин-список-блока Table
  (useAlternationRowColor/initialTreeView остаются table-specific) и из Emit-Table
  (enableStartDrag).
Зеркало в form-compile.py идентично (py==ps1 проверено).

Валидация: FileDragMode/DefaultItem/EnableStartDrag — 0 LOST / 0 ADDED (полностью
закрыты на всех типах); SkipOnInput 141→37 (остаток — companion/nested-cmdbar кнопки,
редундантный false, в BACKLOG); регресс 33/33 ps+py; сертификация в 1С PASS; harness
8300→7971 (−329), 0 fail, match 7→8.

Spec: defaultItem/enableStartDrag/fileDragMode/skipOnInput → раздел 4.1 (общие свойства).
В BACKLOG: хвост SkipOnInput на companion + мис-атрибуция дубликатов в harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 16:16:18 +03:00
Nick Shirokov 1d158e3218 feat(form-decompile,form-compile): блок свойств таблицы динамического списка (кластер DynamicList Table)
Таблица формы, привязанная к динамическому списку, несёт блок специфичных свойств,
который платформа всегда эмитит (n=5079 на дин-список-таблицах, 0 на ValueTable).

Компилятор (ps1+py v1.40): авто-эмиссия блока на дин-список-таблице (Emit-DynListTableBlock):
- Group A (дефолт+override): AutoRefresh(false), AutoRefreshPeriod(60), Period(пустой Custom,
  константа), ChoiceFoldersAndItems(Items), RestoreCurrentRow(false), TopLevelParent(nil,
  константа), ShowRoot(true), AllowRootChoice(false), UpdateOnDataChange(Auto),
  AllowGettingCurrentRowURL(true).
- Group B (условные): DefaultItem, UseAlternationRowColor, FileDragMode (+ существующие
  InitialTreeView/EnableStartDrag).
- Group C: RowPictureDataPath (умный дефолт <Список>.DefaultPicture + override + suppress-маркер
  ""; ИСПРАВЛЕН баг — пустая строка больше не перезатирается дефолтом), RowsPicture, UserSettingsGroup.
- Эвристика 11b.4 теперь обходит ВСЕ DynamicList-реквизиты (не только main) → блок эмитится
  и для не-main/вторичных списков. Внутренний маркер _dynList исключён из валидатора ключей.

Декомпилятор (v0.22): захват блока с инверсией (gate = наличие <UpdateOnDataChange>):
Group A — опускает значения = дефолту; Group B — захват при наличии; RowPictureDataPath —
DefaultPicture опускается, кастом захватывается, отсутствие → "".

Валидация: дин-блок round-trip CLEAN (мультимножество, GUID-норм.) на простом списке и на
форме с кастомным RowPictureDataPath/RowsPicture/UserSettingsGroup; сертификация в 1С PASS;
py==ps1 идентичны; регресс 33/33 ps+py; harness 9634→8300 (−14%), 0 fail; группа «67»
(AutoRefresh/ShowRoot/UpdateOnDataChange/…) ушла из остатка, residual block-теги 47→5.

Spec — раздел «Таблица динамического списка»; тест-кейсы перегенерированы. Хвост и соседний
кластер (свойства Table на не-дин-список таблицах) — в BACKLOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 22:29:30 +03:00
Nick Shirokov 15883a7e7c feat(form-decompile,form-compile): настройки динамического списка — источник, ListSettings, контент filter/order/conditionalAppearance (кластер DynamicList Settings)
Декомпилятор (v0.21): парс <Settings xsi:type="DynamicList"> — mainTable/query/
dynamicDataRead(дефолт true→omit)/fields(только при наличии); вынос query в
<basename>-<имяСписка>.sql рядом с JSON (зеркало skd-decompile); захват контента
ListSettings (filter/order/conditionalAppearance) в skd-грамматику. Пустой/
каноничный скелет опускается (компилятор регенерит).

Компилятор (ps1+py v1.39): query→ManualQuery=true+QueryText (+@file-резолвер);
порядок платформы ManualQuery→DynamicDataRead→QueryText→Field*→MainTable→ListSettings;
прощающий ввод (Справочник.X→Catalog.X через refRootSynonyms; убыв→desc/возр→asc);
каноничный ListSettings-скелет с константными GUID контейнеров (~90% форм бит-в-бит);
эмиттеры filter/order/conditionalAppearance скопированы из skd-compile (навыки автономны).

Валидация: раундтрип CLEAN (бит-в-бит, GUID-норм.) на order/filter/condApp/пустом;
сертификация в 1С PASS (пустой скелет + контент грузятся в базу); py==ps1 идентичны;
регресс 33/33 ps+py; harness 12381→9634 (−22%), 0 fail, LOST контента=0.

Тест-кейс dynamic-list-form дополнен контентом; spec — раздел settings динсписка.
Хвост (минимальный/частичный ListSettings) — в BACKLOG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 21:52:06 +03:00
Nick Shirokov 6857ad5060 fix(form-decompile,form-compile): formatted у LabelDecoration независим от hyperlink (кластер formatted)
Компилятор выводил <Title formatted="…"> из hyperlink (formatted = hyperlink),
но это неверно: атрибут formatted НЕЗАВИСИМ. По корпусу acc+erp:
- 9080 label'ов: hyperlink есть, formatted=false (компилятор давал true);
- 6545: formatted=true без hyperlink (компилятор давал false).
Итого ~15625 расхождений.

Введён отдельный ключ formatted (bool, выводится при true):
- декомпилятор: захват атрибута <Title formatted> у LabelDecoration (независимо
  от <Hyperlink>);
- компилятор Emit-Label: formatted из ключа, не из hyperlink.

Декомпилятор (ps1) + компилятор (ps1+py) + spec (label.formatted). Снэпшот
events обновлён: label с hyperlink:true теперь даёт formatted="false" (фиксирует
развязку) — сертифицирован в 1С 8.3.24. Регресс ps+py 33/33.

Остаток <Title formatted> в раундтрипе принадлежит ExtendedTooltip-с-контентом
и PictureDecoration — отдельные кластеры (в BACKLOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:35:11 +03:00
Nick Shirokov 908af27bf0 feat(form-decompile,form-compile): tooltipRepresentation элемента (кластер ToolTipRepresentation)
<ToolTipRepresentation> (режим показа подсказки: None/Button/ShowBottom/ShowTop/
ShowLeft/ShowRight/ShowAuto/Balloon) — общее свойство элемента (Button 13785,
Popup 6417, ButtonGroup, InputField, CheckBoxField, LabelDecoration, группы и
др.; None доминирует — 25241). Терялся: декомпилятор не читал, компилятор не эмитил.

Введён общий passthrough-ключ tooltipRepresentation:
- декомпилятор: захват в Add-CommonProps;
- компилятор: эмиссия в Emit-Title (после ToolTip) — покрывает все эмиттеры,
  зовущие Emit-Title; плюс отдельно в Emit-Label (свой title-блок, не зовёт
  Emit-Title).

Декомпилятор (ps1) + компилятор (ps1+py) + spec §4.1. Покрытие: input-fields
(input, ShowBottom), events (label-декорация, Button) — сертифицировано в 1С
8.3.24. Раундтрип БанковскиеСчета/Wildberries: остаток ToolTipRepresentation = 0.
Регресс ps+py 33/33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 16:02:06 +03:00
Nick Shirokov 22e929ecb3 feat(form-decompile,form-compile): tooltip элемента + фикс экранирования текста (кластер ToolTip)
Два дефекта вокруг текста <v8:content>, оба вскрылись на формах с подсказками.

1. ToolTip элемента (484 LOST в корпусе). <ToolTip> — прямой мультиязычный
   текст подсказки на элементе (UsualGroup 42150, Popup, Page, InputField,
   и почти все типы). Декомпилятор пропускал (как companion), компилятор не
   эмитил. Введён общий ключ tooltip (string|{ru,en}), как title:
   - декомпилятор: захват в Add-CommonProps;
   - компилятор: эмиссия в Emit-Title (сразу после Title) — покрывает все
     эмиттеры, зовущие Emit-Title.
   Попутно выяснилось, что Emit-Pages/Emit-CommandBar вовсе не звали Emit-Title
   (теряли и Title, и ToolTip), а Emit-Label эмитит Title по-своему — во все три
   добавлена обработка title/tooltip.

2. Экранирование кавычек. Esc-Xml экранировал " → &quot; в тексте элемента,
   но 1С в <v8:content> пишет " литерально (экранирует только & < >).
   Это ломало раундтрип любого текста с кавычками. Убрано экранирование " .

Декомпилятор (ps1) + компилятор (ps1+py) + spec (§4.1 tooltip). Покрытие:
input-fields (input+tooltip), pages (pages/page tooltip, page с кавычкой в
тексте — проверяет литеральность) — сертифицировано в 1С 8.3.24. Раундтрип
БанковскиеСчета/Wildberries/АдреснаяКнига: ToolTip и &quot; остаток = 0.
Регресс ps+py 33/33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 15:36:30 +03:00
Nick Shirokov c43041c0b7 feat(form-decompile,form-compile): TitleLocation у LabelField/PictureField/Table (кластер TitleLocation)
TitleLocation обрабатывался только у input (passthrough), check/radio
(smart-default) и calendar. У LabelField (7362 в корпусе), PictureField (2479)
и Table (381) тег молча терялся — ни декомпилятор, ни компилятор его не знали.

Профиль доли элементов с тегом: Table 2.9%, LabelField 15.8%, PictureField
80.5% (но 20% без тега). Платформа НЕ всегда эмитит → выбран passthrough
(эмитим при наличии ключа, как у input/calendar), не smart-default. Корректно
и консистентно; переиспользован существующий ключ titleLocation + Map-TitleLoc.

Декомпилятор (ps1): захват titleLocation в трёх ветках. Компилятор (ps1+py):
эмиссия в Emit-LabelField/Emit-Table/Emit-PictureField в позиции по схеме.
spec §4.1: titleLocation вынесен в общие свойства с пометкой охвата.

Тест-покрытие добавлено в input-fields (labelField=left), picture-field
(picField=none), table (table=top) — снэпшоты сертифицированы в 1С 8.3.24.
Раундтрип 60 форм с TitleLocation на label/pic/table/radio: остатка нет.
Регресс ps+py 33/33.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:33:05 +03:00
Nick Shirokov 8998c0b5db feat(form-decompile,form-compile): свойства CalendarField (кластер CalendarField)
CalendarField терял специфичные свойства при раундтрипе: декомпилятор их не
читал, компилятор не эмитил. Пробел DSL (класс 3). CalendarField — длинный
хвост (18 форм на 17033, 0.1%), но элемент маленький и ограниченный → решено
покрыть целиком, убрав класс молчаливых потерь.

Добавлены ключи (passthrough, эмитятся только при наличии): selectionMode,
showCurrentDate, widthInMonths, heightInMonths, showMonthsPanel. Плюс
подключён общий titleLocation (раньше у календаря не обрабатывался).

Порядок тегов выверен по корпусу (18 форм): DataPath > Title > TitleLocation
> [layout] > SelectionMode > ShowCurrentDate > WidthInMonths > HeightInMonths
> ShowMonthsPanel > companions > Events.

Декомпилятор (ps1) + компилятор (ps1+py) + spec. Новый тест-кейс calendar
(два календаря: со скалярами+событием и с months-panel), сертифицирован
в 1С 8.3.24. Регресс ps+py 33/33.

Tooltip-свойства календаря (ToolTip/ToolTipRepresentation) намеренно оставлены
будущему общему tooltip-кластеру. Раундтрип календарных форм: ПериодКомандировки
→ match; остаточный TitleLocation на radio/table — отдельная находка (BACKLOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 13:08:09 +03:00
Nick Shirokov 4c2c72abce feat(form-decompile,form-compile): унификация событий элементов на events-мапу (кластер Events DSL)
Несогласованность DSL: события ФОРМЫ описывались интуитивной мапой
events:{Событие:Обработчик}, а события ЭЛЕМЕНТА — двумя сущностями
on:[...] + handlers:{...}. Два способа для одного понятия путали модель.

Унифицировано на единую мапу events:{Событие:ИмяОбработчика} на форме И
элементах (как form-level). Декомпилятор эмитит только её, с явными именами
обработчиков (прозрачно, консистентно с form-level).

Компилятор (ps1+py):
- Emit-Events читает events-мапу (основной формат); значение null/"" →
  имя по конвенции ИмяЭлемента+суффикс (прощающий fallback).
- legacy on/handlers по-прежнему принимаются ради совместимости (не эмитятся).
- choiceButton: проверка StartChoice через оба формата (Test-ElementEvent).
- events добавлен в whitelist ключей элемента.

Декомпилятор: Get-Events → упорядоченная мапа {Событие:Обработчик} в порядке
документа; убраны on/handlers и инверсия авто-имён.

spec/SKILL.md: events как единственный рекомендованный формат, on/handlers
помечены legacy. В SKILL.md только явные имена (null-сахар — деталь spec,
инструкцию не раздуваем).

Корпус acc_8.3.24: 190 элементов в 114/400 форм теряли Events до фикса (баг
on/handlers разобран отдельным коммитом). Раундтрип 2.17: Events ушли из топа
LOST, match 4→6, 0 compile-fail. Регресс ps+py 32/32, снэпшот events (добавлен
блок Events у поля с переименованным обработчиком) сертифицирован в 1С 8.3.24.

Follow-up: form-edit использует расширенный on с {event,callType} —
унификация отдельным решением (см. BACKLOG).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 12:46:56 +03:00
Nick Shirokov a38874280c feat(form-decompile): события с кастомными именами в on (кластер Events handlers-only)
Get-Events клал в on только авто-именованные обработчики, а кастомные
(переименованные или без суффикса: OnActivateDate, ValueChoice, Selection
с нестандартным именем…) — ТОЛЬКО в handlers, минуя on. Компилятор итерирует
по on → такие события не эмитились вообще. Корпус acc_8.3.24: 190 элементов
в 114 формах из 400 теряли Events.

Контракт DSL (spec §4.1/4.2): on = полный список имён событий, handlers =
переопределение имени. Декомпилятор нарушал его — чистый баг класса 1,
компилятор корректен. Чиним: каждое событие → в on (порядок документа),
handlers только для не-авто имён.

Порядок важен: в корпусе 1956 <Events>-блоков, где кастомное событие идёт
перед авто (паттерн не A*C*). Поэтому union в компиляторе (on, затем handlers)
дал бы неверный порядок — единственная упорядоченная структура DSL это on,
её и заполняем полностью.

Зеркало PY компилятора не нужно (правка только декомпилятора). Валидация:
ПериодКомандировки/Банки/АктивныеПользователи — Events ушли из диффов.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:54:36 +03:00
Nick Shirokov bccdf93094 feat(form-decompile): form-level <CommandSet>/ExcludedCommand (кластер ExcludedCommand)
Декомпилятор читал <CommandSet> только внутри Table-элемента; на корне формы
(<Form><CommandSet><ExcludedCommand>…) пропускал → excludedCommands терялись
(LOST=207 в корпусе 2.17). Компилятор и DSL уже поддерживали excludedCommands
на top-level — чистый баг декомпиляции (класс 1).

Читаем корневой CommandSet в $dsl["excludedCommands"], порядок по spec
(title → properties → excludedCommands → events). Зеркало PY не нужно
(decompiler.py отложен), корпус не нужен (читаем поддерживаемую фичу).
Валидация: ФормаЗаписи + ФормаЭлемента раундтрип diff→match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 11:38:15 +03:00
Nick Shirokov d935cfe0cc test(form-compile): дозакоммит CheckBoxType=Auto в item-снапшоты from-object (хвост кластера L)
В коммите кластера L (b4fc9bf) git add не охватил cases/form-compile-from-object/snapshots/,
из-за чего 3 item-снапшота с флажками остались с устаревшим выводом (без CheckBoxType).
Только добавление <CheckBoxType>Auto</CheckBoxType>. Регресс 32/32 PS1+PY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:05:24 +03:00
Nick Shirokov 3483802ab0 docs(form-dsl-spec): уточнение семантики "" — суппресс-маркер, не «дефолт платформы»
"" означает «не выводить тег» (платформа применит своё рантайм-умолчание),
а не «значение = дефолту платформы». Разведение дефолта эмиссии и дефолта рантайма.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 22:02:38 +03:00
Nick Shirokov 3b0061c8a0 feat(form-decompile,form-compile): мультиязычный текст (кластер I)
БАГ: Emit-MLText стрингифицировал мультиязычный объект {ru,en} →
<v8:content>@{ru=…; en=…}</v8:content> (мусор). ERP — двуязычная конфигурация,
поэтому это доминирующий пробел раундтрипа (item/content/lang).

- compiler PS1+PY: Emit-MLItems/emit_ml_items — по <v8:item> на язык; все
  вызывающие (Title/ToolTip/InputHint/реквизиты/колонки/команды/форма + Emit-Label)
  передают сырой объект вместо стрингификации. choice presentation уже был мультиязычен.
- decompiler уже давал {ru,en}; убран мёртвый titleFormatted (компилятор выводит formatted из hyperlink).
- docs/form-dsl-spec: title/tooltip/inputHint принимают объект {ru,en,…}.
- tests: groups (title {ru,en}) сертифицирован в 1С.

Эффект (220 форм 2.17): item 3909→1475, content 3193→737, lang 1861→635. Регресс 32/32 PS1+PY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 21:28:53 +03:00
Nick Shirokov b4fc9bf42c feat(form-decompile,form-compile): листовые свойства полей + фикс Hiperlink (кластер L)
БАГ: у LabelField платформенный тег <Hiperlink> (опечатка 1С), компилятор
эмитил <Hyperlink> — гиперссылка не работала и не роундтрипилась. Проверено
по корпусу: LabelField→Hiperlink во всех версиях формата (2.17 и 2.20).

- compiler PS1+PY: LabelField <Hiperlink>; EditMode (input/check/labelField);
  CheckBoxType (check, умный дефолт Auto + suppress как radioButtonType).
- decompiler: editMode, checkBoxType (Auto→опустить), markIncomplete (раньше не ловился),
  labelField читает <Hiperlink>.
- docs/form-dsl-spec: editMode, checkBoxType, примечание про Hiperlink.
- tests: input-fields расширен (editMode/checkBoxType/labelField+hyperlink), сертифицирован.

Регресс 32/32 PS1+PY, churn по флажкам обновлён и сертифицирован.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:29:31 +03:00
Nick Shirokov f27a17139a feat(form-compile): AdditionSource табличных additions (кластер C)
У всех таблиц SearchStringAddition/ViewStatusAddition/SearchControlAddition
несут <AdditionSource> (Item = имя таблицы, Type фиксирован по виду). Раньше
компилятор эмитил их пустыми self-closing.

- compiler PS1+PY: Emit-TableAddition/emit_table_addition — addition с
  AdditionSource + вложенными ContextMenu/ExtendedTooltip.
- В DSL ничего не добавлено: чистое авто-обогащение (модель объявляет таблицу
  → корректные элементы поиска генерируются сами). decompiler/spec не тронуты.

Эффект на раундтрип: AdditionSource ушёл из LOST; ContextMenu 100→7,
ExtendedTooltip 210→124 (каскад схлопнут). Регресс 32/32 PS1+PY,
12 снапшотов обновлены и сертифицированы в 1С (20/20).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:31:44 +03:00
Nick Shirokov 9c1ea1662a feat(form-decompile,form-compile): CommandSet/excludedCommands таблиц (кластер F)
CommandSet встречается на Table (23) и Form (8). Форменный excludedCommands
уже поддержан, табличный — нет.

- compiler PS1+PY: Emit-Table — excludedCommands → <CommandSet>; заодно
  viewStatusLocation/searchControlLocation (из того же блока свойств таблицы).
- decompiler: Table — CommandSet→excludedCommands, searchStringLocation
  (раньше не ловился), viewStatusLocation/searchControlLocation.
- docs/form-dsl-spec: excludedCommands + view/searchControl у таблицы.
- tests: table расширен, сертифицирован в 1С.

ExcludedCommand ушёл из топа LOST. Регресс 32/32 PS1+PY, churn нулевой.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 19:18:17 +03:00
Nick Shirokov 0941fc717d feat(form-decompile,form-compile): семантика TitleLocation (кластер G2)
Принцип: компилятор не эмитит значение, равное дефолту платформы (который
платформа сама не пишет в XML). Умный дефолт (check→Right, radio→None) —
отдельная вещь, эмитится (он ≠ дефолт платформы Left).

- net ключа titleLocation → умный дефолт; titleLocation: "" → подавить
  (дефолт платформы); значение → эмитить с маппингом регистра.
- compiler PS1+PY: Emit-TitleLocation/emit_title_location + Map-TitleLoc
  (общий маппинг; у check раньше его не было — сырьё).
- decompiler: Add-TitleLocation (дефолт → опустить, нет тега → "", иначе значение).
- docs/form-dsl-spec: семантика titleLocation у check/radio.
- tests: input-fields расширен (Right-дефолт / ""-подавление / явный Top), сертифицирован.

АварийныйРежим: полный MATCH. Регресс 32/32 PS1+PY, churn нулевой.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:53:28 +03:00
Nick Shirokov 4ba1e595bf feat(form-decompile,form-compile): семантика title (кластер G)
Над-генерация заголовков элементов из имени. Различаем:
- нет ключа title → авто-вывод из имени (помощь модели при создании форм);
- title: "" → подавить (<Title> не эмитим);
- непустая строка → как есть.

- compiler PS1+PY: Emit-Title/emit_title + Emit-Label проверяют наличие ключа,
  а не truthiness (раньше "" триггерило авто-вывод).
- decompiler: ставит title:"" для авто-выводящих типов (page/popup/label,
  непривязанные поля, button без команды), когда <Title> в оригинале отсутствует.
- docs/form-dsl-spec: семантика title.
- tests: pages демонстрирует title:"" (+snapshot, сертифицирован в 1С).

АварийныйРежим: diff 13→1. Регресс 32/32 PS1+PY, churn нулевой.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 18:32:02 +03:00
Nick Shirokov e777ded8d2 feat(form-decompile,form-compile): геометрия/layout единым хелпером (кластер E)
- compiler PS1+PY: общий Emit-Layout/emit_layout (width/height/stretch/maxWidth/
  maxHeight/autoMax*/skipOnInput/groupHorizontalAlign/groupVerticalAlign/
  horizontalAlign), вызывается во всех эмиттерах; inline-дубли убраны. Спец-квирки
  сохранены (input multiLine→autoMaxWidth, table height→HeightInTableRows).
- PictureDecoration LoadTransparent больше не захардкожен true — управляется
  loadTransparent (дефолт false).
- decompiler: Add-Layout (DRY, один вызов на элемент), table HeightInTableRows,
  picture loadTransparent.
- docs/form-dsl-spec: блок общих layout-свойств (4.1a), loadTransparent у picture.
- tests: groups расширен layout-свойствами (+snapshot, сертифицирован в 1С).

Churn снапшотов нулевой. АварийныйРежим: LOST полностью закрыт (остаток — над-генерация).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 17:51:13 +03:00
Nick Shirokov 67eaa1c3c8 feat(form-decompile,form-compile): командные панели (кластер D)
- form-decompile: форменный AutoCommandBar → autoCmdBar-элемент; ButtonGroup;
  команды tooltip/currentRowUse; choiceList value type (число/булево) — раньше.
- form-compile (PS1+PY): новый тип ButtonGroup; команды ToolTip/CurrentRowUse.
- docs/form-dsl-spec: buttonGroup, autoCmdBar (панель формы), tooltip/currentRowUse.
- tests: кейс form-compile/button-group (+snapshot, платформенно валидирован).

АварийныйРежим раундтрип: diff 44→18. Регресс form-compile зелёный (PS1+PY).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:22:07 +03:00
Nick Shirokov 7c38422e2c feat(form-decompile): MVP-декомпилятор Form.xml→JSON + компактный вывод (draft, ring-ограничен)
Декомпилятор управляемой формы в формат form-compile (инверсия компилятора):
метаданные, реквизиты, параметры, команды, события, рекурсия ChildItems по
базовым типам, strip companions, инверсия типов и авто-имён обработчиков.
Компактный вывод тем же сериализатором, что skd-decompile (inline в пределах
lineLimit=120). Ring-ограничен (disable-model-invocation): раундтрип не
гарантируется, риск неполноты на пользователе.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 14:37:54 +03:00
Nick Shirokov 6d119eb473 feat(skd-edit): значение-список параметра в шортхенде (+skd-compile)
Значение по умолчанию у параметра СКД может быть списком (несколько <value>
подряд при valueListAllowed=true). Раньше задать список можно было только через
объектную модель skd-compile; шортхенд (add/modify-parameter, parameters) парсил
value= как скаляр.

Теперь в шортхенде: value=v1, v2, v3 задаёт список (кавычки '...' для запятой
внутри значения). Если задан список (>=2 элементов), valueListAllowed выводится
автоматически. Авто-вывод только в шортхенде — объектная модель остаётся
буквальной (bit-perfect round-trip сохранён).

skd-edit (ps1+py v1.25):
- Split-QuotedCsv/Parse-ValueList — токенайзер по запятым с учётом кавычек, БЕЗ
  разреза по ':' (важно для дат вида 2024-01-01T12:30:45)
- add-parameter: эмит N <value>
- modify-parameter: пред-выемка value=-списка, удаление ВСЕХ старых <value>,
  авто valueListAllowed; scalar value= теперь тоже схлопывает список в один <value>

skd-compile (ps1+py v1.105): тот же разбор списка в Parse-ParamShorthand;
объектная модель не тронута.

Документация: skd-edit/skd-compile SKILL.md (поведение), docs/1c-dcs-spec.md и
docs/skd-dsl-spec.md (формат).

Тесты: add-list, modify list<->scalar, список дат (двоеточия целы), compile-
шортхенд. Полный регресс 413/413 на ps1 и py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:26:57 +03:00
Nick Shirokov 9877fe403a feat(skd-info): флаг -Raw для lossless round-trip извлечения запроса
skd-info -Mode query был просмотрщиком (заголовки, оглавление батчей,
разделители --- Batch ---) и терял разделители пакетов при split, поэтому
не годился как источник для skd-edit set-query @file.

Флаг -Raw отдаёт текст запроса целиком, verbatim, без декораций и без
дробления на пакеты — все ; и //// на месте. С -OutFile пишет чистый .sql,
который без потерь возвращается через set-query @file. Stdout не усекается
по -Limit. Версия v1.6 в обоих скриптах (ps1 + py).

Документация: таблица параметров/режимов и round-trip workflow в skd-info,
указатель + разводка patch-query vs set-query+-Raw в skd-edit.

Тесты: query-raw (raw без декораций, разделитель //// сохранён) и query-view
(просмотр не задет). Зелёные на ps1 и py.

Чистка: удалён modes-reference.md — галерея примеров вывода избыточна для
модели (инструмент самодемонстрирующийся), а человек покрыт docs/skd-guide.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 22:11:48 +03:00
Nick Shirokov 46ee078343 docs(web-test): актуализация контракта test CLI (несколько путей, --url)
Спека §1, regress.md, README приведены в соответствие новому контракту:
сигнатура `test <dir|file>...`, несколько путей (дедуп + сортировка), флаг
--url=, заметка про резолв webtest.config.mjs/_hooks.mjs от каталога первого пути.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:07:09 +03:00
Nick Shirokov 7f2bf9d2d3 feat(web-test): test CLI принимает несколько путей + флаг --url, fail-fast валидация
Привод контракта `run.mjs test` к общей практике (pytest/jest/playwright):
- позиционные аргументы = пути к тестам, можно несколько
  (`test a.test.mjs b.test.mjs dir/`); discoverTests объединяет, дедуплицирует,
  сортирует (порядок по числовым префиксам сохраняется);
- URL переехал из первого позиционного во флаг --url= (по умолчанию из
  webtest.config.mjs) — раньше url-первым путал: второй путь уходил в page.goto()
  и падал с криптовым 'Cannot navigate to invalid URL';
- fail-fast: несуществующий путь → понятная ошибка вместо падения goto;
  аргумент, похожий на URL, подсказывает про --url=.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 20:07:03 +03:00
Nick Shirokov 31fa66d8fe test(web-test): регресс на readSpreadsheet до Сформировать + object-search selectValue
- 11-report: чтение несформированного отчёта бросает осмысленную ошибку,
  не ReferenceError (покрывает import checkForErrors в readSpreadsheet);
- 04-selectvalue: объектный поиск selectValue({Наименование}) выбирает через
  форму выбора (покрывает 3A guard + import filterList).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:41:06 +03:00
Nick Shirokov a8e61d02a2 fix(web-test): починка объектного поиска selectValue({field: value})
Три проблемы объектного поиска (следствие рефакторинга на модули):
- импорт filterList отсутствовал — pickFromSelectionForm (Шаг 2) бросал
  ReferenceError, который молча глотался catch'ем, поиск по полю не работал;
- dropdown-путь 3A падал на searchText.toLowerCase() (объект, не строка) —
  теперь объектный search уходит в форму выбора, где обрабатывается per-field;
- сужен catch вокруг filterList: ReferenceError/TypeError пробрасываются,
  чтобы будущие missing-import не маскировались как 'поле не найдено'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:41:00 +03:00
Nick Shirokov c8c0c48ead fix(web-test): импорт checkForErrors в readSpreadsheet
readSpreadsheet() в ветке allCells.size===0 (отчёт не сформирован) вызывал
checkForErrors() без импорта — падало с 'checkForErrors is not defined'
вместо осмысленного сообщения. Следствие рефакторинга на модули.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:40:52 +03:00
Nick Shirokov f1b61b9e9e test(web-test): фокус-клик по полю вместо fillFields для сброса viewport в 18-cell-click
Шаг focus-click пропуска чекбоксов выводил фокус из ТЧ через fillFields({Комментарий}),
что лишний раз перезаписывало значение. clickElement по полю «Комментарий» фокусирует
его без перезаполнения и так же сбрасывает горизонтальный viewport грида. Поведение
шага не меняется (читаются только булевы Товаров), тест зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:47:18 +03:00
Nick Shirokov 9774b8f1c3 fix(web-test): fillTableRow распознаёт переформатированные число/дату в choice-ячейке
fillChoiceCell определял «прижился ли paste» через normYo(after).includes(text),
что ломалось на маск-инпутах: число/дата переформатируются (1234.56 → «1 234,56»,
группировка неразрывным пробелом, запятая) → includes давал false → ложный уход
в F4, где у числа открывался калькулятор и залипал (no_selection_form).

Заменил на поведенческий дискриминатор: появился EDD → ссылка (dropdown);
инпут изменился на непустое без EDD → редактируемая ячейка (direct); инпут
не изменился → НачалоВыбора → F4-форма. + страховка: если F4 открыл не форму
выбора (калькулятор/календарь) — Escape и спасение значения.

Также в EDD-ветке основного Tab-цикла убран слепой fallback items[0]: при
отсутствии exact/includes-совпадения возвращается not_found с очисткой поля,
а не подставляется произвольная первая запись автокомплита.

Регресс: в стенд (дерево) добавлены choice-колонки Число/Дата и булево-поле-ввода;
в 16-tree-form — шаги choice-number/choice-date/bool-input. Полный регресс: 22 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:42:34 +03:00
Nick Shirokov c147fd5cb7 feat(web-test): fillTableRow редактирует строку по фильтру { col: value } + scroll
fillTableRow теперь принимает row как объектный фильтр (одна/несколько колонок,
AND-матч) — как clickElement — и опцию scroll:true для строк за пределами
DOM-окна виртуализации. Фильтр резолвится в числовой индекс один раз в начале
через переиспользование resolveRowIndexByFilter из click-cell.mjs (без дублей
matching/reveal); дальше существующий код row-mode не тронут. row:<число> —
полная обратная совместимость.

Побочно починен баг в общем reveal-цикле (его же использует clickElement scroll):
детектор конца списка опирался на текст первой колонки + selIdx, поэтому на
табчасти с однотипной первой колонкой ложно срабатывал на втором PageDown.
Теперь основной признак конца — hasBelow===false, а сигнатура снимка строится
по всей строке (snapshotGridScript).

Версии: click-cell v1.4, dom/grid v1.9, row-fill v1.22.
Регресс tests/web-test: 22/22 зелёные (live E2E на синтетическом стенде).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 22:03:06 +03:00
Nick Shirokov ffb380187f feat(web-test): exact-match при выборе типа в pickFromTypeDialog
Диалог выбора типа матчил по подстроке и падал «multiple types match»,
даже когда точное совпадение присутствовало в выдаче (напр. поиск
«Контрагент» давал «Банковская карта контрагента», «Договор с контрагентом»,
…, «Контрагент» — и движок ругался, хотя точная строка была видна).

pickFromTypeDialog теперь предпочитает точное совпадение (resolveExact:
единственный матч, либо единственная строка, равная искомому имени после
нормализации регистра/ё) — кликает именно её и жмёт OK. Применяется и в
scan-пути (мелкие списки), и после Ctrl+F (большие виртуальные списки).
Добавлен ограниченный скролл-скан (PageDown ×3) на случай, когда точная
строка чуть ниже первого окна. Ошибка неоднозначности остаётся, только если
единственного точного совпадения действительно нет.

Стенд: в СписокТипов добавлен подстрочный дубль «Дата документа» рядом с
«Дата» для детерминированной проверки exact-match. Тест 16-tree-form
покрывает scan-путь (выбирается точное «Дата»).

Проверено: регресс web-test 22/22, живой E2E на типовой Консоли запросов
(ссылочный тип через Ctrl+F + примитив без регресса).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 20:00:46 +03:00
Nick Shirokov 80ffed9a28 feat(web-test): fillTableRow заполняет редактируемую ячейку-выбор прямым вводом
Ячейка грида с кнопкой выбора (iCB, buttonKind:'choice') бывает двух видов,
неразличимых в DOM (оба editInput, readOnly:false): редактируемое значение
(текст прилипает) и выбор из программного списка (РедактированиеТекста=Ложь —
текст отвергается, readOnly при этом не выставляется). Движок жал F4 на обе и
падал no_selection_form, если форма не открывалась.

Новый общий helper fillChoiceCell различает их поведенчески: пробует прямой
ввод, и если вставленный текст прилип — коммитит (method:'direct'), иначе
открывает форму по F4 (isTypeDialog → pickFromTypeDialog 'choice', иначе
pickFromSelectionForm 'form'). Вызывается из обоих мест (плоский Tab-цикл и
tree direct-edit) — плоский и tree гриды теперь ведут себя одинаково.

Стенд: ДеревоТипЗначения получает textEdit:false (модель выбора-из-списка),
добавлено поле ДеревоРедактируемаяСтрока (кнопка выбора + пустой НачалоВыбора,
модель редактируемой ячейки). Тест 16-tree-form покрывает оба плеча.

Проверено: полный регресс web-test 22/22, живой E2E на типовой Консоли запросов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 19:14:36 +03:00
Nick Shirokov 1106117e33 test(skd-decompile): реалистичная структура в фикстуре auto-group (Период > Auto)
Фикстура dataset-folder-and-auto-group задавала «Auto > Период» — «Авто»-поле
группировки родителем явной группировки по Период. В типовых ERP/БП такого нет:
GroupItemAuto встречается (13 макетов против 753 у GroupItemField), но всегда как
настраиваемый ЛИСТ (с явной выборкой, обычно viewMode=Inaccessible), а не родителем.

Структура заменена на shorthand «Период > Auto» (группировка по Период, внутри «Авто»):
- идиоматично, GroupItemAuto остаётся покрытым (единственная фикстура с ним);
- shorthand-форма даёт Template.xml с auto-полями (как платформа), поэтому
  round-trip снова bit-perfect (object-form без selection/order их не эмитил).

Проверено: платформа принимает (ERF + epf-build), round-trip bit-perfect,
decompile 16/16 на PS и PY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:41:56 +03:00
Nick Shirokov e03ba3b509 fix(skd-decompile): сворачивать дефолтные auto selection/order группы в shorthand
После 6781bb3 компилятор кладёт в каждую группу структуры авто-поле выбора и
авто-порядок (SelectedItemAuto/OrderItemAuto), как делает платформа. decompile
сохранял их как selection:["Auto"]/order:["Auto"], из-за чего Try-StructureShorthand
не сворачивал цепочку и выдавал громоздкую объектную модель вместо строки
"A > B > details".

Теперь selection/order, состоящие ровно из одного "Auto" (предикат Is-AutoOnly /
is_auto_only), считаются дефолтом и не мешают свёртке. Round-trip снова bit-perfect:
shorthand перекомпилируется в идентичный XML (Parse-StructureShorthand сам добавляет
эти auto-поля). Отключённый auto ({auto,use}), смешанные списки и явные поля свёртку
не проходят и остаются в объектной форме.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:12:50 +03:00
Nick Shirokov 3d6a09e90a test(skd): синхронизировать снапшоты с выводом skd-compile
Эталоны skd-info/skd-validate/skd-edit/skd-decompile «вшивают» Template.xml,
генерируемый skd-compile в preRun, но отстали от изменений компилятора:
- 11ddc2b — single-line эмиссия <DataCompositionSchema xmlns=...>
- 6781bb3 — авто-выборка/порядок (SelectedItemAuto/OrderItemAuto) в группах

Регенерированы под текущий вывод. Платформенно сертифицировано через
verify-snapshots: skd-compile 23/23, skd-edit 45/45 (ERF + epf-build).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 16:12:28 +03:00
Nick Shirokov b188d338f9 feat(meta-info): выводить «Представление типа» для ссылочных объектов
В шапке ссылочных объектов (справочники, документы, перечисления, ПВХ/ПВР,
планы счетов, планы обмена, бизнес-процессы, задачи) теперь выводится строка
«Представление типа» — имя ссылочного типа в диалогах выбора типа, с fallback
ObjectPresentation -> Synonym -> Name. В режиме full дополнительно выводятся
заданные сырые представления (объекта/списка и расширенные).

Тесты: раннер принимает stdoutContains строкой или массивом, добавлен
stdoutNotContains. Добавлены кейсы meta-info (ед.ч. ПВХ, full со всеми
представлениями, fallback на синоним) и негативная проверка у регистра.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:40:17 +03:00
Nick Shirokov 7c9769c644 feat(web-test): fillTableRow заполняет ячейку-выбор-из-списка через форму выбора
Поле с кнопкой выбора и обработчиком НачалоВыбора (значение выбирается из программного
списка — например колонка Тип в типовой Консоли запросов) раньше заполнялось plain-paste,
который молча откатывался → ok:true/method:direct (ложный успех). Теперь движок детектит
такую ячейку и выбирает значение из формы выбора.

- dom/grid-edit.mjs: readActiveGridCellScript отдаёт buttonKind активной ячейки
  (ref/calc/date/choice по кнопке _DLB/_CB и её классу).
- engine/table/row-fill.mjs v1.20: для kind=choice — F4 → pickFromTypeDialog
  (скан/Ctrl+F/OK) → method:choice; если после выбора открылась форма значения,
  это составная ячейка (нужен {value,type}). Ветка добавлена в Tab-цикл и directEditPick.
- engine/forms/select-value.mjs v1.21: умный dismiss диалога типов на путях
  not_found/multiple — Escape только пока диалог открыт, больше не закрывает
  исходную форму слепым Escape×3.
- Стенд: строковая колонка-выбор ТипЗначения (НачалоВыбора → ПоказатьВыборЭлемента)
  в ДеревоНоменклатуры; тест 16 покрывает method:choice и негатив not_found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:26:37 +03:00
Nick Shirokov 52478a6c39 fix(form-compile): эмитить <ChoiceButton>true</ChoiceButton> при choiceButton:true + StartChoice
Компилятор выводил тег <ChoiceButton> только для значения false; при choiceButton:true
он не эмитился, и у нессылочного поля (например строкового с обработчиком НачалоВыбора)
кнопка выбора не отрисовывалась — документированный паттерн (SKILL.md: choiceButton:true
+ on:['StartChoice']) фактически не работал.

Теперь true эмитится, но узко: только когда у поля есть обработчик StartChoice — чтобы
не раздувать вывод по ссылочным полям (у них choiceButton=true стоит по умолчанию,
а кнопка платформенная). Порты ps1+py синхронны. Снапшот file-dialog обновлён,
31/31 кейс зелёные на обоих портах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:26:23 +03:00
Nick Shirokov ebdd596d4f fix(web-test): числовое поле с калькулятором (iCalcB) заполнять paste, не selectValue
fillFields классифицировал поля по кнопкам: _DLB → ссылка, _CB → pick (если
класс iCalendB → дата). Числовое поле формы (напр. «Цена») имеет _CB с классом
iCalcB (калькулятор) и isDate=false, поэтому уходило в ветку selectValue, которая
ждёт форму выбора → детерминированный фейл "DLB click did not open a popup or
selection form". Калькулятор формой выбора не является.

- dom/forms.mjs: распознаём iCalcB → флаг isCalc (по аналогии с isDate/iCalendB),
  пробрасываем его в resolveFieldsScript.
- engine/forms/fill.mjs: ветку paste расширяем на hasPick && (isDate || isCalc) —
  калькулятор заполняем через Ctrl+A + paste + Tab, как календарь. Ссылочный
  fallback (hasPick без даты/калькулятора) не тронут.

Пробел покрытия: «Цена» в наборе заполнялась только через fillTableRow (Tab-путь),
а fillFields-ветка калькулятора не гонялась. Добавлен 'Цена' в 03-fillfields.test
с assert method=paste и значением 777,00. E2E: тест 03 зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 15:08:14 +03:00
Nick Shirokov 0dde66e2eb fix(web-test): не держать event loop висячим таймером таймаута после теста
Guard-таймаут теста собирался через Promise.race([t.fn, setTimeout(reject)]),
но setTimeout после победы теста не очищался. На успешном пути раннер не зовёт
process.exit(), поэтому node не мог завершиться, пока сторож не догорит — до
`timeout` мс простоя после последнего теста (на стенде с timeout=60s это ~45с
зависания уже после закрытия браузера).

Оборачиваю гонку в try/finally с clearTimeout. Вердикт теста и таймаут-защита
не меняются: clearTimeout срабатывает только после завершения гонки (тест
добежал или сторож сработал), по уже сработавшему таймеру это no-op. Замер на
одиночном тесте: 64.5s → 18.9s wall-clock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:38:56 +03:00
Nick Shirokov 547f336cf8 feat(web-test): test-раннер пишет человеческий отчёт в stdout, JSON по --report=-
Команда `test` приведена к поведению тест-раннеров (jest/pytest/playwright):
человеческий отчёт со сводкой в последней строке идёт в stdout, а машинный
JSON/JUnit — опционально через `--report=-` (Unix-конвенция `-` = stdout),
при этом прогресс уезжает в stderr. Убран безусловный дамп JSON в stdout,
из-за которого `test … | tail` хоронил сводку под отчётом.

- test.mjs: writer выбирается по режиму (--report=- → stderr-прогресс);
  развилка `-` в обеих ветках записи (json и junit), чтобы не плодить файл "-";
  валидация: --report=- несовместимо с --format=allure (каталог, не поток).
- util.mjs: строка --report=- в справке.
- Документация (spec/guide/regress/README) приведена к фактическому
  английскому выводу и описывает матрицу потоков stdout/stderr.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 14:18:12 +03:00
Nick Shirokov f424d2ac70 feat(web-test): фокус на поле ввода через clickElement (fallback)
clickElement как последний fallback (без table) фокусирует одноимённое
поле ввода, не меняя значение — возвращает focused:{field,id,ok}.
Закрывает пробел: клавиши F4/Shift+F4 требовали сфокусированного поля,
но штатного примитива фокуса не было.

- dom/forms.mjs: резолв input.editInput/textarea по имени/заголовку
  последним шагом findClickTargetScript; имена полей в available
- click-form.mjs: focusFormField (клик по инпуту + isInputFocused → ok)
- click.mjs: ветка диспетчера kind === field
- SKILL.md + docs/web-test-guide.md: focused в extras, пример focus→F4
- tests: 19-focus-field.test.mjs (focus/F4/регресс/негатив)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 22:02:12 +03:00
Nick Shirokov 3a89aa21e6 docs: картиночные колонки readTable + valuesPicture в form DSL
- web-test-guide: раздел про picture-колонки readTable (pic:N/'',
  truthy-наличие, именование по тултипу, read/assert-only — не селектор).
- form-dsl-spec: ключи valuesPicture/loadTransparent у picField.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:52:07 +03:00
Nick Shirokov 7de2689c18 test(web-test): картиночная колонка в стенде ДеревоНоменклатуры
Регресс-покрытие picture-колонок readTable на синтетическом стенде
(без зависимости от реальных баз). В обработку ДеревоНоменклатуры:
- булева колонка Картинка + PictureField (ValuesPicture=StdPicture.Favorites,
  loadTransparent) — иконка у позиций Цена>1000;
- CheckBoxField Флаг на тот же булев (кросс-проверка состояния);
- Selection-обработчик ДеревоВыбор — инверсия по двойному клику.

16-tree-form: обновлён deepEqual колонок (+Картинка +Флаг), добавлены шаги
presence/кросс-проверка (pic:0 ⟺ флаг) и Selection-toggle. Полный регресс
web-test зелёный.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:51:02 +03:00
Nick Shirokov 96926d65ef feat(form-compile): ValuesPicture для PictureField
PictureField, привязанный к булеву/числу, без ValuesPicture не рисует
иконку. Добавлены ключи DSL:
- valuesPicture: ref картинки значения (StdPicture.*, CommonPicture.*)
  → <ValuesPicture><xr:Ref>…</xr:Ref></ValuesPicture>
- loadTransparent: true → <xr:LoadTransparent>true</xr:LoadTransparent>
  (выводится только при true)

Реализовано в обоих портах (ps1 + py, v1.22), добавлены в whitelist
свойств. Регресс: новый кейс picture-field (picField + ValuesPicture +
CheckBoxField + событие Selection), эталон зелёный на ps1 и py, плюс
платформенная form-validate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 20:10:36 +03:00
Nick Shirokov f2b8ad741e feat(web-test): распознавание колонок-картинок в readTable
readTable теперь отдаёт картиночные ячейки как 'pic:<N>' (есть иконка)
или '' (нет): N — индекс кадра спрайта, кодирующий состояние. Присутствие
читается как truthy, разные иконки различаются по индексу. Безымянные
картиночные колонки (напр. индикатор присоединённых файлов) больше не
выпадают — именуются по title-тултипу, fallback '(picture)'.

- dom/grid.mjs: helper picInfo (парсинг gx из pictureCollection url,
  исключение декоративных иконок дерева/групп); ветка пустого заголовка
  добавляет картиночные колонки; resolveCol резолвит колонку по тексту И
  по title (клик по картиночной колонке).
- click-cell.mjs: fail-fast при попытке отбора строки по 'pic:N' —
  понятная ошибка вместо row_not_found (картинки read/assert-only).
- SKILL.md: компактный раздел про картиночные колонки.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 17:25:45 +03:00
Nick Shirokov 89b109ab04 test(web-test): покрыть reveal-loop и hasMore на динамическом списке
3 новых шага в 18-cell-click на группе БольшойСписок (60 элементов)
справочника Номенклатура:

- dyn-list setup — вход в группу, проверка hasMore = {above:false, below:true}
  (определяется через turn-кнопки vertButtonScroll, не через scrollbar табчасти)
- dyn-list reveal — clickElement({row:{filter}}, {scroll:true}) на дин-списке,
  находит Позиция 055 через PageDown loop; после прокрутки above=true
- dyn-list cleanup

Раньше reveal-loop и hasMore проверялись только на табчасти LongDoc;
теперь покрыт и второй тип виртуализированного грида.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:54:42 +03:00
Nick Shirokov 81596503e8 test(web-test): группа БольшойСписок (60 элементов) для дин-список сценариев
Справочник Номенклатура: третья группа БольшойСписок с 60 элементами
(Позиция 001..060) — заведомо больше окна виртуализации (~22-30 строк).
Нужна для тестов reveal-loop и hasMore.above/below на ДИНАМИЧЕСКОМ списке
(до этого длинный список был только в табчасти LongDoc).

Группы Товары (15) и Услуги (10) оставлены как есть — существующие тесты
(05/06/12) полагаются на то, что обе помещаются в DOM-окно.

08-hierarchy и 16-tree-form обновлены под 3 группы верхнего уровня
(было жёстко зашито 2): проверяют наличие всех трёх + БольшойСписок.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:54:30 +03:00
Nick Shirokov e36544c1c7 feat(web-test): hasMore.above для динамических списков через turn-кнопки
Раньше для динамических списков (catalog/journal/register) определялся
только hasMore.below (через scrollH>clientH). Направление выше было
неопределимо потому что у дин-списков нет видимого scrollbar widget'а.

Однако у большинства дин-списков 1С рендерит панель пагинации
#vertButtonScroll_<gridId> (сосед грида) с 4 кнопками: data-home
(в начало), data-up (предыдущая страница), data-down (следующая),
data-end (в конец). Класс "disabled" на кнопке = направление недоступно.

readTableScript и snapshotGridScript теперь сначала смотрят на эти
кнопки (если виджет видим), и только потом фолбачатся на scrollbar
tracks для табчастей и scrollHeight для редких случаев без обоих
виджетов.

Проверено на bp-demo Контрагенты:
- root (6 групп помещаются): {above:false, below:false}
- Покупатели at top: {above:false, below:true}
- after End: {above:true, below:false}
- after Home: {above:false, below:true}

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 14:02:33 +03:00
Nick Shirokov 80323a77cc test(web-test): расширить 18-cell-click — reveal-loop, horizontal scroll, skip-checkbox
3 новых шага на расширенном стенде (LongDoc + 18-колоночная ТЧ):

1. reveal-loop — открыть LongDoc через filterList по Комментарий, ТЧ Товары
   виртуализирована (13 строк в окне из 30). Клик с scroll:true по строке
   с Количество=25,000 — должен пролистать PageDown'ом до окна 20..30.

2. horizontal scroll туда-обратно — клик в Признак контроля (последняя,
   18-я колонка, ArrowRight scroll), потом сразу в Количество (2-я колонка,
   ArrowLeft scroll). Проверяет оба направления.

3. focus-click skip checkbox — кластер ВРезерве/НаКомиссии/Подарок у правого
   края дефолтного viewport. Клик в Серия (за пределами viewport) должен
   вызвать ArrowRight scroll с focus-pick на rightmost non-checkbox cell.
   Проверка: boolean'ы в строке 0 не изменились после клика.

Удалил устаревший Note про "перенесём на будущее" — теперь покрыто.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 12:12:25 +03:00
Nick Shirokov 0e5ad754e8 fix(web-test): focus-click для reveal/scroll не должен входить в edit-mode
После focus-click перед PageDown (reveal-loop) или ArrowRight/Left (horizontal
scroll) клавиатурная навигация ломалась если click попадал в Number/Date
ячейку — она автоматически входила в edit-mode, и стрелки начинали навигацию
внутри input вместо движения по гриду.

Два изменения:

1. findFocusCellScript generic mode (без direction) теперь берёт cells[0]
   вместо cells.slice(1) — то есть первую видимую колонку. В document
   tabular sections это типично Reference (Номенклатура), которая не
   входит в edit-mode по single click. Защиту от tree-toggles оставил
   точечно: для tree-гридов (presence of .gridBoxTree) пропускаем
   первую колонку как и раньше.

2. В click-cell.mjs после focus-click в revealAndFindCell и scrollGridToCell
   добавил тот же isInputFocusedInGrid + Escape страховочный фолбек,
   что и в deleteTableRow — на случай если focus всё же попал в input.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 12:12:08 +03:00
Nick Shirokov 9766b8262e test(web-test): кластер boolean ставим сразу после Источник для edge-теста
Чтобы при дефолтном открытии формы 3 boolean (ВРезерве, НаКомиссии, Подарок)
оказывались у правого края viewport. Это даёт прицельный сценарий для теста
focus-click при horizontal scroll — несколько checkbox подряд на краю
заставляют focus-pick walk их и взять non-checkbox дальше внутрь.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:05:06 +03:00
Nick Shirokov 44521c5c16 test(web-test): расширить синтетический стенд под cell-click сценарии
Готовит почву под полноценные тесты clickElement({row,column}) на гридах
формы и virtualization-сценарии:

ТЧ Товары документа ПриходнаяНакладная — было 6 колонок, стало 18:
- Единица, Скидка, СтавкаНДС, СуммаСНДС — для ширины и mix типов
- кластер 3 boolean (ВРезерве, НаКомиссии, Подарок) подряд — для теста
  что focus-click при horizontal scroll умеет пропускать checkbox edge
- Серия, НомерГТД, СтранаПроисхождения, СрокГодности — text/date
- ПризнакКонтроля (boolean) последней колонкой — отдельный edge-case

Новый enum СтавкиНДС (БезНДС, НДС0, НДС10, НДС20) — реалистичный
тип для СтавкаНДС, чтобы не переиспользовать КатегорииЦен (не по смыслу).

Документ LongDoc с 30 строками в ТЧ — для тестов reveal-loop через
scroll: true, когда строка вне DOM-окна виртуализированного грида.
Идентифицируется через Комментарий='LongDoc'.

Колонка Комментарий в форме списка ПриходнаяНакладная — чтобы можно
было искать LongDoc через filterList.

Существующее покрытие 05-table сохранено: добавление колонок
аддитивно, ранее проверенные ассерты не затронуты.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:42:31 +03:00
Nick Shirokov 8f2fa21814 fix(web-test): deleteTableRow выходит из cell edit-mode перед Delete
Delete-клавиша в режиме редактирования ячейки очищает буфер ввода,
а не удаляет строку. Это становилось проблемой когда:
1. предыдущий fillTableRow закончил Tab-навигацией в input (например
   в Number-ячейку соседней колонки), и фокус остался там;
2. сам click на Number/Date ячейку в deleteTableRow автоматически
   входит в edit-mode (поведение 1С).

Фикс: в deleteTableRow проверяем isInputFocusedInGrid дважды — до и
после click — и шлём Escape если активен INPUT в целевом гриде. Строка
остаётся выделенной после Escape, Delete срабатывает.

Дополнительно: isInputFocusedInGridScript / isInputFocusedInGrid теперь
принимают опциональный gridSelector — чтобы можно было прицельно проверять
конкретный грид на многогрид-формах (а не любой `.grid` на странице).

Покрытие: новый шаг в 05-table проверяет сценарий «фокус снаружи грида
(Комментарий), потом delete» — гарантирует что post-click Escape ловит
автоматический вход в edit-mode при клике на Number-ячейку.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:42:10 +03:00
Nick Shirokov e05c0a4a61 feat(web-test): clickElement({row,column}) для гридов формы + readTable.hasMore
clickElement({row,column}) теперь работает не только на SpreadsheetDocument,
но и на гридах формы (динамические списки, табчасти). Маршрутизация:
spreadsheet приоритет (backward-compat), без spreadsheet — первый видимый
грид; явный table='Имя' форсит конкретный грид.

Поддержка:
- row: number — индекс в текущем DOM окне (виртуализация — документировано)
- row: { Колонка: значение } — фильтр по нормализованному содержимому
- scroll: true | number — reveal-loop через PageDown пока строка не найдена
  или DOM не перестал меняться (с лимитом)
- Автоматический горизонтальный скролл к колонке за viewport
  (учитывает frozen-колонки .gridBoxFix)
- Post-scroll visibility check — throw вместо ложного success

readTable обогащён полем hasMore: { above?, below } — единственный
надёжный сигнал виртуализации. total/shown остаются как DOM-окно
(backward-compat) с честным описанием в SKILL.md.

Общий хелпер scrollHorizontallyByKey вынесен в engine/core/, переиспользуется
spreadsheet'ом и грид-click'ом. DOM-логика (findGridCellScript,
findFocusCellScript, snapshotGridScript, resolveCellTargetScript) живёт
в dom/grid.mjs — engine только оркестрирует.

Покрытие: новый 18-cell-click.test.mjs (7 шагов: spreadsheet
regression-guard, catalog dblclick, табчасть, hasMore, 2 error-paths,
cleanup). Расширен 05-table.test.mjs проверкой hasMore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 19:01:11 +03:00
Nick Shirokov dff3ced847 merge: web-test engine refactor (Phase 1-5 + multi-select tests + README) 2026-05-28 13:28:05 +03:00
Nick Shirokov 403da66dd5 docs(web-test): README с CLI флагами, опциями стенда, известными нюансами
tests/web-test/README.md — практический mini-doc по запуску регресса:
- Запуск (полный / один файл / по тегам / по grep)
- CLI флаги runner'а (--tags, --grep, --bail, --retry, --timeout, --report,
  --format, --screenshot, --report-dir, --record)
- Опции стенда после `--` (--rebuild-config, --reload-data, --rebuild-epf,
  --rebuild-stand)
- Когда пересобирать стенд (warm-старт vs триггеры авто-пересборки vs
  ручные сценарии)
- Конфигурация (webtest.config.mjs с contexts a/b, isolation модели)
- Env переменные (WEB_TEST_PRESERVE_CLIPBOARD, WEBTEST_HOOKS_RUNTIME)
- Артефакты (error-*.png, _allure/, lockfiles)
- Известные нюансы:
  * 15-multi-context-handover накапливает Контрагентов между прогонами —
    `02-crud` ловит «`ООО Север` должен быть в списке» когда total>20.
    Лечится `-- --rebuild-stand`.
  * 04-selectvalue auto-history шаг делает warm-up для детерминизма.
  * --screenshot=every-step для full-trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:21:35 +03:00
Nick Shirokov 70be567b13 test(web-test): покрытие multi-select (Ctrl/Shift + clickElement)
clickElement поддерживает modifier: 'ctrl'|'shift' для multi-select строк
списка с момента введения, но не было ни одного теста. Добавлен
17-multiselect.test.mjs:
- ctrl-add: click+ctrl-click → 2 выделенные строки
- shift-range: shift-click формирует диапазон от anchor'а
- readTable отмечает _selected: true на выделенных строках

Полный регресс 20/20 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:25:26 +03:00
Nick Shirokov 60151c801f refactor(web-test): распил clickElement по доменам (Phase 5, §10)
core/click.mjs (307 LOC) распилен на тонкий dispatcher (~105 LOC) +
3 доменных handler-файла. Закрывает §10 родительского плана (отклонение
на этапе C.10 — «split — наследие плана»).

Структура:
- core/click.mjs (~105 LOC) — dispatcher: ensureConnected, spreadsheet-cell
  spec, highlight, confirmation/popup interception, findTarget, dispatch
  по target.kind
- core/helpers.mjs +modifierClick(x, y, modifier, {dbl?}) — общий
  mouse-click helper с поддержкой Ctrl/Shift модификаторов
- forms/click-popup.mjs (~90 LOC) — clickConfirmationButton +
  tryClickPopupItem (popup/confirmation внутри формы — форменный контекст,
  не навигация)
- forms/click-form.mjs (~107 LOC) — clickFormTarget: button/tab/submenu +
  netMonitor lifecycle + post-click submenu detection + confirmation hint
  propagation
- table/click-row.mjs (~95 LOC) — clickGridGroupTarget,
  clickGridTreeNodeTarget, clickGridRowTarget с переиспользованием
  modifierClick и существующих getGridToggleIcon/shouldClickToggle

Контракт dispatcher → handler: (target, ctx) где
ctx = {formNum, modifier, dblclick, toggle, expand, timeout, table, gridSelector}.
Handler возвращает returnFormState({clicked, ...}).

Граф зависимостей остаётся деревом:
- core/click.mjs → table/click-row, forms/click-popup, forms/click-form, spreadsheet
- table/{filter,grid,row-fill}.mjs → core/click.mjs (другие action-функции)
- handler-модули → helpers, wait, grid-toggle (НЕ click.mjs)

Поведение clickElement 1:1, публичный API без изменений.
netMonitor переехал внутрь clickFormTarget со своим try/finally.
Confirmation hint propagation (тот сайт что Phase 2 НЕ конвертировал)
переехал в clickFormTarget — естественное место.

Точечный регресс 7/7 (02-crud, 05-table, 08-hierarchy, 13-misc, 16-tree-form,
11-report, 01-navigation) + полный 19/19 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 18:00:48 +03:00
Nick Shirokov a9949ff5fe refactor(web-test): uniform ok:true/false в filled-items + контракт fillTableRow (Phase 4)
Раньше per-item shape в filled[] был heterogeneous:
- success: {field, ok: true, method, value}
- failure: {field, error, message} ← без ok!

Естественная проверка `item.ok === false` молча промахивалась (latent bug в
production-клиенте Titan C:\WS\projects\titan\tests\helpers\query.mjs:69).
И документация утверждала «все функции throw'ают на ошибке» (guide.md:352),
что для fillTableRow было неправдой.

Что изменено:
- engine/table/row-fill.mjs v1.18 → v1.19: 15 error-pushes теперь включают
  ok: false. item.ok — единый дискриминатор success/failure.
- SKILL.md: fillFields раздел уточнён («throws on per-field failure — если
  вернулся, всё заполнено»). fillTableRow раздел: документирует контракт
  (НЕ throws на per-field), перечисляет error-коды и recovery hints
  (composite_type → retry с {value, type}, column_not_found → проверить
  readTable, и т.д.).
- docs/web-test-guide.md: строка 352 нюансирована (fillTableRow исключение
  из «все throw'ают»); строка 296 (таблица) уточнена.

Контракт обеих функций теперь сознательно различается и явно описан:
- fillFields = fail-fast (throws на любую ошибку, удобно для fill-and-go)
- fillTableRow = partial-recovery (errors в filled[] как ok:false, модель
  может retry'нуть селективно отдельную ячейку)

Бонус: query.mjs в Titan'е теперь работает корректно без правки клиентского
кода — cell.ok === false наконец-то дискриминирует error-items.

Полный регресс 19/19 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 17:09:59 +03:00
Nick Shirokov 07353c416e refactor(web-test): унификация shape fillFields + fillTableRow (Phase 3)
Все action-функции теперь возвращают плоский form state с extras —
закрыта последняя аномалия API. Раньше:
- fillFields → {filled, form} (вложенный, документировано в SKILL.md)
- fillTableRow → 3 разных shape в 5 ветках (array | {filled, form} | {filled, notFilled, form}),
  при этом документация заявляла плоский — код её игнорировал

Теперь обе функции используют returnFormState({filled, notFilled?}) — тот же
паттерн что у всех action-функций после Phase 1+2 (clickElement, selectValue,
closeForm, filterList и т.д.).

Что закрывает:
1. Тихий баг в production-клиенте C:\WS\projects\titan\tests\helpers\query.mjs
   на res.filled?.find() — array-ветки fillTableRow возвращали [{...}] без .filled
   → ошибки заполнения параметров запросов молча пропускались. R1/R2-аналог.
2. Костыли r.filled || r в tests/web-test/05-table.test.mjs (2 места) —
   убраны, поскольку polymorphism устранён.
3. Расхождение код ↔ документация в fillTableRow.
4. Внутренний polymorphism в row-fill.mjs: убраны два `if (Array.isArray(more))`
   костыля в рекурсивных вызовах самого fillTableRow.

Файлы:
- engine/forms/fill.mjs v1.17 → v1.18 (1 ветка → returnFormState)
- engine/table/row-fill.mjs v1.17 → v1.18 (5 веток + 2 рекурсии)
- tests/web-test/05-table.test.mjs (r.filled || r → r.filled)
- .claude/skills/web-test/SKILL.md (сигнатуры fillFields/fillTableRow + общая
  ремарка про плоский return shape в начале раздела Actions)
- docs/web-test-guide.md (строки fillFields/fillTableRow/navigateSection;
  общая ремарка в начале раздела «Действия»)

В тестах ни один кейс не обращался к .form.X, blast radius нулевой.
Точечный регресс (03/05/06/07/10/16) и полный регресс 19/19 — зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:27:46 +03:00
Nick Shirokov 6e09351730 refactor(web-test): returnFormState idem дедуп (Phase 2, 5 сайтов)
Дедуп 5 идемпотентных хвостов action-функций — паттерн
getFormState + state.X = ... + checkForErrors + if(err) state.errors = err + return
сводится к одному returnFormState(extras). Поведение identical.

- core/click.mjs: ветка popupItem (1 сайт)
- forms/select-value.mjs: openFormAndPick helper, DLB dropdown match,
  DLB dropdown no-search-first, selection-form direct 3B (4 сайта)

Аудит upload/returnFormState-audit.md обещал 13 idem, но при разборе:
- click.mjs:final (custom confirmation/hint), close.mjs save=undefined (R3 предупреждение),
  6 сайтов fillReferenceField (приватный shape {field, ok, method, value}, не form-state)
— осознанно НЕ конвертированы. Реальный Phase 2 — 5 сайтов.

Полный регресс 19/19 зелёный (после rebuild стенда — старый стенд накопил
22 Контрагента вместо 4 за прошлые прогоны, не связано с Phase 2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:09:24 +03:00
Nick Shirokov 961f27afb0 refactor(web-test): deleteTableRow coords + reuse countGridRows (S10)
19 LOC inline cellCoords + 8 LOC inline row-count в engine/table/grid.mjs
deleteTableRow → новый findDeleteRowCoordsScript в dom/grid.mjs + переиспользован
существующий countGridRowsScript (dom/grid.mjs:268, добавлен в S5).

Engine-сторона deleteTableRow становится чистым оркестратором: resolve grid →
get coords → click → Delete → count rows.

DOM-extraction iter 2 / S10 из плана. Точечный регресс 05-table зелёный +
полный регресс 19/19 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:33:21 +03:00
Nick Shirokov 8f0d3937b4 refactor(web-test): scanGridRows вынесен в dom/grid.mjs (S9)
25 LOC inline page.evaluate в forms/select-value.mjs:30 → новый экспорт
scanGridRowsScript(formNum, searchLower) в dom/grid.mjs (рядом с
readTableScript, getSelectedOrLastRowIndexScript). Engine-сторона —
тонкая обёртка одной строкой.

DOM-extraction iter 2 / S9 из плана. Точечный регресс 04-selectvalue зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:18:48 +03:00
Nick Shirokov f554ef4599 refactor(web-test): вынести error-stack scraping в dom/errors-stack.mjs (S8)
5 inline page.evaluate блоков (~50 LOC) из fetchStackViaReport (path-1 OpenReport
flow платформенных исключений) → новый dom/errors-stack.mjs с 5 экспортами:
getOpenReportCoordsScript, isErrorDetailLinkVisibleScript,
readLargestVisibleTextareaScript, clickTopCloudOkButtonScript,
clickReportCloseButtonScript.

Engine-сторона fetchStackViaReport теперь читается как чистый оркестратор
6 шагов без длинных DOM-строк. Поведение 1:1.

DOM-extraction iter 2 / S8 из плана. Точечный регресс 14-errors-stack зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:15:51 +03:00
Nick Shirokov 486890c388 test(web-test): сделать 04-selectvalue auto-history детерминированным
Step auto-history полагался на наполненную UserChoiceHistory от предыдущих тестов
(06-document и т.д.) — в одиночном прогоне history для 'ООО Юг' пустая,
typeahead не активировался, method=form вместо ожидаемого dropdown.

History в 1С — per-value: первый выбор значения через form наполняет историю,
второй выбор того же значения идёт через typeahead-dropdown. Добавлен warm-up:
selectValue('Менеджер', 'ООО Юг') → clear → второй selectValue того же значения
(уже из истории).

Закрывает §0.8 #4 родительского плана. Регресс одиночный + полный 19/19 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:03:00 +03:00
Nick Shirokov 707033e25b refactor(web-test): returnFormState в nav + grid + spreadsheet + selectValue (7 веток)
navigation.mjs: navigateSection (sections+commands ветка), switchTab, openFile
(оба success-сайта). Закрывает R1/R2 для навигационных action-функций.

grid.mjs: deleteTableRow — заодно унификация shape с SKILL.md ("→ form state"
плоский). До этого код возвращал {deleted, ..., form: formData} в нарушение
документации; теперь плоский state с extras. JSDoc обновлён.

spreadsheet.mjs: clickSpreadsheetCell. select-value.mjs: ветка clear-success
selectValue (search=null, method='clear').

Phase 1 / C3 (final) из плана upload/returnFormState-audit.md. Полный регресс 19/19
зелёный. Известный pre-existing test-isolation issue в одиночном прогоне
04-selectvalue (auto-history) — описан в backlog §0.8 #4 родительского плана.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:41:13 +03:00
Nick Shirokov a381fca0a1 refactor(web-test): returnFormState в close.mjs + filter.mjs (7 веток)
closeForm: platform-dialogs, save=true/false, final-escape — теперь подмешивают
state.errors через returnFormState. Ветка save=undefined (hint-return)
осознанно оставлена без errors (юзер ещё не принял решение).

filterList: simple search, advanced search — закрывают R1/R2.
unfilterList: selective (field) + clear-all — аналогично.

Phase 1 / C2 из плана upload/returnFormState-audit.md. Точечный регресс зелёный
(02-crud, 06-document, 09-filter).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:21:00 +03:00
Nick Shirokov 280df54fa6 refactor(web-test): returnFormState в click.mjs (10 веток)
Фикс тихих багов R1/R2 — каждая ветка clickElement теперь подмешивает state.errors
через хелпер returnFormState (engine/core/helpers.mjs). До правки ветки confirmation,
submenuArrow, gridGroup/gridTreeNode (toggle+default), gridRow (click/dblclick),
submenu (pre+post-wait) возвращали state без checkForErrors → exec-wrapper не throw'ал
на soft validation errors (balloon/modal).

Phase 1 / C1 из плана upload/returnFormState-audit.md. Точечный регресс зелёный
(02-crud, 05-table, 08-hierarchy, 13-misc, 16-tree-form).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 12:15:02 +03:00
Nick Shirokov 8fd5544abd refactor(web-test): bump browser.mjs до v1.18 (финализация S1–S6)
Завершён рефакторинг §0.5 п.5 родительского плана (вынос inline DOM в
dom/). Итоговые метрики:

| Файл              | До (LOC, evals) | После (LOC, evals) |
|-------------------|-----------------|--------------------|
| row-fill.mjs      | 1235 / 47       |  793 / 1           |
| select-value.mjs  |  959 / 25       |  827 / 5           |
| filter.mjs        |  390 / 17       |  256 / 0           |
| Σ engine hot      | 2584 / 89       | 1876 / 6           |

Снижение LOC −708 (−27%), inline page.evaluate −83 (−93%).

dom/ расширился с 7 до 11 файлов: новые edd.mjs, edit-state.mjs,
filter.mjs, grid-edit.mjs; расширены forms.mjs (+16 функций) и
grid.mjs (+4 функции).

Engine-модули стали orchestrator-ами. Публичный API browser.mjs —
56 экспортов, без изменений. Полный регресс зелёный после каждого
этапа S1–S6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:32:09 +03:00
Nick Shirokov b518b614bb refactor(web-test): извлечён grid-edit домен в dom/grid-edit.mjs
Тяжёлые DOM-блоки row-fill (15–60 строк) вынесены в 8 именованных
функций dom/grid-edit.mjs. row-fill стал плоским orchestrator-ом.

Новое в dom/grid-edit.mjs:
- sortFieldKeysByColindexScript    — сортировка keys по colindex (Tab-нав)
- findCellCoordsByFieldsScript     — клетка по first matching header (multi)
- findNextCellCoordsByKeyScript    — клетка по single key + no-space fuzzy
- findCheckboxAtPointScript        — checkbox info по координатам elementFromPoint
- findRowCommitClickCoordsScript   — клик OTHER row для commit-edit
- getGridEditCheckScript           — { inEdit, tag?, hint? } диагностика
- readActiveGridCellScript         — активная клетка (id, fullName, headerText)
- getElementCenterCoordsByIdScript — центр по id (дедуп 2 копий)

Метрики row-fill: 971 → 793 LOC (−178, S6 alone), inline page.evaluate
10 → 1 (значительно ниже плановой цели ≤10). Все табличные suite
зелёные (05/06/08/10/16), полный регресс зелёный (Checkpoint-2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:31:02 +03:00
Nick Shirokov b08ee99521 refactor(web-test): извлечены grid read-helpers + cloud-popup в dom/
Новое в dom/grid.mjs (все принимают опциональный gridSelector):
- countGridRowsScript            — кол-во .gridLine в body
- isTreeGridScript               — тип grid'а (есть .gridBoxTree)
- findGridHeadCenterCoordsScript — центр .gridHead для commit-клика
- getSelectedOrLastRowIndexScript — selected row index, fallback на последний

Также:
- isInputFocusedInGrid wrapper (S1) применён в add-row "ready" поллинге
- isNotInListCloudVisibleScript (S3) применён вместо локального notInList
- clickShowAllInNotInListCloudScript — новая в dom/forms.mjs (клик
  "Показать все" в "нет в списке" cloud popup через dispatchEvent)

Метрики row-fill: 1041 → 971 LOC (−70), evaluates 17 → 10. Регресс
05/08/16/10 — зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 21:07:10 +03:00
Nick Shirokov 89efcad125 refactor(web-test): извлечён EDD-домен в dom/edd.mjs
Новое в dom/edd.mjs:
- readEddScript                  — {visible, items:[{name,x,y}]}
- isEddVisibleScript             — boolean, лёгкая проверка
- clickEddItemViaDispatchScript  — клик по name через dispatchEvent (bypass
                                   div.surface); fuzzy с NBSP/ё/bracket-strip
- clickShowAllInEddScript        — "Показать все" в footer

Wrappers в helpers.mjs: readEdd, isEddVisible, clickEddItemViaDispatch,
clickShowAllInEdd. row-fill clickEddItem унифицирован с select-value
вариантом (NBSP/ё normalization теперь работает и для табличных строк).

Метрики: select-value 880 → 827 LOC (−53), row-fill 1065 → 1041 LOC
(−24); evaluates row-fill 20 → 17, select-value 7 → 5. Полный регресс
зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:57:58 +03:00
Nick Shirokov 340142b0a2 refactor(web-test): извлечены DOM-скрипты dialog/picker UI из select-value
Применены shared-функции из S2 (findSearchInputScript, findNamedButton,
findCompareTypeRadio, isFormVisible). Добавлены новые для type-dialog и
picker UI:
- findPatternInputIdScript          — Pattern input id (Alt+F dialog)
- isTypeDialogScript                — OK + ValueList + "Выбор типа" title
- isNotInListCloudVisibleScript     — "нет в списке" tooltip popup
- findChildFormByButtonScript       — поиск child-form по имени кнопки
- readTypeDialogVisibleRowsScript   — visible rows + fuzzy matches в ValueList

select-value.mjs: 950 → 880 LOC (−70), inline page.evaluate 24 → 7
(планировали ≤8). Регресс 06/11/13 зелёный; полный регресс зелёный
(Checkpoint-1 пройден). 04-selectvalue auto-history шаг — pre-existing
test-isolation issue (см. S1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:38:01 +03:00
Nick Shirokov 7f7ab2f217 refactor(web-test): извлечены DOM-скрипты filter.mjs в dom/filter.mjs
Все 17 inline page.evaluate в engine/table/filter.mjs вынесены в
именованные dom-генераторы. Engine-модуль стал чистым orchestrator-ом.

Новое в dom/forms.mjs (shared с будущим S3 select-value):
- findSearchInputScript(formNum)        — поиск SearchString/ПоискаСтроки input
- findNamedButtonScript(text)           — кнопка a.press по innerText (Найти, OK)
- findCompareTypeRadioScript(form, idx) — радио CompareType#N#radio
- isFormVisibleScript(form)             — есть ли видимые элементы form{N}

Новое в dom/filter.mjs:
- findFirstGridCellCoordsScript          — координаты первой клетки грида
- findColumnFirstCellCoordsScript        — клетка по имени колонки (fuzzy header
                                           match с needDlb-fallback)
- readFieldSelectorInfoScript            — FieldSelector value + DLB coords
- pickFieldInSelectorDropdownScript      — выбор поля в FieldSelector DLB-edd
- readFilterDialogInfoScript             — Pattern id+value+isDate+isRef
- findFilterBadgeCloseScript             — × badge по имени поля
- findFirstFilterBadgeCloseScript        — × первого видимого badge (для clear-all)

Попутно: добавлен импорт readSubmenuScript (был pre-existing broken
import в Еще-fallback ветке Alt+F).

Метрики filter.mjs: 390 → 256 LOC (−134, −34%), inline page.evaluate
17 → 0. Регресс 09-filter / 02-crud / 05-table — зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:16:23 +03:00
Nick Shirokov 85003782db refactor(web-test): извлечены detect-new-form и edit-state из inline в dom/
Дедуплицированы 15 копий detect-new-form (13 в row-fill + 2 локальные
обёртки в select-value), 6 копий INPUT-focused, 4 проверки calendar/
calculator popup, 1 INPUT-focused-inside-grid.

Новое:
- dom/forms.mjs: detectNewFormScript(prev, {strict}) — объединяет broad
  и strict варианты
- dom/edit-state.mjs: isInputFocusedScript({allowTextarea}),
  isInputFocusedInGridScript, findOpenPopupScript
- helpers.mjs: переписан detectNewForm на dom-script; добавлены тонкие
  обёртки isInputFocused, isInputFocusedInGrid, findOpenPopup

Метрики row-fill: 1235 → 1065 LOC (−170), inline page.evaluate 47 → 20.
Поведение идентично; точечный регресс зелёный (02/03/05/06/10/16).
04-selectvalue auto-history шаг — pre-existing baseline issue (state-
driven, не связан с S1, воспроизводится на HEAD).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 19:54:36 +03:00
Nick Shirokov 65ea06ab6e refactor(web-test): run.mjs распилен по cli/ (1258 → 65 LOC entry)
Внутренности move в cli/:
- util.mjs — out/die/json/readBody/readStdin/elapsed/elapsed2/slugify/formatDuration/xmlEscape/interpolate/printSteps/usage
- session.mjs — SESSION_FILE, loadSession, cleanup
- exec-context.mjs — buildContext, buildScopedContext, executeScript
- server.mjs — handleRequest (HTTP сервер в процессе start)
- commands/{start,run,exec,shot,stop,status,test}.mjs — по одной команде на файл
- test-runner/assertions.mjs — createAssertions (ctx.assert API)
- test-runner/severity.mjs — SEVERITY_RANK/LEVELS, buildSeverityIndex, resolveSeverity
- test-runner/reporters.mjs — writeAllure, allureStep, syncAllureExtras, buildJUnit
- test-runner/discover.mjs — discoverTests, resetState

run.mjs остался публичным entry-point с CLI-парсингом и dispatcher'ом.
Регресс tests/web-test/ зелёный (19/19, 9m 28s).
2026-05-26 18:08:15 +03:00
Nick Shirokov 71607bef99 refactor(web-test): dom.mjs распилен по dom/ (1434 → 41 LOC facade)
Внутренности movе в dom/:
- _shared.mjs — HAS_VISIBLE_MODAL_FN, DETECT_FORM_FN, DETECT_FORMS_FN, READ_FORM_FN
- forms.mjs — detectFormScript, readFormScript, findClickTargetScript, findFieldButtonScript, resolveFieldsScript
- form-state.mjs — getFormStateScript
- grid.mjs — resolveGridScript, readTableScript
- nav.mjs — readSectionsScript, readTabsScript, switchTabScript, readCommandsScript, navigateSectionScript, openCommandScript
- submenu.mjs — readSubmenuScript, clickPopupItemScript
- errors.mjs — checkErrorsScript

dom.mjs остался публичным entry-point с теми же 17 экспортами.
Регресс tests/web-test/ зелёный (19/19, 9m 22s).
2026-05-26 17:47:13 +03:00
Nick Shirokov c930b4b04d refactor(web-test): spreadsheet выделен в собственную папку
SpreadsheetDocument (отчёты, печатные формы) — другой домен, чем form-grid
(табличные части документов, списки). Раньше лежал внутри table/, что
было обманчиво.

  engine/table/spreadsheet.mjs → engine/spreadsheet/spreadsheet.mjs

Структура engine/:
  core/         плумбинг движка (state, wait, errors, session, click, ...)
  forms/        работа с формами (fill, close, select-value, state)
  nav/          навигация
  table/        form-grid (grid, row-fill, filter, grid-toggle)
  spreadsheet/  SpreadsheetDocument
  recording/    запись + overlays

В будущем при росте spreadsheet можно распилить — engine/spreadsheet/cells.mjs,
engine/spreadsheet/scroll.mjs и т.д. без переименований.

11-report регресс зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:52:15 +03:00
Nick Shirokov 8bdcb9e664 refactor(web-test): form-state переехал из core/ в forms/
getFormState — высокоуровневая операция «прочитать состояние формы»,
семантически в forms/ ближе чем в core/ (foundational плумбинг движка).

  engine/core/form-state.mjs → engine/forms/state.mjs

Все 11 importer'ов обновлены. Внутри state.mjs пути исправлены:
'./state.mjs' → '../core/state.mjs', './errors.mjs' → '../core/errors.mjs'.

03-fillfields регресс зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:48:08 +03:00
Nick Shirokov ab10761667 chore(web-test): почистить устаревшие комментарии и неиспользуемые импорты
После полной чистки cycle-импортов в E.13 остались комментарии типа
"getFormState still in browser.mjs", которые больше не верны (он переехал
в engine/core/form-state.mjs). Сметаем устаревшие "moved to / lives in
browser.mjs" комментарии в 8 файлах.

Дополнительно в engine/table/spreadsheet.mjs:
  - убраны неиспользуемые импорты readTableScript, resolveGridScript, normYo
    (остались с тех пор, как readTable жил в этом файле — до этапа D.12
    rename'а в grid.mjs)
  - заголовочный комментарий обновлён (без упоминания readTable)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:42:17 +03:00
Nick Shirokov a24c39b6de refactor(web-test): этап E.13 — финализация (v1.17 + чистый facade + чистка)
1. Версия v1.16 → v1.17 во всех заголовках движка.

2. browser.mjs стал чистым facade — только re-exports, 0 функций определено.
   Было: 249 LOC с 4 настоящими функциями (saveClipboard, restoreClipboard,
   pasteText, getFormState) — теперь 57 LOC чистых re-export'ов.

3. engine/core/clipboard.mjs — новый модуль:
     pasteText + saveClipboard + restoreClipboard (~85 LOC, был в browser.mjs).

4. engine/core/form-state.mjs — новый модуль:
     getFormState — центральный читатель состояния формы (~30 LOC).

5. Убрано 12 циклических импортов из engine/* → ../../browser.mjs:
   - Все читатели pasteText теперь импортят из engine/core/clipboard.mjs
   - Все читатели getFormState — из engine/core/form-state.mjs
   - session.mjs → nav/navigation.mjs (getPageState напрямую)
   - filter.mjs → core/click.mjs (clickElement напрямую)
   Граф зависимостей стал деревом (без обратных рёбер).

6. Убраны _-префиксы у 9 функций, которые стали приватными внутри своих
   модулей (раньше _ означало "приватная для browser.mjs"):
     _detectPlatformDialogs → detectPlatformDialogs
     _closePlatformDialogs → closePlatformDialogs
     _parseErrorStack → parseErrorStack
     _fetchStackViaReport → fetchStackViaReport
     _fetchStackViaHamburger → fetchStackViaHamburger
     _logoutSlot → logoutSlot
     _saveActiveSlot → saveActiveSlot
     _activateSlot → activateSlot
     _attachSessionListeners → attachSessionListeners

Публичный API: 56 экспортов, идентичный исходному.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:25:15 +03:00
Nick Shirokov 8739d1d15c refactor(web-test): структура — engine/ wrapper для внутренних модулей
Перенос всей внутрянки движка под scripts/engine/:
  - core/, forms/, nav/, table/, recording/ → engine/<same>/

Публичные entry-точки остаются в scripts/ корне без изменений:
  - browser.mjs, dom.mjs, run.mjs — компат не ломаем.

Симметричный layout, легко читать с первого взгляда:
  scripts/
    browser.mjs, dom.mjs, run.mjs    ← публичные entries
    engine/                          ← внутренности движка
    (dom/, cli/ — место под будущий распил dom.mjs / run.mjs)

Технические правки после переезда:
  - browser.mjs: ./core/... → ./engine/core/... (23 импорта)
  - engine/*/* модули: ../browser.mjs → ../../browser.mjs (11 импортов)
  - engine/*/* модули: ../dom.mjs → ../../dom.mjs (12 импортов)
  - engine/recording/capture.mjs: dynamic import('../browser.mjs')
    → import('../../browser.mjs')
  - engine/core/state.mjs: projectRoot пересчитан (5 → 6 уровней вверх)
  - Git rename detection срабатывает — история файлов сохраняется

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 16:03:20 +03:00
Nick Shirokov f31770d79c refactor(web-test): этап D.12 — fillTableRow → row-fill.mjs, deleteTableRow → grid.mjs
table/row-fill.mjs (~1230 LOC): fillTableRow целиком, как entry-point.
table/grid.mjs: добавлен deleteTableRow + расширены импорты (clickElement,
dismissPendingErrors, waitForStable, getFormState).

Дальнейший распил fillTableRow на под-хелперы (trySelect/readVisibleRows/
pickValueWithOptionalType/enterEditMode/fillCellSequentially per плана §12.1-12.3)
отложен — при необходимости создаём table/row-fill/*.mjs subfolder.

browser.mjs: 1532 → 249 LOC (-84% от baseline 6293).
05-table регресс зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 15:02:47 +03:00
Nick Shirokov a5c0be6766 refactor(web-test): переименование — readTable в table/grid.mjs
readTable читает form-grid (.gridLine/.gridBody — табличные части на форме,
списки), а не SpreadsheetDocument. Имя файла table/spreadsheet.mjs было
обманчиво. Разделяем домены:

  table/grid.mjs        ← readTable (form-grid операции, готово
                         для fillTableRow + deleteTableRow в D.12)
  table/spreadsheet.mjs ← readSpreadsheet + cell helpers (только
                         SpreadsheetDocument — отчёты, печатные формы)

Поведение 1-в-1. browser.mjs re-export обновлён.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 14:59:28 +03:00
Nick Shirokov 50d40a9dd5 fix(web-test): добавить getFormState в импорты table/spreadsheet.mjs
clickSpreadsheetCell вызывает getFormState в конце (для drill-down формы),
но import не был добавлен при экстракции на C.11. ReferenceError в
11-report drill-down. Импортируем из browser.mjs (циклически).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:26:48 +03:00
Nick Shirokov 0ba8127d52 refactor(web-test): этап C.11 — table/spreadsheet.mjs + table/filter.mjs
table/spreadsheet.mjs (~580 LOC):
  - readTable, readSpreadsheet
  - scanSpreadsheetCells, buildSpreadsheetMapping (private)
  - scrollSpreadsheetToCell, clickSpreadsheetCell, findSpreadsheetCellByText

table/filter.mjs (~390 LOC): filterList, unfilterList

После C.11 + C.10 clean-up:
  - core/click.mjs импортит clickSpreadsheetCell/findSpreadsheetCellByText
    напрямую из table/spreadsheet.mjs (а не из browser.mjs)
  - browser.mjs больше не реэкспортирует эти два — публичный API
    остаётся 56 экспортов как до рефакторинга
  - Добавил import { clickElement } в browser.mjs для внутренних вызовов
    из fillTableRow/deleteTableRow

browser.mjs: 2470 → 1532 LOC (≈75% от исходных 6293).
05-table + 09-filter регресс зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:14:49 +03:00
Nick Shirokov 9ee0473412 refactor(web-test): этап C.10 — clickElement → core/click.mjs (целиком)
clickElement (~300 LOC) перенесён единым блоком в core/click.mjs.
Поведение 1-в-1. Внутри остаётся всё ветвление (spreadsheet, submenu,
gridGroup/Parent, gridTreeNode, gridRow, tab, button) — разнос на
forms/click-form.mjs + nav/click-popup.mjs + finer table-toggle
отложен на E.13 для безопасности.

clickSpreadsheetCell + findSpreadsheetCellByText временно exported из
browser.mjs (нужны core/click.mjs). На C.11 они переедут в
table/spreadsheet.mjs, экспорт из browser.mjs можно будет убрать.

browser.mjs: 2768 → 2470 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:07:22 +03:00
Nick Shirokov cbd580a0bd refactor(web-test): этап C.9 — выделить forms/fill.mjs + forms/close.mjs
forms/fill.mjs (~140 LOC): fillFields, fillField
forms/close.mjs (~50 LOC): closeForm
clickElement остаётся в browser.mjs до C.10.

Допиленные импорты после первого прохода:
  - fill.mjs: readFormScript, normYo (из dom/state — забыл при экстракции)
  - close.mjs: recorder (используется для паузы 500ms при confirmation
    во время записи)

03-fillfields регресс зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:04:09 +03:00
Nick Shirokov d67874ebd0 Merge branch 'dev' into refactor/web-test-engine 2026-05-26 12:39:22 +03:00
Nick Shirokov 6781bb3ee5 fix(skd-compile): авто-выборка и авто-порядок в группах из shorthand-структуры
Платформа добавляет SelectedItemAuto и OrderItemAuto в каждую группировку при
ручном создании в конфигураторе. Shorthand-запись (например
'Номенклатура > details') теперь даёт эквивалентный результат — каждая
группа получает selection=['Auto'] и order=['Auto']. Без этого roundtrip
decompile→compile терял авто-элементы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:38:13 +03:00
Nick Shirokov 3a6d5abffc refactor(web-test): этап C.8 — выделить forms/select-value.mjs
Перенос selectValue + helpers из browser.mjs (~960 LOC):
  - scanGridRows, dblclickAndVerify, advancedSearchInline
  - pickFromSelectionForm, isTypeDialog, pickFromTypeDialog (экспортируются —
    вызываются из fillFields/fillTableRow в browser.mjs)
  - fillReferenceField (экспортируется — вызывается из fillFields)
  - selectValue

Двумя слайсами вокруг fillFields/fillField/clickElement/closeForm, которые
остаются в browser.mjs до этапов C.9/C.10.

browser.mjs: 4095 → 2933 LOC. 56 публичных экспортов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:28:31 +03:00
Nick Shirokov c4b1aee9c9 refactor(web-test): этап C.7 — выделить nav/navigation.mjs
Перенос navigation-функций из browser.mjs (~240 LOC):
  - getPageState, getSections, navigateSection, getCommands
  - openCommand, switchTab
  - openFile (Ctrl+O + security dialog flow)
  - navigateLink (Shift+F11 e1cib paste)
  - E1CIB_TYPE_MAP, E1CIB_APP_TYPES, normalizeE1cibUrl (приватные)

Цикл с browser.mjs (getFormState, pasteText) — статический ESM-импорт,
разрешается во время вызова (binding live). core/session.mjs продолжает
импортить getPageState из browser.mjs через re-export.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 12:16:56 +03:00
Nick Shirokov 12c5cf5e66 fix(web-test): TDZ в selectValue (detectNewForm) + missing import clipboardWarnLogged
1. В selectValue локальный const detectNewForm = () => ... объявлялся ниже
   composite-type ветки, которая его вызывала → TDZ ReferenceError "Cannot
   access 'detectNewForm' before initialization". Хелпер поднят в начало
   функции, дубликат-объявление убрано.

2. clipboardWarnLogged читается в restoreClipboard (line 92), но не был
   в списке импортов из core/state.mjs (импортировался только setter).
   ReferenceError срабатывал только когда clipboard.read() возвращал
   ошибку — в первом A-регрессе ветка не активировалась случайно.

Регресс 18/19 (одна flake в 11-report — readSpreadsheet timing).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 11:58:26 +03:00
Nick Shirokov 6fb5b9f617 refactor(web-test): этап B.6 — table/grid-toggle.mjs (icon detection shared)
В clickElement две ветки (gridGroup/gridParent + gridTreeNode) имели
почти идентичные page.evaluate-блоки: найти gridLine под target.y,
получить иконку-разворачивалку, вернуть её центр + isExpanded.

table/grid-toggle.mjs:
  - getGridToggleIcon(target, formNum, { iconSelector, isExpandedExpr })
  - shouldClickToggle(iconInfo, expand, toggle)

Поведение 1-в-1. Селекторы и isExpanded-критерий передаются параметрами:
  - groups: '.gridListH, .gridListV' + icon.classList.contains('gridListV')
  - trees:  '.gridBoxImg [tree="true"]' + bg.includes('gx=0')

Экономия ~30 LOC дублей.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:44:18 +03:00
Nick Shirokov 9ac0cb3b87 refactor(web-test): этап B.5.5 — ввести returnFormState (выборочно применить)
core/helpers.mjs: returnFormState(extras) — стандартный хвост action-функций:
getFormState + Object.assign(extras) + checkForErrors → state.errors. Унифицирует
~15 hand-written копий и закрывает R1/R2/R3 (state.errors теперь добавляется
автоматически у любого пользователя хелпера).

В этом коммите конвертированы только 2 простейших P1-сайта (openCommand,
второй handle в navigateLink) — без extras между getFormState и err-проверкой.
Остальные 30+ сайтов сложнее (state.X между, разные return-shape, wrapped
fillFields) — будут мигрированы органически при переносе clickElement/
selectValue/closeForm в forms/* на этапе C.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:42:23 +03:00
Nick Shirokov e215957344 refactor(web-test): этап B.5.4 — readEdd хелпер (2 копии в fillReferenceField)
В fillReferenceField было два места с одинаковым page.evaluate-скриптом
чтения #editDropDown (DLB-popup перед paste и autocomplete после Ctrl+V).

core/helpers.mjs: readEdd() → { visible, items?: [{ name, x, y }] }.

selectValue использует свой clickEddItem через dispatchEvent (bypass div.surface) —
оставлен как есть, специфика API там сильно отличается.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:35:24 +03:00
Nick Shirokov 09b2084672 refactor(web-test): этап B.5.3 — detectNewForm хелпер (3 копии → 1)
В fillReferenceField, selectValue и fillTableRow была одна и та же логика:
сканировать DOM на наличие элемента с id="form{N}_*" где N > prevFormNum.
Две вариации: strict (только visible interactive — input.editInput/a.press)
и broad (любой [id], учитывает type-dialogs с пустыми button-id).

core/helpers.mjs: detectNewForm(prevFormNum, { strict }) → number|null.
Внутри функций оставлены тонкие локальные обёртки (для совместимости
с уже использующейся сигнатурой без аргументов) — будут убраны на C.8/D.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:34:21 +03:00
Nick Shirokov 3fe038277f refactor(web-test): этап B.5.2 — findFieldInputId хелпер (4 копии → 1)
В selectValue было 4 одинаковых блока поиска input-элемента поля
по имени (form{N}_{name} либо form{N}_{name}_i0 для refs):
clear-ветка, composite-type-ветка, F4-fallback, "last resort" F4.

core/helpers.mjs: findFieldInputId(formNum, fieldName) → string|null.
~30 LOC дублей убрано, поведение 1-в-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:33:15 +03:00
Nick Shirokov 5b6243bbcc refactor(web-test): этап B.5.1 — safeClick хелпер вместо 3 копий pointer-events retry
В fillReferenceField, clickElement и DLB-ветке selectValue был один и тот же
паттерн: page.click → catch 'intercepts pointer events' → force-click →
catch снова → Escape + retry. Три копии (плюс одна с dismissPendingErrors).

core/helpers.mjs (новый): safeClick(selector, { timeout, dismissErrors }).
Экономия ~60 LOC дублей. Поведение 1-в-1 (dismissErrors:true только в
fillReferenceField — там единственное место, где исходно было).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:31:45 +03:00
Nick Shirokov 2cba13a8cc fix(web-test): экспортировать _detectPlatformDialogs/_closePlatformDialogs из core/errors.mjs
После A.3 эти helpers стали приватными в core/errors.mjs, но getFormState
(browser.mjs:408) и closeForm (browser.mjs:2168) их по-прежнему вызывают —
ловили ReferenceError на каждое действие. Делаем их экспортируемыми
и импортируем в browser.mjs. Имя с подчёркиванием сохраняется до этапа E.13
(финальная чистка). Регресс 19/19.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:25:54 +03:00
Nick Shirokov fca65ef658 refactor(web-test): этап A.4 — выделить core/session.mjs
Перенос session-функций из browser.mjs (~380 LOC):
  - connect, disconnect, attach, detach, getSession
  - createContext, setActiveContext, listContexts, getActiveContext,
    hasContext, closeContext
  - findExtension (приватная)
  - _logoutSlot, _saveActiveSlot, _activateSlot, _attachSessionListeners
    (приватные multi-context хелперы)

Session-модуль зависит от core/state, core/errors (closeModals),
recording/capture (stopRecording) и циклически от browser.mjs
(getPageState — переедет в nav/navigation.mjs на этапе C.7).
ESM live-binding делает цикл безопасным: getPageState вызывается
только внутри async функций, а не на этапе загрузки модуля.

browser.mjs: 4251 LOC, 56 публичных экспортов. Завершает Чекпоинт A.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:12:07 +03:00
Nick Shirokov 4f01f01286 refactor(web-test): этап A.3 — выделить core/wait.mjs + core/errors.mjs
core/wait.mjs (123 LOC):
  - waitForStable: smart DOM-stability polling
  - waitForCondition: JS-expression polling
  - startNetworkMonitor: CDP network-activity monitor

core/errors.mjs (336 LOC):
  - closeModals, dismissPendingErrors, checkForErrors, fetchErrorStack
  - Платформенные диалоги: _detectPlatformDialogs, _closePlatformDialogs
  - _parseErrorStack, _fetchStackViaReport, _fetchStackViaHamburger (приватные)

browser.mjs импортирует их для внутреннего использования и re-export'ит
только fetchErrorStack (исходно публичный). Остальные функции остаются
приватными — публичный API не меняется (56 экспортов).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:10:31 +03:00
Nick Shirokov 398c515390 refactor(web-test): этап A.2 — вынести recording/* в отдельные модули
Перенос ~1200 LOC из browser.mjs в recording/{tts,captions,capture,highlight,narration}.mjs:
  - tts.mjs: resolveFfmpeg, resolveEdgeTts, edge/openai/elevenlabs providers,
    getTtsProvider, getAudioDuration, generateSilence
  - captions.mjs: showCaption/hideCaption/getCaptions, showTitleSlide/
    hideTitleSlide, showImage/hideImage
  - capture.mjs: screenshot, wait, isRecording, startRecording, stopRecording
  - highlight.mjs: highlight, unhighlight, setHighlight, isHighlightMode
  - narration.mjs: addNarration

browser.mjs стал тоньше на 1200 строк, re-export через `export { ... } from './recording/*.mjs'`.
Публичный API сохранён (56 экспортов). state.mjs нормализован на CRLF.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:07:32 +03:00
Nick Shirokov cecf4dd9a2 refactor(web-test): этап A.1 — выделить module-level state в core/state.mjs
Состояние движка (browser, page, sessionPrefix, seanceId, recorder, контексты,
константы, normYo, isConnected/ensureConnected/getPage) переехало в
core/state.mjs. Импортируется как live-binding; присваивания в browser.mjs
конвертированы в setX(...) — ESM imports read-only.

Публичный API не меняется (56 экспортов). Регресс 19/19 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 22:00:53 +03:00
Nick Shirokov d3be9c8dea Merge branch 'clipboard-preserve' into dev 2026-05-25 20:12:53 +03:00
Nick Shirokov bb2f8fb29e feat(web-test): сохранять и восстанавливать буфер обмена вокруг паст
Тесты активно используют OS clipboard (`writeText` + Ctrl+V — единственный
способ добиться trusted-paste для autocomplete справочников и кириллицы).
При локальном запуске это перетирало пользовательский буфер. Теперь:

- `pasteText(text, {confirm, postDelay})` в browser.mjs делает узкое окно
  save → writeText → confirm-key → restore вокруг каждой пасты (~ms).
- Save/restore через `navigator.clipboard.read()`/`write()` — все MIME
  (текст, картинка, HTML), blob'ы стэшатся на `window` без CDP-сериализации.
- 14 callsites переведены на helper.
- При failure save'а (CF_HDROP из Проводника не виден через web-API) restore
  явно очищает буфер, чтобы тестовое значение не протекало.
- Опт-аут: CLI `--no-preserve-clipboard`, env `WEB_TEST_PRESERVE_CLIPBOARD=0`,
  `preserveClipboard: false` в `webtest.config.mjs`.

Регресс tests/web-test — 6 прогонов 19/19 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 20:12:14 +03:00
Nick Shirokov 60cdbf0aec feat(web-test): настраиваемый таймаут команды exec
--timeout=<ms> / --timeout-min=<n> и WEB_TEST_EXEC_TIMEOUT_MS вместо
захардкоженных 30 мин; сообщение об ошибке строится из фактического
значения. Закрывает кейс длинных записей видео с addNarration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 17:24:20 +03:00
Nick Shirokov cd3e50c408 docs(skd-guide): добавить /skd-decompile и сценарий «по образцу»
Дополняем гайд группы skd-*:
- В таблицу навыков добавлена строка /skd-decompile с пометкой об
  отключённом автоподборе моделью.
- В блок «Рабочий цикл» нарисована обратная стрелка Template.xml →
  /skd-decompile → JSON DSL.
- Новый под-раздел «Когда /skd-decompile, а когда /skd-edit» с явным
  предупреждением о неполноте преобразования и тихих потерях.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 14:37:11 +03:00
Nick Shirokov da6ac2bab8 Merge branch 'skd-decompile' into dev 2026-05-25 13:08:37 +03:00
Nick Shirokov 7a7d03dcff docs(skd-decompile): причёсываем SKILL.md перед merge
- description короче и с явным «не для точечных правок»
- унифицируем терминологию (sentinel-узлы)
- расширяем «Когда не использовать» — почему /skd-edit лучше для адресных правок
- в критичные конструкции добавляем вложенные схемы

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 13:07:53 +03:00
Nick Shirokov 20a243143a fix(skd-decompile): убрать падения на ERP-отчётах с dataSetLink и StandardPeriod без companions
Два бага PS-версии, незаметно роняли 9/30 отчётов sample30 в decompile-fail:

1. Get-Text без xpath → SelectSingleNode("", $ns) с .NET XPathException
   "Результатом выражения должен быть NodeSet". Шесть call-sites в
   dataSetLinks (sourceDataSet/destinationDataSet/sourceExpression/...)
   передавали уже-выбранный узел без второго аргумента; [string]$xpath
   дефолтился в "". Фикс: Get-Text возвращает $node.InnerText, если
   xpath пустой.

2. $paramByName[$startMatch] при $startMatch=$null → "индекс массива
   вычислен как NULL". Возникает на StandardPeriod-параметре, для
   которого в отчёте нет companion expressions. Фикс: guard через if.

Python-порт #2 уже был защищён .get(); по #1 в py был обходной костыль
inline-через-inner_text — заменён на единый get_text(node) после
обновления сигнатуры до get_text(node, xpath=None).

verify-roundtrip sample30: 9 bit-perfect + 20 with-diff + 1 ring3 = 30
(до фикса 9 silently падали как decompile-fail, сумма не сходилась).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:29:06 +03:00
Nick Shirokov fea2f37ba6 feat(skd-decompile): Python-порт зеркалом PS v0.88
Зеркало skd-decompile.ps1 v0.88 (~3022 строки) → skd-decompile.py v0.88
(~3140 строк). По образцу пары skd-compile.ps1 ↔ skd-compile.py.

- runner.mjs --filter skd-decompile --runtime python: 16/16 зелёные
- runner.mjs --filter skd-decompile (PS, регрессия): 16/16
- runner.mjs --filter skd-compile --runtime python (регрессия): 23/23
- verify-roundtrip на titan2-subset (13 отчётов): PS ≡ Py байт-в-байт
- verify-roundtrip на sample30 (20 общих отчётов): тот же распред
  8 BP + 12 diff, у Py чуть меньше diff-строк на edge-кейсах empty
  multilang content

Нетривиальные места порта:
- ET в Python не понимает prefix-aware XPath → тонкая обёртка XNode +
  ручной _xpath_steps/_all/_single для PS-style путей
- ET.Element (C-impl) не позволяет навешивать атрибуты → per-element
  nsmap хранится во внешнем _NSMAP_BY_ID[id(el)], заполняется через
  iterparse + start-ns
- JSON-сериализатор (convert_to_compact_json, try_inline_json,
  lineLimit=400, inline-when-fits) портирован 1-в-1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 12:15:19 +03:00
Nick Shirokov d8457bb307 docs(skd-dsl-spec): актуализация под session-фичи
- value параметра может быть массивом (для valueListAllowed)
- расшифровка namespace-ов цветов: style: (палитра темы), web: (web-имена),
  win: (Windows-системные)
2026-05-24 21:28:30 +03:00
Nick Shirokov daa7716f24 fix(skd-decompile): не сохранять valueType для известных outputParameters keys
Compile имеет map outputParamTypes — для известных ключей (Заголовок,
ВыводитьПараметрыДанных, РасположениеИтогов и др.) тип эмитится
автоматически по имени параметра. Decompile же всегда оборачивал в
wrapper {value, valueType} для не-xs:* типов — лишний шум.

Зеркалирован тот же map в decompile (15 keys), при чтении проверяем
совпадение fullType с map → если auto-detect compile вернёт тот же
тип, валидируем как простое значение.

JSON outputParameters стал заметно компактнее. Эффект на diff
нейтральный (compile эмитит то же).
2026-05-24 21:25:34 +03:00
Nick Shirokov bbed308c85 fix(skd-decompile): не сохранять valueType если auto-detect compile даст тот же тип
Для filter right values с типом dcscor:DesignTimeValue, чьё значение
соответствует ref-pattern (Перечисление.X.Y / Справочник.X.Y / ПланСчетов.X.Y
и др.), compile auto-detect-ит DesignTimeValue из значения. Раньше
decompile всё равно сохранял valueType явно — лишний шум в DSL.

Применено к single-right и multi-right. Эффект на diff нулевой (compile
эмитит то же самое), но decompiled JSON становится компактнее и читабельнее.
2026-05-24 21:17:03 +03:00
Nick Shirokov 11ddc2b5a2 fix(skd-compile): single-line эмиссия <DataCompositionSchema xmlns=...>
Раньше эмитили шапку схемы в 8 строк (каждый xmlns на отдельной строке).
Оригинал платформы пишет всё в одну строку. Был с самого начала
существования skd-compile — не регрессия, но косметика на каждый отчёт
давала 9 строк diff (1 LOST + 8 ADDED).

Sample30 total: 458 → 238 строк diff.
2026-05-24 21:09:34 +03:00
Nick Shirokov e59c3281fd feat(skd): различать 3 namespace цветов (style/web/win)
Раньше decompile Normalize-Color сворачивал любой dN: префикс в style:
независимо от URI. Compile Emit-ColorValue знал только style: → один xmlns
(http://v8.1c.ru/8.1/data/ui/style). Цвета web:GhostWhite и win:Highlight
терялись/искажались.

URI mapping:
- http://v8.1c.ru/8.1/data/ui/style          → style:
- http://v8.1c.ru/8.1/data/ui/colors/web     → web:
- http://v8.1c.ru/8.1/data/ui/colors/windows → win:

decompile: резолвим prefix через GetNamespaceOfPrefix.
compile (PS+Py): Emit-ColorValue для template cells — long form с
локальным xmlns. Emit-AppearanceValue (внутри settings) — short form,
т.к. <dcsset:settings> уже объявляет xmlns:style/web/win/sys на корне.

См. upload/erf/ЦветаВМакете — образец с 4 формами цвета.
2026-05-24 20:57:12 +03:00
Nick Shirokov 609698b00d feat(skd): multi-value параметры (valueListAllowed список значений по умолчанию)
Параметр с valueListAllowed=true может иметь несколько <value> элементов
подряд — список значений по умолчанию (например список счетов плана).
Раньше decompile читал только первый (SelectSingleNode), compile эмитил
один <value> → терялись все остальные значения.

DSL: value в параметре может быть массивом строк (или native bool/int/double).

decompile: SelectNodes("r:value") → если >1 элемент, value = array.
compile (PS+Py): если value — array, эмитим каждый отдельным <value>
через Emit-ParamValue.

Альт-indent корпус: 18009 → 17946 строк diff.
2026-05-24 20:38:10 +03:00
Nick Shirokov ceaaa8bc55 fix(skd-decompile): не сворачивать @autoDates companions с availableAsField=false
Companions НачалоПериода/КонецПериода имеют canonical имена/expression в
некоторых корпусах, но дополнительно несут availableAsField=false
(поле-ограничение, не участвует в выборе). Наш @autoDates auto-generator
не передаёт этот атрибут — для совместимости с другими корпусами без него.

Решение в decompile: не сворачивать в @autoDates если хотя бы один
companion имеет availableAsField=false → companions остаются явными
параметрами, compile эмитит их со всеми атрибутами через стандартный
path.

Sample30: 607 → 458 строк diff (-149).
2026-05-24 20:27:21 +03:00
Nick Shirokov 61cc8f3b9a fix(skd-decompile): сохранять useRestriction=true на не-hidden/не-autoDates параметрах
Раньше параметр с useRestriction=true (без hidden/autoDates/expression/
других object-form-триггеров) сохранялся как shorthand string —
useRestriction в shorthand не выражается, и compile эмитил default
useRestriction=false, теряя ограничение.

Render-Parameter: добавлен тригер object form для случая
useRestriction=true. Объект уже корректно эмитил это поле.
2026-05-24 20:07:07 +03:00
Nick Shirokov 4630af463f feat(skd-decompile): fail-fast (Ring 3) для отчётов без dataSet
Служебные отчёты-обёртки содержат только settingsVariant с outputParameters
(МакетОформления и подобное) и используются для динамического заполнения
из кода. Compile требует ≥1 dataSet, и весь DSL заточен под data-driven
отчёты — раньше decompile проходил, но compile падал с exit 1.

Теперь decompile fail-fast (exit 3, ring3 skip) — это правильнее
классифицирует такие отчёты в verify-roundtrip как "не поддерживается"
вместо "compile сломался".

См. C:/WS/projects/titan/src/cfe/Титан2/Reports/
ккСправкаРасчетРезервыПоСомнительнымДолгам.
2026-05-24 19:12:11 +03:00
Nick Shirokov dd02dcf3c4 feat(skd): TypeSet (композитный тип-набор) в valueType параметра
Параметры типа «исключаемые документы» имеют valueType с
<v8:TypeSet xmlns:dN="...">dN:DocumentRef</v8:TypeSet> — указывает на
все ссылки указанного класса конфигурации, а не на конкретный объект.

Раньше теряли целиком: decompile читал только <v8:Type>, compile
эмитил голое имя как <v8:Type>DocumentRef</v8:Type> (что не валидно).

DSL — голое имя ref-класса без точки (CatalogRef, DocumentRef, EnumRef,
ChartOfAccountsRef, ChartOfCharacteristicTypesRef, ChartOfCalculationTypesRef,
BusinessProcessRef, TaskRef, ExchangePlanRef, InformationRegisterRef,
AnyRef) → TypeSet. С точкой (DocumentRef.X) — конкретный Ref как было.

decompile: Get-ValueTypeShorthand читает v8:TypeSet и сохраняет
local-name (после prefix:).
compile (PS+Py): Emit-SingleValueType распознаёт голое имя из набора и
эмитит <v8:TypeSet xmlns:d5p1=...>d5p1:NAME</v8:TypeSet>.

Sample30 total: 618 → 607 строк diff.
2026-05-24 18:51:46 +03:00
Nick Shirokov e2e3e02a1b fix(skd): сохранять явные startDate/endDate в top-level StandardPeriod parameter
Параметр типа StandardPeriod с variant=Custom может иметь явные даты,
отличающиеся от default 0001-01-01T00:00:00 (например endDate=23:59:59).
См. АнализПричинБлокировкиВычетаНДС @941.

Раньше decompile сохранял variant только если ≠Custom (для Custom —
ничего), и compile эмитил захардкоженные 0001-01-01T00:00:00 для обеих
дат. Теряли явное время.

decompile: для StandardPeriod с явными датами (любая ≠00:00:00) →
object form {variant: Custom, startDate, endDate}.
compile (PS+Py): Emit-ParamValue принимает dict val с variant/startDate/
endDate; для Custom без явных дат сохраняется текущий fallback на
0001-01-01T00:00:00.

Sample30 total: 620 → 618 строк diff.
2026-05-24 18:42:58 +03:00
Nick Shirokov 9b331aa41d feat(skd): user-settings + axis-viewMode + use=false на StructureItemTable/Chart
Раньше для StructureItemTable читали только viewMode/userSettingID/
userSettingPresentation/itemsViewMode, а для StructureItemChart — вовсе
ничего из user-settings. Также не поддерживали axis-level режим
доступности секций (columnsViewMode/rowsViewMode/pointsViewMode/
seriesViewMode) и use=false на самих table/chart.

Расширено:
- table: + use=false, + columnsViewMode, + rowsViewMode
- chart: + use=false, + viewMode, + userSettingID, + userSettingPresentation,
         + itemsViewMode, + pointsViewMode, + seriesViewMode

Все эти атрибуты эмитятся платформой как direct children самой item-ноды
после rows/columns (table) или points/series (chart). DSL — простые поля
прямо на table/chart-объекте (как у table уже было для viewMode/etc).

Sample30 total: 729 → 620 строк diff (-109).
2026-05-24 18:23:36 +03:00
Nick Shirokov 91ef1d07eb feat(skd): v8ui:Line + nested side-styles в appearance
conditionalAppearance может содержать СтильГраницы со сложным value:
<dcscor:value xsi:type="v8ui:Line" width="0" gap="false">
  <v8ui:style xsi:type="v8ui:SpreadsheetDocumentCellLineType">None</v8ui:style>
</dcscor:value>
+ nested <dcscor:item> для side-стилей (СтильГраницы.Сверху/.Снизу/.Слева/.Справа),
каждый со своим v8ui:Line value и опц. <dcscor:use>false</dcscor:use>.

Раньше теряли всю структуру и эмитили <value xsi:type="xs:string">None</value>.

DSL form B (выбранный пользователем) — Line как top-level плоский объект:
"СтильГраницы": {
  "@type": "Line", "width": 0, "gap": false, "style": "None",
  "items": {
    "СтильГраницы.Сверху": {
      "value": { "@type": "Line", "width": 1, "gap": false, "style": "Solid" },
      "use": false
    }
  }
}

Nested items — универсальный wrapper {value, use?, items?} (как у outputParameters).
Эмитятся как siblings <dcscor:item> внутри родительского <dcscor:item> (после
закрытия родительского <dcscor:value>).

decompile: Read-AppearanceValueNode распознаёт Line и возвращает inline объект;
Get-SettingsAppearance читает nested dcscor:item children и собирает их в items.
compile (PS+Py): emit_appearance_value расширен — Line ветка + рекурсивный
вызов для items siblings.

Sample30 total: 767 → 729 строк diff (-38).
2026-05-24 18:10:25 +03:00
Nick Shirokov 8cb7309ee5 fix(skd-decompile): сохранять <selection>Auto</selection> на StructureItemChart
Раньше пропускали top-level selection в chart если она содержит только
SelectedItemAuto (считая дефолтом). Но оригинал платформы всегда эмитит
блок явно — для bit-perfect нужно сохранять presence (по аналогии с
table axis и structure-group, где selection presence уже сохраняется).

Sample30 total: 782 → 767 строк diff.
2026-05-24 17:53:11 +03:00
Nick Shirokov 0425b79a87 fix(skd-compile): не эмитить пустую <dcsat:appearance/> для cells без атрибутов
Cells со style "none" (без цветов/шрифтов/границ) и без width/merge
получали пустой блок <dcsat:appearance></dcsat:appearance>. Оригинал
платформы для таких cells appearance не пишет вовсе.

Добавлен ранний return в Emit-CellAppearance если все атрибуты пустые.

PS + Py синхронизированы. Sample30 total: 794 → 782 строки.
2026-05-24 17:49:30 +03:00
Nick Shirokov c8cba6f7ce feat(skd): локальный xmlns + use=false на nested sub-items outputParameters
В chart-параметрах типа ТипДиаграммы.СоединениеЗначенийПоСериям platform
эмитит nested SettingsParameterValue с:
- <dcscor:use>false</dcscor:use> (sub-параметр отключён)
- <dcscor:value xmlns:dN="http://v8.1c.ru/8.2/data/chart" xsi:type="dN:X">
  (локальный xmlns на value — префикс не объявлен в корне схемы)

Раньше теряли оба:
- decompile не читал sub.use и не резолвил xmlns кастомного xsi:type
- compile эмитил value без xmlns и без use=false

decompile: читаем dcscor:use на sub-item, резолвим prefix→URI через
GetNamespaceOfPrefix; если URI не из стандартных корневых xmlns —
сохраняем valueType как объект {uri, name}.
compile (PS+Py): новый helper Emit-OutputParametersSubItem; если
valueType — объект, эмитим xmlns:dN=<uri> + xsi:type=dN:<name>;
+ <dcscor:use>false</dcscor:use> если sub.use === false.

Sample30 total: 809 → 794 строк diff.
2026-05-24 17:41:44 +03:00
Nick Shirokov f34303f9ed docs(skd-dsl-spec): актуализация под фичи текущей сессии
Откатываем расширение SKILL.md (детальные формы расшифровки —
редкая необходимость) и переносим документацию в spec:

- selection folder placement (Horizontally/Vertically/...)
- groupBy field object-form: periodAdditionType +
  periodAdditionBegin/End (auto-detect xs:dateTime vs dcscor:Field)
- chart multi-series/multi-points (points/series как массив)
- template parameters drilldown форма A/B/C
- cell object-form { value, drilldown } override
- fieldTemplates секция
- choiceParameters values native bool/int/double/string
- filter valueType работает и для массива value
- dataParameters valueType=xs:string как nil-placeholder для use=false
2026-05-24 17:33:33 +03:00
Nick Shirokov c230142bf1 feat(skd): form C drilldown + per-cell drilldown override + fieldTemplates
Расширение DSL для area-templates под отчёты с расшифровкой через
data-параметр (типа АнализВыполненияМаршрутныхЛистов в ERP).

Раньше шорткат `drilldown: "X"` всегда генерировал
`Расшифровка_X` + `ИмяРесурса`/`DrillDown`. Появилось три формы:

- A: { name, expression }                              — без drilldown
- B: { name, expression, drilldown: "X" }              — текущий shortcut (без изменений)
- C: { name, drilldown: { field, expression, action } } — самостоятельный
                                                          DetailsAreaTemplateParameter

Per-cell override: ячейка может быть object `{ value, drilldown }` —
тогда appearance ссылается на указанный параметр расшифровки (без
префикса `Расшифровка_`). Используется когда несколько ячеек должны
указывать на один details-параметр (МаршрутныйЛист).

Новая корневая секция:
  "fieldTemplates": [{ "field": "X", "template": "Макет1" }]
— XML <fieldTemplate><field/><template/></fieldTemplate>.

compile (PS+Py): новый helper Emit-AreaTemplateParameter, ветка form C
в Emit-AreaTemplateDSL/Emit-Templates raw-mode, Emit-FieldTemplates.
Cell rendering: проверка cell.drilldown override перед drilldownMap.

decompile (PS): Build-Template переписан — собирает все
DetailsAreaTemplateParameter в map; распознаёт shortcut form B по
canonical shape (Расшифровка_<Y>/ИмяРесурса/"<Y>"/DrillDown) И наличию
cell appearance ref на этот target; не-shortcut → form C entry.
Cell wrapping: если drilldownTarget ≠ shortcut Расшифровка_Y, ячейка
оборачивается в { value, drilldown }. Plus Build-FieldTemplates.

Размер diff на sample30: 1045 → 809 строк (-22.5% за сессию).
2026-05-24 17:25:46 +03:00
Nick Shirokov d9010cd580 feat(skd): multi-series и multi-point в StructureItemChart
Диаграмма может содержать несколько <dcsset:series> (и аналогично
<dcsset:point>) — каждая со своим groupItems, filter, order, selection,
viewMode, userSettingID и userSettingPresentation. Используется для
multi-series графиков с группировкой по фильтру (например,
АнализЛояльностиКлиентовXYZ — series по СтадияОтношений: Постоянный,
Разовый, Потенциальный, Потерянный клиент).

Раньше декомпилировали только первый блок (SelectSingleNode), теряя
остальные → −113 строк diff на одном этом отчёте (134 → 21).

decompile: SelectNodes → если >1, сохраняем как массив; single → object
(backward-compat).
compile: проверяем тип points/series — array → цикл, иначе single
блок (backward-compat для существующего DSL).

PS и Py compile синхронизированы.
2026-05-24 16:18:16 +03:00
Nick Shirokov 19da4df61f feat(skd): periodAdditionBegin/End с типом dcscor:Field
GroupItemField может иметь periodAdditionBegin/End:
- xsi:type="xs:dateTime" с конкретной датой
- xsi:type="dcscor:Field" с путём к параметру/полю (например
  "ПараметрыДанных.ДатаНачала"). См. АнализДоходовРасходов @8933.

Раньше эмитили захардкоженный default xs:dateTime 0001-01-01T00:00:00,
теряя любые non-default значения.

decompile: читаем оба элемента, сохраняем как string поля
periodAdditionBegin/End в object form group field (если non-default).
compile: auto-detect типа по pattern (ISO date → xs:dateTime,
иначе → dcscor:Field).

PS и Py compile синхронизированы.
2026-05-24 16:10:15 +03:00
Nick Shirokov fe9d8500dc feat(skd): placement в SelectedItemFolder (selection)
SelectedItemFolder (группа полей с заголовком в selection) имеет
<dcsset:placement> — может быть Auto/Horizontally/Vertically/Special.
Мы захардкоженно эмитили Auto, теряя non-default значения.

decompile: читаем placement, сохраняем если ≠ Auto.
compile: эмитим из item.placement (default Auto для bit-perfect-default).

PS и Py compile синхронизированы.
2026-05-24 15:59:25 +03:00
Nick Shirokov 2ad35f484c fix(skd): empty xs:string placeholder в SettingsParameterValue use=false
Параметр типа DateTime в dataParameters внутри settings с use=false и
без значения сохраняется оригиналом как <dcscor:value xsi:type="xs:string"/>
(пустой placeholder), а не xsi:nil. См. АнализПлановыхНачислений @1506
для ОкончаниеПериода.

decompile: detect-ит empty xs:string + use=false → object form
{parameter, use:false, value:'', valueType:'xs:string'}.
compile: Emit-EmptyValue нормализует префикс xs: (xs:string → ^string),
теперь корректно эмитит пустой xs:string вместо fallback xsi:nil.

PS и Py синхронизированы.
2026-05-24 15:56:28 +03:00
Nick Shirokov abca61da66 fix(skd-decompile): сохранять xsi:type в multi-right filter values
При чтении FilterItemComparison с несколькими <right> элементами теряли
xsi:type — все значения уходили как строки, и compile эмитил DesignTimeValue
по auto-detect для строк вида "Перечисление.*". Но оригинал может хранить
эти значения как xs:string (см. АнализНачисленийИУдержаний @22718).

Теперь читаем xsi:type каждого <right>; если все одинаковые — сохраняем
в valueType. Compile уже умеет использовать переданный valueType вместо
auto-detect.
2026-05-24 15:43:41 +03:00
Nick Shirokov db6a1f2212 feat(skd): bool/numeric values в inputParameters choiceParameters
Раньше все values в <dcscor:value xsi:type="dcscor:ChoiceParameters">
эмитились как DesignTimeValue. Оригинал использует xs:boolean для bool-флагов
(пример: Отбор.ОбособленноеПодразделение=false в ChoiceParameters элементе
inputParameters поля ДанныеОрганизации).

decompile: читает тип xsi из <dcscor:value> и конвертит в JSON-native
(true/false для boolean, int/double для decimal, иначе строка).
compile: при эмите inputParameters → choiceParameters → values детектирует
тип значения и эмитит соответствующий xsi:type.

PS и Py синхронизированы.
2026-05-24 15:37:07 +03:00
Nick Shirokov 85d42ec34c docs(skd-dsl-spec): догон по фичам третьей сессии bit-perfect round-trip
Добавлено:
- outputParameters wrapper {value, valueType, use, items, viewMode, USID, USP}
- v8ui:Font в appearance — @type:Font + атрибуты
- dataParameters: valueType, nilValue
- StandardPeriod/StandardBeginningDate shape inference (без @type marker)
- selection/filter/order/CA UserSettingID на settings
- Пустые блоки SF/F/O/CA с только block-level meta
- inputParameters value valueType {uri, name} для кастомных xsi:type
- availableValues — типы значений сохраняются нативно
- itemsViewMode на column/row/table
- nilValue marker для параметров
- StructureItemGroup short form внутри table axis (платформ-паттерн)
2026-05-23 22:37:55 +03:00
Nick Shirokov a7344a1397 feat(skd): StandardBeginningDate в dataParameters через shape inference
Раньше StandardBeginningDate (используется когда top-level param имеет
тип xs:dateTime + UI выбор "Начало дня"/"Custom") декомпилировалась
через InnerText: variant+date конкатенировались в "Custom2022-10-25..."
и compile эмитил как xs:string.

Cross-reference на корпусе (671 отчёт):
  608 v8:StandardPeriod → v8:StandardPeriod
  180 xs:dateTime       → v8:StandardBeginningDate
  120 xs:dateTime       → xs:dateTime (raw)

Decompile теперь сохраняет SBD как объект {variant, date} (без @type marker).
Compile различает SP/SBD по форме value:
  {variant, date}                 → SBD
  {variant, startDate, endDate}   → SP с датами
  {variant} only                  → инференс по имени (BeginningOf* → SBD)

В корпусе ни одного ambiguous (variant=Custom без полей) не существует:
все 33 SBD/Custom имеют date, все 93 SP/Custom — startDate/endDate.

sample30: −22 строки (932 → 910).
2026-05-23 22:29:34 +03:00
Nick Shirokov f642f673d9 fix(skd-compile): не пропускать Auto-items в top-level selection/order
Platform может содержать SelectedItemAuto/OrderItemAuto в top-level
<dcsset:selection>/<dcsset:order> (рядом с явными полями) — это валидно.
Раньше compile использовал -skipAuto на top-level, теряя эти items.

Снапшоты регенерированы.

sample30: −10 строк (942 → 932).
2026-05-23 22:06:03 +03:00
Nick Shirokov fdc8c518aa fix(skd-compile): эмитить пустые selection/filter/order/CA блоки если есть block-level meta
Раньше Emit-Selection/Filter/Order/ConditionalAppearance early-return при
пустых items, даже если decompile сохранил blockViewMode/blockUserSettingID.
Caller (settingsVariant) тоже не вызывал эти функции на пустых items.

Теперь блоки эмитятся когда есть items ИЛИ block-level meta. Реальный кейс
из ERP: <dcsset:conditionalAppearance><dcsset:viewMode>Normal</dcsset:viewMode>
</dcsset:conditionalAppearance> — пустой CA блок только с viewMode.

sample30: −84 строки (1026 → 942).
2026-05-23 22:00:51 +03:00
Nick Shirokov 53536b72f5 fix(skd-compile): short form <dcsset:item> для StructureItemGroup внутри row/column
Анализ корпуса ERP (671 отчёт): items внутри dcsset:row/column/points/series
ВСЕГДА используют короткую форму <dcsset:item> без xsi:type (531 случай,
0 explicit). В остальных контекстах — explicit <dcsset:item xsi:type="dcsset:StructureItemGroup">.

Emit-StructureItem получил switch -shortGroup, который Emit-TableAxisBlock
передаёт для nested children. Флаг наследуется recursive в детей через
Emit-StructureItem рекурсию.

Снапшоты регенерированы.

sample30: −166 строк (1192 → 1026).
2026-05-23 21:46:27 +03:00
Nick Shirokov 21ae9a6d80 Revert "fix(skd-validate): принимать <dcsset:item> без xsi:type как StructureItemGroup"
This reverts commit 3ef4f44028.
2026-05-23 21:30:26 +03:00
Nick Shirokov ad99f3db0b fix(skd-compile): startDate/endDate в StandardPeriod ТОЛЬКО для variant=Custom
Анализ корпуса ERP/БСП (671 отчёт): из 635 StandardPeriod values только
93 (все Custom) имели <v8:startDate>/<v8:endDate>. Остальные 542 варианта
(ThisMonth, LastYear, Today, ThisQuarter, FromBeginningOfThisYear и т.д.)
эмитятся БЕЗ дат — это canonical platform-pattern.

Раньше compile добавлял boilerplate 0001-01-01 даты ко всем вариантам
независимо от типа. Снапшоты регенерированы.

sample30: −138 строк (1330 → 1192).
2026-05-23 21:24:42 +03:00
Nick Shirokov 3ef4f44028 fix(skd-validate): принимать <dcsset:item> без xsi:type как StructureItemGroup
Platform эмитит StructureItemGroup как короткую форму <dcsset:item> без
xsi:type (это дефолтный тип). Раньше валидатор выдавал error на любой
реальный XML с такой формой. Теперь — отсутствие xsi:type трактуется
как dcsset:StructureItemGroup (с warning только для нестандартных типов).
2026-05-23 21:16:43 +03:00
Nick Shirokov bb7696bf28 fix(skd-decompile): StandardPeriod в object-form dataParameter эмитим как {variant}
Когда dataParameter имеет viewMode/userSettingPresentation и значение —
StandardPeriod без явных дат (просто вариант "Custom"/"ThisMonth"),
decompile сохранял value как плоскую строку "Custom" + valueType=v8:StandardPeriod.
Compile, видя valueType с префиксом, эмитил <value xsi:type="v8:StandardPeriod">Custom</value>
— плоский xs:string-like тег вместо StandardPeriod-блока с <v8:variant>.

Теперь когда object-form нужен по другим причинам (viewMode/USP),
StandardPeriod-вариант сохраняется как {variant: ...}, и compile
правильно генерирует <v8:variant xsi:type="v8:StandardPeriodVariant">.

sample30: −66 строк (1396 → 1330).
2026-05-23 21:01:59 +03:00
Nick Shirokov 0466ae8fd8 feat(skd-compile): @autoDates companions использует type=dateTime (канон БСП)
Раньше @autoDates генерировал НачалоПериода/КонецПериода с type=date →
DateFractions=Date. Реальный БСП паттерн использует DateTime (платформа сама
приводит конец периода к 23:59:59 для дат без времени, но DateTime более
явное и матчит шаблоны БСП).

Снапшоты регенерированы.
2026-05-23 20:54:48 +03:00
Nick Shirokov 796403abe3 fix(skd-decompile): fold @autoDates только для канонических имён НачалоПериода/КонецПериода
Раньше любая пара companion-параметров с expression "&P.ДатаНачала"/
"&P.ДатаОкончания" сворачивалась в @autoDates, независимо от их имён.
Compile же всегда генерирует строго "НачалоПериода"/"КонецПериода" +
type=date + DateFractions=Date. Для отчётов с шаблоном "Период<X>" →
"НачалоПериода<X>"/"КонецПериода<X>" + DateFractions=DateTime
(типовой паттерн БСП — ПериодКонтрольСроков, ПериодОбязательств и т.п.)
это давало некорректный round-trip с потерей суффикса и формата дат.

Теперь fold срабатывает ТОЛЬКО для канонической пары — остальные
companion'ы остаются явными параметрами с полным сохранением имени,
type=dateTime, DateFractions=DateTime и expression.

sample30: −152 строки (1548 → 1396).
2026-05-23 20:38:33 +03:00
Nick Shirokov 639568c039 feat(skd): кастомные xsi:type с локальным xmlns в inputParameters values
Параметр ВыборГруппИЭлементов имеет значение типа FoldersAndItemsUse:
<dcscor:value xmlns:d6p1="http://v8.1c.ru/8.1/data/enterprise"
 xsi:type="d6p1:FoldersAndItemsUse">Items</dcscor:value>

Decompile теряла xsi:type → compile эмитил xs:string. Теперь сохраняем
{uri, name} в valueType wrapper'е, compile воспроизводит локальный
xmlns:dN="..." prefix и правильный xsi:type. GetNamespaceOfPrefix
извлекает URI из контекста элемента, что работает для всех 7 известных
xmlns URI (enterprise, current-config, ui/style, chart, types и т.п.).

sample30: −8 строк (1556 → 1548).
2026-05-23 20:24:46 +03:00
Nick Shirokov 9ef554a576 feat(skd): block-level userSettingID на selection/filter/order/CA + meta на StructureItemTable
Раньше теряли userSettingID, который platform пишет:
1. Прямо в блоки selection/filter/order/conditionalAppearance под
   <dcsset:settings> (рядом с block-level viewMode). Расширена цепочка
   Get-BlockUSID + новый -blockUserSettingID параметр в Emit-*.
2. На самой <dcsset:item xsi:type="dcsset:StructureItemTable"> (рядом
   с viewMode/userSettingPresentation/itemsViewMode). Build-Structure
   читает их, Emit-StructureItem (type=table) эмитит.

sample30: −46 строк (1602 → 1556).
2026-05-23 20:12:14 +03:00
Nick Shirokov 1b36aa97c8 feat(skd-decompile): useRestriction=true в object form для non-hidden/non-autoDates параметров
Параметры с явно заданным <useRestriction>true</useRestriction>, которые
не покрываются auto-emit от @hidden (где compile сам ставит true) и не
участвуют в @autoDates fold (где compile также ставит true companion'ам),
теряли это свойство. Типичный кейс: параметры-выражения вида
ПериодНачало = &Период.ДатаНачала с useRestriction=true.

Render-Parameter теперь явно эмитит useRestriction:true в object form
с защитой от двойного перекрытия hidden/autoDates.

sample30: −60 строк (1710 → 1650).
2026-05-23 19:44:58 +03:00
Nick Shirokov 573602ae65 fix(skd-decompile): сохранение кастомных xsi:type в outputParameters items
Параметры типа ВариантИспользованияГруппировки с xsi:type=
dcsset:DataCompositionGroupUseVariant теряли тип, потому что wrapper
создавался только при наличии viewMode/userSettingID/use=false. Теперь
кастомный xsi:type (не xs:* / LocalStringType / Font) сам по себе
триггерит wrapping — сохраняем valueType для bit-perfect эмиссии.

sample30: −28 строк (1738 → 1710).
2026-05-23 19:37:23 +03:00
Nick Shirokov 632c58eef1 fix(skd): сохранение xs:boolean и xs:decimal в filter values + availableValues
Get-FilterValueWithType теряла тип значения — "true"/"false" приходило как
string, и compile эмитил xs:string вместо xs:boolean. Decompile теперь
конвертирует по xsi:type → реальный [bool]/[int]/[double].

Shorthand эмиссия фильтра также фиксирована: bool теперь рендерится как
"true"/"false" (lowercase), не "True"/"False" (PS-style).

Аналогично availableValues в параметрах: bool/decimal значения теперь
сохраняются с правильным типом, а не downgrade в xs:string.

sample30: −30 строк (1768 → 1738).
2026-05-23 19:33:47 +03:00
Nick Shirokov 64c2037fe1 feat(skd): block-level viewMode/userSettingID на <dcsset:order> внутри structure group
Раньше теряли viewMode/userSettingID, которые platform пишет прямо в блок order
(не в item). Build-Structure теперь читает их как orderViewMode/orderUserSettingID,
Emit-Order принимает -blockUserSettingID параметр.

sample30: −10 строк (1778 → 1768).
2026-05-23 19:25:20 +03:00
Nick Shirokov fb9d29408c feat(skd): viewMode/userSettingPresentation на dataParameters items
Build-DataParameters не читал viewMode и userSettingPresentation на отдельных
параметрах данных — теряли 74 viewMode references только в одном крупном отчёте.
Теперь object form {parameter, value, valueType?, viewMode?, userSettingID?, ...}
с auto-конверсией bool/decimal по xsi:type.

Compile добавлен early-branch на полный xsi:type (xs:boolean, dcscor:DesignTimeValue
и т.п.) — раньше string "true" эмитился как xs:string вместо xs:boolean.

Расследование: viewmode-trace.py показал ровно 66 LOST viewMode=Normal
+ 8 Inaccessible на пути '/r:DataCompositionSchema/r:settingsVariant/
dcsset:settings/dcsset:dataParameters/dcscor:item[SettingsParameterValue]'.

sample30: −204 строки (1982 → 1778).
2026-05-23 19:19:39 +03:00
Nick Shirokov 730decf9ce feat(skd): itemsViewMode на table axis (column/row/point/series)
Build-TableAxisBlock не читал itemsViewMode на самой оси (только на
StructureItemGroup). Теперь сохраняется и эмитится — bit-perfect для
шаблонов отчётов с явно заданным itemsViewMode=Normal/Inaccessible
на колонках/строках таблицы.

sample30: −30 строк (2012 → 1982).
2026-05-23 19:04:22 +03:00
Nick Shirokov 48e2b6bd44 feat(skd): nilValue marker для параметров с xsi:nil="true"
Параметры со скалярным типом (decimal/string/dateTime) и <value xsi:nil="true"/>
теперь сохраняют значение nil через object form {nilValue:true}. Раньше compile
эмитил типизированный default (xs:decimal>0, xs:string/>, xs:dateTime>0001-01-01)
вместо nil — мismatch на bit-perfect round-trip.

sample30: −22 строки (2034 → 2012).
2026-05-23 17:51:35 +03:00
Nick Shirokov f75c71064c feat(skd): userSettingPresentation на conditionalAppearance item
Build-ConditionalAppearance не читал userSettingPresentation
(read только viewMode/userSettingID), теперь сохраняет multilang
презентацию в JSON, а Emit-ConditionalAppearance эмитит её обратно.

sample30: −80 строк (2114 → 2034).
2026-05-23 17:27:21 +03:00
Nick Shirokov 5ca8ce2b64 fix(skd-compile): widths-unwrap и indent в template cell appearance
Две bug-фиксы для шаблонов:

1. PS-quirk: \$widths = if (\$t.widths) { @(\$t.widths) } else { @() }
   разворачивал одно-элементный массив в строку, после чего \$widths[0]
   возвращал первый Char (например '1' для "15.625"), а [double][Char]'1'=49.
   Заменил if-expression на обычный if-statement.

2. Indent в <dcsat:appearance>: компайлер ставил 4 таба вместо 5
   (и 5 вместо 6 у items внутри). Поправлено в обоих рантаймах.

sample30: −552 строки (2666 → 2114).
2026-05-23 17:18:50 +03:00
Nick Shirokov 5793f91ebb feat(skd): StandardPeriod с явными datами в dataParameters
Когда v8:StandardPeriod имеет non-default startDate/endDate, decompile
выдаёт object form {parameter, value:{variant, startDate, endDate}, ...}
вместо shorthand "P = Custom". Compile использует переданные даты
вместо boilerplate 0001-01-01.

sample30: −24 строки (2690 → 2666).
2026-05-23 16:54:26 +03:00
Nick Shirokov b83bbc333f feat(skd-compile): multilang static text в ячейках шаблона
Get-CellValue теперь пропускает dict без ключа value (multilang dict
{ru, en, ...}), а главный цикл Emit-Templates эмитит для таких ячеек
<dcsat:item xsi:type="dcsat:Field"><dcsat:value xsi:type="v8:LocalStringType">
с lwsTitle-структурой. Раньше multilang-ячейки терялись (Get-CellValue
возвращал null → cell не эмитился).

sample30: −180 строк (2870 → 2690).
2026-05-23 16:48:08 +03:00
Nick Shirokov a417b76e2c feat(skd-decompile): StandardPeriod Custom + use=false на dataParameters items
Build-DataParameters раньше пропускал variant=Custom в шорткоде, теряя как
сам StandardPeriod-маркер, так и use=false (читался из dcsset:use вместо
правильного dcscor:use). Теперь Custom попадает в shorthand как "=Custom",
compile воспроизводит StandardPeriod tag + boilerplate dates корректно.

sample30: −166 строк (3036 → 2870).
2026-05-23 16:41:38 +03:00
Nick Shirokov 659451815d feat(skd): nested sub-параметры и valueType в outputParameters wrapper
Wrapper расширен полями valueType (полный xsi:type значения, для bit-perfect
неизвестных compile параметров) и items (вложенные dcscor:item, паттерн
ТипДиаграммы.ВидПодписей внутри ТипДиаграммы).

sample30: −194 строки (3230 → 3036).
2026-05-23 16:36:49 +03:00
Nick Shirokov f271a6f6ba feat(skd): viewMode/userSettingID/userSettingPresentation на outputParameters items
Wrapper {value, ...} расширен: помимо use=false поддерживает viewMode,
userSettingID, userSettingPresentation на каждом item внутри
<dcsset:outputParameters>. Также value=Font dict теперь работает в wrapper.

sample30: −92 строки (3322 → 3230).
2026-05-23 16:19:48 +03:00
Nick Shirokov 342b3f0687 feat(skd): v8ui:Font в appearance + use=false в conditionalAppearance
Font хранится как объект {@type:Font, ref, faceName, height, bold, italic,
underline, strikeout, kind, scale} — все исходные атрибуты сохраняются для
bit-perfect round-trip.

Заодно Get-SettingsAppearance теперь читает dcscor:use на conditionalAppearance
items (раньше игнорировал — терялся use=false на appearance value).

sample30: −315 строк (3637 → 3322).
2026-05-23 16:04:12 +03:00
Nick Shirokov 4b3819762c docs(skd-dsl-spec): dataSetLinks полная схема + multi-orderExpression + пустые userField expressions 2026-05-23 15:55:30 +03:00
Nick Shirokov 5e864cb05f feat(skd): пустые detail/totalExpression в userFields
В UserFieldExpression XML присутствуют все 4 элемента (detailExpression,
detailExpressionPresentation, totalExpression, totalExpressionPresentation),
даже когда они пустые (<dcsset:totalExpression/>). Раньше пустые опускались.

decompile теперь читает по присутствию узла, compile эмитит self-closing
форму для пустых строк.

sample30: −106 строк (3743 → 3637).
2026-05-23 13:03:24 +03:00
Nick Shirokov a66246095c feat(skd): multi-orderExpression на dataSet field
На одном поле может быть несколько <orderExpression> (multi-sort fallback).
decompile сохраняет массив (single → object back-compat), compile принимает оба.

sample30: −30 строк, +2 отчёта в bit-perfect (27 with-diff).
2026-05-23 12:54:53 +03:00
Nick Shirokov 9b4bb3d9b8 feat(skd): dataSetLinks с расширенными атрибутами
skd-decompile теперь извлекает блоки <dataSetLink> на уровне схемы,
skd-compile поддерживает поля parameterListAllowed/startExpression/
linkConditionExpression (раньше был только parameter).

На sample30 это даёт −1100 строк diff (4873 → 3773), один отчёт
(АнализНачисленийНДССАвансовПолученных) переходит в bit-perfect.
2026-05-23 12:47:56 +03:00
Nick Shirokov b1eb8bebe3 docs(skd-dsl-spec): догон по последним коммитам
- order item: use=false в object form
- outputParameters: wrapper {value, use: false} для отключённого параметра
- table: top-level selection/conditionalAppearance/outputParameters
  (отдельно от column/row)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:13:47 +03:00
Nick Shirokov 87bc274346 feat(skd-decompile): top-level блоки на StructureItemTable
Build-Structure для table теперь читает selection / outputParameters /
conditionalAppearance прямо на самой <dcsset:item xsi:type="StructureItemTable">,
не только внутри row/column.

Эффект на sample30: −906 строк diff (большой эффект — многие отчёты
с таблицами имеют top-level selection и outputParameters для названия
таблицы и формата вывода).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:07:51 +03:00
Nick Shirokov 6a8efc9538 feat(skd-compile): top-level selection/condApp/outputParameters на StructureItemTable
StructureItemTable может иметь свои selection / conditionalAppearance /
outputParameters прямо на уровне таблицы (отдельно от row/column).
Раньше Emit-StructureItem для table эмитил только columns и rows; теперь
после rows эмитятся top-level блоки.

Аналогично сделано для chart (там было раньше).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 22:07:51 +03:00
Nick Shirokov 0846740db7 feat(skd-compile): пустой <dcsset:filter/> на conditionalAppearance item
Платформа эмитит <dcsset:filter/> (self-closing, без условий) на
каждом condApp item, где фильтр не задан — это нормальная форма
"правило применяется ко всем строкам без дополнительных условий".

Compile теперь эмитит пустой тег если filter отсутствует/пуст.
Decompile-side уже корректно игнорировал пустой filter (Build-CondApp
читает items только если они есть).

Эффект на sample30: −252 строки diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:51:49 +03:00
Nick Shirokov 480d828c35 feat(skd-decompile): use=false на outputParameters item
Build-OutputParameters теперь читает <dcscor:use>false</dcscor:use> на
item и сохраняет значение в форме {value, use: false}.

Эффект на sample30: −198 строк diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:37:27 +03:00
Nick Shirokov 8009a8150f feat(skd-compile): use=false wrapper в outputParameters
outputParameters item тоже может иметь <dcscor:use>false</dcscor:use>
(например — отключённый «Заголовок» в варианте). Emit-OutputParameters
теперь распознаёт wrapper {value, use: false} и эмитит <dcscor:use>
в начале item, как уже делал Emit-AppearanceValue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:37:26 +03:00
Nick Shirokov 29a9fbe950 feat(skd): use=false на OrderItemField
OrderItemField в settings может иметь <dcsset:use>false</dcsset:use>
(отключённая сортировка-пункт пользовательских настроек). Build-Order
теперь читает use=false и переводит item в object form
{field, direction, use, viewMode}. Compile эмитит <dcsset:use> в
начале item, перед <dcsset:field>.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:28:50 +03:00
Nick Shirokov 3832952400 feat(skd-decompile): use=false на appearance value items
Get-AppearanceDict теперь читает <dcscor:use>false</dcscor:use> на
appearance items и возвращает значение в форме {value, use: false}.
Compile-side уже принимал этот wrapper.

Эффект на sample30: −21 строка diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:21:48 +03:00
Nick Shirokov 10fef03681 docs(skd-dsl-spec): догон по последним расширениям DSL
- conditionalAppearance: use=false, useInDontUse массив, multilang
  presentation, userSettingPresentation, расширены auto-detect типов
  appearance (Размещение, ГориZontальноеПоложение, ЦветТекста без
  префикса, числовые строки)
- outputParameters: новые типы для placement (РасположениеИтогов,
  РасположениеГруппировки и др.), ТипМакета
- structure group: use=false, userSettingID, userSettingPresentation
- table column/row + chart axis: conditionalAppearance, children
- settings: additionalProperties (служебные key/value свойства)
- parameter: inputParameters (ФорматРедактирования и т.п.)
- filter shorthand: упомянут auto-detect dcscor:DesignTimeValue

В SKILL.md изменения не вносятся — фичи редкие, для bit-perfect
round-trip с реальных схем.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:14:49 +03:00
Nick Shirokov 957af1c421 feat(skd-decompile): user-settings на StructureItemGroup
Build-Structure для group теперь читает userSettingID и
userSettingPresentation (multilang dict) наряду с viewMode/itemsViewMode.

Try-StructureShorthand расширена — структура не сворачивается в
shorthand при наличии любого из новых полей (use, conditionalAppearance,
outputParameters, userSettingID, userSettingPresentation).

Эффект на sample30: −462 строки diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:09:38 +03:00
Nick Shirokov 616ac2a23e feat(skd-compile): userSettingID/userSettingPresentation на StructureItemGroup
StructureItemGroup может быть зарегистрирована как пункт пользовательских
настроек (например, "По сотрудникам" — позволяет включить/выключить
группировку через UI). Поля userSettingID и userSettingPresentation
эмитятся после viewMode, перед itemsViewMode (платформенный порядок).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:09:37 +03:00
Nick Shirokov a9deeee2d0 feat(skd-compile): auto-detect DesignTimeValue в filter right
При парсинге shorthand "Поле = Перечисление.X.Y" Parse-FilterShorthand
уже распознавал тип dcscor:DesignTimeValue. Но в auto-detect веток
Emit-FilterItem (single-right и multi-right) этой проверки не было,
поэтому ссылочные значения из object form (где valueType не сохранён)
эмитились как xs:string.

Добавлено в обе ветки: проверка regex ^(Перечисление|Справочник|...
|Catalog|Enum|...)\. → dcscor:DesignTimeValue.

Эффект на sample30: −326 строк diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 21:01:38 +03:00
Nick Shirokov 4af51235db feat(skd-decompile): conditionalAppearance внутри table/chart axis
Build-TableAxisBlock теперь читает <dcsset:conditionalAppearance>
блока column/row/point/series. Это типовая категория для table
с условным оформлением колонок (например, разный текст для разных
групп начислений в строке таблицы).

Эффект на sample30: −1026 строк diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:52:08 +03:00
Nick Shirokov da0b326c40 feat(skd-compile): conditionalAppearance внутри table/chart axis
Колонки/строки таблицы и оси диаграммы (column/row/point/series)
могут содержать собственный <dcsset:conditionalAppearance> — правила
оформления специфичные для этой оси. Emit-TableAxisBlock теперь его
эмитит между outputParameters и nested children.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:52:07 +03:00
Nick Shirokov 65a2b5870d feat(skd-compile): типизация appearance/outputParameters значений
Реальные платформенные значения имеют конкретные xsi:type, которые
compile терял в roundtrip:

Output параметры (расширена таблица OUTPUT_PARAM_TYPES):
- РасположениеОбщихИтогов, РасположениеИтогов → DataCompositionTotalPlacement
- РасположениеГруппировки → DataCompositionFieldGroupPlacement
- РасположениеРесурсов → DataCompositionResourcesPlacement
- ТипМакета → DataCompositionGroupTemplateType

Appearance keys (новая key-type карта в Emit-AppearanceValue):
- Размещение → DataCompositionTextPlacementType
- ГоризонтальноеПоложение/ВертикальноеПоложение → v8ui:HorizontalAlign/VerticalAlign
- ОриентацияТекста, РасположениеИтогов, ТипМакета

Auto-detect расширения:
- Числовые строки (МинимальнаяШирина=40 и др.) → xs:decimal
- ЦветТекста/ЦветФона/ЦветГраницы без префикса style:/web:/win: → v8ui:Color
  (для значений "auto", "#FFC8C8" и т.п.)

Эффект на sample30: −1122 строки diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:45:18 +03:00
Nick Shirokov cab0b4d26b feat(skd-decompile): чтение attributeUseRestriction на DataSet field
DataSet field может иметь <attributeUseRestriction> наравне с
<useRestriction> — те же 4 подэлемента (field/condition/group/order),
но ограничения применяются к атрибутам ссылочного поля (например,
"запретить выбирать атрибуты Контрагента в фильтре").

Compile-side уже принимал attrRestrict в JSON; decompile теперь его
заполняет. Item переходит в object form при наличии attrRestrict.

Эффект на sample30: −257 строк diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:32:38 +03:00
Nick Shirokov 092cd8ebb4 feat(skd-decompile): чтение additionalProperties в settings
<dcsset:additionalProperties> → dict в settings.additionalProperties.
Все значения xs:string, простые key→value пары.

Эффект на sample30: −121 строка diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:22:22 +03:00
Nick Shirokov f19032594c feat(skd-compile): additionalProperties в settings
<dcsset:additionalProperties> — список <v8:Property name="X">
<v8:Value xsi:type="xs:string">Y</v8:Value></v8:Property>. Используется
платформой для служебных свойств варианта (ВариантНаименование,
КлючВарианта, Адрес — URL tempstorage).

DSL: settings.additionalProperties = { "имя": "значение", ... }
Эмит — после itemsViewMode, перед закрытием settings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:22:22 +03:00
Nick Shirokov a07a105024 feat(skd-decompile): inputParameters на параметрах + multilang presentation
Три связанных закрытия:
- Build-Parameter вызывает Read-InputParameters (раньше только Field)
- Read-InputParameters читает LocalStringType value как multilang dict
  (ФорматРедактирования и т.п.)
- Build-FilterItem (FilterItemComparison) читает <dcsset:presentation>
  с поддержкой multilang (Get-MLText + fallback InnerText); item
  переходит в object form при наличии presentation.

Эффект на sample30: −533 строки diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:06:47 +03:00
Nick Shirokov f5432eb48d feat(skd-compile): inputParameters на параметрах + multilang в value
Параметр (parameters[]) может иметь свой inputParameters блок —
например <ФорматРедактирования> со значением xs:LocalStringType.
Раньше Emit-InputParameters использовался только для DataSet field;
теперь подключён и к Emit-Parameter (вывод после <use>).

emit_input_parameters value: добавлена поддержка multilang dict
({ru, en, ...} → LocalStringType). Раньше падал в xs:string.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 20:06:46 +03:00
Nick Shirokov b8a6783ccf feat(skd-decompile): чтение multilang presentation в condApp и filter group
Build-ConditionalAppearance и FilterItemGroup читали presentation через
Get-Text (теряли multilang). Теперь читают через Get-MLText с fallback
на InnerText — multilang dict {ru, en, ...} сохраняется в JSON.

Эффект на sample30: −946 строк diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:40:59 +03:00
Nick Shirokov 2b8cdc40ca feat(skd-compile): multilang presentation на conditionalAppearance item
При значении-словаре {ru, en, ...} эмитим <dcsset:presentation> как
LocalStringType с <v8:item>/<v8:lang>/<v8:content>; при строке —
по-прежнему xs:string. Раньше всегда жёстко xs:string, что давало
LOST для multilang.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:40:59 +03:00
Nick Shirokov 013d3c3a01 feat(skd-decompile): чтение useInXxx и use=false на conditionalAppearance
Build-ConditionalAppearance теперь читает:
- <dcsset:use>false</...> → use: false
- любые <dcsset:useInXxx>DontUse</...> → элемент в массиве useInDontUse
  (имена тегов: useInGroup → "group", useInFieldsHeader → "fieldsHeader",
   и т.п.)

Эффект на sample30: −187 строк diff. Существенная часть LOST <use> и
LOST <content>/<lang> (внутри useInXxx-окружения) закрыта.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:34:32 +03:00
Nick Shirokov eee5aaafd3 feat(skd-compile): useInXxx и use=false на conditionalAppearance item
Расширение DSL для бит-перфект roundtrip на условном оформлении:
- use: false — отключённое правило (эмитится в начале item)
- useInDontUse: array — список областей где правило НЕ применяется
  (\"group\", \"hierarchicalGroup\", \"overall\", \"fieldsHeader\",
   \"header\", \"parameters\", \"filter\", \"resourceFieldsHeader\",
   \"overallHeader\", \"overallResourceFieldsHeader\")
  Compile эмитит <dcsset:useInGroup>DontUse</...> и т.п. в платформенном
  порядке.

Семантика: \"useIn\" в платформе — это белый список применения правила;
DSL хранит инверсный список (что отключено) — короче для редких
ограничений.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:34:32 +03:00
Nick Shirokov 32e06cbc56 fix(skd-compile): всегда эмитить useRestriction для параметра
Платформа эмитит <useRestriction>true|false</useRestriction> у каждого
параметра безусловно. Раньше compile эмитил только если =true, что
приводило к LOST <useRestriction>false</useRestriction> в roundtrip.

Эффект на sample30: −84 строки diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 19:27:36 +03:00
Nick Shirokov 77fc0cee2f feat(skd-decompile): nested children в table axis + structure-group default
Три связанных изменения:
- Build-TableAxisBlock читает вложенные <dcsset:item> как children
  (StructureItemGroup внутри row/column/point/series)
- Build-Structure принимает <dcsset:item> без явного xsi:type как
  StructureItemGroup (реальные XML используют такую default-форму
  для вложенных групп — раньше попадало в sentinel)
- Чтение use=false на StructureItemGroup

Эффект на sample30: −3253 строки diff (массовая категория —
table row almost always содержит nested grouping).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:55:02 +03:00
Nick Shirokov ac72ca8a51 feat(skd-compile): nested children + use=false на StructureItemGroup
В реальных отчётах внутри table row / column / chart axis (point/series)
часто живут вложенные группы — StructureItemGroup в children, со своими
groupItems / filter / order / selection / outputParameters / nested
children глубже. До этого Emit-TableAxisBlock эмитил только axis-level
поля, без children.

Также: на самой StructureItemGroup может быть use=false (отключённая
ветка структуры в settings) — добавлено в DSL и в эмит.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:55:02 +03:00
Nick Shirokov 6e3632e5ff revert(skd-decompile): вернуть @normal shorthand-флаг
Раньше при наличии явного <viewMode>Normal</viewMode> decompile
переводил filter item в полноценный object form. Это раздувало JSON
без причины — @normal в shorthand функционально эквивалентен
"viewMode": "Normal" в object form, и compile уже его парсит.

Теперь: object form триггерится только реальными причинами
(userSettingPresentation, value-массив, dcscor:Field валуетайп);
явный Normal сохраняется как @normal в shorthand. Object form
по-прежнему может содержать "viewMode": "Normal" — это равнозначно.

Compile-side изменений не требуется. Spec обновлён.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:41:57 +03:00
Nick Shirokov e843cd8997 docs(skd-dsl-spec): догон по последним расширениям DSL
- selection items: use=false (на field и Auto), пример обновлён
- filter:
  - примеры с valueType: dcscor:Field (field-to-field comparison),
    value: [a,b,c] (multi-right InList), value: [] (ValueListType placeholder)
  - явное описание форм value (скаляр / массив / пустой массив)
  - FilterItemGroup принимает user-settings (viewMode/userSettingID/...)
- table column/row + chart points/series: name на всех осях (раньше
  только row), плюс user-settings поля
- секция «Стратегия сохранения viewMode» — описана модель explicit-only
  (decompile сохраняет точное присутствие, compile эмитит только заданное)
- @normal убран из перечня shorthand-флагов (Normal — default, не
  эмитится shorthand'ом; явный Normal переводит в object form)

В SKILL.md изменения не вносятся — фичи редкие, нужны для bit-perfect
round-trip с реальных схем.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:33:06 +03:00
Nick Shirokov 4c26e97abf feat(skd-decompile): сохранение явного valueType в filter right (dcscor:Field)
Get-FilterValueWithType возвращает xsi:type вместе со значением.
Build-FilterItem теперь сохраняет valueType в object form, если тип
не xs:* (auto-detect compile обрабатывает xs:* сам). Это закрывает
field-to-field comparison: <right xsi:type=\"dcscor:Field\">FieldB</right>
теперь корректно эмитится обратно через valueType=\"dcscor:Field\".

Item переходит в object form при наличии valueType (shorthand не выразим).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:28:48 +03:00
Nick Shirokov cbad0fe743 fix(skd-compile): авто-определение xs:decimal по тексту числа
Для filter right value compile уже различал bool / native-number /
dateTime, но не различал числовые строки. Реальные отчёты часто хранят
сравнения как числа: <right xsi:type=\"xs:decimal\">5</right>.

Decompile при чтении видит "5" как строку (через InnerText), и без
этого фикса compile эмитил xs:string. Теперь добавлена проверка
по regex ^-?\d+(\.\d+)?$ → xs:decimal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:28:48 +03:00
Nick Shirokov 03cc59d243 feat(skd-decompile): чтение multi-right и ValueListType в filter
Build-FilterItem теперь читает все <dcsset:right> элементы (раньше
только первый — терялись значения для InList с несколькими values).
Первый <right> типа v8:ValueListType трактуется как пустой list-placeholder
(`value: []` в JSON).

Item переходит в object form если value — массив (shorthand не выразим
для multi-value/empty-list).

Shorthand fallback для null/empty value теперь снова `_` (placeholder).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:19:20 +03:00
Nick Shirokov 540af9655d feat(skd-compile): filter right поддерживает массив и пустой ValueListType
DSL: value на filter item может быть массив:
- value: []           — пустой ValueListType placeholder (для InList с
                        пользовательскими настройками — пользователь
                        заполнит значения через UI)
- value: [3, 4, 5]    — InList с несколькими конкретными значениями
                        (compile эмитит несколько <right> подряд)
- value: 3            — single value (как раньше)

Compile автоопределяет тип каждого значения (bool/decimal/dateTime/string).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:19:20 +03:00
Nick Shirokov a7d5c46176 feat(skd): use=false на selection items
SelectedItemField и SelectedItemAuto могут иметь <dcsset:use>false</dcsset:use>
— отключённое поле выборки. Раньше игнорировалось при roundtrip.

DSL расширения:
- selection item object form: { field, use: false, title?, viewMode? }
- новый объект для отключённого Auto: { auto: true, use: false }

Decompile переходит в object form если есть use=false (помимо title и
viewMode); compile эмитит <use>false</use> в начале item (XML-порядок).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:05:27 +03:00
Nick Shirokov 38b5445f15 fix(skd): откат implicit viewMode=Normal — сохраняем точное присутствие
Реальные отчёты непоследовательны: одни filter/item имеют
<viewMode>Normal</viewMode> с userSettingID, другие — нет (зависит от
момента редактирования через UI). Стратегия "compile добавляет implicit
Normal когда есть userSettingID" даёт ложные ADDED строки в bit-perfect.

Меняю на корректную модель:
- decompile сохраняет viewMode даже = 'Normal' если node физически
  присутствует в XML (object form переходит автоматически)
- compile эмитит viewMode только если явно задан в JSON

Применено к: filter (item + group), dataParameters, conditionalAppearance,
selection items, order items.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 18:01:09 +03:00
Nick Shirokov 9aac032ac8 feat(skd-decompile): user-settings на table/chart axis и FilterItemGroup
Build-TableAxisBlock теперь читает name на любой оси (раньше только
для row), плюс viewMode (если non-Normal), userSettingID и
userSettingPresentation на самом блоке column/row/point/series.

Build-FilterItem для FilterItemGroup теперь читает presentation,
viewMode (non-Normal), userSettingID, userSettingPresentation —
раньше группа сохраняла только items.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:45:16 +03:00
Nick Shirokov f9774d799c feat(skd-compile): implicit viewMode=Normal + user-settings на FilterItemGroup и axis
Платформа эмитит <viewMode>Normal</viewMode> автоматически когда у
элемента есть <userSettingID> (это сигнал пользовательской настройки).
Теперь compile делает то же:
- filter item, dataParameters item, conditionalAppearance item, table
  axis (column/row/point/series) — все эмитят Normal если userSettingID
  задан и явный viewMode не указан

Кроме того: FilterItemGroup теперь поддерживает свой viewMode /
userSettingID / presentation / userSettingPresentation (наравне с
обычными filter items).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:45:15 +03:00
Nick Shirokov 49f17ef5fd docs(skd-dsl-spec): availableValues на полях + conditionalAppearance в group
Догнал spec за последние коммиты — описаны availableValues на DataSet
fields (по аналогии с parameters) и conditionalAppearance как
доступное поле структурного элемента group.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:31:33 +03:00
Nick Shirokov 515c82c398 feat(skd-decompile): conditionalAppearance + outputParameters внутри structure group
Build-Structure для StructureItemGroup теперь читает локальные
conditionalAppearance и outputParameters — раньше они терялись для
вложенных групп (только для top-level settings работало).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:29:18 +03:00
Nick Shirokov 206fed0125 feat(skd-compile): conditionalAppearance + outputParameters внутри structure group
Реальные отчёты задают conditionalAppearance прямо на вложенной
StructureItemGroup (например — особое оформление шапки группировки).
Compile теперь эмитит её сразу после filter, перед outputParameters,
если задана в JSON.

outputParameters на StructureItemGroup уже эмитился — без изменений.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:29:18 +03:00
Nick Shirokov e5e6392b8c feat(skd-decompile): чтение availableValues на полях dataSet
Build-Field теперь читает <availableValue> на DataSetFieldField,
типизирует value по xsi:type (boolean/decimal/string/dateTime),
сохраняет presentation как multilang dict если возможно.

Поле переходит в object form если есть availableValues.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:25:18 +03:00
Nick Shirokov b58f9aa6a2 feat(skd-compile): availableValues на DataSet fields
Раньше availableValues эмитились только для parameters. Реальные
отчёты также задают availableValues на полях dataSet (например
ТипЗаписи=1..5 со ссылочными значениями), что давало отсутствие
важной семантики при roundtrip.

DSL: `field.availableValues: [{value, presentation, valueType?}]` —
типы значений автоопределяются (bool/decimal/dateTime/string),
presentation поддерживает multilang.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:25:08 +03:00
Nick Shirokov 2235b11700 chore(skd-compile): порт PS → PY + spec для последних расширений
В PS-версии накопилось три блока изменений за сессию, которые не были
отражены в Python-порте — синхронизирую:
- Emit-TableAxisBlock (filter/order/selection/outputParameters на
  column/row/point/series)
- Emit-UserFields (UserFieldExpression / UserFieldCase в settings)

DSL spec обновлён: добавлены разделы userFields, расширены примеры
table column/row и chart points/series.

В SKILL.md изменения не вносятся — фичи редкие, описаны только в spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:09:48 +03:00
Nick Shirokov 04b742fe78 feat(skd-decompile): чтение userFields (UserFieldExpression/Case)
Build userFields array в settings из <dcsset:userFields>. Поддержаны
оба подтипа (Expression с detail+total / Case с cases). Multilang title
и presentation корректно читаются как объекты.

Эффект на sample30: -5500 строк diff (целая ветка пользовательских
полей со всеми expression/presentation/case-структурами).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:05:29 +03:00
Nick Shirokov 1d75456f4e feat(skd-compile): userFields в settings (UserFieldExpression/UserFieldCase)
Пользовательские вычисляемые поля — отдельный блок <dcsset:userFields>
в начале settings. Два подтипа:

- UserFieldExpression: dataPath + title + detail{expression,presentation}
  + total{expression,presentation}
- UserFieldCase: dataPath + title + cases[{filter, value, presentation}]

DSL: тип определяется наличием 'cases' (case-форма) vs detail/total
(expression-форма) — без явного 'type'.

В SKILL.md не упоминаем (редкая фича — обычно настраивается пользователем
через UI «Изменить вариант»). Описано в spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 17:05:18 +03:00
Nick Shirokov 3e0f6bba02 feat(skd-decompile): table/chart axis filter+outputParameters + nestedSchema Ring 3
Build-TableAxisBlock теперь читает filter и outputParameters блока,
order/selection сохраняются с точным присутствием (даже [Auto]) для
bit-perfect round-trip.

Дополнительно: nestedSchema (вложенные DCS-подсхемы) добавлены в Ring 3
fail-fast — фича редкая (15/490 в ERP), требует значительного
расширения DSL (рекурсивная DCS внутри JSON). Поддержку можно
вернуть позднее как nestedSchemas массив в корне JSON.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:55:09 +03:00
Nick Shirokov 19c2557778 feat(skd-compile): filter/outputParameters/order/selection на table/chart axis
Колонки и строки таблицы (StructureItemTable.column/row) и оси диаграммы
(StructureItemChart.point/series) могут иметь свои filter, order,
selection, outputParameters — реальные отчёты активно это используют
для отбора и оформления внутри каждой оси.

Compile теперь:
- эмитит filter и outputParameters на column/row/point/series
- order/selection эмитятся только если заданы в JSON (раньше дефолтили
  [Auto], что иногда расходилось с оригиналом)

Логика вынесена в общий helper Emit-TableAxisBlock.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:54:59 +03:00
Nick Shirokov 3453e64bea fix(skd-decompile): чтение DataSetUnion inner <item> элементов
Build-DataSet для типа DataSetUnion теперь читает <item xsi:type="...">
(платформенный формат), сохранена обратная совместимость с <dataSet>
для XML, сгенерированных предыдущими версиями skd-compile.

Эффект на sample30: -12000 строк diff (LOST <v8:item>/<lang>/<content>
в полях inner-Union datasets).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:01:08 +03:00
Nick Shirokov eac0ae5a02 fix(skd-compile): DataSetUnion inner items оборачиваются как <item>
Платформенный 1С пишет вложенные dataSets внутри DataSetUnion как
<item xsi:type="DataSetQuery">, а наш compile эмитил <dataSet xsi:type=...>.
Это вело к двум проблемам:
- сгенерированный XML отличался от платформенного (косметика для bit-perfect)
- skd-decompile симметрично искал <dataSet> и пропускал inner items
  при чтении реальных схем — теряя все вложенные fields/titles

Эталон: upload/erf/ПроверкаЭкранирования/.../Templates/СКД_Объединение
показывает что Designer всегда пишет <item xsi:type="..."> внутри Union.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 16:00:59 +03:00
Nick Shirokov a46d5a166b feat(skd-decompile): сохранение viewMode/itemsViewMode для round-trip
Decompile теперь читает viewMode/itemsViewMode из XML и сохраняет в JSON
точно как было — даже Normal-значения (платформа эмитит эти теги
контекстно, и для bit-perfect нам важно наличие, а не сам режим).

Чтение:
- item-level: selection item, order item (новая object form)
- block-level: selection/filter/order/conditionalAppearance →
  XViewMode на settings
- structure group: viewMode + itemsViewMode на самом item
- settings: itemsViewMode

Дополнительно:
- Убран shorthand @normal из filter/condApp/dataParam (Normal — default,
  шум в JSON)
- Структурный shorthand "A > B > details" не сворачивается если есть
  viewMode/itemsViewMode на элементе
- Selection/order на structure-item сохраняются даже = [Auto] —
  compile теперь не дефолтит, поэтому наличие важно для bit-perfect

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:38:42 +03:00
Nick Shirokov bf4005bf76 feat(skd-compile): viewMode/itemsViewMode на блоках и structure items
DSL расширения (item-level — паттерн object form расширен):
- selection: {field, viewMode}
- order: {field, direction, viewMode} (новая object form)
- structure group: {type:group, viewMode, itemsViewMode}

DSL расширения (block-level на settings):
- selectionViewMode, filterViewMode, orderViewMode
- conditionalAppearanceViewMode
- itemsViewMode (на самих settings)

Compile эмитит viewMode/itemsViewMode только если явно задано в JSON —
это позволяет decompile сохранить точное наличие/отсутствие из XML и
получить bit-perfect round-trip (платформа эмитит эти теги
контекстно — на ABCXYZ-стиле для каждого блока, а в простых отчётах
без пользовательских настроек — не эмитит).

Дополнительно:
- Пустой LocalStringType теперь эмитится как self-closing (как платформа)
- Убран default order/selection=["Auto"] на StructureItemGroup
  (раньше compile дефолтил, теперь эмитит только если задано)

В SKILL.md не упоминаем — фича редкая. Полное описание в spec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 15:38:29 +03:00
Nick Shirokov 501abd9fac fix(skd-compile): multilang в outputParameters value
Emit-OutputParameters принудительно использовал str(value), теряя
multilang dict {ru, en} → эмитил как "@{ru=...; en=...}". Теперь
auto-promote ptype=mltext если значение — PSCustomObject/dict.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 14:25:32 +03:00
Nick Shirokov c3a8a9c874 fix(skd): multilang в calcField appearance и selection lwsTitle + hidden combo
Найдено через новый debug-tool debug/skd-decompile/verify-roundtrip.ps1
(XML→decompile→compile→diff на сэмпле 30 ERP).

1. Emit-CalcFields appearance: третий дубликат-emitter с тем же
   multilang-багом как был в Emit-AppearanceValue для field/cond. Унифи-
   цирован через Emit-AppearanceValue. (compile.ps1+py)

2. Emit-SelectionItem: lwsTitle для folder + field-with-title
   эмитили "$($item.folder)" — для hashtable получали "@{ru=X; en=Y}".
   Унифицирован через Emit-MLText с новой опцией -NoXsiType
   (lwsTitle в оригинале без xsi:type, в отличие от <title> в fields).
   (compile.ps1+py)

3. Build-Parameter hidden detection: combo availableAsField=false +
   useRestriction=true. Только availableAsField=false (без
   useRestriction) → object form `availableAsField: false`.

На сэмпле 30 ERP roundtrip-diff: ADDED <content> 667 → 129
(multilang-потери в selection закрыты). Остаточные LOST <item> ~10k —
другие потери (attributeUseRestriction и проч.) — отдельная задача.

Versions: compile v1.35→v1.36, decompile v0.20→v0.21.
2026-05-21 21:50:46 +03:00
Nick Shirokov 8b71054478 feat(skd-decompile): query file префикс + inline объекты/массивы по lineLimit
1. Внешний .sql теперь именуется <outputBasename>-<datasetName>.sql
   (раньше просто <datasetName>.sql). Защищает от коллизий при
   batch-decompile нескольких отчётов в одну папку: имена dataset'ов
   часто совпадают ("НаборДанных1" — почти везде).
   В JSON: "query": "@<outputBasename>-<datasetName>.sql".

2. ConvertTo-CompactJson: Try-InlineJson — пытается сериализовать
   container на одну строку. Если результат + текущий indent ≤120
   chars → inline; иначе multi-line. Применяется и к объектам и к
   массивам (включая массивы из примитивов — раньше они всегда были
   inline, что давало гигантские строки на длинных fields).

   Примеры inline (объекты ≤120 chars):
   - { "value": "B", "style": "header" }
   - { "name": "Имя", "expression": "Имя" }
   Длинные объекты и массивы — multi-line как раньше.

v0.19 → v0.20.
2026-05-21 21:04:21 +03:00
Nick Shirokov 55b80fdc08 feat(skd-decompile): компактный JSON-сериализатор + расширенный shorthand totalFields
Заменил ConvertTo-Json (PS5.1) на собственный ConvertTo-CompactJson:
- 2-пробельный indent (вместо 4 + выравнивание keys по длине)
- Массивы примитивов (string/number/bool/null) — inline `[a, b, c]`
- Массивы с объектами/nested arrays — multi-line как раньше
- Кириллица в UTF-8 (без \uXXXX-escapes)
- Корректный escape строк (\\", \\, \n, \r, \t, \uXXXX для control chars)

Build-TotalField: shorthand "name: expr" для любого однострочного
expression. Раньше object form применялась когда expression не Func(arg).
Теперь — только когда есть group или expression многострочный.
Compile принимает любой shorthand вида "dataPath: expression" (Parse-
TotalShorthand делает split по первому ":").

Save-UserStyles тоже использует новый сериализатор.

Все 16 декомпиляционных snapshot'ов обновлены (косметика — JSON
структурно тот же, тесты round-trip проходят).

На реальном отчёте (целевой корпус): 405 → 264 строк (-35%).
v0.18 → v0.19.
2026-05-21 20:50:34 +03:00
Nick Shirokov e0ee927156 feat(skd-decompile): externalize multi-line queries в отдельные .sql файлы
Если query ≥3 строк и указан -OutputPath, decompile выносит SQL в
<datasetName>.sql рядом с decompiled.json. В JSON эмитится "@<name>.sql"
вместо inline-строки.

- Имя файла: dataset name (sanitized — non-word chars → _), коллизии
  разрешаются суффиксом _2/_3/...
- compile уже поддерживает синтаксис @file.sql (Resolve-QueryValue)
  — round-trip симметричен.
- Тесты не изменились: все тестовые queries по 1 строке (порог не
  срабатывает).
- На реальных отчётах (ERP/ACC main DCS — 10-50 строк query типично)
  даёт значительно более компактный JSON + читаемый .sql с подсветкой
  синтаксиса в IDE.

Новый тест dataset-query-multiline (round-trip с внешним .sql).
v0.17 → v0.18.
2026-05-21 20:37:15 +03:00
Nick Shirokov a1131965cc feat(skd): DataSetFieldFolder + GroupItemAuto + empty-field selection
Закрывает три gap'a, выявленных при полном прогоне ERP+ACC:

1. DataSetFieldFolder — поле-папка для UI-группировки полей в композиторе
   настроек. Только dataPath + title, без valueType/role.
   - DSL: object form поля с `folder: true`.
   - Compile: при folder=true → <field xsi:type="DataSetFieldFolder">.
   - Decompile: распознать xsi:type, эмитить object form.

2. GroupItemAuto — пустой <item xsi:type="GroupItemAuto"/> в groupItems
   (auto-grouping, аналогично "Auto" в selection).
   - DSL: строка "Auto" в groupFields.
   - Compile/decompile: round-trip.

3. Empty <field/> в conditionalAppearance/selection (wildcard — apply
   to all). Раньше — SelectionItem: sentinel. Теперь эмитим как "Auto"
   (семантический эквивалент через SelectedItemAuto).

Новый тест dataset-folder-and-auto-group (round-trip).
Versions: compile v1.34→v1.35, decompile v0.16→v0.17.

На предыдущем прогоне ERP+ACC: 227 sentinel'ов (218 DataSetFieldFolder
+ 7 GroupItemAuto + 2 SelectionItem). После — 0.
2026-05-21 20:28:45 +03:00
Nick Shirokov be9ebedf14 fix(skd-compile): multilang appearance value (Формат={ru,en} и др.)
Emit-AppearanceValue / emit_appearance_value: hashtable/PSCustomObject/dict
значение → LocalStringType независимо от ключа. Раньше для значения
{ru: "ДЛФ=D", en: "DLF=D"} compile эмитил xs:string "@{ru=ДЛФ=D; en=DLF=D}"
(строковое представление PS hashtable) — потеря структуры и неверный XML.

Wrapper {use: false, value: ...} распознаётся точечно (требуются оба ключа,
чтобы не путать с multilang dict без 'use').

Унификация field-level appearance: parse сохраняет значение как есть
(а не str(v)), emit использует Emit-AppearanceValue вместо дублированной
mini-логики. Side-effect: "true"/"false" в field appearance теперь эмитятся
как xs:boolean (раньше — xs:string). Корректнее для 1С; обновлён один
snapshot теста compile.

Новый тест appearance-multilang-value (поле + conditionalAppearance с
multilang Формат — round-trip bit-perfect).
Versions: compile v1.33→v1.34.

Закрывает п.2 из handoff («известный баг с multilang appearance values»).
2026-05-21 20:01:31 +03:00
Nick Shirokov 7f3a8861ad feat(skd): inline cell style override + закрытие категории C
Cell в rows теперь может быть либо string ("text"/"{param}"/"|"/">"/null),
либо объектом {value, style: "presetName"}. Object form применяется когда
стиль ячейки отличается от template default.

compile (ps1+py): helpers _get_cell_value / _get_cell_style_or_default.
Emit-AreaTemplateDSL / _emit_area_template_dsl используют per-cell style
для appearance вместо единого template style.

decompile: refactor Build-Template. Первый pass — собрать style name per
cell в cellStyleMap. Второй pass — выбрать template default как most
frequent style. Третий pass — обернуть в {value, style} ячейки, чьи
стили отличаются от default. TemplateStyleMismatch sentinel удалён —
теперь все случаи покрываются через inline override.

Дедуп при обоих pass'ах (Match-PresetByShape) работает через
effectivePresets (built-in + user + ранее аллоцированные customN), так
что одинаковые shape'ы получают одно имя.

Новый тест template-inline-cell-style (round-trip bit-perfect).
Versions: compile v1.32→v1.33, decompile v0.15→v0.16.

Метрики на момент коммита:
- ERP-сэмпл 30: 30/30 clean, 0 sentinel'ов
- Корпус из 40 отчётов целевого класса: 40/40 clean, 0 sentinel'ов

Закрывает категории A, B, C полностью на обоих корпусах.
2026-05-21 19:53:41 +03:00
Nick Shirokov 3119700c71 feat(skd-decompile): авто-генерация skd-styles.json для custom appearance
Категория C — закрыта для однородных шаблонов с custom appearance.

Refactor fingerprint → preset shape (11 полей: font/fontSize/bold/italic/
hAlign/vAlign/wrap/bgColor/textColor/borderColor/borders). vAlign теперь
учитывается в matching (раньше игнорировался).

Алгоритм:
1. При -OutputPath загружается existing skd-styles.json рядом (если есть);
   user presets накладываются на built-in по той же логике что и compile.
2. Каждая ячейка → Extract-CellPreset → Match-PresetByShape против
   effectivePresets (built-in + user).
3. Если не match — Allocate-CustomStyle: новый customN, регистрируется
   в effectivePresets и accumulator.
4. По окончании Save-UserStyles пишет skd-styles.json рядом с outputPath
   (preserved existing + новые customN).
5. Compile подхватит файл по своим search-путям (cwd/dirname/scan-up).

В SKILL.md не добавляем (custom стили — для round-trip, не для написания
модель с нуля; built-in `data/header/subheader/total/none` остаются
основным интерфейсом для модели).

- runner.mjs: новый preRun step `writeFile` для подготовки fixture-файлов
  в workDir (нужен для теста с предзаписанным skd-styles.json).
- Новый тест template-custom-style: preRun пишет myHeader preset,
  скомпилирует темплейт, decompile reverse'ит → переиспользует имя
  myHeader (не создаёт customN).
- v0.14 → v0.15.

Метрики:
- ERP-сэмпл 30: 24 → 0 sentinel'ов, clean 26 → 30/30
- Целевой корпус 40 отчётов: 39 → 25 sentinel'ов (часть закрыта), clean
  19 → 20/40. Остаточные — шаблоны с разными стилями в разных ячейках
  одного шаблона (нужно per-cell style override — отдельная задача).
2026-05-21 19:32:17 +03:00
Nick Shirokov 4bd8f27dec feat(skd): preset style=none + детект пустого fingerprint в decompile
Закрывает простую часть категории C: шаблоны где у ячеек appearance
содержит только per-cell атрибуты (МинимальнаяШирина и др.) без font/
borders/colors. Раньше такие шаблоны попадали в TemplateStyleMismatch.

- skd-compile (ps1+py): новый preset 'none' со всеми стилевыми полями
  null/false. Emit-CellAppearance / _emit_cell_appearance пропускают
  Font-элемент когда style.font=null.
- skd-decompile: пустой fingerprint (после отсева per-cell ключей) не
  считается за стиль ячейки; если все non-merge ячейки шаблона имели
  пустой fp — эмитим style="none" вместо sentinel.
- Новый тест template-no-style (round-trip bit-perfect).
- Versions: compile v1.31→v1.32, decompile v0.13→v0.14.

Метрики:
- ERP-сэмпл 30: 32 → 24 sentinel'ов, clean 24→26/30
- Корпус из 40 отчётов целевого класса: 45 → 39 sentinel'ов, 19/40 clean

Остаточные sentinel'ы — реальный custom appearance (нестандартный шрифт/
выравнивание/цвет вне built-in пресетов). Требует расширения DSL под
hashtable-style — отдельная задача.
2026-05-21 18:38:34 +03:00
Nick Shirokov a73517ee07 feat(skd): nested folder + nestedObject + groupItem object form (round-trip)
Закрывает категорию B полностью на ERP-корпусе:
- selection.folder теперь рекурсивный: внутри items могут быть string,
  {field, title}, или ещё одна {folder, items: [...]}. Compile/decompile
  обходят дерево рекурсивно (Emit-SelectionItem / Build-SelectionItem).
- structure: новая ветка type=nestedObject с {objectID, settings:
  {selection, filter, order, conditionalAppearance, outputParameters}}.
- groupFields теперь объектная форма {field, groupType?, periodAdditionType?}
  когда не дефолт (Items / None). Compile уже принимал; decompile перестаёт
  ставить warning GroupItemDetails. Try-StructureShorthand игнорирует
  object-form поля при сворачивании в строку.
- Refactor: Build-Structure для StructureItemGroup теперь использует
  общий Get-GroupFields вместо дублированного inline-кода.

В SKILL.md не добавляем (формы редкие/сложные, модель не пишет с нуля).

Новый тест structure-nested-and-folder покрывает все три случая bit-perfect.
Versions: compile v1.30→v1.31, decompile v0.12→v0.13.

На сэмпле 30 ERP-отчётов: 754 → 32 sentinel'ов (-96%), clean 4 → 24/30.
Остаточные 32 — все TemplateStyleMismatch (категория C, диагностика).
2026-05-21 18:19:49 +03:00
Nick Shirokov 3a68e1cb44 chore: ignore debug-templates.txt (локальный листинг ERP-путей) 2026-05-21 18:08:16 +03:00
Nick Shirokov cbc9f0cf61 feat(skd): inputParameters — ChoiceParameters/Links + typed values (round-trip)
DSL: object-form ключ inputParameters — массив элементов, каждый типизирован
по форме value:
- choiceParameters: [{name, values: [...]}] — параметры выбора (DesignTimeValue)
- choiceParameterLinks: [{name, value, mode}] — связи параметров выбора
- value (+ optional use=false) — простое типизированное значение (bool/string/number)

Compile: Emit-InputParameters / emit_input_parameters → <r:inputParameters>...
Decompile: Read-InputParameters читает любой xsi:type, без SilentDrop warnings.
Build-Parameter — убран вызов несуществующего Check-InputParameters.

В SKILL.md не добавляем (форма сложная — модель не пишет с нуля, но при
декомпиляции из реального отчёта получает корректно и compile примет назад).

Новый тест field-input-parameters (3 типа элементов, bit-perfect round-trip).
Versions: compile v1.29→v1.30, decompile v0.11→v0.12.

На сэмпле 30 ERP-отчётов: SilentDrop:ChoiceParameters/Links 51 → 0,
clean reports 8 → 21, total sentinel'ы 109 → 58.
2026-05-21 18:07:59 +03:00
Nick Shirokov 4413a06c49 feat(skd): orderExpression — сортировка поля по выражению (round-trip)
- skd-compile (ps1+py): object-form ключ orderExpression{expression,orderType,autoOrder}
  → <r:orderExpression><dcscom:expression/><dcscom:orderType/><dcscom:autoOrder/>
- skd-decompile: читает <r:orderExpression> → object form поля, без SilentDrop warning
- SKILL.md skd-compile: одна строка в "Дополнительные ключи объектной формы"
- docs/skd-dsl-spec.md: пример в объектной форме поля
- Новый тест field-order-expression (round-trip bit-perfect)
- Versions: compile v1.28→v1.29, decompile v0.10→v0.11

На сэмпле 30 ERP-отчётов: SilentDrop:orderExpression 11 → 0.
2026-05-21 17:59:19 +03:00
Nick Shirokov 537adfd3f8 feat(skd-decompile): shorthand-render роли + extras без whitelist
- Get-RoleInfo: любой <dcscom:KEY> со строковым значением → extras; whitelist убран
- Render-Role: shorthand-строка "@flag K=V" когда все extras-значения простые
  (regex ^[\w\.\-]+$); иначе object form
- Build-Field: shorthand-роль встраивается в field-shorthand-строку
- v0.9 → v0.10

Новый тест-кейс field-roles-rich (балансовые поля с balanceGroupName/balanceType,
@dimension @required) — bit-perfect round-trip с compile.

На сэмпле 30 ERP-отчётов: 754 → 120 sentinel'ов (-84%), 8/30 clean.
ComplexRole 27 → 0.
2026-05-21 17:43:05 +03:00
Nick Shirokov 009656991f feat(skd-compile): расширенный синтаксис role — shorthand + KV без whitelist
- Parse-RoleSpec (ps1+py): принимает string ("dim"/"flag1 flag2 K=V") / array / object
- Parse-FieldShorthand: извлекает K=V из shorthand поля (regex \w+=\S+)
- emit: токены → <dcscom:KEY>true</dcscom:KEY>; extras → <dcscom:KEY>VALUE</dcscom:KEY>
  (без whitelist; раньше принимались только accountTypeExpression и balanceGroup)
- @period sugar поддерживает override через periodNumber/periodType KV
- Fix имени: balanceGroup в JSON принимается как deprecated alias для balanceGroupName
  (в реальном XML 1С элемент называется balanceGroupName; старый код compile эмитил
   несуществующий <dcscom:balanceGroup> — ни одного попадания в ERP-корпусе)
- SKILL.md, docs/skd-dsl-spec.md: единое описание четырёх форм роли
- v1.27 → v1.28
2026-05-21 17:42:52 +03:00
Nick Shirokov 8cf29c601e feat(skd-decompile): расширенные role (object form) + silent-drop visibility
- Get-RoleInfo (вместо Get-RoleTokens): любой dcscom:KEY=true → @KEY;
  accountTypeExpression/balanceGroup → extras → object form role
  (compile уже поддерживает object form через {key: true})
- Build-Field: object form role при наличии extras
- Silent-drop → warnings (без ломания round-trip):
  * Check-InputParameters — ChoiceParameters/ChoiceParameterLinks (non-empty)
  * orderExpression на field
  * scope в conditionalAppearance item

На сэмпле 30 ERP-отчётов: 754 → 147 sentinel'ов (-80%), 8/30 clean.
2026-05-21 17:13:17 +03:00
Nick Shirokov d8d80af88e feat(skd-decompile): категория A — SelectionItem implicit, StructureItemTable, StructureItemChart
- Build-Selection: implicit SelectedItemField при пустом xsi:type (платформа эмитит в conditionalAppearance)
- Build-Structure: ветки для StructureItemTable (columns/rows) и StructureItemChart (points/series/selection/outputParameters)
- Try-StructureShorthand: отказ от свёртки при type≠group
- Вспомогательные Get-GroupFields, Build-TableAxisBlock

На сэмпле 30 ERP-отчётов sentinel'ов категории A: 0 (было 640).
2026-05-21 17:03:31 +03:00
Nick Shirokov 48b08d77e5 test(skd-decompile): 6 snapshot-based test cases по слоям
Кейсы создают исходник через preRun (skd-compile), декомпилируют его и
сравнивают workDir со снапшотом (Template.xml + decompiled.json):

- minimal-query — базовый DataSetQuery
- fields-types-and-restrictions — типы, роли, restrictions, multilang
  title, appearance, composite type, presentationExpression
- calc-total-params — calculatedFields, totalFields, parameters с
  autoDates/valueList/hidden/availableValues
- templates-with-style-merge-drilldown — built-in стили header/data,
  merge >/|, drilldown свёртка
- variant-full — selection с folder, filter Or, conditionalAppearance,
  outputParameters, dataParameters="auto", structure shorthand,
  groupTemplates
- dataset-types — DataSetQuery + DataSetObject + DataSetUnion

Все 6 passes на runtime=powershell. Готовая база для регрессии при
питон-порте (можно прогнать тот же набор через --runtime python).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:20:59 +03:00
Nick Shirokov 31d1ae2650 feat(skd-compile): sentinel-check для интеграции со skd-decompile
При наличии __unsupported__ маркеров в JSON (от skd-decompile, Кольцо 2)
compile завершается с exit 4 и понятным сообщением: id/kind/loc каждого
sentinel и подсказка про .warnings.md рядом.

Рекурсивный walk JSON покрывает hashtable/PSCustomObject/array. Sentinel
в любом месте дерева — фейл.

Bump skd-compile v1.26 → v1.27.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:10:14 +03:00
Nick Shirokov 54d4dd6904 feat(skd-decompile): слои 16-17 — sentinel, warnings.md, fail-fast Ring 3
Sentinel/warnings уже работали по ходу разработки слоёв. Добавил
явные Ring 3 проверки до основного pipeline:
- Picture cells в шаблонах (<dcsat:item xsi:type=Picture>) → exit 3
- Параметры типа ХранилищеЗначения (v8:ValueStorage) → exit 3
- templateCondition (вариативные шаблоны) → exit 3
- Не-DCS корневой элемент → exit 2 (теперь через [Console]::Error,
  без обвязки Write-Error и с понятным сообщением по-русски).

Сообщения fail-fast включают рекомендацию использовать /skd-edit
для точечной работы.

Регрессий нет — все 7 синтетических тестов остаются 0 diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:09:11 +03:00
Nick Shirokov 840e3ed768 feat(skd-decompile): слой 15 — DataSetObject + DataSetUnion
- DataSetObject: уже была базовая поддержка objectName, теперь fields
  тоже извлекаются (отвязали от switch специально для query).
- DataSetUnion: рекурсивный walk вложенных <dataSet> элементов через
  Build-DataSet → items[] с полными nested-dataset объектами.
- Вынес логику в Build-DataSet функцию.

Round-trip clean (GUID-normalized) на всех 7 синтетических тестах,
включая ds-types-test с Query + Object + Union в одной схеме.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 16:07:11 +03:00
Nick Shirokov 4497b1d4e2 feat(skd-decompile): слои 10-14 — groupTemplates, settingsVariants
groupTemplates:
- <groupHeaderTemplate> → templateType=GroupHeader.
- <groupTemplate> → templateType из inner <templateType>.
- groupField/groupName/template — прямой перенос.

settingsVariants:
- selection: SelectedItemAuto/Field/Folder → "Auto"/имя/{folder,items}.
- filter: shorthand "Field op value @flags" + Or/And/Not-группы;
  reverse-map для всех операторов сравнения/списка/строкового/nullity.
- order: shorthand "Field" или "Field desc".
- conditionalAppearance: selection/filter/appearance/presentation/viewMode/userSettingID.
- outputParameters с LocalStringType поддержкой.
- dataParameters: auto-детект "auto" формы; иначе явный список.
- structure: рекурсивный walk StructureItemGroup; попытка свернуть
  линейную цепочку в string shorthand "A > B > details".
- Skip pure-default variant — compile его сгенерирует сам.

Auxiliary:
- Comma-operator `,$arr` на возвратах функций — избегаем разворот single-item
  массивов PS pipeline в скаляр.
- autoDates companions исключаются из visibleTop в Build-DataParameters,
  чтобы можно было свернуть в "auto".
- "_" placeholder восстанавливается для nil/empty filter right.
- Cleanup форматирования warnings.md (PS interpolation issue с `$).

Round-trip clean (GUID-normalized): все 6 синтетических тестов 0 diff —
fields/calc-params/templates/merge/variant/gt-test.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 15:09:05 +03:00
Nick Shirokov 61c4bd418d feat(skd-decompile): слои 6-9 — templates (cells, merge, style, drilldown)
- Парсинг <template>/<template xsi:type=AreaTemplate>/<dcsat:TableRow>/
  <dcsat:tableCell> в rows[][].
- Распознавание содержимого ячейки: dcsat:Text → строка, dcsat:Field с
  dcscor:Parameter → "{Имя}", dcsat:Field с LocalStringType → строка/multilang,
  пустая → null.
- Merge через appearance-флаги ОбъединятьПоВертикали/ОбъединятьПоГоризонтали
  на пустых ячейках → "|"/">".
- Детект built-in стилей (header/data/subheader/total) через нормализованный
  fingerprint appearance — без сравнения per-cell ширин/высот/merge-флагов.
  При несовпадении или неоднородности — sentinel TemplateStyleMismatch.
- Извлечение widths из appearance первого row и minHeight из первой ячейки.
- Drilldown-свёртка: для cells с appearance Расшифровка=Расшифровка_X
  и template-параметром DetailsAreaTemplateParameter Расшифровка_X →
  свертываем в `{name, expression, drilldown: X}`.
- Сохранение порядка template parameters через [ordered]@{}.
- Fix namespace URI для areatemplate (`area-template` с дефисом).

Bit-perfect round-trip 55924→55924 и 28590→28590 на синтетике с header/data
стилями, merge, drilldown, шаблонными параметрами.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 14:58:48 +03:00
Nick Shirokov be69bc231c feat(skd-decompile): слои 4-5 — calculatedFields, totalFields, parameters
- calculatedFields: shorthand с [title], type, expression и #restrict-флагами;
  object form при appearance или multilang title.
- totalFields: детект Func(name) и Func(expr) → shorthand "name: Func"/"name: Func(expr)";
  object form при привязке к группе.
- parameters:
  - shorthand с [title], type, value, @-флагами;
  - распознавание StandardPeriod variants → значение в shorthand;
  - @valueList, @hidden флаги;
  - availableValues с presentation;
  - object form для availableValues/multilang/composite type/expression.
- autoDates-сворачивание: для каждого StandardPeriod-параметра ищем пару
  dependent с expression `&P.ДатаНачала`/`&P.ДатаОкончания` (распознаём по
  expression, не по имени) и сворачиваем в @autoDates на родителе.
- decimal-тип всегда эмитится с явными (D,F) — JSON читаемее.
- useRestriction суппрессим в параметрах (auto-generated для @hidden).

Bit-perfect round-trip 7468→7468 байт на синтетике
(3 calc + 2 total + 5 параметров включая @autoDates).
Реальный ERP «АнализИзмененийЛичныхДанныхСотрудников» (1035 строк) —
0 warnings при декомпиляции.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 14:41:15 +03:00
Nick Shirokov 765e1d8885 feat(skd-decompile): слои 2-3 — dataSources, dataSets, поля
- Распознавание типов (string/decimal/boolean/date/dateTime/time/CatalogRef.X
  и пр.) с qualifiers (decimal-точность/знак, string Length/AllowedLength,
  date fractions) → shorthand или composite-массив.
- Роли (@dimension/@account/@balance/@period) с детектом сложных roleAttributes
  как sentinel.
- Restrictions (#noField/#noFilter/#noGroup/#noOrder) из useRestriction.
- Multilingual title с авто-сворачиванием {ru:"..."} в строку.
- appearance с поддержкой LocalStringType значений (например, Формат).
- presentationExpression.
- Свёртка дефолтного dataSource (ИсточникДанных1/Local) в умолчание.
- Автодетект object vs shorthand формы поля.

Bit-perfect round-trip на синтетике из 11 разнотипных полей.
Реальный ERP-отчёт АнализВерсийОбъектов декомпилируется с 0 warnings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 13:54:17 +03:00
Nick Shirokov 643211f2fb docs(skd-decompile): честная формулировка scope в SKILL.md
Убрал ложное обещание «структурной эквивалентности» (DSL покрывает
подмножество СКД). Слил «Гарантии» и «Не поддерживается» в раздел
«Что получаешь» с тремя категориями (покрытое / sentinel / fail-fast).
Добавил Workflow — декомпил это начало процесса, а не финал.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 13:50:46 +03:00
Nick Shirokov 5ec21f24b4 feat(skd-decompile): scaffold — Ring 3 fail-fast, sentinel/warnings, query extraction
Layer 1 of the skd-decompile plan: SKILL.md with disable-model-invocation,
ps1 skeleton with XML→JSON pipeline, namespace probe for non-DCS root,
sentinel/warnings accumulator, and DataSetQuery extraction (query only).
Test case minimal-query demonstrates round-trip via skd-compile preRun.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 13:40:46 +03:00
Nick Shirokov 048edafc15 docs(skd-edit): document group-selection idiom for add-field
Добавляет одну строку-подсказку в add-field: для попадания в Selection
конкретной группировки (а не variant) — связка -NoSelection + add-selection
с @group=. Это уже работало, но не было явно зафиксировано в SKILL.md.

Расширять сам add-field параметром -Group/@group= не стали — текущий
двухкомандный идиом более атомарен и не создаёт edge cases вроде
взаимодействия @group= и -NoSelection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:30:29 +03:00
Nick Shirokov 334241bea4 fix(skd-info): handle absolute -OutFile paths correctly
Раньше PS1-порт делал `Join-Path (Get-Location) $OutFile` без проверки,
что приводило к невалидным склейкам типа `C:\cwd\C:\abs\path.txt`, и
запись падала с «The given path's format is not supported».

Теперь: если путь абсолютный — нормализуется через `Path::GetFullPath`,
если относительный — резолвится против CWD. Python-порт уже был корректен,
только version bump.

Дополнительно: `args_extra` в runner.mjs теперь поддерживает подстановку
`{workDir}` — нужно для тестов с абсолютными путями внутри workspace.

Тесты: `skd-info/outfile-absolute-cyrillic` (PS + Python).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 10:19:20 +03:00
Nick Shirokov ce1ba0bab1 feat(skd-edit): normalize line endings + diagnostics on patch-query not-found
patch-query теперь нормализует CRLF/CR → LF в old/new/query перед поиском,
поэтому многострочные шаблоны с любым стилем переводов строк находятся
корректно (XmlDocument декодирует text-узлы как LF).

При not-found вместо сухого сообщения выводится воронка диагностики:
  1) cross-dataset probe — «Found in dataset 'Y' instead — wrong -DataSet?»
  2) tolerant probe (collapse whitespace + NBSP) — «would match with
     whitespace normalized» + точка расхождения
  3) prefix divergence — «matched N of M chars, expected 'X' (U+...) but
     got 'Y' (U+...)» + короткий контекст

Тесты: 4 новых кейса (positive CRLF-tolerant + 3 диагностических negative).
Регрессия 45/45 PS + 45/45 Python.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 19:53:24 +03:00
Nick Shirokov 6e14f2502e feat(skd-edit): empty parameter values, decimal/time/fix/composite
Brings skd-edit to parity with the skd-compile fixes from 449f814 / 0537410
/ ff2d851. Same helpers (Test-EmptyValue / Build-EmptyValueXml in ps1,
is_empty_value / build_empty_value_xml in py) shared by add-parameter,
modify-parameter (value=...), availableValues, add-dataParameter and
modify-dataParameter.

Behavior:
- Sentinel empty (null / "" / "_" / "null") serializes per declared type,
  matching what 1C Designer writes — ref/no-type → xsi:nil, string →
  xsi:type="xs:string" empty, date/time/decimal/boolean → typed zero,
  StandardPeriod → Custom + zero dates, dataParameters → dcscor:value
  xsi:nil="true". @valueList omits <value> entirely.
- Build-ValueTypeXml accepts bare decimal (10,2), decimal(N) (N,0),
  string(N,fix) (AllowedLength=Fixed), time (DateFractions=Time), and
  composite array of types.
- Parse-ParamShorthand / Parse-DataParamShorthand regex .+ → .* so a
  trailing `=` is treated as the empty-value sentinel. New @valueList flag.

New test cases: empty-param-values-add / -modify / empty-dataparam-values.
Three outdated skd-edit snapshots regenerated to reflect upstream skd-compile
empty-value emission (rename-parameter, reorder-parameters,
conditional-appearance-v2).

Regression: 41/41 ps1 + 41/41 py runner; 41/41 verify-snapshots ps1 + py
(live load into 1С 8.3.24). skd-compile 23/23 and skd-validate 15/15
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 13:38:52 +03:00
Nick Shirokov ff2d8513c4 feat(skd-compile): time type, string(N,fix), and composite type parameters
Calibrated against live Designer output in upload/erf/ПроверкаЭкранирования.

- New type 'time' (synonym 'время'): xs:dateTime with DateFractions=Time
  for time-of-day values. Designer uses the same xs:dateTime XSD type as
  date/dateTime — only DateFractions differs. Empty value: typed-zero
  0001-01-01T00:00:00 (same as dateTime).

- Extended string regex to accept (N,fix) → AllowedLength=Fixed (was
  Variable-only). Non-empty fixed-string values are emitted as-given
  without space-padding to Length — the platform handles padding on save.

- Composite types in parameters (array of types in object form, e.g.
  ["string(10,fix)", "CatalogRef.X"]) now work end-to-end: valueType
  emits each type with its qualifiers, and empty composite values
  serialize as <value xsi:nil="true"/> matching Designer.

Test case empty-param-values extended with 5 new params covering all
three additions. Snapshot validated by skd-validate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:52:26 +03:00
Nick Shirokov efdf56691c fix(skd-validate): eliminate false positives on real ERP/БП reports
Calibrated against 1106 vendor reports (ERP 8.3.24 + БП 8.3.27).
Three categories of false positive removed:

- CalculatedField with empty <expression/> demoted error→warning.
  Three legitimate vendor patterns surfaced:
    * sibling totalField with same dataPath provides the formula
      (used in cancellation-rate and share-percentage reports)
    * groupTemplate references the field as group name
    * field exists only as a declarative anchor for settingsVariants
  Warning preserved so genuinely-missing formulas still surface.

- Duplicate template name demoted error→warning. Vendor configs ship
  reports (БазаНормируемыхРасходов/Выручка) with three <template> blocks
  named Макет1 — the platform identifies templates by position, not by
  <name>. Warning still flags the collision without failing validation.

- comparisonType whitelist extended with NotInHierarchy and
  NotInListByHierarchy. Existing list was missing the negated
  hierarchy operators used in 20 of the 1106 reports.

Result: 0 false positives across the corpus, all genuine errors still
caught (verified separately against intentionally-broken fixtures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:27:21 +03:00
Nick Shirokov 12745b14c3 fix(skd-validate): handle composite valueType + system-type namespace
Calibrated against ~868 real ERP/БП reports — three false positives caught:

1. Composite types: <v8:Type>xs:string</v8:Type> followed by
   <v8:Type>d4p1:CatalogRef.X</v8:Type> with a single trailing
   <v8:StringQualifiers> is a legitimate pattern. Rewritten check to
   collect all <v8:Type> and qualifier blocks per <valueType>, then
   verify each qualifier has a matching scalar type anywhere in the
   block — not necessarily right before it.

2. System types: AccumulationRecordType (and similar enum-like system
   types) use the http://v8.1c.ru/8.1/data/enterprise namespace
   (without /current-config) and a plain TypeName local name with no
   dot. Whitelisted as a second valid namespace for ref-like types.

3. v8: scalar types extended: v8:Null, v8:Type, v8:ValueStorage —
   present in real configs as type-less placeholders.

Also reverted SKILL.md change from previous commit (validator details
don't belong in user-facing docs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 12:12:48 +03:00
Nick Shirokov a5a1636918 feat(skd-validate): catch broken XDTO in valueType and value
skd-validate was purely structural (names/refs/duplicates) and missed an
entire class of bugs that XDTO rejects at db-load-xml — exactly the
kinds of mistakes the LLM (or hand-edits) commonly introduce.

New section 16: valueType structural — each <v8:Type> must have a known
prefix (xs:/v8: or any prefix bound to enterprise/current-config),
qualifier blocks must match their preceding type, and qualifier
internals (Digits/FractionDigits/AllowedSign, Length/AllowedLength,
DateFractions) must use legal tokens.

New section 17: value content — <value xsi:type="dcscor:DesignTimeValue">
rejects literal placeholders ('_') and empty strings, since these are
the exact symptom of the titan team's BUG-2.

5 new fixtures cover: bare-decimal, missing-qualifiers,
qualifier/type mismatch, ref-literal '_', bad AllowedSign token.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:57:20 +03:00
Nick Shirokov 05374100c1 fix(skd-compile): accept bare decimal and decimal(N) with sensible defaults
Emit-SingleValueType / emit_single_value_type previously required full
decimal(D,F) — anything else fell through to a fallback that produced
invalid <v8:Type>decimal</v8:Type> (no xs: prefix, no qualifiers).

New regex `^decimal(\((\d+)(,(\d+))?(,nonneg)?\))?$` accepts:
- decimal                → 10,2,Any (money default — most common 1C intent)
- decimal(N)             → N,0,Any (integer)
- decimal(N,nonneg)      → N,0,Nonnegative
- decimal(N,M)           → as before
- decimal(N,M,nonneg)    → as before

Synonyms (число, число(N), etc.) inherit the same forms via Resolve-TypeStr.

Shared Emit-ValueType is called from fields, parameters, and output
parameters — one fix covers all three paths. 3 existing snapshots
regenerated with proper xs:decimal + qualifiers, plus new
decimal-qualifier-defaults test case covering all 5 forms × synonyms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:45:23 +03:00
Nick Shirokov 449f814d16 fix(skd-compile): Designer-compatible empty parameter values
Centralized empty-value handling: shorthand `=`, `= _`, `= null` and
object-form `value: null` / `""` now serialize per type, matching what 1C
Designer writes:
- ref / no-type → <value xsi:nil="true"/>
- string → <value xsi:type="xs:string"/>
- date/decimal/boolean → typed zero (0001-01-01 / 0 / false)
- StandardPeriod → Custom variant with zero dates
- @valueList → omit <value> entirely

Closes BUG-1 (StandardPeriod @autoDates) and BUG-2 (CatalogRef.X = _
producing invalid <value>_</value>) reported by titan team. New helpers
Test-EmptyValue / Emit-EmptyValue (ps1) and is_empty_value /
emit_empty_value (py) shared by Emit-ParamValue, availableValues loop,
and explicit dataParameters emit. Shorthand regex .+ → .* so trailing
`=` parses as empty.

Reference: upload/erf/ПроверкаЭкранирования (live Designer dump).
New test case empty-param-values covers all 10 type×sentinel combos;
3 existing snapshots regenerated to include the now-correct <value>
tags.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 11:27:24 +03:00
Nick Shirokov 3eaa7ffa3b fix(skd-edit): drop unneeded " → &quot; in query/expression
Зеркалим решение из skd-compile: убираем .Replace('"','&quot;') из Esc-Xml
и удаляем post-process, который принудительно ставил &quot; внутри
<query>/<expression>. Реальный Конфигуратор так не пишет — экранирование
было анти-1С-стилем и портило round-trip diff.

Снимок add-calculated-field-restrict обновлён под новый формат.
Кейс preserve-entities-modify-parameter-title удалён: его смысл
инвертировался (теперь проверял бы нормализацию, а не сохранение),
а часть про многострочный xmlns уже покрыта preserve-xmlns-multiline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:51:04 +03:00
Nick Shirokov 98ebb478ee fix(skd-compile): drop unneeded " → &quot; — matches Designer style
Конфигуратор внутри текстового контента <query>/<expression> оставляет " сырыми
(проверено на ERP DCS: 1504 raw " против 0 &quot;). Убираем .Replace('"','&quot;')
из esc_xml — теперь round-trip diff против типовых остаётся чистым.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:50:55 +03:00
Nick Shirokov fb67b1b80d fix(skd-edit): realistic multilang fixture (ERP-style appearance block)
multilang-base/Template.xml содержал <editFormat xsi:type="v8:LocalStringType">
на <field xsi:type="DataSetFieldField">, что нелегально по XDTO-схеме DCS —
1С Designer падал с "Исключение XDTO" при загрузке через
LoadExternalDataProcessorOrReportFromFiles. Snapshot-тесты этого не ловили
(только byte-equality), а platform-verify (tests/skills/verify-snapshots.mjs)
ронялся на трёх кейсах с этой фикстурой.

Заменил <editFormat> на реалистичный <appearance> блок с вложенным
<dcscor:item xsi:type="dcsset:SettingsParameterValue"> и многоязычным
<dcscor:value> (ru + en) — структура взята из типовой ERP-выгрузки. Это
даёт более правильный test для preserve-unknown-children: <appearance>
содержит вложенный multi-lang xsi:type-узел, который точно прошёл бы
через DOM round-trip с искажениями, если бы _unknownChildren не работал.

preserve-unknown-children-modify-field: shorthand изменён с
"@ignoreNullsInGroups" на "@dimension" (no-op по составу role, но
триггерит rebuild). Прежний @ignoreNullsInGroups без @dimension давал
комбинацию, которую Designer отвергает (ignoreNullsInGroups валиден
только в контексте resource-роли).

39/39 snapshot suite (PS+PY) + 39/39 platform verify через erf-build →
Designer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:19:13 +03:00
Nick Shirokov 79db5de6ee fix(skd-edit): preserve multi-lang title + unknown children in modify-*
В типовых конфигурациях (ERP, БП, ЗУП и т.д.) у полей и параметров обычно
есть мульти-язык title (ru + en, иногда + локализация). До этого modify-field /
modify-parameter / modify-dataParameter, перестраивая элемент через
Build-MLTextXml, оставляли только последнее найденное <v8:content> в ru —
en/uk/kk siblings молча терялись, и при следующей выгрузке Designer
ломал миграцию.

Read-FieldProperties сохраняет полный OuterXml <title> в _rawTitle и
коллекционирует OuterXml неизвестных дочерних элементов
(<editFormat>, <appearance>, кастомные расширения) в _unknownChildren.
Build-FieldFragment эмитит:
* _rawTitle как есть, если user не задал новый title;
* Patch-MLTextRu(_rawTitle, newRu) если user задал ru-override — патчит
  только <v8:content> в <v8:lang>ru</v8:lang>, остальные языки сохраняет;
* _unknownChildren в конце поля (после valueType).

modify-parameter аналогично: при title-override проверяет multi-lang
(>1 <v8:item>) и патчит ru через Patch-MLTextRu, иначе ребилдит ru-only.

set-field-role сохраняет нестандартные подэлементы <role> (например
<dcscom:addition>, <dcscom:groupFields>), не входящие в фиксированный
known-children set и не указанные через kv в shorthand.

xmlns-стрип на захваченных OuterXml — лишние декларации (которые сериализаторы
добавляют для standalone-фрагментов) убираются.

PY: lxml etree.tostring по умолчанию включает .tail (whitespace после
закрывающего тега), что приводило к non-idempotent ростy whitespace при
повторных прогонах. Везде добавлен with_tail=False.

Новые тесты с idempotent: true:
* preserve-multilang-modify-field (ru-override на multi-lang title);
* preserve-multilang-modify-parameter (то же для параметра);
* preserve-unknown-children-modify-field (role flag, проверяем что
  <editFormat> и en title не теряются).

Общая fixture: multilang-base/Template.xml с полем и параметром,
у каждого ru + en title; поле также имеет <editFormat>.

39/39 PS + 39/39 PY. skd-edit v1.20 -> v1.21.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:18:07 +03:00
Nick Shirokov 23d2cb42de fix(skd-edit): preserve <valueType>, detect line endings, drop CRLF leak
Targeted follow-ups к round-trip фиксу:

* modify-field больше не теряет <valueType> при перестройке поля —
  Read-FieldProperties сохраняет полный OuterXml элемента (StringQualifiers,
  NumberQualifiers, DateQualifiers и т.п.), Build-FieldFragment отдаёт его
  обратно. Лишние xmlns-декларации, добавляемые сериализатором при
  выгрузке поддерева, стрипаются регексом.
* Line-ending convention теперь определяется при load (CRLF vs LF) и
  единообразно применяется в финале save. Раньше CreateWhitespace и
  Build-*Fragment везде использовали CRLF, что приводило к смешанным
  переносам в LF-исходниках (и наоборот) и к non-idempotent выходу
  modify-parameter title (run 1 → \n\t\t<title>\r\n... → run 2 →
  \r\n\t\t<title>\r\n...).
* PS Insert-BeforeElement переведён на LF; все -join "`r`n" → "`n";
  py "\r\n".join → "\n". Конечная нормализация переносов делается в
  save в соответствии со script:LineEnding.
* preserve-entities-modify-parameter-title.json теперь idempotent: true
  (после фикса CRLF leak'а двойной прогон byte-identical).

На реальной схеме diff после modify-field составил 30 строк: целевая
вставка title плюс полезная одноразовая коррекция ранее повреждённых
&quot; в text-content <dcsat:expression>. modify-field идемпотентен.

skd-edit v1.19 -> v1.20.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:18:07 +03:00
Nick Shirokov 511bfe7fdf fix(skd-edit): NO-OP skip + format-preserve post-process (round-trip)
XmlDocument round-trip искажал Template.xml даже при отсутствии правок:
декодировал &quot; в <query>/<expression>, схлопывал многострочный xmlns
корня, добавлял пробел перед /> и записывал файл при [WARN] not found.

Дирти-флаг ($script:Dirty / dirty) ставится только на успешной мутации;
финальный save пропускается с [INFO] No changes -- file untouched, если
ни одна операция в batch ничего не изменила. Post-process после OuterXml
восстанавливает raw-форматирование корневого xmlns из исходного файла,
re-escape `"` в текстах <query>/<expression> с anchored regex (не задевая
xsi:type="..."), и нормализует <foo .../> к <foo.../>.

Замеры на реальной схеме после modify-field: diff упал с 423 строк до 37
(94% шума устранено), повторный прогон byte-identical.

В runner.mjs добавлен caseData.idempotent: re-run + byte-equality на всех
файлах workDir. Три новых кейса (NO-OP, entity-preserve, xmlns-multiline)
+ общий fixture roundtrip-base. Все 33 ранее существовавших snapshot
перегенерированы под корректное форматирование (восстанавливают то, что
старый skd-edit ломал).

skd-edit v1.18 -> v1.19. PS и PY порты синхронизированы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 15:17:42 +03:00
Nick Shirokov f91b569564 docs(web-test): спецификация регресс-движка + чистка regress.md
Новый канонический документ docs/web-test-regression-spec.md —
техническое описание движка регрессионных тестов: CLI, формат
тест-модулей, ctx-контракт, утверждения, три уровня хуков
(инфра/тест/контекст), конфиг, контексты Playwright и режимы
изоляции, форматы отчётов (JSON/Allure/JUnit), обнаружение тестов,
ошибки/таймауты/повторы, анализ результатов, глоссарий.

Документ предназначен для CI-интеграторов, ручного редактирования
сгенерированных тестов и сопровождения самого движка. Без дорожной
карты и внутренних self-тестов — только публичный контракт.

regress.md в скилле почищен: добавлены контракт ctx и список
утверждений (раньше модели приходилось читать исходники), срезаны
дубликаты с SKILL.md (live recon, паттерны catalog/document),
переформулированы анти-паттерны под специфику регресс-движка.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:01:38 +03:00
Nick Shirokov e93185c18b fix(web-test): сериализовать onecError и платформенный стек 1С в JSON / Allure
В тест-обёртке ACTION_FN при 1С-исключении на throw-ed Error
вешалась полная структура (step, args, errors, formState, stack,
screenshot), но при сборке отчёта движок брал из неё только
{message, step, screenshot} — остальные поля терялись. Платформенный
стек 1С, ради которого делается fetchErrorStack, в JSON-отчёт не
попадал; в Allure statusDetails.trace писался только log()-вывод
теста.

Что поменялось:
- errInfo собирается один раз после teardown (раньше был дубликат на
  732 и 745), используется и для ctx.testResult (afterEach), и для
  lastError, и для итоговой записи в results[].
- В errInfo добавлено поле onecError: e.onecError — структура с
  stack.entries[{location, code}], formState, args, errors доезжает
  до JSON-отчёта без обрезания.
- writeAllure склеивает statusDetails.trace из tr.output + (если есть)
  onecError.stack.raw под разделителем "--- 1C stack ---". В Allure UI
  платформенный стек теперь виден прямо в карточке упавшего теста.

Обратная совместимость: для падений без 1С-исключения (assertion,
навигация и т.п.) e.onecError === undefined → JSON.stringify его
выкидывает, форма записи { message, screenshot } сохраняется в
точности.

Проверено вручную на стенде tests/web-test/ — падающий тест с
ВызватьИсключение, JSON и Allure оба содержат полный stack.entries и
формированный trace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:01:25 +03:00
Nick Shirokov 7fa279c354 feat(skd-edit): clear-conditionalAppearance + multiline patch-query (доки)
- Новая операция clear-conditionalAppearance в стиле clear-selection/
  order/filter. Закрывает потребность "заменить набор правил оформления"
  через clear + re-add.
- patch-query: многострочные подстроки уже работали (string.Replace
  корректно обрабатывает \n). Зафиксировано в SKILL.md.
- add-total: shorthand-шаблон с тремя случаями (Func, Func(expr),
  identity-выражение) — после fix Bug 6 поведение нужно явно объяснить.
- Косметика: убрана утечка XML-внутренностей в комментарии примера
  set-field-role @period.
- Пример patch-query @once заменён на более типовой случай уникальной
  подстроки (КАК ВТ_СтароеИмя вместо ЛЕВОЕ СОЕДИНЕНИЕ).

Регресс: 33/33 PS, 33/33 PY, 33/33 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 17:39:24 +03:00
Nick Shirokov 28a2a34c84 fix(skd-edit): add-total identity expression для не-аггрегатных функций
Раньше "DataPath: X" всегда заворачивалось в X(DataPath). Если X не
аггрегатная функция (например, имя другого ресурса или сам DataPath),
получалось некорректное выражение типа Проверка(Проверка).

Зеркалю логику из skd-compile: whitelist аггрегатных функций
(Сумма, Количество, Минимум, Максимум, Среднее + EN-варианты).
Для остального — identity (использовать funcPart как есть).

Сообщение [OK] теперь показывает фактически записанный expression.

Регресс: 32/32 PS, 32/32 PY, 32/32 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:16:27 +03:00
Nick Shirokov f0f1e88aaa feat(skd-edit): patch-query @once — assert ровно одно вхождение
Защищает от случайных замен в комментариях/совпадениях имён:

  "ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ @once"
  # fail, если в запросе 0 или 2+ вхождений

Без флага default — replace-all (как раньше, обратная совместимость).

При успехе сообщение содержит фактическое число вхождений
"(N occurrence(s))", помогает заметить неожиданную множественность
без явного @once.

Регресс: 31/31 PS, 31/31 PY, 31/31 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:00:55 +03:00
Nick Shirokov e7cbf306a0 feat(skd-edit): availableValue — список с replace-семантикой и в add-parameter
- Единый list-синтаксис: availableValue=v1[: p1], v2[: p2], ...
  Элементы через запятую, представление после двоеточия.
- Запятые/двоеточия внутри значений и представлений — в одинарных кавычках:
  availableValue=Окр1: 'руб., коп.', Окр1000: руб.
- add-parameter теперь принимает availableValue= и создаёт начальный список
  в одном вызове (раньше требовался последующий modify-parameter).
- modify-parameter availableValue=... ЗАМЕНЯЕТ весь список (раньше
  append). Согласуется с остальными modify-* для одиночных свойств.
- SKILL.md: добавлен shorthand-шаблон для modify-parameter,
  расширен для add-parameter [availableValue=список].

Существующие тесты мигрированы со старого ;;-batch на новый list-синтаксис.
Снапшоты сохранились (тесты стартовали с пустого списка — semantics
совпадает для greenfield).

Регресс: 29/29 PS, 29/29 PY, 29/29 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:37:56 +03:00
Nick Shirokov 610720334b feat(skd-info): kv-параметры роли в детализации поля
В -Mode fields -Name <field> к сводке Role добавляются не-bool
параметры роли (balanceGroupName, balanceType, parentDimension,
accountTypeExpression и т.д.) в формате name=value.

Bool-флаги (@balance, @dimension, ...) отображаются как раньше.
False-значения по-прежнему скрыты.

Регресс: 6/6 PS, 6/6 PY (существующие snapshots не задеты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:54:03 +03:00
Nick Shirokov 5090deb5bc feat(skd-edit): set-field-role — управление ролями поля
Новая операция: полная замена <role>-блока поля dataSet.

- Shorthand: "<dataPath> [@флаги] [kv=значение]"
- Флаги (зеркало skd-compile): @balance, @dimension, @account, @period,
  @required, @autoOrder, @ignoreNullValues
- KV: balanceGroupName, balanceType, parentDimension, accountTypeExpression,
  orderType, expression, periodNumber, periodType
- Пустой spec (только dataPath) — снимает роль целиком
- Поддерживает пакетный режим

Закрывает потребность временного toggle off/on роли при отладке
(было: ручной Edit XML), а также корректировку balance/dimension
после add-total.

Регресс: 27/27 PS, 27/27 PY, 27/27 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:53:55 +03:00
Nick Shirokov 8b0bcf0194 feat(skd-edit): флаги @hidden и @always для параметров
- @hidden — скрывает параметр от пользовательских настроек
  (useRestriction=true + availableAsField=false). Для констант-параметров.
- @always — параметр всегда подставляется в запрос (use=Always).
  Используется самостоятельно для видимых обязательных параметров.
- Композируются: @hidden @always одной строкой даёт типовой паттерн
  "скрытая константа всегда применяется".
- Поддержка в add-parameter и modify-parameter, идемпотентны.

Регресс: 25/25 PS, 25/25 PY, 25/25 платформенный verify.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:14:19 +03:00
Nick Shirokov 529a5cacae feat(skd-edit): modify-structure + фиксы set-structure/parameter/patch-query
- modify-structure: новая операция, меняет groupItems группы по @name=,
  сохраняя Selection/order/filter/conditionalAppearance (Bug 1)
- set-structure: shorthand поддерживает запятую для нескольких полей
  в одном уровне группировки (Bug 2)
- set-structure: @name= с обрамляющими кавычками (двойными/одинарными)
  снимает их при записи в <dcsset:name> (Bug 3)
- add-parameter: ссылочные типы (CatalogRef, ChartOfAccountsRef, …)
  пишут <value xsi:type="dcscor:DesignTimeValue">, не xs:string (Bug 4a)
- modify-parameter: namespace-aware lookup существующих свойств
  — обновляет inplace, не плодит дубли (Bug 4b)
- modify-parameter value=…: пересборка <value> с корректным xsi:type
  из <valueType> (попутно лечит ранее битый XML)
- patch-query: батч ;;-сегментов триммится по краям (Bug 5)
- skd-compile: симметричный фикс ссылочных типов в emit_value

Регресс: 23/23 PS, 23/23 PY (skd-edit), 21/21 PS+PY (skd-compile),
23/23 платформенный verify-snapshots.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:40:49 +03:00
Nick Shirokov 8b0f55f1cc feat(form-validate): silent-skip числовых и UUID-DataPath в Check 5
В реальных выгрузках ERP/БП встречаются непрозрачные платформенные
DataPath, которые невозможно проверить из одного Form.xml:
- bare numeric ("10", "1000003") — внутренние индексы платформы
- "N/M:<uuid>" — ссылка на метаданные по UUID

Раньше Check 5 ругался на них "attribute not found". Теперь такие
пути пропускаются без счёта в paths checked и без ошибки.

Реалистичные пользовательские опечатки (кириллица в имени атрибута)
продолжают ловиться обычной проверкой attrMap.

Добавлен тест-кейс datapath-opaque-refs, версия v1.5 → v1.6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:40:27 +03:00
Nick Shirokov 54cbc69a59 feat(form-validate): резолв Items.<Table>.CurrentData.* и ~<Attr>.* в DataPath
В Check 5 раньше брался первый сегмент DataPath и искался в attrMap,
из-за чего ложно ругались реальные формы ERP/БП с путями вида
Items.<TableName>.CurrentData.<Field> (подвалы, инфо-панели) и
~<DynamicListAttr>.<Field> (текущая строка списка).

Теперь:
- ведущий ~ стрипается перед разбором сегментов;
- для Items.<Table>.CurrentData.* находим элемент-таблицу по name,
  берём её <DataPath> (атрибут DynamicList/TableSection) и проверяем
  его в attrMap. Если таблицы нет — Error; если третий сегмент не
  CurrentData — Warn.

Добавлен тест-кейс datapath-currentdata, версия скриптов v1.4 → v1.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:38:56 +03:00
Nick Shirokov ac3047cf55 docs(readme): ссылка на гайд регрессионного тестирования
Добавлена строка в таблицу навыков и в перечень docs/ для нового
docs/web-test-regression-guide.md (был забыт при первичном коммите
регресс-гайда).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:41:33 +03:00
Nick Shirokov 5da154adea Merge feature/web-test-runner into dev
web-test regression runner: M5-pre синтетика + M6 автономный стенд +
M7 testInfo/contexts/testlevel-хуки + M8 per-context lifecycle +
Allure-форматтер с auto-suite/severity + _allure/categories.json +
пользовательский гайд регресса (docs/web-test-regression-guide.md) +
skill-инструкция regress.md.
2026-05-13 20:28:39 +03:00
Nick Shirokov f4748d76af docs(web-test): пользовательский гайд регресса + skill-инструкция regress.md
- docs/web-test-regression-guide.md — пользовательские сценарии работы
  с моделью для покрытия прикладного решения регрессом (русский, по
  аналогии с web-test-recording-guide.md): структура tests/<app-name>/,
  диалоги с моделью, пример организации покрытия, отчёты Allure +
  categories.json.
- .claude/skills/web-test/regress.md — инструкция модели по написанию
  регрессионного набора: разведка (метаданные + живой проход через exec),
  layout по фичам, готовые шаблоны (CRUD/document/DCS/multi-user/repro),
  severity, anti-patterns, failure triage, _allure/ конвенция.
- SKILL.md — указатель на regress.md в конце файла (рядом с recording).
- docs/web-test-runner-spec.md → upload/ (был внутренним планом
  разработки, не пользовательской документацией).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:25:44 +03:00
Nick Shirokov b992cd11c5 feat(web-test): _allure/ конвенция + categories.json для триажа падений
run.mjs:
- syncAllureExtras(testDir, reportDir) копирует все файлы из
  <testDir>/_allure/ в reportDir перед генерацией отчёта. Underscore
  в имени параллелен _hooks.mjs (инфра, не тест) — discovery его
  пропускает.
- Вызов после writeAllure при --format=allure.

tests/web-test/_allure/categories.json — 7 правил классификации падений
по нашему 1С-домену:
  1. License pool exhausted (1C) — известный multi-context flake.
  2. 1C application error (modal) — exception modal через fetchErrorStack.
  3. Section panel icon-only — деградация состояния стенда.
  4. Navigation lookup miss — navigateSection/openCommand/navigateLink/switchTab.
  5. Element not found — clickElement/fillField/selectValue/closeForm/fillTableRow/deleteTableRow.
  6. Test timeout — Timeout (Nms) от раннера.
  7. Assertion failure — наши createAssertions + 1С-specific (formHasField/tableHasRow/noErrors).

spec §9: раздел «Доп. файлы Allure через <testDir>/_allure/» с таблицей
поддерживаемых типов (categories.json / environment.properties /
executor.json) и минимальным примером.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:53:09 +03:00
Nick Shirokov fc76407877 feat(web-test): auto-suite + severity-резолвер для Allure
run.mjs:
- buildSeverityIndex(config) — валидация config.severity (inverted map
  «уровень → [теги]») при загрузке: ключи только из blocker|critical|
  normal|minor|trivial, теги не дублируются между bucket'ами,
  defaultSeverity тоже валидируется. fail-fast через die.
- resolveSeverity(t, severityIndex):
  1. mod.severity если задан и валидный — выигрывает.
  2. max-rank среди тегов (стандартные имена severity или маппинг).
  3. config.defaultSeverity или 'normal'.
  Rank: blocker(5) > critical(4) > normal(3) > minor(2) > trivial(1).
  Max-wins инвариантен к порядку тегов.
- writeAllure: добавлены labels suite (= dirname(t.file) или 'root') +
  severity. Тег `tag` остался как раньше.
- testResult пробрасывает t.severity (для passed/failed веток).
- SEVERITY_RANK/LEVELS объявлены в модульной шапке (top-level await на
  cmdTest начинается до конца тела модуля, TDZ-аккуратность).

webtest.config.mjs: severity policy для нашего сьюта (smoke +
multi-context → critical, recording → minor, defaultSeverity = normal).

spec.md §7: раздел про severity-policy в конфиге с валидацией.
spec.md §9: «Авто-эмиссия label-ов» — tag/suite/severity + правила резолва.

Регресс 19/19 ✓ (9m 7.6s). Распределение по уровням после исправления
'record' → 'recording' в маппинге: 13 critical / 5 normal / 1 minor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:37:58 +03:00
Nick Shirokov a55195ab66 docs(web-test): §16.1 — вложенные каталоги (что работает, что нет)
Зафиксирована конвенция:
- Discovery рекурсивный, путь попадает в отчёт.
- Per-folder hooks/config/context-default НЕ поддерживаются (by design).
- Группировку в отчётах делать через tags, не через путь.
- Сортировка по полному пути (`warehouse/01-x` после `sales/02-y`) —
  для глобального порядка нужны 3-значные префиксы или теги-фазы.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:33:10 +03:00
Nick Shirokov 1eff62de42 docs(web-test): полный sync спеки + contexts[] в testResult
spec.md v0.2 (последний sync 2026-05-13):

§1 CLI: добавлены --report-dir и `--` separator в таблицу флагов.
§1 «Режим выполнения»: убрана несуществующая «группировка по контексту»,
  описана реальная алфавитная модель + lazy ensureContext.
§2 пример multi-context: latin ID контекстов вместо кириллицы (clerk/manager)
  + showcase closeContext в финальном шаге.
§3 список API расширен: контексты (createContext/closeContext/setActive/
  listContexts/hasContext/getActiveContext), overlay-helpers (hideTitleSlide/
  hideImage/setHighlight/isHighlightMode), error-helpers (dismissPendingErrors/
  fetchErrorStack).
§6 пример _hooks.mjs: убран mock-навигация в beforeAll, добавлены примеры
  afterOpenContext/beforeCloseContext, afterEach показывает testResult.
§8 переписан раздел «Реализация в browser.mjs» (мульти-контекст уже live)
  + новая таблица режимов изоляции tab/window.
§9 JSON example: поле "context" → "contexts": [...] (массив).
§10: убрано упоминание несуществующего verbose-режима.
§13 «Параметризация»: убран статус «будущее», описана реальная семантика
  T6 (template name, param 2-м аргументом, testInfo.param).
§14 buildContext: переписан под done-состояние + scoped-вариант.
§16 каталог тест-кейсов: 13 → 19 файлов (multi-context, recording,
  errors-stack, tree-form, misc, hooks).
§17 дорожная карта: 10 → 18 пунктов, M4–M8 включены.

run.mjs:
- testResult получил поле contexts: [...names] во всех ветках
  (passed/failed/skipped/context-setup-failed). Раннер передаёт
  declaredContexts из единой точки до if(skip), чтобы skip-результаты
  тоже несли структурную информацию.

Регресс 19/19 ✓ (9m 8.7s) после --rebuild-stand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:11:51 +03:00
Nick Shirokov eb87be5c04 feat(web-test): M8 — per-context lifecycle (closeContext + afterOpenContext/beforeCloseContext)
browser.mjs:
- + closeContext(name): logout slot + close page (tab) или context (window),
  удаление из реестра. Throw если name неактивен (рулило: nicht den aktiven
  closen, recorder always attached к active → invariant простой).
- _logoutSlot(slot, waitMs) — извлечён из disconnect, переиспользуется в
  closeContext.

run.mjs:
- ensureContext() после createContext вызывает hooks.afterOpenContext(ctx, name, spec).
- wrapCloseContextHook() оборачивает ctx.closeContext (и каждую scoped-обёртку)
  чтобы перед browser.closeContext fir'ить hooks.beforeCloseContext.
- Финальный teardown в finally: для всех живых контекстов кроме первого
  (survivor) — beforeCloseContext + closeContext; для survivor только хук,
  его закрывает disconnect().

_hooks.mjs v0.5:
- afterOpenContext инжектит persistent DOM-badge с displayName в правый
  верхний угол page — в записанном видео всегда видно, какой контекст.
- beforeCloseContext counter-only.
- _state расширен полями afterOpenContext / beforeCloseContext.

15-multi-context-handover.test.mjs:
- +2 шага: closeContext('b') после handover, попытка closeContext(active)
  ловится throw'ом с проверкой message.

00-hooks.test.mjs:
- +1 ассерт: afterOpenContext >= 1 (default уже создан), beforeCloseContext === 0
  в теле первого теста.

spec §6:
- Раздел «Контекстный уровень» (afterOpenContext / beforeCloseContext + правила closeContext).
- ASCII-диаграмма порядка хуков обновлена с per-context lifecycle.

Регресс 19/19 ✓ (9m 16.8s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 16:07:45 +03:00
Nick Shirokov 43ed9ba142 feat(web-test): M7.5 — title slide в beforeEach для --record
_hooks.mjs v0.4: beforeEach под условием ctx.isRecording() показывает
title slide с testInfo.name + displayName первичного контекста как
subtitle, ждёт 1.5с через ctx.wait() и убирает.

В обычном регрессе (без --record) — ветка скипается, overhead ноль.
Под --record: 01-navigation 12.1s → 13.9s (+1.8с на слайд).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:45:14 +03:00
Nick Shirokov 588382cec1 feat(web-test): M7.4 — testlevel-хуки + 00-hooks индикатор
_hooks.mjs v0.3: добавлены beforeAll/afterAll/beforeEach/afterEach
(counter-only) и shared `_state` (счётчики + events log).

tests/web-test/00-hooks.test.mjs (новый, 4 шага, 0s) — индикатор
порядка вызовов: проверяет beforeAll===1, beforeEach для текущего
теста, доступность ctx.testInfo, afterEach < beforeEach.

Multi-context хуки оставлены one-shot. Разведка beforeAll:
navigateSection не нужен, 1С после входа уже на дефолтной секции.

Регресс 19/19 ✓ (9m 12.7s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:35:20 +03:00
Nick Shirokov e0197683e1 feat(web-test): M7.1+M7.2 — ctx.testInfo + проброс custom-полей контекстов
- ctx.testInfo (name/file/filePath/tags/timeout/attempt/maxAttempts/param/contexts/primaryContext)
  выставляется перед каждой попыткой, доступен в beforeEach/test/afterEach
- ctx.testResult (status/duration/attempts/error/steps) доступен в afterEach
- run.mjs:411 spread полного contextSpec (был whitelist {url, isolation});
  CLI --url override сохраняет custom-поля через merge
- webtest.config.mjs: displayName для a/b
- spec §3 — подраздел «Метаданные теста», §6 — availability testInfo/testResult,
  §7 — рекомендация латинский ID + кириллический displayName
- Full regression 18/18 ✓ (9m 9.8s)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:44:07 +03:00
Nick Shirokov 96dad75b2f feat(web-test): M6-MVP follow-up — 13-misc setup + URL webtest-runner
13-misc.test.mjs: setup-шаг упрощён до `assert.ok(existsSync(epfPath))`.
EPF-сборку (epf-init → form-add → form-compile → epf-build) забрал
_hooks.mjs.prepare() — здесь только проверка артефакта с понятной
ошибкой при отсутствии: «запустите раннер с `-- --rebuild-epf`».

webtest.config.mjs: URL обоих контекстов переключён на
`/webtest-runner/ru_RU` — отдельная публикация автономного стенда,
не конфликтует с интерактивной разведкой через `/webtest` на 8081.
2026-05-12 20:25:54 +03:00
Nick Shirokov 5c734202b6 feat(web-test): M6-MVP — автономный стенд через _hooks.mjs
Новый tests/web-test/_hooks.mjs v0.2 с prepare()/cleanup().
prepare() поднимает изолированный стенд:
- Hash-locks `tests/skills/.cache/webtest-stand/{config,epf}.lock`
  на sha256 от build-steps и EPF_SPEC — автоматический skip
  пересборки при отсутствии изменений.
- Слои конфиг XML / БД / EPF пересобираются независимо. Триггер
  ручной — флаги `--rebuild-config`/`--reload-data`/`--rebuild-epf`/
  `--rebuild-stand` (через `-- ...` после CLI раннера).
- Smart Apache: web-stop+web-publish выполняются только когда
  пересоздаём БД (нужно освободить блокировку). Иначе probe-first:
  жив (200) → no-op; мёртв → publish + probeReady. На warm-старте
  prepare сводится к чтению локов и одному probe (~200ms).
- web-publish на собственном AppName `webtest-runner` :9191 — не
  пересекается с интерактивной публикацией `webtest`.
- Кросс-платформенно: env WEBTEST_HOOKS_RUNTIME=python переключает
  на зеркальные py-порты скиллов (для не-Windows стендов).

cleanup() пока stub — оставляем стенд поднятым между прогонами,
для full-shutdown ручной /web-stop или `-- --rebuild-stand`.

E2E-проверено: cold-start `--rebuild-stand` поднимает стенд за
~38s; warm-старт prepare = 0.0s; полный регресс 18/18 зелёный
за 9m 7.1s (включая оба multi-context-теста, которые исторически
флапали).
2026-05-12 20:25:47 +03:00
Nick Shirokov a92bce05fb feat(web-test): runner v1.11 — -- separator + spec §6.1
В CLI раннера всё после `--` собирается в массив hookArgs и
передаётся в инфра-хуки prepare/cleanup без интерпретации со
стороны раннера. Сигнатура расширена до { hookArgs, log, config }:
log — структурированный вывод раннера, config — разобранный
webtest.config.mjs. Шаблон «всё после `--` принадлежит вложенному
инструменту» — стандартная shell-конвенция (npm, cargo, pytest).

Спека §6 обновлена под новую сигнатуру, §6.1 закрепляет контракт
`--` ↔ hookArgs с примером. Help-строка раннера упоминает
разделитель.
2026-05-12 20:25:33 +03:00
Nick Shirokov b8ebbf6a6f feat(build-webtest-db): v0.2 — dual-mode CLI + module exports
Извлечены exports: getProjectInfo, resolveScript, execSkill,
replacePlaceholders, runSteps, platformLoadSteps, loadBuildSteps.
CLI-режим сохранён через import.meta.url-guard. Подготовка к
переиспользованию из tests/web-test/_hooks.mjs без дублирования
exec-логики и pipeline-шагов.
2026-05-12 20:25:25 +03:00
Nick Shirokov 43ba6ce16c feat(web-test): M5-pre #4b — 09-filter/unfilter-specific (multi-badge)
Раньше шаг был deferred с комментарием «требует список с видимой
filter-панелью». На самом деле существующая абстракция работает:
два advanced filterList на разных колонках Контрагентов создают
два badge'а в state.filters[], а unfilterList({field}) снимает
конкретный — оставляя остальные.

Новый шаг 09-filter/unfilter-specific (~14s):
- filterList('ООО', {field:'Наименование'}) + filterList('123', {field:'ИНН'})
  → state.filters = [{field:'Наименование',value:'ООО'}, {field:'ИНН',value:'123'}]
- unfilterList({field:'ИНН'}) → остался только Наименование badge
- unfilterList() → пусто

Старый комментарий «defer to filter-panel synthetic» удалён —
оказался устаревшим (видимо unfilterList({field}) уже умел работать
с advanced-filter badge'ами на синтетических списках).

timeout 09-filter поднят с 60000 → 120000ms (8 шагов теперь, +14s
для unfilter-specific).

Регресс: 16/18 зелёных. Два multi-context-теста (14/15) упали на
лицензионном пределе 1С — known environmental issue, не связано с
этим коммитом.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:01:00 +03:00
Nick Shirokov 51e37f9874 feat(web-test): M5-pre #4a — Менеджер (choiceHistoryOnInput=Auto) + selectValue/auto-history
Реквизит шапки ПриходнаяНакладная.Менеджер типа CatalogRef.Контрагенты
с дефолтным choiceHistoryOnInput=Auto. Существующий Контрагент в той же
шапке имеет DontUse, что даёт парный контраст для тестирования влияния
флага на selectValue.

Новый шаг 04-selectvalue/auto-history:
- selectValue('Менеджер', 'ООО Юг') → method='dropdown' (typeahead активен,
  префиксный поиск по Description находит «ООО Юг» в catalogue).
- Парный 04-selectvalue/direct-form (existing): selectValue('Контрагент',
  'Север') → method='form' (typeahead подавлен DontUse → форма выбора).

Тест покрывает существующее ветвление selectValue по флагу
choiceHistoryOnInput без engine-доработок. Истории на сервере писать
заранее не нужно: typeahead использует prefix-match по Description,
а не статистику истории.

Полный регресс **18/18 зелёный** (8m 47.3s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:56:00 +03:00
Nick Shirokov 62e864e474 feat(web-test): M5-pre #3 — textEdit:false поле + 03-fillfields/direct-edit-form
Расширение синтетики: реквизит Поставщик типа CatalogRef.Контрагенты
добавлен в шапку ПриходнаяНакладная. Элемент формы Поставщик скомпилирован
с textEdit:false (новый DSL ключ form-compile v1.21 из коммита 32bf9c1):
ручной ввод запрещён, селект-кнопки нет, выбор только через форму выбора
по pick-кнопке.

Новый шаг 03-fillfields/direct-edit-form (~7s) — fillFields на Поставщик
('ООО Юг') возвращает method:'form', минуя обычные paste/typeahead/dropdown
ветки. fillFields внутренне детектит textEdit:false и сразу идёт через
форму выбора (selectValue path).

Полный регресс **18/18 зелёный** (8m 40.6s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:11:46 +03:00
Nick Shirokov ddebd7b6df feat(web-test): M5-pre #2 — составной тип Источник + 03-fillfields/composite
Расширение синтетики: реквизит Источник составного типа
(CatalogRef.Контрагенты + CatalogRef.Номенклатура + CatalogRef.Организации)
добавлен в шапку ПриходнаяНакладная и в ТЧ Товары. meta-compile принимает
составной тип через строковый синтаксис `A + B + C` (см. SKILL.md:56) —
эмитит три `<v8:Type>` элемента с правильным `d5p1:` префиксом.

Элемент ТЧ-колонки переименован в ИсточникТЧ (path/title оставлены
оригинальные) — иначе form-compile генерирует одинаковые companion-имена
(`ИсточникКонтекстноеМеню`) для шапки и ТЧ, и платформа отказывает в
открытии формы документа: "К сожалению, возникла непредвиденная ошибка"
(server-side, без полезного stack). TODO в form-compile-bugs.md: учитывать
путь поля при генерации companion-имён, чтобы избежать конфликта.

Новый шаг 03-fillfields/composite (~25s) — покрывает selectValue с
параметром `{type}` на составном поле:
- Шапка: selectValue('Источник', 'ООО Север', {type:'Контрагенты'})
  → method:'form', type:'Контрагенты', выбор через каталог-форму.
- ТЧ: fillTableRow({Источник: {value:'Альфа', type:'Организации'}},
  {row:0}) → method:'form', type:'Организации' (quickChoice=true →
  без формы выбора, прямой dropdown).

fillFields на composite без type выбрасывает понятную ошибку
с инструкцией «specify the type: selectValue(...,{type:'ИмяТипа'})» —
поведение API стабильно.

timeout 03-fillfields поднят с 60000 → 120000ms (6 шагов суммарно
~63s, новый composite step добавляет ~25s).

Полный регресс **18/18 зелёный** (8m 28.7s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 15:51:41 +03:00
Nick Shirokov 3d16e35e80 feat(web-test): M5-pre #1 — ValueTree + ДеревоНоменклатуры + tree-form smoke
Расширение синтетики: новая обработка ДеревоНоменклатуры с реквизитом
формы Дерево типа ДеревоЗначений и колонками Номенклатура (ссылка,
read-only) + Цена (Number, editable). ПриСозданииНаСервере рекурсивно
обходит Справочник.Номенклатура и заполняет дерево, отражая иерархию
групп/элементов из справочника.

Обработка зарегистрирована в подсистеме Администрирование и в роли
Администратор (Use+View).

Новый smoke 16-tree-form.test.mjs (5 шагов, 17.1s) — покрывает
05-table/edit-form (fillTableRow method:'direct' на FormDataTree-колонке)
и 08-hierarchy/tree-edit (expand узла + правка Цены через index-row):
- setup: navigateLink('Обработка.ДеревоНоменклатуры'), таблица Дерево
- read-roots: 2 корневые группы (_kind:'group'), columns=Номенклатура,Цена
- expand: clickElement('Товары',{expand:true}) → 16 строк (1 + 15)
- tree-edit: fillTableRow({Цена:1500},{row:1}) → method:'direct',
  Цена становится '1 500,00' (с non-breaking space 1С)
- cleanup: closeForm

Гэп: fillTableRow с row-by-name ('Товар 01') ловит SyntaxError в JS
eval. Использую row-by-index (TODO в web-test-bugs).

Полный регресс **18/18 зелёный** (8m 9.8s) на порту 9191.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:03:28 +03:00
Nick Shirokov 56822c4533 test(web-test): switch webtest publication to port 9191
Чтобы не конфликтовать с интерактивной разработкой на основном
Apache (8081, занят сторонним проектом), регрессионный регресс
теперь использует отдельный httpd-процесс на порту 9191. Тот же
httpd запускает /web-publish webtest -Port 9191 -V8Path 8.3.24.

Один процесс Apache → собственный пул лицензий 1С. На 8081 другие
проекты — наши тесты их не блокируют и наоборот.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:03:15 +03:00
Nick Shirokov 32bf9c1a3f feat(form-compile): textEdit key for InputField (TextEdit=false)
v1.20 → v1.21 (ps1 + py).

Добавлен ключ DSL `textEdit` для элемента input. Эмитит
`<TextEdit>false</TextEdit>` после AutoMarkIncomplete (значение
true — дефолт платформы, не эмитируется). Закрывает блокер для
03-fillfields/direct-edit-form в синтетике web-test: поле с
запрещённым ручным вводом → выбор только через pick-кнопку/F4.

Snapshot-тест: tests/skills/cases/form-compile/text-edit-flag.json
(2 поля, проверяет наличие TextEdit только на втором). 30/30
form-compile зелёные обоих runtime'ов.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 12:56:35 +03:00
Nick Shirokov c94f86a9cd test(web-test): M4.D2 — openFile EPF + security confirm
Новый 13-misc.test.mjs (3 шага, 11s) — покрытие openFile() для
внешних обработок с автоматической обработкой security confirmation.

- setup: автономный билд EPF (идемпотентный) через epf-init →
  form-add → form-compile (с текстовой декорацией) → epf-build.
  child_process.spawnSync для вызова PowerShell скриптов.
- openFile: проверки state.form, activeTab='Тест открытия',
  state.texts[] содержит декорацию с ожидаемым value,
  opened.attempt>=1, security confirm modal не пробивается.
- cleanup: closeForm + soft-проверка activeTab (между тестами в
  desktop могут оставаться формы от других тестов — не настаиваем
  на formCount=0).

Артефакты в test-tmp/13-openfile/ (.gitignore). Полный регресс
17/17 зелёный (8m 8s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 18:27:22 +03:00
Nick Shirokov 8b5fed98e0 test(web-test): M4.E — hierarchy + tree-grid (Номенклатура)
Новый 08-hierarchy.test.mjs (7 шагов, 24s) — покрывает группы и
tree-grid режима «Дерево» на форме списка Номенклатуры через UI
переключение viewMode. Без расширения синтетики.

- setup: явное переключение в «Иерархический список» через Ещё →
  Режим просмотра (viewMode сохраняется между сессиями и НЕ
  сбрасывается «Установить стандартные настройки»).
- read-groups (P1): readTable возвращает 2 группы (_kind=group).
- group-expand (P1): clickElement({expand:true}) развёртывает группу,
  внутри 15 элементов.
- switch-tree: «Ещё → Режим просмотра → Дерево» → viewMode='tree'.
- read-tree (P2): readTable.rows[]._tree (collapsed|expanded) — проверка
  только наличия поля (состояние сохраняется между сессиями).
- tree-expand (P1): defensive свёртка через {expand:false} если узел
  expanded, затем {expand:true} → kind='gridTreeNode' toggled=true,
  видны 15 элементов под Товарами.
- cleanup: восстановить иерархический список.

Замечание: clickElement({expand:true}) — только развернуть (no-op для
expanded), {expand:false} — только свернуть, {toggle:true} —
безусловно переключить.

05-table/direct-edit-form, edit-dblclick остаются deferred — нужен
документ с иерархической ТЧ. Полный регресс 16/16 зелёный (7m 53s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:43:31 +03:00
Nick Shirokov 9e677cfc61 test(web-test): M4.F — recording smoke (video + captions + TTS + overlays)
Новый 15-recording.test.mjs (5 шагов, 20.7s) — покрытие полного
публичного API recording.md.

- record + captions: startRecording → 2× showCaption → stopRecording.
  Проверки isRecording, duration/size, mp4 на диске, .captions.json,
  getCaptions с правильными text и time.
- narration: addNarration через Edge TTS (ru-RU-DmitryNeural), narrated
  mp4 больше исходного (добавлен аудио-трек).
- title-slide: showTitleSlide/hideTitleSlide — overlay fullscreen
  (w==innerWidth, h==innerHeight).
- image-overlay: showImage/hideImage с тестовой картинкой из screenshot.
- highlight: setHighlight toggles isHighlightMode, manual highlight на
  кнопке «Создать» создаёт overlay позиционированный на элементе.

Артефакты в test-tmp/recording-smoke/ (.gitignore), идемпотентный.
Полный регресс 15/15 зелёный (7m 27s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:20:13 +03:00
Nick Shirokov 211a4726d6 test(web-test): M4.C+D — drill-down + submenu-read
11-report/drill-down: dblclick по ячейке Номенклатуры сформированного
DCS-отчёта открывает форму элемента (DCS auto-drill). После Сформировать
ищется первая строка с заполненной номенклатурой, проверяется что после
clickElement({row,column},{dblclick:true}) form изменился и есть кнопка
«Записать».

02-crud/more-menu усилен под P2 submenu-read: добавлены явные проверки
clicked.kind='submenu', наличия типовых пунктов «Создать», «Изменить»,
«Расширенный поиск» (length>=5).

Покрыто 2 P2-кейса coverage matrix (11-report/drill-down,
02-crud/submenu-read). Полный регресс 14/14 зелёный (7m 1.6s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:58:28 +03:00
Nick Shirokov 91b39b758b test(web-test): M4.B+G — subordinate-nav + platform dialogs в 12-formstate
Расширены тесты getFormState: проверка ветвей navigation[] и
platformDialogs[] возвращаемой структуры.

- subordinate-nav: форма элемента Контрагент → state.navigation содержит
  «Основное» (active) и «Контактные лица» (подчинённый каталог).
- platform-dialogs: открытый через hamburger «О программе…» виден в
  state.platformDialogs[{type:'about'}].
- platform-dialog-close: closeForm закрывает платформенный диалог,
  массив становится пустым.

Покрыто 3 P2-кейса coverage matrix (12-formstate/subordinate-nav,
platform-dialogs, platform-dialog-close). Полный регресс 14/14 зелёный.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:30:51 +03:00
Nick Shirokov 4af69f1600 test(web-test): M4.A — validation messages + exception modal + error stack
10-validation.test.mjs (3 шага): Сообщить() → state.errors.messages,
ВызватьИсключение → onecError.errors.modal с автоматическим закрытием
fetchErrorStack.

14-errors-stack.test.mjs (3 шага): Path 1 OpenReport автоматически фетчит
стек для серверных исключений (entries[] содержит кадр ОбщиеФункции);
оставленная error modal через raw page.click закрывается closeForm;
платформенный диалог «О программе» виден в state.platformDialogs и
закрывается closeForm.

Покрыто 4 P2-кейса coverage matrix: 10-validation/messages,
10-validation/exception-modal, 14-errors/path1, 14-errors/dismiss-platform
+ бонус dismiss-modal. Открытие обработки ТестовыеОшибки через
navigateLink('Обработка.ТестовыеОшибки') — стандартные команды у
DataProcessor отключены.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 15:21:11 +03:00
Nick Shirokov 8c7c442705 feat(meta-compile): DSL choiceHistoryOnInput для аттрибутов
meta-compile v1.12 (ps1 + py): Parse-AttributeShorthand принимает поле
choiceHistoryOnInput в object-форме аттрибута, Emit-Attribute эмитит его
вместо хардкода Auto. Покрывает атрибуты Catalog/Document/TabularSection
(Emit-Attribute, единственная точка эмиссии в работе). Другие контексты
(register dimensions, resources, etc.) пока эмитят Auto — расширим
при необходимости.

build-webtest-config: реквизит Документ.ПриходнаяНакладная.Контрагент
получил choiceHistoryOnInput='DontUse'. Это убирает 1С-историю выбора
для поля и фиксит pre-existing flake 04-selectvalue/direct-form:
после 03 значение «ООО Север» оставалось в истории и selectValue
выбирал его через dropdown вместо ожидаемой формы выбора.

Live: полный регресс 12/12 впервые зелёный (5m 28s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 13:47:50 +03:00
Nick Shirokov c541d51f33 fix(web-test): resetState не закрывал form 0 + error screenshot снимался после reset
run.mjs:

1. resetState проверял `if (!state.form) break`. form === 0 (фоновая
   форма 1С, которую detectForm может вернуть) рассматривался как
   "форм нет" → cleanup прерывался, форма оставалась → следующий тест
   получал грязное состояние. Замена на `state.form == null` корректно
   различает null (desktop) и 0 (реальная фоновая форма).

2. Error screenshot в catch-блоке cmdTest снимался ПОСЛЕ resetState,
   который уже закрывал все формы → скрин показывал пустой рабочий
   стол вместо места падения. Перенёс снимок в начало catch (до
   teardown/afterEach/resetState).

Эффекты:
- 15-multi-context-handover теперь стабильно проходит в полном прогоне
  (раньше падал когда предыдущий тест оставлял form=0).
- 04-selectvalue/direct-form остался pre-existing flake (история
  выбора 1С после 03 — отдельная задача в синтетике).
- Скриншоты падения теперь показывают реальный UI на момент исключения.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:52:22 +03:00
Nick Shirokov a650325baf fix(web-test): убрать стуб showCaption/hideCaption в cmdTest
run.mjs v1.10: cmdTest больше не передаёт noRecord:true в buildContext.
Тестам доступен полный API browser.mjs (showCaption, hideCaption,
startRecording, stopRecording, addNarration).

Изначальный стуб с noRecord:true прятал showCaption/hideCaption тестов
вместе с recording-функциями. Это блокировало визуальные оверлеи в
мульти-контекстных тестах: a.showCaption() тихо превращался в no-op,
баннер никогда не отображался даже под --record.

Smart wait внутри showCaption и так гейтится на наличие recorder
(`if (recorder && ...)`), поэтому без --record тесты остаются быстрыми
(никаких 2-секундных пауз на каждый вызов).

startRecording/stopRecording/addNarration теперь тоже доступны тестам.
При попытке вызвать startRecording в момент активной runner-записи
browser.startRecording бросает "Already recording" — loud failure
лучше silent no-op.

Регресс: 15-multi-context-handover один проходит за 19.9s. Полный
прогон 10/12 (04 и 15 флапают независимо в последовательности).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:37:42 +03:00
Nick Shirokov 6c19846051 feat(web-test): T4.6 — гибридные режимы изоляции контекстов (tab default, window opt-in)
browser.mjs v1.12 + run.mjs v1.9: createContext принимает isolation параметр.
По умолчанию 'tab' — все контексты живут в одном launchPersistentContext, каждый
слот получает свою Page (вкладку). Преимущества: 1С extension грузится
надёжно (через --load-extension в persistent profile), один процесс Chromium,
дешёвая память. Cookies делятся между вкладками, но скоупятся по URL-path —
для модели «разные пользователи через разные vrd-публикации» это естественно
и достаточно.

isolation: 'window' (opt-in) — старый путь chromium.launch() + newContext():
полная изоляция cookies, отдельный BrowserContext (и окно) на каждый слот,
но extension может не подняться. Использовать когда нужна изоляция auth
внутри одного URL.

Смешивать режимы в одном прогоне нельзя — createContext бросает явную
ошибку (первый createContext устанавливает activeMode, остальные обязаны
совпадать).

Конфиг tests/web-test/webtest.config.mjs: добавлен комментарий с описанием
обоих режимов. По умолчанию tab — синтетика и наши smoke-тесты идут им.

Live: 11/12 в полном прогоне (default tab) + 3/3 sanity-check в window mode
(01-navigation + 14 + 15). Видеозапись из T4.5 работает в обоих режимах.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:34:44 +03:00
Nick Shirokov eef4f4bcea feat(web-test): T4.5 — мульти-контекстная запись видео
browser.mjs v1.11: recorder стал глобальным (не per-slot) — один ffmpeg,
один mp4 на тест с любым числом переключений контекста.

Frame state (lastFrameBuf/lastFrameTime/handler) переехал в поля recorder.
Добавлен recorder._attachPage(targetPage) — стопает старый CDP screencast,
заводит новый на нужной странице, route'ит фреймы в тот же ffmpeg pipe.

setActiveContext: при активной записи делает _flushFrames (замораживает
хвост уходящего окна), затем _attachPage(page) после _activateSlot. Видео
получается непрерывным с плавным сюжетом — пока активен a, видно a; пока
активен b, видно b.

_saveActiveSlot/_activateSlot больше не трогают recorder/lastCaptions/
lastRecordingDuration — recorder следует за активной страницей через
_attachPage, не через slot mirror.

disconnect: убрал leftover из T4.1, который пытался итерировать slot.recorder.

Live: 15-multi-context-handover с --record → 17.84s mp4, 446 кадров @ 25fps,
извлечённые кадры показывают переключение между окнами a (1920x1042) и
b (982x546). Полный регресс 11/12 (04-selectvalue — pre-existing flake).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:58:31 +03:00
Nick Shirokov 2c553fee98 feat(web-test): T4 — мульти-контекст BrowserContext
browser.mjs v1.10: createContext/setActiveContext/listContexts/getActiveContext/
hasContext. Несколько изолированных BrowserContext в одном Chromium-процессе через
chromium.launch() + newContext(). Module-level page/sessionPrefix/seanceId/recorder
зеркалят активный слот (атомарный своп через _saveActiveSlot/_activateSlot).
connect() оставлен для exec/run/start без изменений (launchPersistentContext).

run.mjs v1.8: ensureContext(name) + ленивое создание. Single-routing через
export const context = 'name'. Multi через export const contexts = ['a','b'] +
buildScopedContext(name) строит ctx.a/ctx.b — каждое действие префиксится
setActiveContext. Reset state после теста по всем активным контекстам.

Конфиг tests/web-test/webtest.config.mjs: два контекста a/b на одну webtest
публикацию (изолированные cookies через newContext).

Smoke-тесты:
- 14-multi-context-routing.test.mjs — single routing в b (2.6s)
- 15-multi-context-handover.test.mjs — ctx.a создаёт Контрагента, ctx.b в
  независимой сессии видит запись через filterList, ctx.a cleanup (14.5s, 4/4)

Live: 11/12 в полном прогоне. 04-selectvalue/direct-form флапает —
pre-existing, воспроизводится на baseline 95e4674 (03→04 sequence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:24:24 +03:00
Nick Shirokov 95e4674825 test(01-navigation): M3 P1 — section/command/switchTab errors + navigateLink
section-error / command-error / switchTab error: проверка throw для
несуществующих имён.

navigateLink: link-type (Catalog.Контрагенты) + e1cib URL (с soft-skip
для платформ без поддержки e1cib через Shift+F11).

Live на webtest: 10/10 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:29:54 +03:00
Nick Shirokov 9751840cc8 test(09-filter): M3 P1 — exact, hidden-field, date, reference, unfilter-all
exact: filterList exact:true строго 1 совпадение.
hidden-field: filterList по неотображённому реквизиту через FieldSelector
DLB (КодКПП в синтетике нет — soft-skip).
date: filterList по колонке Дата поступления (синтетика выводит её в форму
списка Номенклатуры).
reference: filterList по ссылочной колонке Контрагент (форма списка ПН).
unfilter-all: unfilterList() полностью восстанавливает список.

unfilter-specific отложен — требует списка с видимой filter-панелью,
synthetic списки фильтруют без создания badge.
cancel-search/clear-input семантически дубликаты unfilter-all через
публичный API.
show-all-form требует quickChoice=true каталога с количеством > порога
(в синтетике нет).

Live на webtest: все 7 шагов passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:28:09 +03:00
Nick Shirokov f257bb428c test(12-formstate): M3 P1 — modal + tabs
modal: F4 на ref-поле открывает модальную форму выбора Контрагентов,
state.modal=true, formCount=2.

tabs: форма элемента Номенклатуры с двумя табами (Основное/Дополнительно)
возвращает state.tabs[].

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:43:53 +03:00
Nick Shirokov 71e3691cf1 test(web-test): M3 P1 batch 1 — confirm-save-no/pending, more-menu, clear/ref-form, table checkbox/clear
02-crud: confirm-save-no (rollback при save:false), confirm-pending
(closeForm() без решения возвращает confirmation), more-menu (clickElement
'Ещё' возвращает submenu).

03-fillfields: clear (Shift+F4 через пустое значение), reference-non-quickchoice
(fillFields на quickChoice=false поле — method=dropdown через DLB; чистый
form-path требует hasPick && !hasSelect, такого поля в синтетике нет).

04-selectvalue: clear (selectValue '' → Shift+F4). show-all-form отложен —
требует quickChoice=true каталога с количеством > порога dropdown
(в синтетике нет).

05-table: checkbox (fillTableRow с Boolean), clear (Shift+F4 на ref-ячейке +
восстановление для последующего delete).

Live на webtest: все шаги проходят.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:40:27 +03:00
Nick Shirokov 1af318325d test(05-table): добавить явный tab-loop step с двумя числовыми полями
fillTableRow({Количество, Цена}, {row:1}) — purpose-built проверка inEdit
multi-cell tab-loop. method='direct' для обоих полей, значения
подставляются корректно (live на webtest).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:23:03 +03:00
Nick Shirokov 986480748e Merge branch 'dev' into feature/web-test-runner 2026-05-10 15:10:38 +03:00
Nick Shirokov 7561faf736 test(web-test): покрыть Tumbler через clickElement в radio-шаге
Tumbler-представление RadioButtonField не парсится fillFields, но варианты
видны в state.buttons[] и кликаются через clickElement. Уточнили шаг radio:
- RadioButtons (КатегорияЦены) → fillFields с method=radio
- Tumbler (СпособУчёта) → проверка наличия в buttons[] + clickElement('ФИФО')

Семантика Tumbler через fillFields остаётся как баг web-test/browser.mjs
(см. upload/web-test-bugs.md пункт 5), но рабочий путь интеракции есть.

10/10 smoke зелёные после рестарта Apache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:19:42 +03:00
Nick Shirokov 2849087fd9 test(web-test): покрытие quickChoice + radio (RadioButtons)
03-fillfields:
- reference-dropdown: переведён с Контрагент на Организация
  (после смены quickChoice Контрагенты идут через форму выбора)
- новый шаг radio: КатегорияЦены через method=radio (RadioButtons)

04-selectvalue:
- dropdown: переведён на Организация (quickChoice=true)
- новый шаг direct-form: Контрагент (quickChoice=false), method=form

Закрывает selectValue#3 dropdown (P0), selectValue#6 direct-form (P1),
fillFields#3 radio (P1) из coverage matrix.

Tumbler-представление радио (СпособУчёта) пока не покрыто — getFormState
не возвращает Tumbler в fields[]. Зафиксировано в upload/web-test-bugs.md
пункт 5.

10/10 smoke зелёные на webtest базе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 16:17:19 +03:00
Nick Shirokov 105171cdc2 test(webtest-config): Организации/quickChoice + radio (RadioButtons+Tumbler)
Расширение синтетики под новые возможности meta-compile/form-compile,
закрывает три ветки coverage matrix:
- Catalog.Организации (quickChoice: true) → selectValue#3 dropdown (P0)
- Catalog.Контрагенты (дефолт quickChoice: false) → selectValue#6 direct-form (P1)
- form-compile radio с видами RadioButtons (КатегорияЦены) и Tumbler
  (СпособУчёта) → fillFields#3 radio (P1)

В шапку ПриходнаяНакладная добавлен реквизит Организация (dropdown ветка),
Контрагент остаётся на форме выбора. Фикстура ЗаполнитьОрганизации создаёт
2 организации (Альфа, Бета); первая подставляется в документы.

Платформенная верификация: build-webtest-db (45 шагов, 30.3s) зелёная,
db-create + db-load-xml + db-update проходят. Функциональный прогон
runner.mjs integration/build-webtest — 42 шага зелёные.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:58:27 +03:00
Nick Shirokov c9cd0d62ab Merge branch 'dev' into feature/web-test-runner 2026-05-04 13:15:49 +03:00
Nick Shirokov c1a0a54971 feat(web-test): --record и export const params
Раннер v1.7.

T5 --record: startRecording перед каждым тестом, stopRecording
после (и в passed, и в failed ветке). Файл
{reportDir}/{testIdx}-{slug}.mp4. testResult.video содержит путь.
В Allure — attachment типа video/mp4. config.record читается
тоже. Использует существующую инфраструктуру browser.mjs.

T6 export const params: материализация в N тестов на этапе
discovery. Имя через {key}-шаблон в mod.name (например
'demo {type}'); если шаблона нет — суффикс [index]. Тест-функция
получает param как второй аргумент: default(ctx, param).
В отчёте каждый набор — отдельная test entry с собственным uuid
в Allure / testcase в JUnit.

Live-проверка:
- params: 2 теста с именами demo A / demo B из шаблона.
- record: mp4 91KB на 6-секундном тесте, путь в JSON и
  Allure attachment video/mp4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:19:52 +03:00
Nick Shirokov 927c0827f3 feat(web-test): --format=allure и --format=junit
Раннер v1.6. Реализованы оба формата отчётов из spec §9.

allure: {reportDir}/{uuid}-result.json на каждый тест. uuid через
randomUUID, labels из tags, steps рекурсивно с attachments из
step.screenshot, statusDetails для упавших шагов и тестов.
Пропускает skipped (нет start/stop).

junit: один XML в --report=path.xml. Валидация: --format=junit
требует --report=. xmlEscape для name/message/trace. <failure>
для упавших, <skipped/> для пропущенных, <system-out> со ссылкой
на screenshot.

Валидация формата (json|allure|junit) на старте cmdTest.
testResult теперь хранит start/stop в мс — нужно для Allure
и полезно в JSON-отчёте.

Live-проверка: 01-navigation в Allure (5 шагов с attachments,
все ссылки на существующие PNG); JUnit с passed и forced-fail
(спецсимволы корректно экранированы).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 16:03:31 +03:00
Nick Shirokov 56cd18a6b4 feat(web-test): --screenshot=on-failure|every-step|off + --report-dir
Раннер v1.5. Парсит --screenshot и --report-dir, мерж с config.screenshot.
- every-step: после успешного step() пишет {reportDir}/{testIdx}-{stepIdx}-{slug}.png,
  путь в step.screenshot.
- off: ни пошаговых, ни error-shot.
- on-failure (default): error-shot уехал из .claude/skills/web-test/
  в {reportDir}/error-{testIdx}-{slug}.png.

reportDir фоллбэчит: --report-dir → dirname(--report) → testDir.

Известная нестыковка: error-shot из buildContext/executeScript остаётся в
.claude/skills/web-test/error-shot.png — затронем при T2 (Allure).

Live-проверка: 01-navigation с every-step (5 PNG), off (пусто),
default on-failure на стуб-failing тесте (error-shot в reportDir).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:54:38 +03:00
Nick Shirokov 3ac1d425cd test(11-report): DCS-отчёт ОстаткиТоваров + smoke с быстрым фильтром
Синтетика: добавлен template-add ОсновнаяСхемаКомпоновкиДанных к отчёту
(без него skd-compile писал Template.xml в незарегистрированный путь),
переписан DSL skd-compile — fields внутри dataSets, типы полей, totalFields,
явный settingsVariants со structure и быстрым отбором по Номенклатуре
(@off @user @quickAccess).

Тест 11-report покрывает: регистрацию команды в подсистеме, открытие формы
отчёта с дефолтной кнопкой Сформировать, видимость и структуру быстрого
DCS-фильтра, формирование отчёта, применение фильтра через selectValue
(auto-enable чекбокса + значение), пересчёт с фильтром, снятие фильтра
через fillFields toggle off с восстановлением исходных данных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 15:22:22 +03:00
Nick Shirokov 3c596f4550 test(12-formstate): smoke базовых полей getFormState
Покрывает форму списка (form, formCount, openForms, tables, buttons)
и форму элемента (fields с label и value, проверка по конкретному
полю Наименование).
2026-05-02 20:15:20 +03:00
Nick Shirokov 36d29a51a9 test(09-filter): smoke filterList simple-search и advanced-column
Покрывает:
- filterList('Север') — поиск по всем колонкам списка Контрагенты
- filterList('Север', { field: 'Наименование' }) — фильтр по
  конкретной колонке через расширенный поиск
- unfilterList — восстановление исходного набора

Третий запланированный кейс (text-field filter) семантически совпадает
с advanced-column когда колонка строкового типа — оставлен на регресс P1.
2026-05-02 20:11:58 +03:00
Nick Shirokov 11e961c816 test(07-tabs): smoke переключение страниц формы Основное/Дополнительно
Покрывает clickElement по имени страницы как механизм переключения
вкладок формы. Используем форму элемента Номенклатура: page1
показывает шапку (Артикул, ВидНоменклатуры, ...), page2 — Дополнительно
(ЕдиницаИзмерения, Комментарий). Verify: набор state.fields различен
после переключения и совпадает после возврата.
2026-05-02 20:07:46 +03:00
Nick Shirokov 05ca810461 test(06-document): сверка с Комментарий=docId, защита от грязной базы
Раньше verify-list брал первый попавшийся проведённый документ Север —
если в базе уже лежал проведённый Север из прошлого прогона, тест
проходил даже если текущий не сохранился. Теперь среди кандидатов
открываем каждый и сверяем Комментарий с уникальным docId текущего
прогона; ассерт срабатывает только при совпадении.
2026-05-02 20:04:38 +03:00
Nick Shirokov a0407b74dc test(06-document): проверка закрытия по смене номера формы вместо костыля
Раньше использовалось отсутствие поля Контрагент после Провести и закрыть
как косвенный признак закрытия — это работало, но было привязано к
конкретному реквизиту накладной. Заменил на сравнение state.form до и
после: номер активной формы меняется (11 → 5), это прямой и общий
признак, что мы переключились с формы документа на другую.
2026-05-02 19:58:56 +03:00
Nick Shirokov 3aad254399 test(06-document): smoke workflow проведения накладной
Создание, заполнение шапки и табличной части, Провести и закрыть,
проверка появления документа в списке с Проведён=Да.

Проверка закрытия формы документа: в синтетике web-test форма списка и
форма документа делят один слот (formCount=1 в обоих состояниях),
поэтому используем признак отсутствия поля Контрагент в текущем
state.fields после Провести и закрыть — если поле есть, мы остались
на форме документа.
2026-05-02 19:54:34 +03:00
Nick Shirokov 07753921be test(05-table): smoke add/edit/delete для табличной части накладной
Покрывает работу с табличной частью Товары документа Приходная накладная:
- fillTableRow с add:true добавляет строки последовательно
- fillTableRow с row:N редактирует существующую строку (Tab-навигация)
- deleteTableRow удаляет строку по индексу

Закрытие формы без сохранения (save:false) — соответствует новой
семантике после фикса form-compile (SavedData).
2026-05-02 19:49:19 +03:00
Nick Shirokov ba0c71fa45 test(smoke): починить 01-navigation и 04-selectvalue после фикса form-compile
01-navigation: первое открытое окно 1С имеет form=0 (number), и
assert.ok(state.form, ...) валился на falsy при первом запуске сессии.
Сменил на state.form != null.

04-selectvalue: явный save:false при закрытии модифицированной формы
накладной — после фикса SavedData=true главного реквизита платформа
требует решения по confirmation dialog.
2026-05-02 19:45:15 +03:00
Nick Shirokov 33c9fdade0 test(03-fillfields): boolean → CheckBoxField, явный save:false при закрытии
После фикса form-compile (kind=check для Boolean + SavedData=true для
главного реквизита) Активен передаётся как настоящий boolean (toggle),
getFormState возвращает value:true/false. Закрытие модифицированных форм
теперь требует явного save:false — иначе платформа показывает
confirmation dialog «Записать?».
2026-05-02 19:40:26 +03:00
Nick Shirokov 1c1fe7b2d9 test(02-crud): убрать устаревший комментарий про T11/SavedData
После фикса form-compile (a59be4b SavedData=true для главного реквизита)
canonical confirm-save-yes flow работает без ручного патча Form.xml —
предупреждение в шаге неактуально.
2026-05-02 19:26:56 +03:00
Nick Shirokov 0bd2587e74 test(build-webtest-config): Активен как check вместо input для Boolean
После фикса form-compile (Дефект 2: kind=check → CheckBoxField) булевый
реквизит Активен в форме элемента и форме списка Номенклатуры теперь
описывается как check — рендерится настоящим чекбоксом.
2026-05-02 19:24:50 +03:00
Nick Shirokov 6f17b1c2f6 Merge branch 'dev' into feature/web-test-runner 2026-05-02 19:08:28 +03:00
Nick Shirokov 36ad686316 feat(web-test): smoke-тест 04-selectvalue (dropdown быстрый выбор)
Один P0 кейс из coverage matrix:
- dropdown: selectValue('Контрагент', 'ООО Север') → method='dropdown'
  на форме новой ПриходнойНакладной (CatalogRef + малый список)

API возвращает form state с .selected = {field, search, method}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:57:37 +03:00
Nick Shirokov 66e37fb8cc feat(web-test): smoke-тест 03-fillfields (text, dropdown, date, reference)
2 шага, 5 типов полей зелёные на синтетике webtest:
- text (paste): Артикул на форме Номенклатура
- dropdown (Да/Нет): Активен — Boolean рендерится как Да/Нет селектор
- dropdown (EnumRef): ВидНоменклатуры
- date (paste): ДатаПоступления
- reference (dropdown CatalogRef): Контрагент в новой ПриходнаяНакладная

NB: 1C рендерит Boolean-атрибут не как чекбокс, а как dropdown «Да/Нет»
(actions: ["select"]) — fillFields правильно определяет это.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:55:14 +03:00
Nick Shirokov 99c77e1dde fix(web-test): 02-crud использует canonical closeForm({save:true})
Гипотеза о баге fillField paste была ошибочной — реальная причина в form-compile
который не эмитит <SavedData>true</SavedData> для MainAttribute главной формы.
Платформа без SavedData не трекает modified-state, confirmation dialog не
появляется.

Платформенная верификация на патченной Form.xml: closeForm({save:true})
после fillField корректно ловит confirmation, жмёт «Да», изменения
сохраняются. См. T11 в upload/web-test-runner-tasks.md.

ВНИМАНИЕ: тест зависит от ручного патча Form.xml. После прогона
build-webtest-db.mjs тест упадёт до фикса form-compile (T11).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:44:39 +03:00
Nick Shirokov 8d6612027f feat(web-test): smoke-тест 02-crud (open-item, close-clean, read, save)
4 шага зелёные на синтетике webtest:
- read: список Контрагентов отдаёт колонки/строки/total
- open-item: dblclick открывает форму элемента
- close-clean: Escape без изменений закрывает форму без диалога
- save-via-button: fillField + «Записать и закрыть» → значение сохраняется

confirm-save-yes (P0 из coverage matrix) отложен — fillField через paste не
выставляет 1C "modified" флаг, confirmation dialog не появляется. Зафиксировано
в upload/web-test-runner-tasks.md как T11.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:34:37 +03:00
Nick Shirokov c3b67a18cb feat(tests): build-webtest-db скрипт для постоянной webtest базы
Заменяет одноразовый platform-webtest-config.test.mjs на скрипт сборки в
постоянные пути из .v8-project.json (tests/skills/.cache/webtest-config
+ C:\edt\IB\webtest). Переиспользует steps из build-webtest-config.test.mjs.

Generic platform-config.test.mjs уже покрывает regression «платформа принимает
сборку» — отдельный синтетический тест дублировал.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 15:23:42 +03:00
Nick Shirokov 4d1b66638c Merge branch 'dev' into feature/web-test-runner 2026-05-02 14:54:50 +03:00
Nick Shirokov 363a9f34f2 Merge dev: cf-info раскладка, cf-edit set-panels с русскими синонимами 2026-05-01 17:06:51 +03:00
Nick Shirokov 4f8ce7b747 chore(web-test): убрать инлайн ClientApplicationInterface.xml
Файл теперь генерируется самим cf-init с ERP-дефолтом (см. предыдущий
коммит на dev), отдельный writeFile в build-webtest-config больше не нужен.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 16:48:13 +03:00
Nick Shirokov fc48d68ed1 Merge dev: cf-init создаёт Ext/ClientApplicationInterface.xml 2026-05-01 16:46:30 +03:00
Nick Shirokov 3e34ec0bdd fix(web-test): заголовки и стиль вкладок на форме Номенклатуры
Page элементы в DSL получали name (через ключ 'page'), но не получали
title, поэтому вкладки рендерились пустыми квадратиками. Также Pages
без явного pagesRepresentation отображались в режиме None (без табов).

- Добавил title к каждой Page (Основное, Дополнительно)
- pagesRepresentation: 'TabsOnTop' на Pages

После: getFormState().tabs возвращает [{name:'Основное'},{name:'Дополнительно'}]
вместо пустого массива.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:33:19 +03:00
Nick Shirokov fff2e83960 fix(web-test): починить runtime синтетики для тонкого клиента
Два бага, найденные при попытке запустить синтетическую ИБ через
web-publish + web-test:

1. ОбщиеФункции без ServerCall=true — ManagedApplicationModule (клиент)
   не мог звать процедуры серверного модуля напрямую. ПриНачалеРаботыСистемы
   падал с ошибкой компиляции в runtime, страница не догружалась. Добавил
   serverCall: true в DSL meta-compile.

2. Без Ext/ClientApplicationInterface.xml панель разделов рендерилась
   icon-only (без подписей), web-test navigateSection не находил секции.
   Добавил writeFile-шаг с раскладкой панелей как в acc/erp:
   - top: панель разделов (8e10648b...) + панель информации (cbab57f2...)
   - left: панель функций текущего раздела (b553047f...)

Проверено end-to-end: после пересборки runner-ом + web-publish + start
работают navigateSection, openCommand, readTable. Фикстуры (4 контрагента,
25 номенклатуры в группах, 3 документа) автоматически заполняются при
первом старте через ManagedApplicationModule → ОбщиеФункции.ЗаполнитьФикстурыЕслиНужно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:12:43 +03:00
Nick Shirokov 1ff209849f feat(web-test): первоначальное заполнение фикстур (M1 Step 5)
Покрытие matrix #9 — данные для smoke-тестов:
- Константа ДанныеЗаполнены (Boolean) — флаг идемпотентности
- ОбщиеФункции.ЗаполнитьФикстурыЕслиНужно() — транзакционно создаёт:
  * 4 контрагента (ООО Север/Юг/Восток, АО Запад)
  * 25 номенклатуры в группах Товары (15) и Услуги (10)
  * 3 приходных накладных по 3 строки
- Ext/ManagedApplicationModule.bsl с ПриНачалеРаботыСистемы — вызывает
  заполнение при первом старте тонкого клиента

Платформенная верификация компилирует BSL (43 шага, 23.7s). Реальное
выполнение заполнения произойдёт при первом подключении web-test
runner-а к синтетической базе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:36:13 +03:00
Nick Shirokov 1a8415283e chore(web-test): убрать избыточный cf-edit (объекты регистрируются автоматически)
meta-compile/subsystem-compile/role-compile сами добавляют записи в
Configuration.xml. cf-edit в каждом прогоне рапортовал Added: 0 — был
no-op + дублировал список объектов, который надо было синхронизировать
руками при каждом изменении.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:32:48 +03:00
Nick Shirokov db1e78a534 feat(web-test): подчинённый каталог КонтактныеЛица (M1 Step 4)
Покрытие matrix #8 — getFormState.navigation (12-formstate/subordinate-nav):
- Catalog.КонтактныеЛица с Owner=Catalog.Контрагенты
- Реквизиты: Должность, Телефон
- ФормаЭлемента (с владельцем) + ФормаСписка
- Регистрация в Configuration + Subsystem.Склад + Role

Платформенная верификация: 41 шаг, 23.8s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:30:15 +03:00
Nick Shirokov a828f1847f feat(web-test): обработка ТестовыеОшибки + bsl-модули (M1 Step 3)
Покрытие matrix #6 — errors balloon/messages/modal (10-validation,
fetchErrorStack Path 2):
- ОбщиеФункции.ПоказатьСообщение() → Сообщить("Тестовое сообщение")
- ОбщиеФункции.ВызватьТестовоеИсключение() → ВызватьИсключение
- DataProcessor.ТестовыеОшибки + ФормаОбработки с двумя кнопками,
  каждая делает клиент→сервер вызов соответствующей процедуры
- Регистрация в Configuration + Subsystem.Администрирование

Runner расширен step-типом writeFile — записывает произвольный текст
(обычно Module.bsl) в workDir. Нет навыка для модификации bsl-кода
модулей, и плодить отдельный навык под одну задачу избыточно.

Платформенная верификация: 36 шагов, 21.2s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:27:38 +03:00
Nick Shirokov 3e8159b591 feat(web-test): формы списков с правильными колонками (M1 Step 2)
Расширение синтетики (пункты 3, 4, 5 из M1):
- Контрагенты.КодКПП: новый String реквизит, НЕ выводимый в форму списка
  (для теста filterList #5 — FieldSelector DLB по скрытой колонке)
- Catalog.Контрагенты.ФормаСписка: Code, Description, ИНН, Телефон, Адрес
- Catalog.Номенклатура.ФормаСписка: Code, Description, Артикул,
  ВидНоменклатуры, ДатаПоступления, Цена, Активен (date-колонка для
  filterList #6)
- Document.ПриходнаяНакладная.ФормаСписка: Date, Number, Контрагент, Posted
  (reference-колонка для filterList #7)

Платформенная верификация: 31 шаг, 21.4s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:23:38 +03:00
Nick Shirokov 57bb964c1e feat(web-test): ссылочные типы и Boolean в синтетике (M1 Step 1)
Расширение build-webtest-config под coverage matrix (пункты 1, 2, 7
из upload/web-test-runner-tasks.md M1):
- Перечисление КатегорииЦен (для будущего radio-button теста)
- Номенклатура.ВидНоменклатуры → EnumRef.ВидыНоменклатуры
- Номенклатура.КатегорияЦены → EnumRef.КатегорииЦен
- ПриходнаяНакладная.Контрагент: String → CatalogRef.Контрагенты
- ПриходнаяНакладная.Товары.Номенклатура: String → CatalogRef.Номенклатура
- ПриходнаяНакладная.Товары.Согласовано: новый Boolean (для checkbox
  в grid, fillTableRow ветка #6)
- Формы Номенклатура и Документ обновлены под новые поля
- Subsystem.Склад: добавлены Enum.* в content
- Configuration.xml регистрирует Enum.КатегорииЦен

Платформенная верификация (platform-webtest-config.test.mjs) зелёная,
25 шагов 16.7s.

Гэп: form-compile не умеет рендерить RadioButtonField — представление
КатегорияЦены остаётся обычным input. Будет отдельной задачей перед
тестами P1 fillFields/radio.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:21:22 +03:00
Nick Shirokov 41c4b6b1f7 fix(skills/tests): cleanupWorkspace терпимо переживает EBUSY от 1cv8
После платформенных тестов (db-create/db-load-xml/db-update) Windows
держит файловые хэндлы 1cv8 ещё несколько сотен миллисекунд. rmSync без
ретраев падал EBUSY на Roles/.../Rights.xml, и uncaught-ошибка в finally
рушила весь node-процесс — теряли результат теста.

Теперь rmSync с maxRetries: 10, retryDelay: 200 (≈2с буфер) и try/catch
вокруг — в худшем случае warning + лишняя tmp-папка вместо краша.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 14:21:08 +03:00
Nick Shirokov ffb0ee740d fix(web-test): восстановить синтетическую конфу + платформенная верификация
build-webtest-config упал после ужесточения form-compile (запрет runtime-типа
FormDataStructure для главного реквизита). Перевёл типы на конкретные
CatalogObject.X / DocumentObject.X без cfg:-префикса. Добавил
platform-webtest-config.test.mjs — переиспользует шаги сборки и в хвосте
делает db-create + db-load-xml + db-update. Зелёный, 24 шага.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:12:45 +03:00
Nick Shirokov f5e487096f Merge branch 'dev' into feature/web-test-runner 2026-05-01 11:58:28 +03:00
Nick Shirokov 6d5c1a0b19 Merge branch 'dev' into feature/web-test-runner 2026-04-05 18:18:25 +03:00
Nick Shirokov b322c02fdb fix(web-test): discoverTests для одиночного файла + первый smoke-тест
- Fix: discoverTests падал с ENOTDIR при передаче .test.mjs файла
- Добавлен 01-navigation.test.mjs — навигация по разделам, открытие
  списков через navigateLink, переключение между подсистемами

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:22:40 +03:00
Nick Shirokov 61ef7ac891 fix(web-test): фикс синтетической конфигурации для загрузки в платформу
- Подсистемы: singular формы в Content (Catalog вместо Catalogs)
- КурсыВалют: Independent вместо RecorderSubordinate
- Убран AccumulationRegister (требует регистратор, не нужен для UI)
- Отчёт: запрос из ТЧ документа вместо регистра

Формы загружаются без Form.xml (автогенерация платформой) —
баг form-compile (XDTO exception) требует отдельного исследования.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:09:43 +03:00
Nick Shirokov ba19b4111d feat(web-test): синтетическая конфигурация для регресс-тестов
22 шага: cf-init → meta-compile (10 объектов) → form-compile (3 формы,
вкл. 2 вкладки для Номенклатуры) → skd-compile → subsystem-compile
(Склад + Администрирование) → role-compile (полные права) → cf-validate.

Расширения: иерархический справочник, разнотипные реквизиты (Number,
Boolean, Date, String unlimited), FillChecking, вторая подсистема.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:57:52 +03:00
Nick Shirokov ded11437c5 docs(web-test): обновить статус дорожной карты — #1-5 done
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:55:00 +03:00
Nick Shirokov 5eda7f8eb3 feat(web-test): test runner — buildContext, cmdTest, assertions, step
- Извлечён buildContext() из executeScript (переиспользуется)
- Новая команда `test [url] <dir> [--tags/--bail/--retry/--timeout/--report]`
- Обнаружение *.test.mjs, импорт ES-модулей, фильтрация по тегам/grep/only
- Хуки: prepare/cleanup (без браузера) + beforeAll/afterAll/beforeEach/afterEach
- Встроенный сброс состояния (dismissPendingErrors + closeForm) после каждого теста
- step(name, fn) обёртка с вложенностью и таймингами
- Assertions: ok/equal/deepEqual/includes/match/throws + 1C-специфичные
- Консольный вывод с деревом шагов, JSON-отчёт
- Поддержка webtest.config.mjs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:53:33 +03:00
Nick Shirokov f39a0d9c5e docs(web-test): BrowserContext вместо sequential reconnect для мульти-контекста
Один процесс браузера, несколько изолированных BrowserContext'ов.
Мгновенное переключение между пользователями, состояние каждой
сессии сохраняется. Не требует полного рефакторинга createContext().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:47:26 +03:00
Nick Shirokov 2347859bdd docs(web-test): спецификация test runner для регрессионного тестирования
Единый механизм для внутреннего регресса browser.mjs API и
пользовательского регресса 1С-приложений. Паттерны Playwright Test.

Содержание: CLI, формат тестов, контексты, хуки, assertions, step(),
отчёты (JSON/Allure/JUnit), синтетическая конфигурация, дорожная карта.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 14:37:47 +03:00
699 changed files with 78144 additions and 15194 deletions
+121 -1
View File
@@ -1,4 +1,4 @@
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -29,6 +29,126 @@ if (-not (Test-Path $ConfigPath)) { Write-Error "File not found: $ConfigPath"; e
$resolvedPath = (Resolve-Path $ConfigPath).Path
$script:configDir = [System.IO.Path]::GetDirectoryName($resolvedPath)
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
Assert-EditAllowed $resolvedPath 'editable'
# --- Load XML with PreserveWhitespace ---
$script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true
+165 -2
View File
@@ -1,16 +1,177 @@
#!/usr/bin/env python3
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import subprocess
import sys
import uuid as _uuid
from html import escape as html_escape
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
@@ -190,6 +351,8 @@ def main():
resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path)
assert_edit_allowed(resolved_path, "editable")
xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
@@ -806,7 +969,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path])
subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
# --- Summary ---
print()
+76 -1
View File
@@ -1,4 +1,4 @@
# cf-info v1.2 — Compact summary of 1C configuration root
# cf-info v1.3 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
@@ -218,6 +218,78 @@ function Get-HomePageLayout {
$script:homePage = Get-HomePageLayout
# --- Support state (Ext/ParentConfigurations.bin) ---
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
# Returns $null on absent/error; else hashtable: State='absent'|'removed'|'parsed',
# G (0=editing on, 1=off), K (vendor configs), Vendors @(@{Vendor;Name;Version}),
# Counts @(locked, editable, removed) by f1 — record tally (K>1 counts each
# vendor block separately); only computed when G=0.
function Read-SupportState([string]$binPath) {
try {
if (-not (Test-Path $binPath)) { return @{ State = 'absent' } }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return @{ State = 'removed' } }
$startIdx = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $startIdx = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $startIdx, $bytes.Length - $startIdx)
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $h.Success) { return $null }
$G = [int]$h.Groups[1].Value
$K = [int]$h.Groups[2].Value
if ($K -eq 0) { return @{ State = 'removed' } }
# Vendor descriptors: ...,"ver","vendor","name",count,
$vendors = @()
$vRe = [regex]'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,'
foreach ($m in $vRe.Matches($text)) {
$vendors += @{
Version = ($m.Groups[1].Value -replace '""','"')
Vendor = ($m.Groups[2].Value -replace '""','"')
Name = ($m.Groups[3].Value -replace '""','"')
}
}
# Per-object counts only matter when editing is enabled (G=0); when G=1 the
# whole config is read-only and stored f1 values are the inactive default.
$counts = $null
if ($G -eq 0) {
$counts = @(0, 0, 0)
# Object records: f1,0,uuidLocal[,uuidVendor] — flags precede the uuid.
$rRe = [regex]'([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
foreach ($m in $rRe.Matches($text)) {
$counts[[int]$m.Groups[1].Value]++
}
}
return @{ State = 'parsed'; G = $G; K = $K; Vendors = $vendors; Counts = $counts }
} catch { return $null }
}
function Get-SupportLines {
$configDir = [System.IO.Path]::GetDirectoryName($ConfigPath)
$binPath = Join-Path (Join-Path $configDir "Ext") "ParentConfigurations.bin"
$st = Read-SupportState $binPath
$out = @()
if (-not $st -or $st.State -eq 'absent') {
if ($cfgExtPurpose) { $out += "Поддержка: расширение (CFE), правки свободны" }
else { $out += "Поддержка: не на поддержке (своя конфигурация)" }
return $out
}
if ($st.State -eq 'removed') {
$out += "Поддержка: снята с поддержки полностью"
return $out
}
$out += "Поддержка: на поддержке"
if ($st.G -eq 0) {
$out += " Возможность изменения: включена"
$out += " Объектов: на замке $($st.Counts[0]) / редактируется $($st.Counts[1]) / снято $($st.Counts[2])"
} else {
$out += " Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)"
}
$out += " Конфигураций поставщика: $($st.K)"
if ($st.K -gt 1) {
foreach ($v in $st.Vendors) { $out += " Поставщик: $($v.Vendor)$($v.Name) $($v.Version)" }
}
return $out
}
function Format-HomePageItem($it, [bool]$detailed) {
$badges = @()
$badges += "h=$($it.height)"
@@ -253,6 +325,7 @@ $cfgVersion = Get-PropText "Version"
$cfgVendor = Get-PropText "Vendor"
$cfgCompat = Get-PropText "CompatibilityMode"
$cfgExtCompat = Get-PropText "ConfigurationExtensionCompatibilityMode"
$cfgExtPurpose = Get-PropText "ConfigurationExtensionPurpose"
$cfgDefaultRun = Get-PropText "DefaultRunMode"
$cfgScript = Get-PropText "ScriptVariant"
$cfgDefaultLang = Get-PropText "DefaultLanguage"
@@ -284,6 +357,7 @@ if ($Mode -eq "overview" -and -not $Section) {
Out "Формат: $version"
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
if ($cfgVersion) { Out "Версия: $cfgVersion" }
foreach ($l in (Get-SupportLines)) { Out $l }
Out "Совместимость: $cfgCompat"
Out "Режим запуска: $cfgDefaultRun"
Out "Язык скриптов: $cfgScript"
@@ -386,6 +460,7 @@ if ($Mode -eq "full" -and -not $Section) {
if ($cfgPrefix) { Out "Префикс: $cfgPrefix" }
if ($cfgVendor) { Out "Поставщик: $cfgVendor" }
if ($cfgVersion) { Out "Версия: $cfgVersion" }
foreach ($l in (Get-SupportLines)) { Out $l }
$cfgUpdateAddr = Get-PropText "UpdateCatalogAddress"
if ($cfgUpdateAddr) { Out "Каталог обн.: $cfgUpdateAddr" }
Out ""
+72 -1
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# cf-info v1.2 — Compact summary of 1C configuration root
# cf-info v1.3 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from collections import OrderedDict
from lxml import etree
@@ -219,6 +220,71 @@ def get_home_page_layout():
home_page = get_home_page_layout()
# --- Support state (Ext/ParentConfigurations.bin) ---
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
# vendor block separately); only computed when g==0.
def read_support_state(bin_path):
try:
if not os.path.isfile(bin_path):
return {"state": "absent"}
data = open(bin_path, "rb").read()
if len(data) <= 32:
return {"state": "removed"}
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return None
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return {"state": "removed"}
vendors = []
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
vendors.append({
"version": m.group(1).replace('""', '"'),
"vendor": m.group(2).replace('""', '"'),
"name": m.group(3).replace('""', '"'),
})
counts = None
if g == 0:
counts = [0, 0, 0]
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
counts[int(m.group(1))] += 1
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
except Exception:
return None
def get_support_lines():
config_dir = os.path.dirname(config_path)
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
st = read_support_state(bin_path)
res = []
if not st or st["state"] == "absent":
if cfg_ext_purpose:
res.append("Поддержка: расширение (CFE), правки свободны")
else:
res.append("Поддержка: не на поддержке (своя конфигурация)")
return res
if st["state"] == "removed":
res.append("Поддержка: снята с поддержки полностью")
return res
res.append("Поддержка: на поддержке")
if st["g"] == 0:
res.append(" Возможность изменения: включена")
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
else:
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
res.append(f" Конфигураций поставщика: {st['k']}")
if st["k"] > 1:
for v in st["vendors"]:
res.append(f" Поставщик: {v['vendor']}{v['name']} {v['version']}")
return res
def format_home_page_item(it, detailed):
badges = [f"h={it['height']}"]
if not it["common"]:
@@ -249,6 +315,7 @@ cfg_version = get_prop_text("Version")
cfg_vendor = get_prop_text("Vendor")
cfg_compat = get_prop_text("CompatibilityMode")
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
cfg_default_run = get_prop_text("DefaultRunMode")
cfg_script = get_prop_text("ScriptVariant")
cfg_default_lang = get_prop_text("DefaultLanguage")
@@ -281,6 +348,8 @@ if args.Mode == "overview" and not args.Section:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
for ln in get_support_lines():
out(ln)
out(f"Совместимость: {cfg_compat}")
out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}")
@@ -369,6 +438,8 @@ if args.Mode == "full" and not args.Section:
out(f"Поставщик: {cfg_vendor}")
if cfg_version:
out(f"Версия: {cfg_version}")
for ln in get_support_lines():
out(ln)
cfg_update_addr = get_prop_text("UpdateCatalogAddress")
if cfg_update_addr:
out(f"Каталог обн.: {cfg_update_addr}")
+1 -9
View File
@@ -48,17 +48,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После создания
1. Прочитай лог-файл и покажи результат
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
## Примеры
+28 -3
View File
@@ -1,4 +1,4 @@
# db-create v1.0 — Create 1C information base
# db-create v1.1 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -67,15 +67,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
+41 -6
View File
@@ -1,29 +1,64 @@
#!/usr/bin/env python3
# db-create v1.0 — Create 1C information base
# db-create v1.1 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
-11
View File
@@ -54,17 +54,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После выполнения
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
## Примеры
```powershell
@@ -1,4 +1,4 @@
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# db-dump-cf v1.1 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -76,15 +76,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
@@ -1,29 +1,64 @@
#!/usr/bin/env python3
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# db-dump-cf v1.1 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
+72
View File
@@ -0,0 +1,72 @@
---
name: db-dump-dt
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
argument-hint: "[database] [output.dt]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-dt — Выгрузка информационной базы в DT-файл
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
## Usage
```
/db-dump-dt [database] [output.dt]
/db-dump-dt dev backup.dt
/db-dump-dt — база по умолчанию, имя файла по базе и дате
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Примеры
```powershell
# Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
```
## Связанные навыки
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,168 @@
# db-dump-dt v1.1 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Выгрузка информационной базы 1С в DT-файл
.DESCRIPTION
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному DT-файлу
.EXAMPLE
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$OutputFile
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpIB", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "dump_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -0,0 +1,155 @@
#!/usr/bin/env python3
# db-dump-dt v1.1 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Dump 1C information base to DT file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
else:
arguments.extend(["/F", args.InfoBasePath])
if args.UserName:
arguments.append(f"/N{args.UserName}")
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.extend(["/DumpIB", args.OutputFile])
# --- Output ---
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
-7
View File
@@ -68,13 +68,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
## Примеры
@@ -1,5 +1,5 @@
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# db-dump-xml v1.1 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы
@@ -99,15 +99,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
@@ -1,34 +1,67 @@
#!/usr/bin/env python3
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# db-dump-xml v1.1 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
candidates.sort()
return candidates[-1]
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
+1 -9
View File
@@ -55,17 +55,9 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После выполнения
1. Прочитай лог-файл и покажи результат
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
## Примеры
@@ -1,4 +1,4 @@
# db-load-cf v1.0 — Load 1C configuration from CF file
# db-load-cf v1.1 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -76,15 +76,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
@@ -1,29 +1,64 @@
#!/usr/bin/env python3
# db-load-cf v1.0 — Load 1C configuration from CF file
# db-load-cf v1.1 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
+92
View File
@@ -0,0 +1,92 @@
---
name: db-load-dt
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
disable-model-invocation: true
argument-hint: <input.dt> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-dt — Загрузка информационной базы из DT-файла
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
## Когда НЕ использовать
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
в существующую.
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
## Usage
```
/db-load-dt <input.dt> [database]
/db-load-dt backup.dt dev
```
## Порядок действий перед загрузкой
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
отката (восстановиться будет нечем, если не сохранить).
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
3. Только после подтверждения выполняй загрузку.
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к DT-файлу |
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
передать `-UnlockCode`; иначе освободи базу и повтори.
## Примеры
```powershell
# Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
```
## Связанные навыки
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,183 @@
# db-load-dt v1.1 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка информационной базы 1С из DT-файла
.DESCRIPTION
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
ВНИМАНИЕ: операция полностью перезаписывает базу.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к DT-файлу для загрузки
.PARAMETER JobsCount
Количество фоновых заданий для загрузки (0 = по числу процессоров)
.PARAMETER UnlockCode
Код разблокировки базы (/UC) — если заблокировано начало сеансов
.EXAMPLE
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$InputFile,
[Parameter(Mandatory=$false)]
[int]$JobsCount = 0,
[Parameter(Mandatory=$false)]
[string]$UnlockCode
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
$arguments += "/RestoreIB", "`"$InputFile`""
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
# --- Output ---
$outFile = Join-Path $tempDir "load_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
if ($exitCode -eq 0) {
Write-Host "Information base restored successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error restoring information base (code: $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
# db-load-dt v1.1 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Load 1C information base from DT file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-InputFile", required=True)
parser.add_argument("-JobsCount", type=int, default=0)
parser.add_argument("-UnlockCode", default="")
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
else:
arguments.extend(["/F", args.InfoBasePath])
if args.UserName:
arguments.append(f"/N{args.UserName}")
if args.Password:
arguments.append(f"/P{args.Password}")
if args.UnlockCode:
arguments.append(f"/UC{args.UnlockCode}")
arguments.extend(["/RestoreIB", args.InputFile])
if args.JobsCount > 0:
arguments.extend(["-JobsCount", str(args.JobsCount)])
# --- Output ---
out_file = os.path.join(temp_dir, "load_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.append("/DisableStartupDialogs")
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {exit_code})", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+1 -2
View File
@@ -64,8 +64,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
## После выполнения
1. Показать список загруженных файлов и результат из лога
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
@@ -1,4 +1,4 @@
# db-load-git v1.3 — Load Git changes into 1C database
# db-load-git v1.5 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -120,15 +120,40 @@ function Get-ObjectXmlFromSubFile {
# --- Resolve V8Path (skip if DryRun) ---
if (-not $DryRun) {
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
@@ -216,13 +241,16 @@ Write-Host "Git changes detected: $($changedFiles.Count) files"
# --- Filter and map to config files ---
$configFiles = @()
$supportSkipped = @()
foreach ($file in $changedFiles) {
$file = $file.Trim().Replace('\', '/')
if ([string]::IsNullOrWhiteSpace($file)) { continue }
# Skip service files
if ($file -eq "ConfigDumpInfo.xml") { continue }
# Skip service files (not partially loadable). Support-state files are tracked
# to warn the user: support changes apply only via a full load.
if ($file -match 'ParentConfigurations\.bin$') { $supportSkipped += $file; continue }
if ($file -eq "ConfigDumpInfo.xml" -or $file -match '(^|/)ConfigDumpInfo\.xml$') { continue }
$fullPath = Join-Path $ConfigDir $file
@@ -265,6 +293,12 @@ foreach ($file in $changedFiles) {
}
}
if ($supportSkipped.Count -gt 0) {
Write-Host "[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):" -ForegroundColor Yellow
foreach ($sf in $supportSkipped) { Write-Host " - $sf" -ForegroundColor Yellow }
Write-Host " Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full)." -ForegroundColor Yellow
}
if ($configFiles.Count -eq 0) {
Write-Host "No configuration files found in changes"
exit 0
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# db-load-git v1.3 — Load Git changes into 1C database
# db-load-git v1.5 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
@@ -13,23 +14,54 @@ import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
candidates.sort()
return candidates[-1]
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
@@ -146,14 +178,19 @@ def main():
# --- Filter and map to config files ---
config_files = []
support_skipped = []
for file in changed_files:
file = file.strip().replace("\\", "/")
if not file:
continue
# Skip service files
if file == "ConfigDumpInfo.xml":
# Skip service files (not partially loadable). Support-state files are
# tracked to warn: support changes apply only via a full load.
if file.endswith("ParentConfigurations.bin"):
support_skipped.append(file)
continue
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
continue
full_path = os.path.join(args.ConfigDir, file)
@@ -186,6 +223,12 @@ def main():
if rel_path not in config_files:
config_files.append(rel_path)
if support_skipped:
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
for sf in support_skipped:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
if len(config_files) == 0:
print("No configuration files found in changes")
sys.exit(0)
+1 -9
View File
@@ -80,17 +80,9 @@ Documents/Заказ.xml
Documents/Заказ/Forms/ФормаДокумента.xml
```
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После выполнения
1. Прочитай лог и покажи результат
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
## Примеры
@@ -1,4 +1,4 @@
# db-load-xml v1.3 — Load 1C configuration from XML files
# db-load-xml v1.5 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -108,15 +108,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
@@ -168,25 +193,36 @@ try {
Write-Host "Executing partial configuration load..."
# Build list file
$generatedListFile = $null
$rawList = @()
if ($ListFile) {
# Use provided list file
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$generatedListFile = $ListFile
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} else {
# Generate from -Files parameter
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$generatedListFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
Write-Host "Files to load: $($fileList.Count)"
foreach ($f in $fileList) { Write-Host " $f" }
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
# Support-state service files are NOT partially loadable — exclude with a hint.
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
if ($supportFiles.Count -gt 0) {
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
}
if ($fileList.Count -eq 0) {
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
exit 1
}
$generatedListFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
Write-Host "Files to load: $($fileList.Count)"
foreach ($f in $fileList) { Write-Host " $f" }
$arguments += "-listFile", "`"$generatedListFile`""
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
@@ -1,34 +1,67 @@
#!/usr/bin/env python3
# db-load-xml v1.3 — Load 1C configuration from XML files
# db-load-xml v1.5 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
candidates.sort()
return candidates[-1]
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
@@ -114,23 +147,33 @@ def main():
print("Executing partial configuration load...")
# Build list file
generated_list_file = None
if args.ListFile:
# Use provided list file
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
generated_list_file = args.ListFile
with open(args.ListFile, encoding="utf-8-sig") as f:
raw_list = [ln.strip() for ln in f if ln.strip()]
else:
# Generate from -Files parameter
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
# Support-state service files are NOT partially loadable — exclude with a hint.
support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
support_files = [x for x in raw_list if support_re.search(x)]
file_list = [x for x in raw_list if not support_re.search(x)]
if support_files:
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
for sf in support_files:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
if not file_list:
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
sys.exit(1)
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", generated_list_file]
arguments.append("-partial")
+28 -3
View File
@@ -1,4 +1,4 @@
# db-run v1.0 — Launch 1C:Enterprise
# db-run v1.1 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -79,15 +79,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
+41 -6
View File
@@ -1,26 +1,61 @@
#!/usr/bin/env python3
# db-run v1.0 — Launch 1C:Enterprise
# db-run v1.1 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import re
import subprocess
import sys
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
-7
View File
@@ -66,13 +66,6 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
| `-BackgroundSuspend` | Приостановить |
| `-BackgroundResume` | Возобновить |
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## Предупреждения
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
+28 -3
View File
@@ -1,4 +1,4 @@
# db-update v1.0 — Update 1C database configuration
# db-update v1.1 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -89,15 +89,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
+41 -6
View File
@@ -1,29 +1,64 @@
#!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration
# db-update v1.1 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe"))
if found:
return found[-1]
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
+28 -3
View File
@@ -1,4 +1,4 @@
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -70,15 +70,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
+40 -7
View File
@@ -1,34 +1,67 @@
#!/usr/bin/env python3
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.1 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
candidates.sort()
return candidates[-1]
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
+28 -3
View File
@@ -1,4 +1,4 @@
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
@@ -77,15 +77,40 @@ $OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (Test-Path $V8Path -PathType Container) {
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
+40 -7
View File
@@ -1,34 +1,67 @@
#!/usr/bin/env python3
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.1 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
def _version_key(p):
"""Numeric sort key from version dir name (.../1cv8/<ver>/bin/1cv8.exe)."""
ver = os.path.basename(os.path.dirname(os.path.dirname(p)))
return [int(x) for x in re.findall(r"\d+", ver)]
def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe."""
if not v8path:
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
v8path = _find_project_v8path()
if not v8path:
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
if candidates:
candidates.sort()
return candidates[-1]
v8path = max(candidates, key=_version_key)
ver = os.path.basename(os.path.dirname(os.path.dirname(v8path)))
print(f"Auto-selected platform {ver}: {v8path}")
else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
elif os.path.isdir(v8path):
if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe")
if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
+120 -1
View File
@@ -1,4 +1,4 @@
# form-add v1.5 — Add managed form to 1C config object
# form-add v1.7 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -18,6 +18,124 @@ $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Detect XML format version ---
function Detect-FormatVersion([string]$dir) {
@@ -55,6 +173,7 @@ if (-not (Test-Path $ObjectPath)) {
}
$objectXmlFull = Resolve-Path $ObjectPath
Assert-EditAllowed $objectXmlFull.Path 'editable'
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent)
$xmlDoc = New-Object System.Xml.XmlDocument
+163 -1
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# form-add v1.5 — Add managed form to 1C config object
# form-add v1.7 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
@@ -10,6 +11,166 @@ import uuid
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core",
@@ -84,6 +245,7 @@ def main():
sys.exit(1)
object_xml_full = os.path.abspath(object_path)
assert_edit_allowed(object_xml_full, "editable")
format_version = detect_format_version(os.path.dirname(object_xml_full))
parser_xml = etree.XMLParser(remove_blank_text=False)
+40 -14
View File
@@ -61,7 +61,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| DSL ключ | XML элемент | Значение ключа |
|--------------|-------------------|---------------------------------------------------|
| `"group"` | UsualGroup | `"horizontal"` / `"vertical"` / `"alwaysHorizontal"` / `"alwaysVertical"` / `"collapsible"` |
| `"group"` | UsualGroup | ориентация: `"vertical"` / `"horizontalIfPossible"` / `"alwaysHorizontal"` (поведение — отдельный ключ `behavior`) |
| `"columnGroup"` | ColumnGroup | `"horizontal"` / `"vertical"` / `"inCell"` — только внутри `columns` таблицы |
| `"input"` | InputField | имя элемента |
| `"check"` | CheckBoxField | имя |
@@ -83,15 +83,15 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| Ключ | Описание |
|------|----------|
| `name` | Переопределить имя (по умолчанию = значение ключа типа) |
| `name` | Переопределить имя (по умолчанию = значение ключа типа). Имена уникальны во всех коллекциях формы (элементы, реквизиты, команды, колонки) |
| `title` | Заголовок элемента |
| `tooltip` | Всплывающая подсказка элемента (строка или `{ru,en}`) |
| `visible: false` | Скрыть (синоним: `hidden: true`) |
| `enabled: false` | Сделать недоступным (синоним: `disabled: true`) |
| `readOnly: true` | Только чтение |
| `on: [...]` | События с автоименованием обработчиков |
| `handlers: {...}` | Явное задание имён обработчиков: `{"OnChange": "МоёИмя"}` |
| `events: {...}` | Обработчики событий: `{ "OnChange": "ИмяОбработчика" }`. Тот же формат, что у событий формы. Значение `null` → имя обработчика сгенерируется автоматически |
### Допустимые имена событий (`on`)
### Допустимые имена событий (`events`)
Компилятор предупреждает о неизвестных событиях. Имена регистрозависимы — используйте точно как указано.
@@ -116,7 +116,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| Ключ | Описание | Пример |
|------|----------|--------|
| `path` | DataPath — привязка к данным | `"Объект.Организация"` |
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"top"` |
| `titleLocation` | Размещение заголовка | `"none"`, `"left"`, `"right"`, `"top"`, `"bottom"`, `"auto"` |
| `multiLine: true` | Многострочное поле | текстовое поле, комментарий |
| `passwordMode: true` | Режим пароля (звёздочки) | поле ввода пароля |
| `choiceButton: true` | Кнопка выбора ("...") | ссылочное поле |
@@ -179,13 +179,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
### Группа (group)
Значение ключа задаёт ориентацию: `"horizontal"`, `"vertical"`, `"alwaysHorizontal"`, `"alwaysVertical"`, `"collapsible"`.
Значение ключа задаёт **ориентацию**: `"vertical"`, `"horizontalIfPossible"`, `"alwaysHorizontal"`.
| Ключ | Описание |
|------|----------|
| `behavior` | Поведение группы: `"collapsible"` (сворачиваемая) / `"popup"` (всплывающая). Опустить = обычная |
| `showTitle: true` | Показывать заголовок группы |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Только для `"group": "collapsible"` — группа создаётся свёрнутой |
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы |
@@ -202,8 +203,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| `height` | Высота в строках таблицы |
| `header: false` | Скрыть шапку |
| `footer: true` | Показать подвал |
| `commandBarLocation` | `"None"`, `"Top"`, `"Auto"` |
| `searchStringLocation` | `"None"`, `"Top"`, `"Auto"` |
| `commandBarLocation` | `"None"`, `"Top"`, `"Bottom"`, `"Auto"` |
| `searchStringLocation` | `"None"`, `"Top"`, `"Bottom"`, `"CommandBar"`, `"PullFromTop"`, `"Auto"` |
| `choiceMode: true` | Режим выбора (для форм выбора) |
| `initialTreeView` | `"ExpandTopLevel"` и др. (иерархические списки) |
| `enableDrag: true` | Разрешить перетаскивание |
@@ -240,6 +241,15 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
]}
```
### Картинка-поле (picField)
PictureField, привязанный к булеву/числу, рисует иконку только при заданном `valuesPicture`:
| Ключ | Описание |
|------|----------|
| `valuesPicture` | Ref картинки значения: `"StdPicture.Favorites"`, `"CommonPicture.X"` |
| `loadTransparent: true` | Скрыть кадр «нет значения» |
### Страницы (pages + page)
| Ключ (pages) | Описание |
@@ -431,7 +441,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
"events": { "OnCreateAtServer": "ПриСозданииНаСервере" },
"elements": [
{ "group": "horizontal", "name": "ГруппаФайл", "children": [
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "on": ["StartChoice"] },
{ "input": "ИмяФайла", "path": "ИмяФайла", "title": "Файл", "inputHint": "Выберите файл...", "choiceButton": true, "events": { "StartChoice": "ИмяФайлаНачалоВыбора" } },
{ "check": "ПерваяСтрокаЗаголовок", "path": "ПерваяСтрокаЗаголовок" }
]},
{ "input": "Результат", "path": "Результат", "multiLine": true, "height": 8, "readOnly": true, "title": "Лог" },
@@ -491,8 +501,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
"title": "Просмотр данных",
"elements": [
{ "group": "horizontal", "name": "Фильтр", "children": [
{ "input": "Период", "path": "Период", "on": ["OnChange"] },
{ "input": "Организация", "path": "Организация", "on": ["OnChange"] }
{ "input": "Период", "path": "Период", "events": { "OnChange": "ПериодПриИзменении" } },
{ "input": "Организация", "path": "Организация", "events": { "OnChange": "ОрганизацияПриИзменении" } }
]},
{ "table": "Данные", "path": "Данные", "changeRowSet": true, "columns": [
{ "input": "Дата", "path": "Данные.Дата" },
@@ -513,10 +523,26 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
}
```
## Продвинутые конструкции (по необходимости)
Описанного выше хватает для большинства форм. Под конкретную задачу подгрузите файл из `references/`:
- `dynamic-list.md` — форма списка: источник, отбор, сортировка, группировки, параметры запроса
- `appearance.md` — условное и статическое оформление элементов (цвета/шрифты/рамки)
- `choice-params.md` — параметры и связи выбора у полей ввода
- `command-interface.md` — командный интерфейс формы
- `roles-access.md` — пользовательская видимость и доступ по ролям
- `companion-panels.md` — контент расширенной подсказки и контекстного меню
- `special-fields.md` — поля документа/датчика (HTML, текст, индикатор, ползунок)
- `charts.md` — диаграммы и планировщик
- `report-form.md` — свойства формы отчёта
- `type-system-advanced.md` — наборы и составные типы
- `table-advanced.md` — расширенные свойства таблиц
- `layout-advanced.md` — тонкая компоновка и геометрия
## Автогенерация
- **Companion-элементы**: ContextMenu, ExtendedTooltip и др. создаются автоматически
- **Обработчики событий**: `"on": ["OnChange"]``ОрганизацияПриИзменении`
- **Namespace**: все 17 namespace-деклараций
- **ID**: последовательная нумерация, AutoCommandBar = id="-1"
- **Unknown keys**: выводится предупреждение о нераспознанных ключах
@@ -0,0 +1,123 @@
# Оформление
Два независимых механизма: **оформление элемента** (постоянные цвета/шрифт/граница на конкретном элементе) и **условное оформление формы** (`conditionalAppearance` — правила, применяемые при выполнении условия).
## Оформление элемента (цвета / шрифты / граница)
Свойства задаются прямо на элементе. Применимо к полям (`input`/`check`/`radio`/`labelField`/`picField`/`calendar`), декорациям (`label`/`picture`), кнопкам (`button`), группам (`group`/`columnGroup`), страницам (`page`/`pages`), попапам (`popup`) и таблицам (`table`). Каждое свойство необязательно.
| Ключ | Что задаёт |
|------|-----------|
| `textColor` | Цвет текста |
| `backColor` | Цвет фона |
| `borderColor` | Цвет рамки |
| `font` | Шрифт |
| `border` | Граница |
| `titleTextColor` / `titleBackColor` / `titleFont` | Цвет текста / цвет фона / шрифт заголовка колонки (`labelField`, колонки таблицы); у `page`/`pages`/`popup``titleTextColor`/`titleFont` заголовка страницы/попапа |
| `footerTextColor` / `footerBackColor` / `footerFont` | Цвет текста / цвет фона / шрифт подвала колонки |
Те же свойства доступны и через словарь `appearance` элемента — под русскими именами параметров платформы: `ЦветТекста`, `ЦветФона`, `ЦветРамки`, `Шрифт`, `Граница`, `ЦветТекстаЗаголовка`, `ЦветФонаЗаголовка`, `ШрифтЗаголовка`, `ЦветТекстаПодвала`, `ЦветФонаПодвала`, `ШрифтПодвала`. Это та же запись, что и в правилах условного оформления (ниже) и в `appearance` поля дин-списка.
### Цвет
Строка в одной из форм:
| Форма | Значение |
|-------|----------|
| `web:Имя` | Цвет из web-палитры, напр. `web:Red`, `web:FireBrick`, `web:HoneyDew` |
| `win:Имя` | Системный цвет Windows, напр. `win:MenuBar`, `win:ButtonText`, `win:DisabledText` |
| `style:ИмяСтиля` | Ссылка на элемент стиля конфигурации/платформы, напр. `style:FormBackColor`, `style:BorderColor` |
| `#RRGGBB` | RGB-hex, напр. `#FF0000` |
Имя должно существовать в своей палитре (несуществующий web-/win-цвет или ссылка на отсутствующий `style:`-элемент — ошибка загрузки формы).
### Шрифт (`font` / `titleFont` / `footerFont`)
- Строка `"style:ИмяСтиля"` — шрифт из элемента стиля. Минимальная форма.
- Объект — задаются только нужные атрибуты:
| Ключ | Назначение |
|------|-----------|
| `ref` | Ссылка на стиль (`"style:X"`) или системный шрифт (`"sys:…"`) |
| `faceName` | Имя гарнитуры (для собственного шрифта) |
| `height` | Размер |
| `bold` / `italic` / `underline` / `strikeout` | `true`/`false` — начертание |
| `scale` | Масштаб, % |
| `kind` | `Absolute` (собственный шрифт — с `faceName`+`height`) / `WindowsFont` (системный — с `ref:"sys:…"`) |
```json
{ "label": "Внимание!", "textColor": "web:FireBrick",
"font": { "faceName": "Arial", "height": 12, "bold": true, "kind": "Absolute", "scale": 100 } }
```
### Граница (`border`)
- Строка `"style:ИмяСтиля"` (или объект `{ "ref": "style:X" }`) — граница из стиля.
- Объект `{ "width": N, "style": "..." }` — собственная граница. `style` — один из: `Single`, `Double`, `Underline`, `DoubleUnderline`, `Overline`, `Embossed`, `Indented`, `WithoutBorder`.
```json
{ "input": "Цена", "path": "Объект.Цена", "textColor": "#FF0000",
"borderColor": "style:BorderColor", "border": { "width": 1, "style": "Single" } }
{ "labelField": "Код", "titleTextColor": "web:HoneyDew", "border": "style:ControlBorder" }
```
## Условное оформление формы (`conditionalAppearance`)
Форменный ключ верхнего уровня — массив правил. Каждое правило применяет оформление к перечисленным полям, когда выполняется его условие.
```json
"conditionalAppearance": [
{ "selection": ["ОбычноеПоле"], "filter": ["ЧисловоеПоле > 100"],
"appearance": { "ЦветФона": "style:FormBackColor" },
"presentation": { "ru": "Подсветка", "en": "Highlight" } }
]
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `selection` | array | Имена форматируемых полей формы |
| `filter` | array | Условие применения (грамматика — ниже) |
| `appearance` | object | Словарь «параметр платформы: значение» |
| `presentation` | string / object | Подпись правила в списке настроек |
| `use` | bool | `false` — правило отключено |
| `viewMode` | string | Режим отображения настройки |
| `userSettingID` | string | Идентификатор пользовательской настройки; `"auto"` — сгенерировать |
### filter
Та же грамматика, что в отборе списка — shorthand `"Поле оператор значение @флаги"` или объект:
```json
"filter": [
"Статус = 3",
{ "field": "Сумма", "op": ">=", "value": 1000 },
{ "group": "Or", "items": [ "Просрочено = true", "Заблокирован = true" ] }
]
```
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
- **Флаги:** `@off` (отключён), `@user`, `@quickAccess`; `_` = пустое значение.
- **Группа:** `{ "group": "And"|"Or"|"Not", "items": [...], "use"? }`.
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата; именованный относительный период — строкой `"BeginningOfThisWeek"` с `"valueType": "v8:StandardBeginningDate"` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
### appearance
Словарь «параметр платформы: значение». Имена параметров — русские: `ЦветТекста`, `ЦветФона`, `Шрифт`, `Граница`, `Текст`, `Заголовок`, `Формат`, `ВидимостьЭлемента`, `Доступность` и другие параметры оформления компоновки.
Значения:
- **Цвет** (`ЦветТекста`/`ЦветФона`/…) и **шрифт** (`Шрифт`) — те же формы, что в оформлении элемента выше (`web:`/`win:`/`style:`/`#RRGGBB`; шрифт — строка `"style:X"` или объект).
- **Текстовые параметры** (`Текст`/`Заголовок`/`Формат`) — по форме значения:
- голая строка → нелокализованный литерал (`""` → пустое значение);
- объект `{ "ru": "...", "en": "..." }` → локализуемая строка;
- объект `{ "field": "путь" }` → ссылка на поле компоновки.
```json
"conditionalAppearance": [
{ "selection": ["Остаток"], "filter": ["Остаток < 0"],
"appearance": { "ЦветТекста": "web:Red", "Шрифт": { "bold": true } } },
{ "selection": ["Комментарий"], "filter": ["Комментарий notFilled"],
"appearance": { "Текст": { "ru": "— нет данных —" }, "ЦветТекста": "win:DisabledText" } }
]
```
> Условное оформление **самого дин-списка** задаётся не здесь, а в `settings.conditionalAppearance` реквизита-списка — см. `references/dynamic-list.md`.
@@ -0,0 +1,143 @@
# Диаграммы, диаграмма Ганта, планировщик
Поле-диаграмма (`chart` / `ganttChart`), поле-планировщик (`planner`) и дендрограмма выводят значение из реквизита соответствующего типа. Конструкция всегда двойная:
1. **Реквизит** chart/planner-типа (несёт данные и, при необходимости, design-time конфиг).
2. **Элемент** формы, привязанный к реквизиту через `path`.
Минимум — реквизит нужного типа плюс элемент с тем же `path`:
```json
"attributes": [ { "name": "Диаграмма", "type": "d5p1:Chart" } ],
"items": [ { "chart": "ПолеДиаграммы", "path": "Диаграмма" } ]
```
Реквизит, заполняемый в коде (без встроенной настройки), достаточно объявить типом — элемент привязывается и работает.
## Типы реквизита и элемента
| Элемент | Ключ типа | Тип реквизита | Что несёт элемент дополнительно |
|---------|-----------|---------------|---------------------------------|
| Диаграмма | `chart` | `d5p1:Chart` | — |
| Диаграмма Ганта | `ganttChart` | `d5p1:GanttChart` | `ganttTable` — вложенная таблица (см. ниже) |
| Планировщик | `planner` | `pl:Planner` | — |
| График. схема | `graphicalSchema` | `d5p1:FlowchartContextType` | `edit`, `warningOnEditRepresentation` |
| Период | `periodField` | `v8:StandardPeriod` | — |
| Дендрограмма | `dendrogram` | — | — |
Имя элемента — значение ключа (`"chart": "ПолеДиаграммы"`); `path` — короткое имя реквизита.
### Элемент диаграммы Ганта (`ganttTable`)
У поля Ганта внутри лежит полноценная таблица — задаётся ключом `ganttTable` (та же грамматика, что у обычной `table`):
```json
{ "ganttChart": "Ганта", "path": "Ганта",
"ganttTable": { "table": "ТаблицаГанта", "path": "Ганта", "height": 3 } }
```
## Design-time конфиг диаграммы (`chart`)
Реквизит типа `d5p1:Chart` / `d5p1:GanttChart` может нести встроенную настройку диаграммы — объект `chart` на реквизите. Платформа всегда пишет полный набор свойств (~127: тип, серии, легенда, заголовок, шкалы, цвета, шрифты, оси), поэтому **авторинг с нуля непрактичен** — возьмите рабочую диаграмму за основу и правьте смысловое ядро.
Ключи `chart` = канонические имена свойств диаграммы; задавайте только те, что меняете:
```json
{ "name": "Диаграмма", "type": "d5p1:Chart", "chart": {
"chartType": "Line",
"isSeriesDesign": true, "realSeriesCount": "2",
"realSeriesData": [
{ "id": "1", "color": "auto", "line": {"width":2,"gap":false,"style":"Solid"},
"marker": "Auto", "text": "Серия 1", "strIsChanged": false, "isExpand": false,
"isIndicator": false, "colorPriority": false }
],
"isShowTitle": true, "title": "Продажи",
"isShowLegend": true, "legendPlacement": "Bottom",
"paletteKind": "Auto"
} }
```
Смысловое ядро для правки:
| Ключ | Назначение |
|------|------------|
| `chartType` | Тип: `Line` / `Pie` / `Bar` / `Histogram` / `Column` / `Area` / … |
| `realSeriesData` | Массив серий — объекты `{ id, text, color, line, marker, … }` |
| `isShowTitle` + `title` | Показ и текст заголовка |
| `isShowLegend` + `legendPlacement` | Показ и расположение легенды (`Bottom` / `Right` / …) |
| `paletteKind` | Палитра (`Auto` / …) |
| `bkgColor` / `labelsColor` / … | Базовые цвета |
Формы значений внутри `chart`:
- **Цвета** — verbatim: `auto`, `style:ИмяСтиля`, `web:Red`, `#hex`.
- **`line`** — `{ width, gap, style }` (стиль линии: `Solid` / …).
- **`border`** — `{ width, style }`.
- **`font`** — `{ kind: "AutoFont" }` либо атрибуты шрифта.
- **Локализуемые строки** (`title`, `vsFormat`, `lbFormat`, `labelFormat`, серия `text`, …) — голая строка либо `{ "ru": "…", "en": "…" }`.
- **Области** (`elementsChart` / `elementsLegend` / `elementsTitle`) — `{ left, right, top, bottom }`.
- **Серии** (`realSeriesData` / `realExSeriesData`) — массивы объектов.
Любое из ~127 свойств переопределяется по каноническому имени; остальное оставляйте дефолтным (не указывайте — берётся из основы).
### Диаграмма Ганта (`d5p1:GanttChart`)
Реквизит типа `d5p1:GanttChart` использует **тот же** ключ `chart`. Внутри — вложенный полный `chart`-блок плюс гант-специфика (`points` / `series` / `timeScale` / `drawEmpty` / …). Так же берите рабочую диаграмму Ганта за основу.
> **Ограничение.** Диаграммы (Chart/Gantt) с заполненными **точками/осями** (`realPointData` / `realDataItems`, заполненные `valuesAxis` / `pointsAxis`) генерик-движком не поддержаны — это редкий вариант. Частые дашборд-диаграммы и диаграммы Ганта (серии / легенда / оформление / шкалы) поддержаны полностью.
## Design-time конфиг планировщика (`planner`)
Реквизит типа `pl:Planner` несёт встроенную настройку планировщика — объект `planner`. Компилятор подставляет умолчания для пропущенных ключей, поэтому авторинг может быть кратким:
```json
{ "name": "Планировщик", "type": "pl:Planner", "planner": {
"items": [
{ "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00",
"borderColor": "auto", "backColor": "auto", "deleted": false, "editMode": "EnableEdit" }
],
"period": { "begin": "2026-06-09T00:00:00", "end": "2026-06-09T23:59:59" },
"displayCurrentDate": true, "itemsTimeRepresentation": "BeginTime",
"timeScale": { "placement": "Left", "levels": [ { "measure": "Hour", "interval": 1 } ] }
} }
```
Минимум — один `item`:
```json
"planner": { "items": [ { "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00" } ] }
```
| Ключ `planner` | Тип | Назначение |
|----------------|-----|------------|
| `items` | array | Элементы расписания. Поля элемента: `text`, `tooltip`, `begin`, `end`, `value`, `borderColor`, `backColor`, `textColor`, `font`, `border`, `replacementDate`, `deleted` (bool), `editMode` (`EnableEdit` / …), `id` (необязательно — авто-GUID), `textFormatted` |
| `dimensions` | array | Измерения (разрезы) планировщика. Поля: `value` (объект разреза — ссылка `Enum.X.EnumValue.Y` / `Справочник.X`; опустить → пусто), `text` (заголовок), `borderColor`, `backColor`, `textColor`, `font`, `textFormatted`, `elements`. `elements` — элементы измерения, рекурсивны (могут нести вложенные `elements`): `value`, `text`, цвета, `font`, `showOnlySubordinatesAreas` (bool), `textFormatted` |
| `period` | object | Отображаемый период `{ begin, end }` (необязательно) |
| `timeScale` | object | Шкала времени (см. ниже) |
| `borderColor` / `backColor` / `textColor` / `lineColor` | color | Цвета (умолч. `auto`) |
| `font` | font | Шрифт (умолч. `{ kind: "AutoFont" }`) |
| `border` | border | Рамка `{ width, style }` |
| `beginOfRepresentationPeriod` / `endOfRepresentationPeriod` | dateTime | Период представления |
| `displayCurrentDate` / `displayWrapHeaders` / `displayTimeScaleWrapHeaders` / `alignElementsOfTimeScale` | bool | Флаги отображения |
| `timeScaleWrapHeadersFormat` | ML | Формат перенесённых заголовков шкалы |
| `timeScaleWrapBeginIndent` / `timeScaleWrapEndIndent` | int | Отступы переноса шкалы |
| `periodicVariantUnit` / `periodicVariantRepetition` | value / int | Единица и кратность периодического варианта |
| `itemsTimeRepresentation` | value | Представление времени элементов (`BeginTime` / …) |
| `itemsBehaviorWhenSpaceInsufficient` / `newItemsTextType` / `fixDimensionsHeader` / `fixTimeScaleHeader` | value | Поведение элементов и заголовков |
| `autoMinColumnWidth` / `autoMinRowHeight` | bool | Авто-минимум размеров |
| `minColumnWidth` / `minRowHeight` | int | Минимальные размеры |
Шкала времени (`timeScale`):
```json
"timeScale": {
"placement": "Left",
"levels": [ { "measure": "Hour", "interval": 1 } ]
}
```
Ключи: `placement`, `levels` (массив уровней), `transparent`, `backColor`, `textColor`, `currentLevel`. Уровень: `measure` (`Hour` / `Day` / …), `interval`, `show`, `line` (`{ width, gap, style }`), `scaleColor`, `dayFormatRule`, `format` (ML), `labels` (`{ ticks }`), `backColor`, `textColor`, `showPereodicalLabels`.
Формы значений в `planner` те же, что у диаграммы: цвета verbatim (`auto` / `style:X` / `web:Red` / `#hex`); шрифт `{ kind: "AutoFont" }` либо ref-строка; граница `{ width, style }`; ML-форматы — строка или `{ "ru": …, "en": … }`.
> **Ограничение.** Привязка элемента расписания к элементам измерений (`item.dimensionValues`) пока всегда пустая. Сами измерения (`dimensions`) задавать можно.
@@ -0,0 +1,73 @@
# Параметры выбора и связь по типу
Свойства поля ввода (`input`), управляющие выбором значения: чем ограничен список выбора и каким будет тип значения. Имена параметров — строки 1С как есть (`"Отбор.Х"`).
```json
{ "input": "Контрагент", "path": "Объект.Контрагент",
"choiceParameters": [
{ "name": "Отбор.Активный", "value": true },
{ "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] }
],
"choiceParameterLinks": [
{ "name": "Отбор.Организация", "dataPath": "Объект.Организация" },
{ "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" }
],
"typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 }
}
```
## Параметры выбора (`choiceParameters`)
Фиксированные значения параметров выбора, отбирающие список значений независимо от данных формы. Массив объектов `{ name, value }`:
- `name` — имя параметра (`"Отбор.Активный"`).
- `value` — значение. Допустимы: bool, число, строка, ISO-дата (`"2020-01-01T00:00:00"`), ссылка-путь (`Enum.X.Y`, `Catalog.X`). **Массив** значений задаёт фиксированный массив.
Короткая форма — строки `"name=value"`; значение с запятыми становится массивом, `true`/`false` → bool, число → число, остальное → строка/ссылка:
```json
"choiceParameters": [
"Отбор.Активный=true",
"Отбор.ВидПродукции=Enum.Виды.Агрохимикат, Enum.Виды.Пестицид"
]
```
## Связи параметров выбора (`choiceParameterLinks`)
Параметры выбора, значение которых берётся из **другого поля формы** (а не задано фиксированно). Типовой случай — отбор списка договоров по выбранному контрагенту. Массив объектов `{ name, dataPath, valueChange? }`:
- `name` — имя параметра выбора.
- `dataPath` — путь к полю формы, чьё значение подставляется в параметр.
- `valueChange` — что делать с уже выбранным значением при смене источника: `Clear` (очистить, необязательно — поведение по умолчанию) / `DontChange` (не менять).
```json
{ "input": "Договор", "path": "Объект.Договор",
"choiceParameterLinks": [
{ "name": "Отбор.Владелец", "dataPath": "Объект.Контрагент" }
]
}
```
Короткая форма — строки `"name=dataPath"`, опциональный хвост `:Clear` / `:DontChange`:
```json
"choiceParameterLinks": [ "Отбор.Организация=Объект.Организация", "Отбор.Тип=Объект.Тип:DontChange" ]
```
## Связь по типу (`typeLink`)
Тип значения поля определяется другим полем формы (напр. поле «Значение» субконто, тип которого задаётся выбранным видом субконто). Объект `{ dataPath, linkItem }`:
- `dataPath` — путь к полю, задающему тип.
- `linkItem` — индекс элемента связи (необязательно, по умолчанию `0`).
```json
"typeLink": { "dataPath": "Объект.ВидСубконто", "linkItem": 0 }
```
Короткая форма — строка `"dataPath"` либо `"dataPath#linkItem"`:
```json
"typeLink": "Объект.ВидСубконто"
"typeLink": "Объект.ВидСубконто#1"
```
@@ -0,0 +1,86 @@
# Командный интерфейс формы
Форменный ключ `commandInterface` управляет расстановкой команд по двум панелям формы:
- `commandBar` — командная панель формы;
- `navigationPanel` — панель навигации.
Указывать нужно **только команды, у которых меняется расстановка по умолчанию** (видимость, группа, порядок). Команды, которые платформа размещает автоматически и без изменений, в блок не включают.
```json
"commandInterface": {
"commandBar": [
{ "command": "Form.Command.Печать", "defaultVisible": false, "group": "FormCommandBarImportant",
"visible": { "common": false, "roles": { "Бухгалтер": true } } },
"CommonCommand.История"
],
"navigationPanel": {
"important": [ { "command": "CommonCommand.СвязанныеДокументы", "defaultVisible": false, "visible": false } ],
"seeAlso": [ { "command": "CommonCommand.Заметки", "defaultVisible": false, "visible": false } ]
}
}
```
## Элемент-команда
Каждый элемент панели — объект, либо строка-shorthand (= голый `command` со всеми остальными свойствами по умолчанию):
```json
"CommonCommand.История"
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `command` | string | Ссылка на команду дословно: `CommonCommand.X`, `Document.X.StandardCommand.Y`, `Form.Command.X`, `Form.StandardCommand.OK`, `"0"` (пустой / разделитель) |
| `type` | string | `Auto` (по умолчанию, необязательно) или `Added` |
| `defaultVisible` | bool | Видимость по умолчанию. На практике задаётся только `false` — чтобы скрыть команду, которая иначе видна |
| `visible` | bool / object | Видимость с исключениями по ролям: `bool` либо `{ "common": bool, "roles": { "Имя": bool } }` |
| `group` | string | Группа размещения дословно: предопределённая (`FormCommandBarImportant`, `FormNavigationPanelGoTo`, …), именованная (`CommandGroup.X`) или GUID-группа расширения |
| `index` | int | Порядок команды внутри группы |
| `attribute` | string | Путь реквизита для элемента панели навигации |
## Две формы записи панели
Панель можно описать **плоским массивом** или **деревом по группам** — выбирайте любую.
**Плоский массив** — каждый элемент при необходимости несёт собственный `group`:
```json
"commandBar": [
{ "command": "Form.Command.Печать", "group": "FormCommandBarImportant", "defaultVisible": false },
{ "command": "CommonCommand.История", "group": "FormCommandBarImportant", "index": 1 }
]
```
**Дерево** — объект `{ группа: [команды] }`; группа берётся из ключа, элементы её не повторяют:
```json
"navigationPanel": {
"important": [ "CommonCommand.СвязанныеДокументы" ],
"goTo": [ { "command": "Document.Заказ.StandardCommand.Movements", "defaultVisible": false, "visible": false } ],
"seeAlso": [ "CommonCommand.Заметки" ]
}
```
Ключи-группы дерева зависят от панели:
- `navigationPanel`: `important`, `goTo`, `seeAlso` (можно по-русски — `важное`, `перейти`, `смТакже`);
- `commandBar`: `important`, `createBasedOn`;
- любой другой ключ (`CommandGroup.X` или GUID) подставляется в группу дословно.
## Скрыть видимую команду
Самый частый случай — убрать команду, которую платформа показывает по умолчанию:
```json
"commandBar": [
{ "command": "Form.Command.Печать", "defaultVisible": false, "visible": false }
]
```
Показать команду только некоторым ролям:
```json
{ "command": "Form.Command.Печать", "defaultVisible": false,
"visible": { "common": false, "roles": { "Бухгалтер": true } } }
```
@@ -0,0 +1,131 @@
# Companion-панели и расширенная подсказка элемента
Любой элемент формы может нести свой собственный контент в трёх companion-свойствах: расширенную подсказку (`extendedTooltip`), командную панель (`commandBar`) и контекстное меню (`contextMenu`). Все три задаются ключами прямо на объекте элемента.
```jsonc
{ "table": "Список", "path": "Список",
"commandBar": { "children": [ ] },
"contextMenu": { "children": [ ] },
"extendedTooltip": "Двойной клик открывает карточку" }
```
## Расширенная подсказка (`extendedTooltip`)
Подсказка-надпись рядом с элементом. Две формы записи.
**Текст-форма** — просто текст подсказки:
```jsonc
"extendedTooltip": "Укажите ИНН контрагента"
"extendedTooltip": { "ru": "Сумма с НДС", "en": "Amount incl. VAT" }
"extendedTooltip": { "text": "Всего <b>с НДС</b>", "formatted": true }
```
- строка — ru-текст;
- `{ "ru": …, "en": … }` — многоязычный (как `title`);
- `{ "text": …, "formatted": true }` — форматированный текст (inline-разметка 1С: `<b>…</>`, `<i>`, `<u>`, `<color web:Red>…</>`, `<bgColor …>`, `<font …>`, `<fontSize …>`, `<link URL>…</>`, `<img …>`; закрывающий тег — `</>`). `formatted` нужен только когда текст содержит такую разметку.
**Own-content форма** — объект с раскладкой/оформлением/флагами, когда подсказке нужны размеры, цвет, гиперссылка и т.п.:
```jsonc
"extendedTooltip": {
"text": "Перейти к инструкции",
"hyperlink": true,
"textColor": "web:Blue",
"events": { "URLProcessing": "ПодсказкаОбработкаНавигационнойСсылки" }
}
```
Ключи own-content объекта (все необязательны):
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `text` | string/ML | Текст подсказки (с `formatted` — форматированный) |
| `formatted` | bool | Интерпретировать inline-разметку в `text` |
| `tooltip` | string/ML | Всплывающая подсказка самой расширенной подсказки (редко; ≠ обычному `tooltip` элемента) |
| `hyperlink` | bool | Сделать подсказку гиперссылкой |
| `visible` / `enabled` | bool | Видимость / доступность подсказки |
| `width` / `height` | number | Размеры |
| `maxWidth` / `autoMaxWidth` | number / bool | Максимальная ширина / авто-максимум |
| `titleHeight` | number | Высота заголовка |
| `horizontalStretch` | bool | Горизонтальное растяжение |
| `verticalAlign` | string | Вертикальное выравнивание |
| `textColor` / `font` | string/object | Цвет текста / шрифт (см. `references/appearance.md`) |
| `events` | object | Обработчики событий подсказки, напр. `{ "URLProcessing": "Имя" }` у гиперссылочной подсказки |
## Командная панель (`commandBar`)
Собственная командная панель элемента (обычно таблицы или группы).
**Значение** — массив или объект:
```jsonc
"commandBar": [ { "button": "Создать", "command": "СоздатьЭлемент" } ]
"commandBar": {
"autofill": false,
"horizontalAlign": "Right",
"children": [
{ "button": "Создать", "command": "СоздатьЭлемент" },
{ "buttonGroup": "Печать", "children": [ ] }
]
}
```
- массив `[ … ]` — краткая запись для `{ "children": [ … ] }`;
- объект — `children` плюс необязательные `autofill` и `horizontalAlign`.
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `children` | array | Содержимое панели — обычная грамматика кнопок (см. основную инструкцию) |
| `autofill` | bool | `false` — подавить автозаполнение панели стандартными командами. Необязательно (по умолчанию панель автозаполняется) |
| `horizontalAlign` | string | Горизонтальное выравнивание содержимого: `Left` / `Center` / `Right`. Необязательно |
`children` — кнопки: `button` (с `command` / `commandName` / `stdCommand`), `buttonGroup`, `popup` — как в основной инструкции по кнопкам.
> Для таблицы динамического списка панель по умолчанию подавлена (чтобы не дублировать командную панель формы). Чтобы оставить автозаполняемую панель у самой таблицы — задайте `commandBar: { "autofill": true }`.
## Контекстное меню (`contextMenu`)
Собственное контекстное меню элемента. Грамматика та же, что у `commandBar`, но без `horizontalAlign`.
```jsonc
"contextMenu": [ { "button": "Карта маршрута", "commandName": "CommonCommand.КартаМаршрута" } ]
"contextMenu": {
"autofill": false,
"children": [
{ "button": "Скопировать ссылку", "command": "СкопироватьСсылку" }
]
}
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `children` | array | Пункты меню — обычная грамматика кнопок |
| `autofill` | bool | `false` — подавить автозаполнение меню. Необязательно |
## Пример: таблица со своим меню и инфо-баннером
```jsonc
{ "table": "Заказы", "path": "Объект.Заказы",
"extendedTooltip": {
"text": "Строки с просрочкой выделены <color web:FireBrick>красным</>",
"formatted": true
},
"commandBar": {
"autofill": false,
"horizontalAlign": "Right",
"children": [
{ "button": "Добавить", "command": "ДобавитьЗаказ" },
{ "button": "Удалить", "command": "УдалитьЗаказ" }
]
},
"contextMenu": {
"children": [
{ "button": "Открыть документ", "command": "ОткрытьЗаказ" },
{ "buttonGroup": "Экспорт", "children": [
{ "button": "В Excel", "command": "ВыгрузитьВExcel" } ] }
]
} }
```
@@ -0,0 +1,144 @@
# Динамический список
Реквизит с `type: "DynamicList"` (обычно `main: true`) — основа формы списка. Объект `settings` описывает источник данных и настройки списка. Минимум — указать источник:
```json
{ "name": "Список", "type": "DynamicList", "main": true,
"settings": { "mainTable": "Catalog.Контрагенты" } }
```
К списку привязывается таблица-элемент (`table`), ссылающаяся на реквизит через `path` — см. основную инструкцию.
## Источник данных
Два взаимоисключающих режима:
**Таблично-ориентированный** — основная таблица метаданных:
```json
"settings": { "mainTable": "Catalog.Контрагенты" }
```
**Запросный** — произвольный запрос:
```json
"settings": {
"query": "ВЫБРАТЬ Т.Ссылка, Т.Наименование, Т.Сумма ИЗ Документ.Заказ КАК Т ГДЕ Т.Сумма > &Порог",
"mainTable": "Document.Заказ"
}
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `mainTable` | string | Основная таблица (`Catalog.X` / `Document.X` / …). Можно вместе с `query` |
| `query` | string | Текст запроса. Поддерживает `@file.sql` (путь к файлу запроса рядом с JSON) |
| `keyType` | string | Запросный список без `mainTable`: тип ключа набора — `FieldValue` / `RowKey` / `RowNumber` |
| `keyFields` | array | Поля ключа набора (для `keyType` без `mainTable`) |
Параметры запроса (`&Имя`) задаются в `parameters` (ниже).
`"dynamicDataRead": false` отключает динамическое считывание (список читается обычным запросом, без фонового обновления) — нужно для тяжёлых/агрегатных запросов.
## Параметры запроса (`parameters`)
Значения для `&параметров` текста запроса. Shorthand `"Имя [Заголовок]: тип = Значение"` (всё кроме имени необязательно) либо объект:
```json
"settings": {
"query": "… ГДЕ Т.Артикул = &Артикул И Т.Цена ПОДОБНО &Маска",
"parameters": [
"Артикул",
"Маска: string = %",
{ "name": "ВидЦен", "valueListAllowed": true },
{ "name": "Период", "type": "dateTime" }
]
}
```
Ключи объекта: `name`, `title`, `type` (грамматика типов — см. основную инструкцию), `value`, `valueListAllowed` (разрешить список значений), `availableValues` (`[{ value, presentation }]`), `expression`, `use`.
## Значения параметров в настройках (`dataParameters`)
Предустановленные значения параметров на уровне настроек списка. Shorthand `"Имя = Значение"` или объект `{ parameter, value?, use?, viewMode? }`:
```json
"dataParameters": [ "Организация = _", "ВидЦен" ]
```
## Поля набора (`fields`)
Обычно поля выводятся из источника сами — `fields` нужен **только чтобы переопределить** свойства отдельного поля:
```json
"fields": [
{ "field": "Сумма", "title": "Сумма, руб", "appearance": { "Формат": "ЧДЦ=2" } },
{ "field": "Остаток", "valueType": "number(15,2)" }
]
```
Ключи поля: `field`, `dataPath`, `title`, `valueType`, `appearance` (как в условном оформлении), `presentationExpression`, `inputParameters` (связь по параметрам выбора), `typeLink` (`{ field, linkItem }` — связь по типу, напр. субконто).
## Вычисляемые поля (`calculatedFields`)
Поля, считаемые выражением. Shorthand `"Имя [Заголовок]: тип = Выражение"`:
```json
"calculatedFields": [
"Метка = Code + \" \" + Description",
"Маржа [Маржа, руб]: number(15,2) = Цена - Закупка"
]
```
Объектная форма — для `presentationExpression` / `orderExpression`:
```json
{ "dataPath": "Сорт", "expression": "Code", "title": "Сорт",
"valueType": "string(10)", "presentationExpression": "Code" }
```
## Отбор (`filter`)
Shorthand `"Поле оператор значение @флаги"` или объект:
```json
"filter": [
"Организация = _ @off @user",
"Сумма > 1000",
{ "field": "Дата", "op": ">=", "value": "2024-01-01T00:00:00" },
{ "group": "Or", "items": [ "Статус = 1", "Статус = 2" ] }
]
```
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
- **Флаги:** `@off` (отключён), `@user` (в пользовательских настройках), `@quickAccess`; `_` = пустое значение.
- **Группа:** `{ group: "And"|"Or"|"Not", items: [...] }`.
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата. Именованный относительный период — строкой с типом: `{ "value": "BeginningOfThisWeek", "valueType": "v8:StandardBeginningDate" }` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
## Сортировка (`order`)
Строка `"Поле"` (по возр.) / `"Поле desc"`, либо объект `{ field, direction? }`. `"Auto"` — автосортировка:
```json
"order": [ "Дата desc", "Наименование", "Auto" ]
```
## Группировка строк (`grouping`)
Линейная цепочка уровней (внешний → внутренний). Шорткат `>` или массив:
```json
"grouping": "Контрагент > Договор"
"grouping": [ "Контрагент", { "field": "Дата", "groupType": "Hierarchy" } ]
```
Ключи уровня-объекта: `field`, `groupType` (`Items` / `Hierarchy`).
## Условное оформление (`conditionalAppearance`)
```json
"conditionalAppearance": [
{ "filter": [ "Просрочено = true" ], "appearance": { "ЦветТекста": "web:Red" } }
]
```
`filter` — та же грамматика, что выше. `appearance` — словарь «параметр платформы: значение» (`ЦветТекста`, `ЦветФона`, `Шрифт`, `Текст`, `Формат`, …). Значение `Текст`/`Заголовок`/`Формат`: голая строка — нелокализованный литерал; `{ru,en}` — локализуемая строка; `{ field: "путь" }` — ссылка на поле. Подробнее об оформлении — `references/appearance.md`.
@@ -0,0 +1,111 @@
# Продвинутая раскладка
Тонкая настройка размещения элемента внутри родителя сверх базовой геометрии (`width`/`height`/`horizontalStretch`/`verticalStretch`/`visible`/`enabled` и ориентации групп/страниц — они в основной инструкции). Все ключи ниже задаются прямо на элементе и **необязательны** — без них действует поведение платформы по умолчанию.
## Выравнивание внутри родителя
Различают **выравнивание самого элемента** в отведённой ему ячейке и **выравнивание содержимого** элемента.
| Ключ | Значения | Что выравнивает |
|------|----------|-----------------|
| `groupHorizontalAlign` | `Left` / `Center` / `Right` | Положение **элемента** по горизонтали в родительской группе (когда элемент у́же доступного места) |
| `groupVerticalAlign` | `Top` / `Center` / `Bottom` | Положение **элемента** по вертикали в родительской группе |
| `horizontalAlign` | `Left` / `Center` / `Right` | Выравнивание **содержимого** (текста/значения) внутри самого элемента |
| `verticalAlign` | `Top` / `Center` / `Bottom` | Выравнивание содержимого по вертикали внутри элемента |
`group*Align` отвечает на вопрос «куда сдвинуть нерастянутый элемент в его ячейке», `horizontalAlign`/`verticalAlign` — «как разместить текст внутри элемента». Это разные оси настройки, их часто комбинируют.
```json
{ "button": "ОК", "groupHorizontalAlign": "Right" }
{ "input": "Сумма", "path": "Объект.Сумма", "horizontalAlign": "Right" }
{ "label": "Итого", "groupHorizontalAlign": "Center", "horizontalAlign": "Center" }
```
## Ограничение максимального размера
По умолчанию растягивающийся элемент имеет авто-вычисляемый предел ширины/высоты. Чтобы задать жёсткий предел или вовсе снять авто-предел:
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `maxWidth` | число | Жёсткий максимум ширины элемента |
| `maxHeight` | число | Жёсткий максимум высоты элемента |
| `autoMaxWidth` | `false` | Отключить авто-предел ширины (элемент тянется без ограничения сверху) |
| `autoMaxHeight` | `false` | Отключить авто-предел высоты |
`autoMaxWidth: false` нужен, например, для широкого многострочного поля или растянутого по всей форме поля ввода, чтобы платформа не «прижимала» его к авто-пределу. Указывают именно отклонение от дефолта; обычное значение `true` писать не нужно.
```json
{ "input": "Комментарий", "path": "Объект.Комментарий", "multiLine": true,
"horizontalStretch": true, "autoMaxWidth": false }
{ "input": "Поиск", "path": "СтрокаПоиска", "horizontalStretch": true, "maxWidth": 600 }
```
## Поведение при вводе и активации
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `skipOnInput` | `true` / `false` | Пропускать элемент при обходе по Enter/Tab (фокус через него не проходит). Указывают явно, в т.ч. `false` чтобы вернуть в обход поле, которое платформа пропустила бы |
| `defaultItem` | `true` | Элемент получает фокус по умолчанию при открытии формы (поле/таблица для немедленного ввода) |
```json
{ "input": "Идентификатор", "path": "Объект.Идентификатор", "skipOnInput": true }
{ "input": "Штрихкод", "path": "Штрихкод", "defaultItem": true }
```
`skipOnInput: true` — для служебных/расчётных полей, которые видны, но не редактируются вводом с клавиатуры в общем потоке. `defaultItem: true` ставят на одном элементе формы — точке, с которой пользователь начнёт работу.
## Перетаскивание
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `enableStartDrag` | `true` | Разрешить начинать перетаскивание из элемента (источник drag-n-drop) |
Для таблиц приём/перемещение строк управляется ключами таблицы (`enableDrag`, `changeRowOrder`) — см. основную инструкцию; `enableStartDrag` — общий низкоуровневый флаг «этот элемент может быть источником перетаскивания».
## Закрепление колонки в таблице (`fixingInTable`)
Свойство поля-колонки внутри таблицы: закрепить колонку у края, чтобы она не уходила при горизонтальной прокрутке.
| Значения |
|----------|
| `None` (по умолчанию — не закреплена) / `Left` / `Right` |
```json
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
{ "input": "Количество", "path": "Объект.Товары.Количество" },
{ "input": "Сумма", "path": "Объект.Товары.Сумма", "fixingInTable": "Right" } ] }
```
Закрепляют ключевые колонки (идентифицирующую слева, итоговую справа), чтобы они оставались видны при прокрутке широкой таблицы.
## Ячейки колонок: шапка и подвал
Для поля-колонки внутри таблицы (и `columnGroup`) — размещение в шапке/подвале и выравнивание текста ячеек. Применять только к элементам внутри `columns` таблицы.
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `showInHeader` | `true` / `false` | Показывать колонку в шапке таблицы |
| `showInFooter` | `true` / `false` | Показывать колонку в подвале (нужно для итогов; подвал самой таблицы включается `footer: true`) |
| `headerHorizontalAlign` | `Left` / `Right` / `Center` / `Auto` | Выравнивание текста в шапке колонки |
| `footerHorizontalAlign` | `Left` / `Right` / `Center` | Выравнивание текста в подвале колонки |
| `autoCellHeight` | `true` / `false` | Авто-высота ячейки (перенос содержимого на несколько строк) |
```json
{ "table": "Товары", "path": "Объект.Товары", "footer": true, "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "autoCellHeight": true },
{ "input": "Сумма", "path": "Объект.Товары.Сумма",
"headerHorizontalAlign": "Right", "showInFooter": true, "footerHorizontalAlign": "Right" } ] }
```
## Адаптивная важность (`displayImportance`)
| Значения |
|----------|
| `VeryHigh` / `High` / `Usual` / `VeryLow` / `Low` |
Приоритет элемента при адаптивной перекомпоновке формы на узких/мобильных экранах: элементы с меньшей важностью сворачиваются/прячутся первыми. Применимо к любому элементу.
```json
{ "input": "Комментарий", "path": "Объект.Комментарий", "displayImportance": "Low" }
```
@@ -0,0 +1,79 @@
# Форма отчёта
Форма, подключённая к объекту-отчёту (`Report`). Кроме обычных свойств формы у неё есть несколько свойств в `properties`, связывающих форму с механизмом компоновки (СКД): куда выводится результат, где данные расшифровки, какого она типа. Все они задаются в блоке `properties` верхнего уровня.
```json
"properties": {
"reportFormType": "Main",
"reportResult": "РезультатОтчета",
"detailsData": "ДанныеРасшифровки"
}
```
Ни одно из этих свойств не обязательно — указывайте только те, что нужны конкретной форме.
## Тип формы отчёта (`reportFormType`)
Роль формы в составе отчёта:
| Значение | Назначение |
|----------|-----------|
| `Main` | Основная форма отчёта (результат + настройки) |
| `Settings` | Форма настроек |
| `Variant` | Форма варианта |
```json
"reportFormType": "Main"
```
## Привязка к компоновке
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `reportResult` | string | Имя реквизита-результата формы (табличный документ, куда выводится отчёт) |
| `detailsData` | string | Имя реквизита данных расшифровки |
| `variantAppearance` | string | Имя реквизита оформления варианта |
Значение каждого ключа — имя реквизита формы (а не путь к данным). Реквизит с таким именем должен присутствовать в `attributes` формы.
## Группа пользовательских настроек (`customSettingsFolder`)
Группа-элемент формы, в которую генерируются пользовательские настройки компоновщика. Задаётся **по имени** элемента-группы:
```json
"customSettingsFolder": "ГруппаПользовательскихНастроек"
```
## Прочие свойства компоновки
Редкие, задавайте только при явной необходимости:
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `autoShowState` | `Auto`, `DontShow`, `ShowOnComposition` | Автопоказ состояния формирования |
| `reportResultViewMode` | `Auto` | Режим просмотра результата |
| `viewModeApplicationOnSetReportResult` | `Auto` | Применение режима просмотра при установке результата |
## Реалистичный пример
Основная форма отчёта со СКД: реквизит-результат, данные расшифровки и группа пользовательских настроек.
```json
{
"properties": {
"reportFormType": "Main",
"reportResult": "РезультатОтчета",
"detailsData": "ДанныеРасшифровки",
"customSettingsFolder": "ГруппаПользовательскихНастроек"
},
"attributes": [
{ "name": "РезультатОтчета", "type": "SpreadsheetDocument" },
{ "name": "ДанныеРасшифровки", "type": "DataCompositionDetailsData" }
],
"elements": [
{ "group": "vertical", "name": "ГруппаПользовательскихНастроек" },
{ "spreadsheet": "РезультатОтчета", "path": "РезультатОтчета",
"titleLocation": "none" }
]
}
```
@@ -0,0 +1,73 @@
# Доступ по ролям
Единый механизм платформы для разграничения по ролям: задаётся общее значение для всех ролей плюс исключения для конкретных ролей. Один и тот же формат значения у четырёх ключей — каждый на своём владельце:
| Ключ | Владелец | Смысл |
|------|----------|-------|
| `userVisible` | элемент формы | пользовательская видимость элемента |
| `view` | реквизит формы | право просмотра |
| `edit` | реквизит формы | право редактирования |
| `use` | команда формы | доступность команды |
Ключ необязателен: его отсутствие = полный доступ для всех ролей.
## Значение
Две формы (одинаковы для всех четырёх ключей):
**Скаляр** `true` / `false` — общее значение для всех ролей, без исключений:
```json
{ "input": "Поле", "userVisible": false }
```
**Объект** `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }` — общее значение `common` плюс явные исключения по ролям:
```json
{ "name": "Реквизит",
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
```
Роль, **не указанная** в `roles`, наследует `common`. Указанная — задаёт явный `true`/`false` (может и совпадать с `common`).
## Имя роли
Ключи в `roles` — имена ролей конфигурации (`ПолныеПрава`, `Бухгалтер`, …).
## Примеры
Элемент скрыт у всех пользователей:
```json
{ "input": "Комментарий", "userVisible": false }
```
Реквизит не виден никому и редактируется только одной ролью:
```json
{ "name": "СуммаБонуса",
"view": false,
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
```
Поле доступно для просмотра всем, но редактируемо только администратору:
```json
{ "name": "Статус",
"view": true,
"edit": { "common": false, "roles": { "Администратор": true } } }
```
Команда недоступна по умолчанию, разрешена только бухгалтеру:
```json
{ "name": "ПровестиЗакрытие",
"use": { "common": false, "roles": { "Бухгалтер": true } } }
```
Обратный случай — доступно всем, кроме одной роли:
```json
{ "name": "РедактироватьЦену",
"edit": { "common": true, "roles": { "Кладовщик": false } } }
```
@@ -0,0 +1,109 @@
# Спец-поля «документ/датчик»
Поля для отображения специальных данных: табличный документ, HTML, текст, форматированный документ, индикатор, ползунок. Каждое привязывается к реквизиту своего платформенного типа.
Структурно это обычные поля — поддерживают общий скелет поля (`path`, `title`, `titleLocation`, флаги `readOnly`/`enabled`/`visible`, `layout`, оформление, события). Ниже — только ключ `type` (имя элемента задаётся значением ключа) и собственные скаляры каждого семейства. Все скаляры необязательны.
| Ключ типа | Тип реквизита |
|-----------|---------------|
| `spreadsheet` | `mxl:SpreadsheetDocument` (ТабличныйДокумент) |
| `html` | `string` |
| `textDoc` | `d5p1:TextDocument` (ТекстовыйДокумент) |
| `formattedDoc` | `fd:FormattedDocument` (ФорматированныйДокумент) |
| `progressBar` | число |
| `trackBar` | число |
## spreadsheet — поле табличного документа
Просмотр/редактирование табличного документа (отчёт, печатная форма).
```json
{ "spreadsheet": "ТаблицаОтчета", "path": "ТаблицаОтчета",
"titleLocation": "none", "readOnly": true,
"output": "Disable", "protection": true }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `output` | string | Использование вывода: `Enable` / `Disable` |
| `protection` | bool | Защита от изменений |
| `edit` | bool | Разрешить редактирование |
| `showGrid` | bool | Показывать сетку |
| `showHeaders` | bool | Показывать заголовки строк/колонок |
| `showGroups` | bool | Показывать группировки |
| `showRowAndColumnNames` | bool | Показывать имена строк и колонок |
| `showCellNames` | bool | Показывать имена ячеек |
| `verticalScrollBar` / `horizontalScrollBar` | string | Режим полос прокрутки |
| `viewScalingMode` | string | Режим масштабирования просмотра |
| `selectionShowMode` | string | Режим отображения выделения |
| `pointerType` | string | Тип указателя |
| `enableDrag` / `enableStartDrag` | bool | Разрешить перетаскивание / начало перетаскивания |
## html — поле HTML-документа
Просмотр HTML. Реквизит — строка (содержит HTML-текст или адрес).
```json
{ "html": "Просмотр", "path": "СодержимоеHTML", "titleLocation": "none",
"output": "Enable", "warningOnEditRepresentation": false }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `output` | string | Использование вывода: `Enable` / `Disable` |
| `warningOnEditRepresentation` | bool | Предупреждать при изменении представления |
## textDoc — поле текстового документа
Просмотр/редактирование текстового документа.
```json
{ "textDoc": "Текст", "path": "ТекстДокумента", "editMode": "Edit" }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
## formattedDoc — поле форматированного документа
Просмотр/редактирование форматированного документа.
```json
{ "formattedDoc": "Описание", "path": "ФорматированноеОписание", "editMode": "Edit" }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
## progressBar — поле индикатора
Индикатор прогресса. Реквизит — числовой.
```json
{ "progressBar": "Прогресс", "path": "Прогресс",
"minValue": 0, "maxValue": 100, "showPercent": true }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
| `showPercent` | bool | Показывать проценты |
## trackBar — поле ползунка
Регулятор-ползунок. Реквизит — числовой.
```json
{ "trackBar": "Масштаб", "path": "Масштаб",
"minValue": 20, "maxValue": 400, "markingStep": 20 }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
| `step` | число | Шаг изменения |
| `largeStep` | число | Крупный шаг |
| `markingStep` | число | Шаг разметки |
| `markingAppearance` | string | Оформление разметки |
@@ -0,0 +1,132 @@
# Таблица — продвинутые возможности
Базовый элемент таблицы (`type: "table"`, колонки, основные свойства) описан в основной инструкции, раздел «Таблица (table)». Здесь — продвинутые возможности: дополнения командной панели, специфика таблицы динамического списка и неочевидные свойства/режимы.
## Представление (`representation`)
Как таблица рисует строки:
```json
{ "table": "Список", "path": "Список", "representation": "Tree" }
```
`List` — плоский список (по умолчанию), `Tree` — дерево, `HierarchicalList` — иерархический список (группы + элементы на одном уровне).
Для дерева/иерархии управляйте раскрытием уровней через `initialTreeView` (`ExpandTopLevel` / `ExpandAllLevels` / `NoExpand`).
## Выделение и текущая строка
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `selectionMode` | `SingleRow` / `MultiRow` | Режим выделения строк |
| `multipleChoice` | bool | Разрешить множественный выбор (для форм выбора) |
| `currentRowUse` | `DontUse` / `Use` / `SelectionPresentation` / `SelectionPresentationAndChoice` / `Choice` | Использование текущей строки таблицы |
```json
{ "table": "Список", "path": "Список", "selectionMode": "MultiRow", "multipleChoice": true }
```
## Поиск при вводе (`searchOnInput`)
Поведение встроенного поиска при наборе текста в таблице:
```json
{ "table": "Список", "path": "Список", "searchOnInput": "Use" }
```
`Auto` (по умолчанию) / `Use` (искать) / `DontUse` (не искать).
Где располагать сами элементы поиска — управляется `searchStringLocation` / `viewStatusLocation` / `searchControlLocation` (`None` / `Top` / `Bottom` / `CommandBar` / `Auto`).
## Прочие свойства таблицы
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `useAlternationRowColor` | bool | Чередование цвета строк |
| `verticalLines` / `horizontalLines` | bool | Линии сетки (укажите `false`, чтобы скрыть) |
| `markIncomplete` | bool | Автоотметка незаполненных ячеек |
| `heightInTableRows` | int | Высота элемента в строках (отдельно от `height`) |
| `autoInsertNewRow` | bool | Автодобавление новой строки при вводе в последнюю |
| `rowsPicture` | string \| object | Картинка строк. Ссылка (`"CommonPicture.X"`, `"abs:..."`) либо объект `{ src, loadTransparent?, transparentPixel? }` |
| `tooltipRepresentation` | string | Режим показа подсказки таблицы: `None`, `Button`, `ShowBottom`, `ShowTop`, `ShowLeft`, `ShowRight`, `ShowAuto`, `Balloon` |
## Фиксация колонки (`fixingInTable`)
Свойство **колонки** (на `input` / `labelField` / `check` / `picField` внутри `columns`), а не самой таблицы. Закрепляет колонку у края при горизонтальной прокрутке:
```json
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
{ "input": "Количество", "path": "Объект.Товары.Количество" }
]}
```
`Left` / `Right` / `None`.
## Исключённые команды (`excludedCommands`)
Убрать стандартные команды редактора таблицы (кнопки добавления/перемещения/сортировки):
```json
{ "table": "Товары", "path": "Объект.Товары",
"excludedCommands": [ "Add", "Delete", "MoveUp", "SortListAsc" ] }
```
Свойство работает на любом поле и на уровне формы; для таблицы значимы команды вида `Add` / `Delete` / `MoveUp` / `MoveDown` / `SortListAsc` / `SortListDesc`.
## Дополнения командной панели (`additions`)
Дополнения — это «представления» встроенного поиска таблицы:
- `searchString` — отображение строки поиска,
- `viewStatus` — состояние просмотра,
- `searchControl` — управление поиском.
Каждое дополнение — полноценный элемент (полный набор свойств поля). Размещать их можно двумя способами.
**(1) Стандартные дополнения** генерирует платформа на уровне таблицы. В DSL указывайте **только отклонения** от стандартного вида — через карту `additions` (ключ = тип дополнения):
```json
{ "table": "Список", "path": "Список",
"additions": { "viewStatus": { "horizontalLocation": "left" } } }
```
**(2) Кастомное дополнение**, размещённое прямо в командной панели — обычный элемент в `commandBar` с ключом-типом:
```json
{ "table": "Список", "path": "Список", "commandBar": [
{ "searchString": "ПоискСписка", "source": "Список", "width": 15, "horizontalStretch": true }
]}
```
- Тип-ключ: `searchString` / `viewStatus` / `searchControl`.
- `source` — имя таблицы-источника; необязательно, по умолчанию = имя родительской таблицы.
- `horizontalLocation`: `auto` (по умолчанию) / `left` / `right`. Применимо и к обычным элементам командных панелей.
- Прочие свойства как у поля: `title`, `visible`, `userVisible`, `enabled`, `tooltip`, оформление, `width` / `maxWidth` / `autoMaxWidth` / `horizontalStretch` / `groupHorizontalAlign` и др.
## Таблица динамического списка
Когда `path` таблицы указывает на реквизит `type: "DynamicList"` (см. `references/dynamic-list.md`), доступен блок специфичных свойств. Указывайте **только отличия** от умолчания.
| Ключ | Тип | Умолчание | Назначение |
|------|-----|-----------|-----------|
| `rowPictureDataPath` | string | картинка осн. таблицы | Путь к картинке строки. `""` — подавить картинку |
| `rowsPicture` | string | — | Картинка строк (`"CommonPicture.X"`) |
| `autoRefresh` | bool | `false` | Автообновление списка |
| `autoRefreshPeriod` | int | `60` | Период автообновления, сек |
| `updateOnDataChange` | string | `Auto` | Обновлять при изменении данных: `Auto` / `DontUpdate` |
| `choiceFoldersAndItems` | string | `Items` | Что выбирать: `Items` / `Folders` / `FoldersAndItems` |
| `restoreCurrentRow` | bool | `false` | Восстанавливать текущую строку при обновлении |
| `showRoot` | bool | `true` | Показывать корень |
| `allowRootChoice` | bool | `false` | Разрешить выбор корня |
| `allowGettingCurrentRowURL` | bool | `true` | Разрешить получение URL текущей строки |
| `userSettingsGroup` | string | — | Группа пользовательских настроек (привязка к одноимённой группе настроек) |
```json
{ "table": "Список", "path": "Список",
"representation": "Tree",
"rowPictureDataPath": "Список.DefaultPicture",
"choiceFoldersAndItems": "FoldersAndItems",
"allowRootChoice": true,
"updateOnDataChange": "DontUpdate" }
```
@@ -0,0 +1,77 @@
# Продвинутые конструкции типов
Примитивы (`string(n)`, `number(p,s)`, `boolean`, `date`/`dateTime`, …) и одиночные ссылки (`CatalogRef.Контрагенты`, `DocumentRef.Заказ`, `EnumRef.X`, …) описаны в основной инструкции. Здесь — типы, которые нельзя выразить одним именем: составные типы, наборы типов и платформенные наборы ссылок.
Любая из этих конструкций пишется в поле `type` реквизита, реквизита-параметра или поля.
## Составные типы
Несколько типов на одном реквизите — части перечисляются через разделитель `" | "` (можно `+`). Реквизит сможет принимать значение любого из перечисленных типов:
```json
{ "name": "Плательщик",
"type": "CatalogRef.Организации | CatalogRef.ИндивидуальныеПредприниматели" }
```
Смешивать можно типы из разных категорий — ссылки, примитивы, наборы типов:
```json
{ "name": "Источник",
"type": "CatalogRef.Контрагенты | DocumentRef.Заказ | string(150)" }
```
Каждая часть — самостоятельный токен из этого файла или из основной инструкции. Порядок частей произвольный.
## Наборы типов (TypeSet)
«Набор типов» подставляется вместо конкретного типа — это один токен, а не перечисление. Применимо и в составном типе как одна из частей.
| Токен `type` | Смысл |
|------|-------|
| `"DefinedType.ИмяТипа"` | определяемый тип конфигурации |
| `"Characteristic.ИмяПлана"` | тип значения характеристики (по плану видов характеристик) |
| `"AnyRef"` | любая ссылка |
| `"AnyIBRef"` | любая ссылка информационной базы |
Определяемый тип — реквизит принимает то, что задано в определяемом типе конфигурации (например `DefinedType.ДенежнаяСумма`):
```json
{ "name": "Сумма", "type": "DefinedType.ДенежнаяСумма" }
```
Характеристика — тип значения берётся из плана видов характеристик:
```json
{ "name": "Значение", "type": "Characteristic.ДополнительныеРеквизиты" }
```
## Платформенные наборы ссылок
«Голый» ссылочный токен **без `.Имя`** означает «любая ссылка этой категории объектов»:
| Токен `type` | Смысл |
|------|-------|
| `"CatalogRef"` | любая ссылка справочника |
| `"DocumentRef"` | любая ссылка документа |
| `"EnumRef"` | любая ссылка перечисления |
| `"ExchangePlanRef"` | любая ссылка плана обмена |
| `"TaskRef"` | любая ссылка задачи |
| `"BusinessProcessRef"` | любая ссылка бизнес-процесса |
| `"ChartOfCharacteristicTypesRef"` | любая ссылка плана видов характеристик |
| `"ChartOfAccountsRef"` | любая ссылка плана счетов |
| `"ChartOfCalculationTypesRef"` | любая ссылка плана видов расчёта |
Различие с одиночной ссылкой — только в наличии `.Имя`:
- `"CatalogRef.Валюты"` — конкретный справочник «Валюты»;
- `"CatalogRef"` — любой справочник.
```json
{ "name": "ЛюбойСправочник", "type": "CatalogRef" }
```
Эти наборы тоже комбинируются в составном типе:
```json
{ "name": "Объект", "type": "CatalogRef | DocumentRef" }
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
---
name: form-decompile
description: Декомпиляция управляемой формы 1С (Form.xml) в JSON-черновик в формате form-compile. Используй для scaffold новой формы по образцу или структурного рефакторинга. Не для точечных правок
argument-hint: <FormPath> [-OutputPath <out.json>]
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /form-decompile — JSON-черновик из Form.xml управляемой формы
Читает Form.xml и эмитит компактный JSON в формате `form-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь».
## Когда использовать
- **Scaffold новой формы по образцу** — взять существующую форму, получить JSON, поправить и скомпилировать в новую.
- **Структурный рефакторинг** — перебрать дерево элементов, реквизиты, команды.
## Когда **не** использовать
- **Точечные правки готовой формы** (добавить элемент, реквизит, команду) → `/form-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает форму целиком, может терять непокрытые конструкции и даёт большой diff. `/form-edit` правит адресно.
## Параметры
| Параметр | Описание |
|----------|----------|
| `FormPath` | Путь к Form.xml (обязательный) |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-decompile.ps1" -FormPath "<Form.xml>" -OutputPath "<out.json>"
```
## Что получаешь
JSON-черновик в формате `/form-compile`**не полное обратимое представление**: раундтрип `xml → json → xml` не гарантируется, часть конструкций DSL не покрывает и **теряет молча**.
Критичные конструкции (`ConditionalAppearance` со scope, design-time диаграммы/планировщики на реквизите, неизвестный тип элемента, не-Form root) → скрипт падает с ненулевым кодом и сообщением в stderr; для правок такой формы — `/form-edit`.
## Workflow
1. `/form-decompile <Form.xml> -OutputPath draft.json` — получить черновик.
2. Поправить JSON под задачу.
3. `/form-compile -JsonPath draft.json -OutputPath new/Form.xml` — собрать обратно.
4. `/form-validate` + `/form-info` — проверить результат.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+174 -4
View File
@@ -1,4 +1,4 @@
# form-edit v1.0 — Edit 1C managed form elements
# form-edit v1.3 — Edit 1C managed form elements
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -12,6 +12,124 @@ param(
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# === 1. Load Form.xml ===
if (-not (Test-Path $FormPath)) {
@@ -24,6 +142,7 @@ if (-not (Test-Path $JsonPath)) {
}
$resolvedFormPath = (Resolve-Path $FormPath).Path
Assert-EditAllowed $resolvedFormPath 'editable'
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
try {
@@ -291,6 +410,16 @@ function Get-ElementName {
return "$($el.$typeKey)"
}
# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс).
function Assert-EditUnique {
param([string]$name, [hashtable]$seen, [string]$ctx)
if ($seen.ContainsKey($name)) {
Write-Host "[ERROR] Duplicate $ctx '$name' in JSON definition — names must be unique in 1C form"
exit 1
}
$seen[$name] = $true
}
$script:knownEvents = @{
"input" = @("OnChange","StartChoice","ChoiceProcessing","AutoComplete","TextEditEnd","Clearing","Creating","EditTextChange")
"check" = @("OnChange")
@@ -864,17 +993,31 @@ if ($def.elements -and $def.elements.Count -gt 0) {
# Detect indent level
$childIndent = Get-ChildIndent $targetCI
# Check for duplicate element names
# Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения
# (рекурсивно по children/columns).
$elemTypeKeys = @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")
function Walk-ElemNames($el, [hashtable]$seen) {
$tk = $null
foreach ($k in $elemTypeKeys) { if ($el.$k -ne $null) { $tk = $k; break } }
if ($tk) { Assert-EditUnique -name (Get-ElementName -el $el -typeKey $tk) -seen $seen -ctx 'element name' }
if ($el.children) { foreach ($c in $el.children) { Walk-ElemNames $c $seen } }
if ($el.columns) { foreach ($c in $el.columns) { Walk-ElemNames $c $seen } }
}
$dslElemNames = @{}
foreach ($el in $def.elements) { Walk-ElemNames $el $dslElemNames }
# Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется)
foreach ($el in $def.elements) {
$typeKey = $null
foreach ($key in @("group","input","check","label","labelField","table","pages","page","button","picture","picField","calendar","cmdBar","popup")) {
foreach ($key in $elemTypeKeys) {
if ($el.$key -ne $null) { $typeKey = $key; break }
}
if ($typeKey) {
$elName = Get-ElementName -el $el -typeKey $typeKey
$existing = Find-Element $rootCI $elName
if ($existing) {
Write-Host "[WARN] Element '$elName' already exists in form (id=$($existing.GetAttribute('id')))"
Write-Host "[ERROR] Element '$elName' already exists in form (id=$($existing.GetAttribute('id'))) — element names must be unique"
exit 1
}
}
}
@@ -953,6 +1096,22 @@ if ($def.attributes -and $def.attributes.Count -gt 0) {
$attrChildIndent = Get-ChildIndent $attrsSection
if (-not $attrChildIndent -or $attrChildIndent -eq "") { $attrChildIndent = "`t`t" }
# Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и
# против уже существующих реквизитов формы.
$dslAttrNames = @{}
foreach ($attr in $def.attributes) {
Assert-EditUnique -name "$($attr.name)" -seen $dslAttrNames -ctx 'attribute name'
if ($attr.columns) {
$dslColNames = @{}
foreach ($col in $attr.columns) { Assert-EditUnique -name "$($col.name)" -seen $dslColNames -ctx "column name of '$($attr.name)'" }
}
$existingAttr = $attrsSection.SelectSingleNode("f:Attribute[@name='$($attr.name)']", $nsMgr)
if ($existingAttr) {
Write-Host "[ERROR] Attribute '$($attr.name)' already exists in form — attribute names must be unique"
exit 1
}
}
# Generate attribute fragments
$script:xml = New-Object System.Text.StringBuilder 2048
X "<_F $allNsDecl>"
@@ -1021,6 +1180,17 @@ if ($def.commands -and $def.commands.Count -gt 0) {
$cmdChildIndent = Get-ChildIndent $cmdsSection
if (-not $cmdChildIndent -or $cmdChildIndent -eq "") { $cmdChildIndent = "`t`t" }
# Уникальность имён команд: внутри JSON-определения и против существующих команд формы.
$dslCmdNames = @{}
foreach ($cmd in $def.commands) {
Assert-EditUnique -name "$($cmd.name)" -seen $dslCmdNames -ctx 'command name'
$existingCmd = $cmdsSection.SelectSingleNode("f:Command[@name='$($cmd.name)']", $nsMgr)
if ($existingCmd) {
Write-Host "[ERROR] Command '$($cmd.name)' already exists in form — command names must be unique"
exit 1
}
}
# Generate command fragments
$script:xml = New-Object System.Text.StringBuilder 1024
X "<_F $allNsDecl>"
+212 -3
View File
@@ -1,4 +1,4 @@
# form-edit v1.0 — Edit 1C managed form elements (Python port)
# form-edit v1.3 — Edit 1C managed form elements (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -11,6 +11,165 @@ from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
# ── arg parsing ──────────────────────────────────────────────
parser = argparse.ArgumentParser(allow_abbrev=False)
@@ -63,6 +222,7 @@ if not os.path.exists(json_path):
sys.exit(1)
resolved_form_path = os.path.abspath(form_path)
assert_edit_allowed(resolved_form_path, "editable")
xml_parser = etree.XMLParser(remove_blank_text=False)
try:
tree = etree.parse(resolved_form_path, xml_parser)
@@ -365,6 +525,14 @@ def get_element_name(el, type_key):
return str(el[type_key])
def _assert_edit_unique(name, seen, ctx):
# Уникальность имён внутри JSON-определения (1С: своя коллекция — свой неймспейс).
if name in seen:
print(f"[ERROR] Duplicate {ctx} '{name}' in JSON definition — names must be unique in 1C form")
sys.exit(1)
seen.add(name)
known_events = {
"input": ["OnChange", "StartChoice", "ChoiceProcessing", "AutoComplete", "TextEditEnd", "Clearing", "Creating", "EditTextChange"],
"check": ["OnChange"],
@@ -988,7 +1156,26 @@ if elements_list:
# Detect indent level
child_indent = get_child_indent(target_ci)
# Check for duplicate element names
# Имена элементов уникальны (требование 1С). Сначала — внутри самого JSON-определения
# (рекурсивно по children/columns).
def _walk_elem_names(el, seen):
tk = None
for key in ELEMENT_KEYS:
if key in el and el[key] is not None:
tk = key
break
if tk:
_assert_edit_unique(get_element_name(el, tk), seen, "element name")
for c in el.get("children", []):
_walk_elem_names(c, seen)
for c in el.get("columns", []):
_walk_elem_names(c, seen)
dsl_elem_names = set()
for el in elements_list:
_walk_elem_names(el, dsl_elem_names)
# Затем — против уже существующих элементов формы (дубль = битый XML, форма не откроется).
for el in elements_list:
type_key = None
for key in ELEMENT_KEYS:
@@ -999,7 +1186,8 @@ if elements_list:
el_name = get_element_name(el, type_key)
existing = find_element(root_ci, el_name) if root_ci is not None else None
if existing is not None:
print(f"[WARN] Element '{el_name}' already exists in form (id={existing.get('id')})")
print(f"[ERROR] Element '{el_name}' already exists in form (id={existing.get('id')}) — element names must be unique")
sys.exit(1)
# Remember starting element ID for companion counting
start_elem_id = next_elem_id
@@ -1055,6 +1243,19 @@ if attrs_list:
if not attr_child_indent:
attr_child_indent = "\t\t"
# Уникальность имён реквизитов: внутри JSON-определения (+ колонки в пределах реквизита) и
# против уже существующих реквизитов формы.
dsl_attr_names = set()
for attr in attrs_list:
_assert_edit_unique(str(attr["name"]), dsl_attr_names, "attribute name")
if attr.get("columns"):
dsl_col_names = set()
for col in attr["columns"]:
_assert_edit_unique(str(col["name"]), dsl_col_names, f"column name of '{attr['name']}'")
if attrs_section.find(f"f:Attribute[@name='{attr['name']}']", NS) is not None:
print(f"[ERROR] Attribute '{attr['name']}' already exists in form — attribute names must be unique")
sys.exit(1)
# Generate attribute fragments
xml_lines.clear()
X(f"<_F {ALL_NS_DECL}>")
@@ -1116,6 +1317,14 @@ if cmds_list:
if not cmd_child_indent:
cmd_child_indent = "\t\t"
# Уникальность имён команд: внутри JSON-определения и против существующих команд формы.
dsl_cmd_names = set()
for cmd in cmds_list:
_assert_edit_unique(str(cmd["name"]), dsl_cmd_names, "command name")
if cmds_section.find(f"f:Command[@name='{cmd['name']}']", NS) is not None:
print(f"[ERROR] Command '{cmd['name']}' already exists in form — command names must be unique")
sys.exit(1)
xml_lines.clear()
X(f"<_F {ALL_NS_DECL}>")
for cmd in cmds_list:
+65 -1
View File
@@ -1,4 +1,4 @@
# form-info v1.3 — Analyze 1C managed form structure
# form-info v1.4 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
@@ -368,6 +368,69 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
}
}
# --- Support status (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
# uuid of the nearest element meta-xml (form/template/etc.) and the config root
# bin. Never throws — degrades to "не на поддержке".
function Get-SupportStatusForPath([string]$targetPath) {
try {
$rp = (Resolve-Path $targetPath).Path
$elemUuid = $null
$binPath = $null
# Reads the uuid of the first metadata element in an .xml file (or $null).
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp
$d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
}
if ($elemUuid -and $binPath) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $h.Success) { return "не на поддержке" }
$G = [int]$h.Groups[1].Value
$K = [int]$h.Groups[2].Value
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
if (-not $elemUuid) { return "не на поддержке" }
$u = [regex]::Escape($elemUuid.ToLower())
$best = $null
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
if ($null -eq $best) { return "не на поддержке" }
switch ($best) {
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
1 { return "редактируется с сохранением поддержки" }
2 { return "снято с поддержки (правки свободны)" }
}
return "не на поддержке"
} catch { return "не на поддержке" }
}
# --- Collect output ---
$lines = @()
@@ -385,6 +448,7 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
if ($objectContext) { $header += " ($objectContext)" }
$header += " ==="
$lines += $header
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
# --- Form properties (Title excluded — shown in header) ---
+74 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.3 — Analyze 1C managed form structure
# form-info v1.4 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -336,6 +336,78 @@ def build_tree(child_items_node, prefix, tree_lines, expand="", state=None):
build_tree(ci, prefix + continuation, tree_lines, expand, state)
# --- Support status (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Walks up from the target path, taking the
# uuid of the nearest element meta-xml and the config root bin. Never throws —
# degrades to "не на поддержке".
def get_support_status_for_path(target_path):
try:
def root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
pass
return None
rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp)
bin_path = None
d = os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = root_uuid(d + ".xml")
if not bin_path:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
bin_path = cand
if elem_uuid and bin_path:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not bin_path or not os.path.exists(bin_path):
return "не на поддержке"
data = open(bin_path, "rb").read()
if len(data) <= 32:
return "снято с поддержки (правки свободны)"
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return "не на поддержке"
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return "снято с поддержки (правки свободны)"
if g == 1:
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
if not elem_uuid:
return "не на поддержке"
best = None
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
if best is None:
return "не на поддержке"
return {
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
1: "редактируется с сохранением поддержки",
2: "снято с поддержки (правки свободны)",
}.get(best, "не на поддержке")
except Exception:
return "не на поддержке"
# --- Main ---
def main():
@@ -441,6 +513,7 @@ def main():
header += f" ({object_context})"
header += " ==="
lines.append(header)
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}")
# --- Form properties (Title excluded -- shown in header) ---
prop_names = [
@@ -1,4 +1,4 @@
# form-validate v1.4 — Validate 1C managed form
# form-validate v1.7 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -155,7 +155,8 @@ if (-not $stopped) {
# --- Collect all elements with IDs ---
$elementIds = @{} # id -> name (element ID pool)
$elementIds = @{} # id -> name (element ID pool)
$elementNames = @{} # name -> id (имена элементов уникальны в пределах формы)
$allElements = @() # @{Name; Tag; Id; ParentName; Node}
function Collect-Elements {
@@ -185,6 +186,13 @@ function Collect-Elements {
} else {
$elementIds[$id] = $name
}
# Имена элементов уникальны (требование 1С)
if ($elementNames.ContainsKey($name)) {
Report-Error "Duplicate element name '$name': id=${id} and id=$($elementNames[$name])"
} else {
$elementNames[$name] = $id
}
}
# Recurse into ChildItems
@@ -229,6 +237,10 @@ foreach ($attr in $attrNodes) {
$attrName = $attr.GetAttribute("name")
$attrId = $attr.GetAttribute("id")
if ($attrName) {
# Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов)
if ($attrMap.ContainsKey($attrName)) {
Report-Error "Duplicate attribute name '$attrName': id=${attrId} and id=$($attrMap[$attrName].GetAttribute('id'))"
}
$attrMap[$attrName] = $attr
}
if ($attrId -and $attrId -ne "") {
@@ -241,6 +253,7 @@ foreach ($attr in $attrNodes) {
# Column IDs are a separate sub-pool per attribute — check uniqueness within parent
$colIds = @{}
$colNames = @{} # имена колонок уникальны в пределах своего реквизита
foreach ($col in $attr.SelectNodes("f:Columns/f:Column", $nsMgr)) {
$colId = $col.GetAttribute("id")
$colName = $col.GetAttribute("name")
@@ -251,6 +264,13 @@ foreach ($attr in $attrNodes) {
$colIds[$colId] = $colName
}
}
if ($colName) {
if ($colNames.ContainsKey($colName)) {
Report-Error "Duplicate column name '$colName' in '$attrName': id=${colId} and id=$($colNames[$colName])"
} else {
$colNames[$colName] = $colId
}
}
}
}
@@ -270,6 +290,10 @@ foreach ($cmd in $cmdNodes) {
$cmdName = $cmd.GetAttribute("name")
$cmdId = $cmd.GetAttribute("id")
if ($cmdName) {
# Имена команд уникальны среди команд (отдельный неймспейс)
if ($cmdMap.ContainsKey($cmdName)) {
Report-Error "Duplicate command name '$cmdName': id=${cmdId} and id=$($cmdMap[$cmdName].GetAttribute('id'))"
}
$cmdMap[$cmdName] = $cmd
}
if ($cmdId -and $cmdId -ne "") {
@@ -290,6 +314,20 @@ if (-not $stopped) {
}
}
# --- Collect parameters (separate name pool, без id) ---
$paramNames = @{} # name -> $true (имена параметров уникальны среди параметров)
foreach ($param in $root.SelectNodes("f:Parameters/f:Parameter", $nsMgr)) {
$paramName = $param.GetAttribute("name")
if ($paramName) {
if ($paramNames.ContainsKey($paramName)) {
Report-Error "Duplicate parameter name '$paramName'"
} else {
$paramNames[$paramName] = $true
}
}
}
# --- Check 4: Companion elements ---
# Define required companions per element type
@@ -366,13 +404,51 @@ if (-not $stopped) {
$dataPath = $dpNode.InnerText.Trim()
if (-not $dataPath) { continue }
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
# - bare numeric (e.g. "10", "1000003") — internal index
# - "N/M:<uuid>" — metadata reference by UUID
if ($dataPath -match '^\d+$' -or $dataPath -match '^\d+/\d+:[0-9a-fA-F-]+$') {
continue
}
$pathChecked++
# Extract root segment of path, strip array indices like [0]
$cleanPath = $dataPath -replace '\[\d+\]', ''
# Strip leading '~' (current row of DynamicList: ~Список.Поле)
if ($cleanPath.StartsWith('~')) { $cleanPath = $cleanPath.Substring(1) }
$segments = $cleanPath -split '\.'
$rootAttr = $segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... — table element, not attribute
if ($rootAttr -eq 'Items') {
if ($segments.Count -lt 3 -or $segments[2] -ne 'CurrentData') {
Report-Warn "[$tag] '$elName': DataPath='$dataPath' — unknown Items.* shape, expected Items.<Table>.CurrentData.*"
continue
}
$tableName = $segments[1]
$tableEl = $null
foreach ($candidate in $allElements) {
if ($candidate.Tag -eq 'Table' -and $candidate.Name -eq $tableName) {
$tableEl = $candidate
break
}
}
if (-not $tableEl) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — table element '$tableName' not found"
$pathErrors++
continue
}
$tableDpNode = $tableEl.Node.SelectSingleNode("f:DataPath", $nsMgr)
if (-not $tableDpNode -or -not $tableDpNode.InnerText.Trim()) {
# Table without DataPath — can't resolve further, accept silently
continue
}
$tableDp = $tableDpNode.InnerText.Trim() -replace '\[\d+\]', ''
if ($tableDp.StartsWith('~')) { $tableDp = $tableDp.Substring(1) }
$rootAttr = ($tableDp -split '\.')[0]
}
if (-not $attrMap.ContainsKey($rootAttr)) {
Report-Error "[$tag] '$elName': DataPath='$dataPath' — attribute '$rootAttr' not found"
$pathErrors++
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.4 — Validate 1C managed form
# form-validate v1.7 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -182,7 +182,8 @@ def main():
report_error("AutoCommandBar element missing")
# --- Collect all elements with IDs ---
element_ids = {} # id -> name
element_ids = {} # id -> name
element_names = {} # name -> id (имена элементов уникальны в пределах формы)
all_elements = [] # list of dicts {Name, Tag, Id, ParentName, Node}
def collect_elements(node, parent_name):
@@ -211,6 +212,12 @@ def main():
else:
element_ids[eid] = name
# Имена элементов уникальны (требование 1С)
if name in element_names:
report_error(f"Duplicate element name '{name}': id={eid} and id={element_names[name]}")
else:
element_names[name] = eid
child_items = child.find(f"{{{F_NS}}}ChildItems")
if child_items is not None:
collect_elements(child_items, name)
@@ -252,6 +259,9 @@ def main():
attr_name = attr.get("name", "")
attr_id = attr.get("id", "")
if attr_name:
# Имена реквизитов уникальны среди реквизитов (отдельный неймспейс от элементов)
if attr_name in attr_map:
report_error(f"Duplicate attribute name '{attr_name}': id={attr_id} and id={attr_map[attr_name].get('id', '')}")
attr_map[attr_name] = attr
if attr_id:
if attr_id in attr_ids:
@@ -261,6 +271,7 @@ def main():
# Column IDs uniqueness within parent
col_ids = {}
col_names = {} # имена колонок уникальны в пределах своего реквизита
columns = attr.find(f"{{{F_NS}}}Columns")
if columns is not None:
for col in columns.findall(f"{{{F_NS}}}Column"):
@@ -271,6 +282,11 @@ def main():
report_error(f"Duplicate column id={col_id} in '{attr_name}': '{col_name}' and '{col_ids[col_id]}'")
else:
col_ids[col_id] = col_name
if col_name:
if col_name in col_names:
report_error(f"Duplicate column name '{col_name}' in '{attr_name}': id={col_id} and id={col_names[col_name]}")
else:
col_names[col_name] = col_id
if not stopped:
if attr_ids:
@@ -289,6 +305,9 @@ def main():
cmd_name = cmd.get("name", "")
cmd_id = cmd.get("id", "")
if cmd_name:
# Имена команд уникальны среди команд (отдельный неймспейс)
if cmd_name in cmd_map:
report_error(f"Duplicate command name '{cmd_name}': id={cmd_id} and id={cmd_map[cmd_name].get('id', '')}")
cmd_map[cmd_name] = cmd
if cmd_id:
if cmd_id in cmd_ids:
@@ -300,6 +319,18 @@ def main():
if cmd_ids:
report_ok(f"Unique command IDs: {len(cmd_ids)} entries")
# --- Collect parameters (separate name pool, без id) ---
param_names = {} # name -> True (имена параметров уникальны среди параметров)
params_parent = root.find(f"{{{F_NS}}}Parameters")
if params_parent is not None:
for param in params_parent.findall(f"{{{F_NS}}}Parameter"):
param_name = param.get("name", "")
if param_name:
if param_name in param_names:
report_error(f"Duplicate parameter name '{param_name}'")
else:
param_names[param_name] = True
# --- Check 4: Companion elements ---
companion_rules = {
"InputField": ["ContextMenu", "ExtendedTooltip"],
@@ -376,12 +407,44 @@ def main():
if not data_path:
continue
# Opaque platform-internal DataPath shapes — not validatable from Form.xml alone:
# - bare numeric (e.g. "10", "1000003") — internal index
# - "N/M:<uuid>" — metadata reference by UUID
if re.match(r'^\d+$', data_path) or re.match(r'^\d+/\d+:[0-9a-fA-F-]+$', data_path):
continue
path_checked += 1
clean_path = re.sub(r'\[\d+\]', '', data_path)
# Strip leading '~' (current row of DynamicList: ~\u0421\u043f\u0438\u0441\u043e\u043a.\u041f\u043e\u043b\u0435)
if clean_path.startswith('~'):
clean_path = clean_path[1:]
segments = clean_path.split(".")
root_attr = segments[0]
# Resolve Items.<TableName>.CurrentData.<Field>... \u2014 table element, not attribute
if root_attr == 'Items':
if len(segments) < 3 or segments[2] != 'CurrentData':
report_warn(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 unknown Items.* shape, expected Items.<Table>.CurrentData.*")
continue
table_name = segments[1]
table_el = None
for candidate in all_elements:
if candidate["Tag"] == 'Table' and candidate["Name"] == table_name:
table_el = candidate
break
if table_el is None:
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 table element '{table_name}' not found")
path_errors += 1
continue
table_dp_node = table_el["Node"].find(f"{{{F_NS}}}DataPath")
if table_dp_node is None or not (table_dp_node.text or "").strip():
continue
table_dp = re.sub(r'\[\d+\]', '', (table_dp_node.text or "").strip())
if table_dp.startswith('~'):
table_dp = table_dp[1:]
root_attr = table_dp.split(".")[0]
if root_attr not in attr_map:
report_error(f"[{tag}] '{el_name}': DataPath='{data_path}' \u2014 attribute '{root_attr}' not found")
path_errors += 1
+123 -2
View File
@@ -1,4 +1,4 @@
# help-add v1.4 — Add built-in help to 1C object
# help-add v1.7 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -13,6 +13,124 @@ $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Detect format version ---
function Detect-FormatVersion([string]$dir) {
@@ -20,7 +138,8 @@ function Detect-FormatVersion([string]$dir) {
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$content = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
$head = $content.Substring(0, [Math]::Min(2000, $content.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -48,6 +167,8 @@ if (Test-Path $helpXmlPath) {
exit 1
}
Assert-EditAllowed $objectDir 'editable'
# --- Кодировка ---
$encBom = New-Object System.Text.UTF8Encoding($true)
+163 -1
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# add-help v1.4 — Add built-in help to 1C object
# add-help v1.7 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
@@ -12,6 +13,165 @@ from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
@@ -74,6 +234,8 @@ def main():
print(f"Справка уже существует: {help_xml_path}", file=sys.stderr)
sys.exit(1)
assert_edit_allowed(object_dir, "editable")
# --- 1. Help.xml ---
help_xml = (
@@ -1,4 +1,4 @@
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# interface-edit v1.6 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -23,6 +23,127 @@ if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
}
$resolvedPath = $CIPath
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow. Walk-up from Subsystems/X/Ext/
# CommandInterface.xml reaches Subsystems/X.xml (owning subsystem uuid).
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
Assert-EditAllowed $CIPath 'editable'
# --- Detect format version ---
function Detect-FormatVersion([string]$dir) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# interface-edit v1.3 — Edit 1C CommandInterface.xml
# interface-edit v1.6 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -10,6 +10,166 @@ import subprocess
import sys
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
def detect_format_version(d):
while d:
cfg_path = os.path.join(d, "Configuration.xml")
@@ -208,6 +368,8 @@ def main():
ci_path = os.path.join(os.getcwd(), ci_path)
resolved_path = ci_path
assert_edit_allowed(ci_path, "editable")
# --- Create if missing ---
if not os.path.isfile(ci_path):
if args.CreateIfMissing:
@@ -504,7 +666,7 @@ def main():
if os.path.isfile(validate_script):
print()
print("--- Running interface-validate ---")
subprocess.run([sys.executable, validate_script, "-CIPath", "-Path", resolved_path])
subprocess.run([sys.executable, validate_script, "-CIPath", resolved_path])
# --- Summary ---
print()
@@ -1,4 +1,4 @@
# meta-compile v1.11 — Compile 1C metadata object from JSON
# meta-compile v1.14 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -21,6 +21,126 @@ if (-not (Test-Path $JsonPath)) {
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
Assert-EditAllowed $OutputDir 'editable'
# --- Batch mode: JSON array of objects ---
if ($def -is [array] -or ($null -ne $def -and $def.GetType().BaseType.Name -eq 'Array')) {
$batchOk = 0
@@ -502,6 +622,7 @@ function Parse-AttributeShorthand {
fillChecking = if ($val.fillChecking) { "$($val.fillChecking)" } else { "" }
indexing = if ($val.indexing) { "$($val.indexing)" } else { "" }
multiLine = if ($val.multiLine -eq $true) { $true } else { $false }
choiceHistoryOnInput = if ($val.choiceHistoryOnInput) { "$($val.choiceHistoryOnInput)" } else { "" }
}
}
@@ -822,7 +943,8 @@ function Emit-Attribute {
X "$indent`t`t<CreateOnInput>Auto</CreateOnInput>"
X "$indent`t`t<ChoiceForm/>"
X "$indent`t`t<LinkByType/>"
X "$indent`t`t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>"
$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"
# Use — only for catalog top-level attributes
if ($context -eq "catalog") {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.11 — Compile 1C metadata object from JSON
# meta-compile v1.14 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -11,10 +11,169 @@ import sys
import tempfile
import uuid
import xml.etree.ElementTree as ET
from lxml import etree
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
# ---------------------------------------------------------------------------
# Inline utilities
# ---------------------------------------------------------------------------
@@ -83,6 +242,8 @@ with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = json.loads(json_text)
assert_edit_allowed(output_dir, "editable")
# --- Batch mode: JSON array of objects ---
if isinstance(defn, list):
batch_ok = 0
@@ -465,6 +626,7 @@ def parse_attribute_shorthand(val):
'fillChecking': str(val['fillChecking']) if val.get('fillChecking') else '',
'indexing': str(val['indexing']) if val.get('indexing') else '',
'multiLine': True if val.get('multiLine') is True else False,
'choiceHistoryOnInput': str(val['choiceHistoryOnInput']) if val.get('choiceHistoryOnInput') else '',
}
def parse_enum_value_shorthand(val):
@@ -774,7 +936,8 @@ def emit_attribute(indent, parsed, context):
X(f'{indent}\t\t<CreateOnInput>Auto</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm/>')
X(f'{indent}\t\t<LinkByType/>')
X(f'{indent}\t\t<ChoiceHistoryOnInput>Auto</ChoiceHistoryOnInput>')
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
if context == 'catalog':
X(f'{indent}\t\t<Use>ForItem</Use>')
if context not in ('processor', 'processor-tabular'):
+121 -1
View File
@@ -1,4 +1,4 @@
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -152,6 +152,126 @@ if (-not (Test-Path $ObjectPath)) {
}
$resolvedPath = (Resolve-Path $ObjectPath).Path
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
Assert-EditAllowed $resolvedPath 'editable'
# --- Load XML ---
$script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true
+164 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-edit v1.6 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# meta-edit v1.9 — Edit existing 1C metadata object XML (inline mode + complex properties + TS attribute ops + modify-ts)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -11,6 +11,166 @@ import sys
import uuid
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
# ============================================================
# Namespaces
# ============================================================
@@ -2169,6 +2329,8 @@ def main():
resolved_path = os.path.abspath(object_path)
assert_edit_allowed(resolved_path, "editable")
# --- Load XML ---
xml_parser = etree.XMLParser(remove_blank_text=False)
xml_tree = etree.parse(resolved_path, xml_parser)
@@ -2257,7 +2419,7 @@ def main():
print()
print("--- Running meta-validate ---")
python_exe = sys.executable
subprocess.run([python_exe, validate_script, "-ObjectPath", "-Path", resolved_path])
subprocess.run([python_exe, validate_script, "-ObjectPath", resolved_path])
else:
print()
print(f"[SKIP] meta-validate not found at: {validate_script}")
+74 -1
View File
@@ -1,4 +1,4 @@
# meta-info v1.1 — Compact summary of 1C metadata object
# meta-info v1.3 — Compact summary of 1C metadata object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ObjectPath,
@@ -415,6 +415,51 @@ function Get-WSOperations($childObjs) {
return $result
}
# --- Support status of this object (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
# object's support rule. Never throws — degrades to "не на поддержке".
function Get-ObjectSupportStatus([string]$objUuid) {
try {
# Walk up to the config root (dir with Configuration.xml or Ext/ParentConfigurations.bin).
$d = [System.IO.Path]::GetDirectoryName($ObjectPath)
$binPath = $null
for ($i = 0; $i -lt 8 -and $d; $i++) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand; break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $h.Success) { return "не на поддержке" }
$G = [int]$h.Groups[1].Value
$K = [int]$h.Groups[2].Value
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
if (-not $objUuid) { return "не на поддержке" }
# Conservative fold: locked if f1=0 in ANY vendor block.
$u = [regex]::Escape($objUuid.ToLower())
$best = $null
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
if ($null -eq $best) { return "не на поддержке" }
switch ($best) {
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
1 { return "редактируется с сохранением поддержки" }
2 { return "снято с поддержки (правки свободны)" }
}
return "не на поддержке"
} catch { return "не на поддержке" }
}
# --- Extract metadata ---
$props = $typeNode.SelectSingleNode("md:Properties", $ns)
$childObjs = $typeNode.SelectSingleNode("md:ChildObjects", $ns)
@@ -422,6 +467,22 @@ $objName = $props.SelectSingleNode("md:Name", $ns).InnerText
$synNode = $props.SelectSingleNode("md:Synonym", $ns)
$synonym = Get-MLText $synNode
# Presentations (type-choice dialogs show "Представление объекта" as the ref type name)
$objPresentation = Get-MLText $props.SelectSingleNode("md:ObjectPresentation", $ns)
$extObjPresentation = Get-MLText $props.SelectSingleNode("md:ExtendedObjectPresentation", $ns)
$listPresentation = Get-MLText $props.SelectSingleNode("md:ListPresentation", $ns)
$extListPresentation = Get-MLText $props.SelectSingleNode("md:ExtendedListPresentation", $ns)
# Reference (ref-typed) metadata objects — those with a ...Ref type
$refMdTypes = @("Catalog","Document","Enum","ChartOfAccounts","ChartOfCharacteristicTypes",
"ChartOfCalculationTypes","ExchangePlan","BusinessProcess","Task")
$isRefObject = $refMdTypes -contains $mdType
# Effective type presentation: ObjectPresentation -> Synonym -> Name
$typePresentation = if ($objPresentation) { $objPresentation }
elseif ($synonym) { $synonym }
else { $objName }
# --- Handle -Name drill-down ---
$drillDone = $false
if ($Name -and $childObjs) {
@@ -592,6 +653,18 @@ if (-not $drillDone) {
if ($synonym -and $synonym -ne $objName) { $header += "`"$synonym`"" }
$header += " ==="
Out $header
Out "Поддержка: $(Get-ObjectSupportStatus $typeNode.GetAttribute('uuid'))"
# --- Type presentation (ref objects) ---
if ($isRefObject) {
Out "Представление типа: $typePresentation"
if ($Mode -eq "full") {
if ($objPresentation) { Out "Представление объекта: $objPresentation" }
if ($extObjPresentation) { Out "Расширенное представление объекта: $extObjPresentation" }
if ($listPresentation) { Out "Представление списка: $listPresentation" }
if ($extListPresentation) { Out "Расширенное представление списка: $extListPresentation" }
}
}
# --- Mode: brief ---
if ($Mode -eq "brief") {
+83 -1
View File
@@ -1,4 +1,4 @@
# meta-info v1.1 — Compact summary of 1C metadata object (Python port)
# meta-info v1.3 — Compact summary of 1C metadata object (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -469,6 +469,59 @@ def get_ws_operations(child_objs):
return result
# ── Support status of this object (Ext/ParentConfigurations.bin) ──
# See docs/1c-support-state-spec.md. Walks up to the config root, decodes the
# object's support rule. Never throws — degrades to "не на поддержке".
def get_object_support_status(obj_uuid):
try:
d = os.path.dirname(object_path)
bin_path = None
for _ in range(8):
if not d:
break
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
bin_path = cand
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not bin_path or not os.path.exists(bin_path):
return "не на поддержке"
data = open(bin_path, "rb").read()
if len(data) <= 32:
return "снято с поддержки (правки свободны)"
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return "не на поддержке"
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return "снято с поддержки (правки свободны)"
if g == 1:
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
if not obj_uuid:
return "не на поддержке"
best = None
for m in re.finditer(r"([0-2]),0," + re.escape(obj_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
if best is None:
return "не на поддержке"
return {
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
1: "редактируется с сохранением поддержки",
2: "снято с поддержки (правки свободны)",
}.get(best, "не на поддержке")
except Exception:
return "не на поддержке"
# ── Extract metadata ─────────────────────────────────────────
props = find(type_node, "md:Properties")
@@ -477,6 +530,21 @@ obj_name = inner_text(find(props, "md:Name"))
syn_node = find(props, "md:Synonym")
synonym = get_ml_text(syn_node)
# Presentations (type-choice dialogs show "Представление объекта" as the ref type name)
obj_presentation = get_ml_text(find(props, "md:ObjectPresentation"))
ext_obj_presentation = get_ml_text(find(props, "md:ExtendedObjectPresentation"))
list_presentation = get_ml_text(find(props, "md:ListPresentation"))
ext_list_presentation = get_ml_text(find(props, "md:ExtendedListPresentation"))
# Reference (ref-typed) metadata objects — those with a ...Ref type
ref_md_types = {"Catalog", "Document", "Enum", "ChartOfAccounts",
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"ExchangePlan", "BusinessProcess", "Task"}
is_ref_object = md_type in ref_md_types
# Effective type presentation: ObjectPresentation -> Synonym -> Name
type_presentation = obj_presentation or synonym or obj_name
# ── Handle -Name drill-down ──────────────────────────────────
drill_done = False
@@ -635,6 +703,20 @@ if not drill_done:
header += f' \u2014 "{synonym}"'
header += " ==="
out(header)
out(f"Поддержка: {get_object_support_status(type_node.get('uuid', ''))}")
# Type presentation (ref objects)
if is_ref_object:
out(f"Представление типа: {type_presentation}")
if mode == "full":
if obj_presentation:
out(f"Представление объекта: {obj_presentation}")
if ext_obj_presentation:
out(f"Расширенное представление объекта: {ext_obj_presentation}")
if list_presentation:
out(f"Представление списка: {list_presentation}")
if ext_list_presentation:
out(f"Расширенное представление списка: {ext_list_presentation}")
if mode == "brief":
# Attributes
@@ -1,4 +1,4 @@
# meta-remove v1.1 — Remove metadata object from 1C configuration dump
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -78,6 +78,124 @@ if (-not (Test-Path $configXml)) {
exit 1
}
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Parse object spec ---
$parts = $Object -split "\.", 2
@@ -113,6 +231,9 @@ $typeDir = Join-Path $ConfigDir $typePlural
$objXml = Join-Path $typeDir "$objName.xml"
$objDir = Join-Path $typeDir $objName
# Support guard — removal requires the object be снят-с-поддержки (f1=2).
Assert-EditAllowed $objXml 'removed'
$hasXml = Test-Path $objXml
$hasDir = Test-Path $objDir -PathType Container
@@ -1,13 +1,174 @@
#!/usr/bin/env python3
# meta-remove v1.1 — Remove metadata object from 1C configuration dump
# meta-remove v1.3 — Remove metadata object from 1C configuration dump
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
import shutil
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
# --- Type -> plural directory mapping ---
TYPE_PLURAL_MAP = {
@@ -161,6 +322,9 @@ def main():
obj_xml = os.path.join(type_dir, f"{obj_name}.xml")
obj_dir = os.path.join(type_dir, obj_name)
# Support guard — removal requires the object be снят-с-поддержки (f1=2).
assert_edit_allowed(obj_xml, "removed")
has_xml = os.path.isfile(obj_xml)
has_dir = os.path.isdir(obj_dir)
@@ -1,4 +1,4 @@
# meta-validate v1.3 — Validate 1C metadata object structure
# meta-validate v1.4 — Validate 1C metadata object structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1,4 +1,4 @@
# meta-validate v1.3 — Validate 1C metadata object structure (Python port)
# meta-validate v1.4 — Validate 1C metadata object structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -31,7 +31,7 @@ if len(path_list) > 1:
batch_ok = 0
batch_fail = 0
for single_path in path_list:
cmd = [sys.executable, __file__, "-ObjectPath", "-Path", single_path, "-MaxErrors", str(max_errors)]
cmd = [sys.executable, __file__, "-ObjectPath", single_path, "-MaxErrors", str(max_errors)]
if detailed:
cmd.append("-Detailed")
if out_file:
@@ -1,4 +1,4 @@
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
# mxl-compile v1.3 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -11,6 +11,124 @@ param(
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- 1. Load and validate JSON ---
if (-not (Test-Path $JsonPath)) {
@@ -720,6 +838,7 @@ X '</document>'
$enc = New-Object System.Text.UTF8Encoding($true)
$resolvedPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path (Get-Location) $OutputPath }
Assert-EditAllowed $resolvedPath 'editable'
[System.IO.File]::WriteAllText($resolvedPath, $xml.ToString(), $enc)
# --- 9. Summary ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-compile v1.1 — Compile 1C spreadsheet from JSON
# mxl-compile v1.3 — Compile 1C spreadsheet from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -8,6 +8,167 @@ import os
import re
import sys
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
@@ -616,6 +777,8 @@ def main():
if not os.path.isabs(out_path):
out_path = os.path.join(os.getcwd(), out_path)
assert_edit_allowed(out_path, "editable")
out_dir = os.path.dirname(out_path)
if out_dir and not os.path.exists(out_dir):
os.makedirs(out_dir, exist_ok=True)
+61 -1
View File
@@ -1,4 +1,4 @@
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# mxl-info v1.1 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Alias('Path')]
@@ -321,11 +321,71 @@ if ($Format -eq "json") {
exit 0
}
function Get-SupportStatusForPath([string]$targetPath) {
try {
$rp = (Resolve-Path $targetPath).Path
$elemUuid = $null
$binPath = $null
# Reads the uuid of the first metadata element in an .xml file (or $null).
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp
$d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
}
if ($elemUuid -and $binPath) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $h.Success) { return "не на поддержке" }
$G = [int]$h.Groups[1].Value
$K = [int]$h.Groups[2].Value
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
if (-not $elemUuid) { return "не на поддержке" }
$u = [regex]::Escape($elemUuid.ToLower())
$best = $null
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
if ($null -eq $best) { return "не на поддержке" }
switch ($best) {
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
1 { return "редактируется с сохранением поддержки" }
2 { return "снято с поддержки (правки свободны)" }
}
return "не на поддержке"
} catch { return "не на поддержке" }
}
# --- Text format output ---
$lines = @()
$lines += "=== $templateName ==="
$lines += "Поддержка: $(Get-SupportStatusForPath $TemplatePath)"
$lines += " Rows: $docHeight, Columns: $defaultColCount"
if ($columnSets.Count -eq 0) {
+70 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# mxl-info v1.0 — Analyze 1C spreadsheet structure
# mxl-info v1.1 — Analyze 1C spreadsheet structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -308,10 +308,79 @@ if args.Format == "json":
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0)
def get_support_status_for_path(target_path):
try:
def root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
pass
return None
rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp)
bin_path = None
d = os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = root_uuid(d + ".xml")
if not bin_path:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
bin_path = cand
if elem_uuid and bin_path:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not bin_path or not os.path.exists(bin_path):
return "не на поддержке"
data = open(bin_path, "rb").read()
if len(data) <= 32:
return "снято с поддержки (правки свободны)"
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return "не на поддержке"
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return "снято с поддержки (правки свободны)"
if g == 1:
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
if not elem_uuid:
return "не на поддержке"
best = None
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
if best is None:
return "не на поддержке"
return {
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
1: "редактируется с сохранением поддержки",
2: "снято с поддержки (правки свободны)",
}.get(best, "не на поддержке")
except Exception:
return "не на поддержке"
# --- Text format output ---
lines = []
lines.append(f"=== {template_name} ===")
lines.append(f"Поддержка: {get_support_status_for_path(template_path)}")
lines.append(f" Rows: {doc_height}, Columns: {default_col_count}")
if len(column_sets) == 0:
@@ -1,4 +1,4 @@
# role-compile v1.5 — Compile 1C role from JSON
# role-compile v1.7 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -11,6 +11,124 @@ param(
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
$elemUuid = Get-RootUuid $rp
$cfgDir = $null; $binPath = $null
$d = if (Test-Path $rp -PathType Container) { $rp } else { [System.IO.Path]::GetDirectoryName($rp) }
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- 1. Load and validate JSON ---
if (-not (Test-Path $JsonPath)) {
@@ -523,6 +641,7 @@ function Detect-FormatVersion([string]$dir) {
}
$resolvedOutputDir = if ([System.IO.Path]::IsPathRooted($OutputDir)) { $OutputDir } else { Join-Path (Get-Location) $OutputDir }
Assert-EditAllowed $resolvedOutputDir 'editable'
$formatVersion = Detect-FormatVersion $resolvedOutputDir
# --- 8. Generate UUID ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# role-compile v1.4 — Compile 1C role from JSON
# role-compile v1.7 — Compile 1C role from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -8,6 +8,167 @@ import re
import sys
import uuid
from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
def detect_format_version(d):
while d:
@@ -476,6 +637,7 @@ def main():
defn['objects'] = defn['rights']
out_dir_resolved = args.OutputDir if os.path.isabs(args.OutputDir) else os.path.join(os.getcwd(), args.OutputDir)
assert_edit_allowed(out_dir_resolved, "editable")
format_version = detect_format_version(out_dir_resolved)
# --- 2. Parse all object entries ---
+61 -1
View File
@@ -1,4 +1,4 @@
# role-info v1.0 — Analyze 1C role rights
# role-info v1.1 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$RightsPath,
@@ -145,11 +145,71 @@ foreach ($tpl in $tplNodes) {
}
}
function Get-SupportStatusForPath([string]$targetPath) {
try {
$rp = (Resolve-Path $targetPath).Path
$elemUuid = $null
$binPath = $null
# Reads the uuid of the first metadata element in an .xml file (or $null).
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp
$d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
}
if ($elemUuid -and $binPath) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $h.Success) { return "не на поддержке" }
$G = [int]$h.Groups[1].Value
$K = [int]$h.Groups[2].Value
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
if (-not $elemUuid) { return "не на поддержке" }
$u = [regex]::Escape($elemUuid.ToLower())
$best = $null
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
if ($null -eq $best) { return "не на поддержке" }
switch ($best) {
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
1 { return "редактируется с сохранением поддержки" }
2 { return "снято с поддержки (правки свободны)" }
}
return "не на поддержке"
} catch { return "не на поддержке" }
}
# --- Output ---
$header = "=== Role: $roleName"
if ($roleSynonym) { $header += " --- `"$roleSynonym`"" }
$header += " ==="
Out $header
Out "Поддержка: $(Get-SupportStatusForPath $RightsPath)"
Out ""
Out "Properties: setForNewObjects=$setForNew, setForAttributesByDefault=$setForAttrs, independentRightsOfChildObjects=$independentChild"
+71 -1
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
# role-info v1.0 — Analyze 1C role rights
# role-info v1.1 — Analyze 1C role rights
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
import re
import sys
from collections import OrderedDict
from lxml import etree
@@ -147,12 +148,81 @@ for tpl in root.findall("r:restrictionTemplate", NSMAP):
t_name = t_name[:paren_idx]
templates.append(t_name)
def get_support_status_for_path(target_path):
try:
def root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
pass
return None
rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp)
bin_path = None
d = os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = root_uuid(d + ".xml")
if not bin_path:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
bin_path = cand
if elem_uuid and bin_path:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not bin_path or not os.path.exists(bin_path):
return "не на поддержке"
data = open(bin_path, "rb").read()
if len(data) <= 32:
return "снято с поддержки (правки свободны)"
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return "не на поддержке"
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return "снято с поддержки (правки свободны)"
if g == 1:
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
if not elem_uuid:
return "не на поддержке"
best = None
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
if best is None:
return "не на поддержке"
return {
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
1: "редактируется с сохранением поддержки",
2: "снято с поддержки (правки свободны)",
}.get(best, "не на поддержке")
except Exception:
return "не на поддержке"
# --- Output ---
header = f"=== Role: {role_name}"
if role_synonym:
header += f' --- "{role_synonym}"'
header += " ==="
out(header)
out(f"Поддержка: {get_support_status_for_path(rights_path)}")
out()
out(f"Properties: setForNewObjects={set_for_new}, setForAttributesByDefault={set_for_attrs}, independentRightsOfChildObjects={independent_child}")
+23 -3
View File
@@ -88,11 +88,20 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -V
Многоязычный заголовок: `"title": { "ru": "...", "en": "..." }`. Применимо везде, где принимается title/presentation (поля, calculatedFields, parameters, settingsVariants, availableValues и пр.). Строка эквивалентна `{ "ru": "..." }`.
Типы: `string`, `string(N)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
Типы: `string`, `string(N)`, `decimal`, `decimal(D)`, `decimal(D,F)`, `boolean`, `date`, `dateTime`, `CatalogRef.X`, `DocumentRef.X`, `EnumRef.X`, `StandardPeriod`. Ссылочные типы эмитируются с inline namespace `d5p1:` (`http://v8.1c.ru/8.1/data/enterprise/current-config`). Сборка EPF со ссылочными типами требует базу с соответствующей конфигурацией.
`decimal` без скобок = `10,2` (деньги по умолчанию), `decimal(N)` = `N,0` (целое); `,nonneg` в конце скобок → AllowedSign=Nonnegative.
Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу.
Роли: `@dimension`, `@account`, `@balance`, `@period`.
Роли (shorthand или объект):
- `@`-флаги: `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`
- KV: `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType`
```
"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
```
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
@@ -101,6 +110,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -V
Дополнительные ключи объектной формы:
- `"presentationExpression": "<выражение>"` — что показывать вместо значения поля. Исходное значение остаётся «под капотом» для перехода/расшифровки.
- `"appearance": { "<параметр>": "<значение>" }` — оформление колонки по умолчанию (применяется во всех вариантах настроек). Ключи — параметры платформы (`ГоризонтальноеПоложение`, `МинимальнаяШирина`, `Формат`, `Текст` и т.п.).
- `"orderExpression": { "expression": "<выражение>", "orderType": "Asc"/"Desc", "autoOrder": true/false }` — сортировка поля по выражению (например `ЕстьNULL(Поле.Порядок, 10000)`).
```json
{ "field": "Сумма", "title": "Сумма продажи", "type": "decimal(15,2)",
@@ -142,11 +152,21 @@ Shorthand: `"Имя [Заголовок]: тип = значение @флаги"
Флаги shorthand:
- `@autoDates` — добавляет к параметру StandardPeriod пару дат `НачалоПериода`/`КонецПериода`, вычисляемых из него. Используй их в тексте запроса как `&НачалоПериода`/`&КонецПериода`; пользователь выбирает только сам период. По умолчанию сам параметр получает `use=Always` и `denyIncompleteValues=true` (чтобы производные даты всегда были заполнены); в объектной форме можно явно переопределить.
- `@valueList``<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений
- `@valueList``<valueListAllowed>true</valueListAllowed>` — разрешает передавать список значений (при значении-списке ниже подразумевается автоматически)
- `@hidden` — скрытый параметр: `availableAsField=false` + исключается из `"dataParameters": "auto"`
Объектная форма: `title`, `hidden: true`, `valueListAllowed: true`, `availableAsField: false`, `denyIncompleteValues: true`, `use: "Always"`.
Значение-список: несколько значений по умолчанию через запятую в `значение` (для запятой внутри значения — кавычки `'...'`). В объектной форме — массив в `value`.
```json
"parameters": [
"Виды: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
]
```
Если значения по умолчанию нет — пропусти `=` в shorthand или укажи `"value": null` в объектной форме.
Список допустимых значений (availableValues):
```json
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
---
name: skd-decompile
description: Декомпиляция схемы компоновки данных 1С (СКД) в JSON-черновик в формате skd-compile. Используй для scaffold нового отчёта по образцу или структурного рефакторинга. Не для точечных правок
argument-hint: <TemplatePath> [-OutputPath <out.json>]
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /skd-decompile — JSON-черновик из Template.xml СКД
Читает Template.xml и эмитит JSON в формате `skd-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь».
## Когда использовать
- **Scaffold нового отчёта по образцу** — взять существующий СКД, получить JSON, поправить и скомпилировать в новый.
- **Структурный рефакторинг** — переписать вариант, перерисовать шаблон, перебрать набор полей.
## Когда **не** использовать
- **Точечные правки готового отчёта** (добавить поле, фильтр, итог, переименовать) → `/skd-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает шаблон целиком, может терять непокрытые конструкции и даёт большой diff в исходниках. `/skd-edit` правит адресно, без полной реконструкции.
## Параметры
| Параметр | Описание |
|----------|----------|
| `TemplatePath` | Путь к Template.xml (обязательный) |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-decompile.ps1" -TemplatePath "<Template.xml>" -OutputPath "<out.json>"
```
## Что получаешь
JSON-черновик в формате `/skd-compile`**не полное обратимое представление СКД**. На вход компилятору такой JSON напрямую может не пойти: в нём встречаются sentinel-узлы (маркер `__unsupported__`).
- **Готовые узлы** — большая часть СКД (поля, параметры, шаблоны, варианты со structure/filter/order/conditionalAppearance и т.п.) ложится в JSON как обычные узлы DSL.
- **Sentinel-узлы** — места, где встретилась конструкция, которую декомпилятор не умеет выразить в DSL. JSON остаётся валидным, но компилятор откажется его собирать, пока sentinel не **заменён ручной реализацией** (явный raw `template`, прописанный appearance и т.п.) **или не удалён**, если в новом отчёте конструкция не нужна. Это намеренный барьер — чтобы непокрытое не уехало в финальный отчёт незамеченным.
- **`<basename>.warnings.md`** рядом с `OutputPath` — список всех sentinel-узлов с координатами в исходнике, по нему удобно обходить места под ручную доработку.
- **Критичные конструкции** (Picture cells, ХранилищеЗначения, вложенные схемы, не-СКД root) — скрипт падает с ненулевым кодом и сообщением в stderr; такой Template как образец не годится.
## Workflow
1. `/skd-decompile <Template.xml> -OutputPath draft.json` — получить черновик.
2. Открыть `draft.warnings.md`, посмотреть, что не покрылось.
3. Поправить JSON под задачу. Sentinel-узлы — заменить на ручную реализацию (через явный raw `template`, через ручное описание appearance и т.п.) либо удалить, если конструкция в новом отчёте не нужна.
4. `/skd-compile -DefinitionFile draft.json -OutputPath new-Template.xml` — собрать обратно.
5. `/skd-validate` + `/skd-info` — проверить.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+80 -10
View File
@@ -52,11 +52,16 @@ Shorthand: `"Имя [Заголовок]: тип @роль #ограничени
Поле добавляется в набор и в selection варианта (если нет `-NoSelection`). Дубликат dataPath — предупреждение, пропуск.
Чтобы поле попало в selection не варианта, а конкретной группировки структуры — используй `-NoSelection` и затем `add-selection "Имя @group=ИмяГруппы"`.
### add-total — добавить итог
Shorthand: `"<dataPath>: <выражение>"`. Если выражение — известная аггрегатная функция без скобок (`Сумма`, `Количество`, `Минимум`, `Максимум`, `Среднее`), оно автоматически оборачивается в `Func(dataPath)`. Если функция со скобками или произвольное выражение — используется как есть.
```
"Цена: Среднее"
"Стоимость: Сумма(Кол * Цена)"
"Цена: Среднее" # → Среднее(Цена)
"Стоимость: Сумма(Кол * Цена)" # → как есть
"Проверка: Проверка" # identity: выражение = Проверка
```
### add-calculated-field — добавить вычисляемое поле
@@ -80,24 +85,54 @@ Shorthand: `"Имя [Заголовок]: тип = Выражение #noFilter
"Организация: CatalogRef.Организации"
```
Shorthand: `"Имя [Заголовок]: тип = значение @флаги"`. `[Заголовок]` опциональный — добавляет `<title>`.
Shorthand: `"Имя [Заголовок]: тип = значение [availableValue=список] [@флаги]"`. `[Заголовок]` опциональный — добавляет `<title>`.
`@autoDates` генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра — для БСП-отчётов, чтобы получить пару полей «Начало/Конец» в панели быстрых настроек.
Флаги:
- `@autoDates` — генерирует пару скрытых параметров `ДатаНачала`/`ДатаОкончания` для StandardPeriod-параметра.
- `@hidden` — скрывает параметр от пользовательских настроек (для параметров-констант, используемых в запросе).
- `@always` — параметр всегда подставляется в запрос. Часто вместе с `@hidden`, но используется и отдельно (для видимых обязательных параметров типа отчётного периода).
- `@valueList` — разрешает передавать в параметр список значений (при значении-списке ниже подразумевается автоматически, отдельно указывать не обязательно).
Значение-список: несколько значений по умолчанию задаются через запятую в `значение`. Для запятой внутри одного значения — кавычки `'...'`.
```
"Виды [Виды субконто]: ChartOfCharacteristicTypesRef.ВидыСубконтоХозрасчетные = ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
```
```
"ПС: CatalogRef.Контрагенты = Справочник.Контрагенты.ПустаяСсылка @hidden"
"Период: StandardPeriod = LastMonth @always"
"ПСчет: ChartOfAccountsRef.Хозрасчетный = ПланСчетов.Хозрасчетный.X @hidden @always"
"Округление: EnumRef.Округления = Окр1 availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс."
```
`availableValue=` задаёт начальный список допустимых значений. Формат списка: `v1[: p1], v2[: p2], ...` — элементы через `,`, представление после `:`. Если в значении или представлении встречается `,` или `:` — оборачивай в одинарные кавычки `'...'`:
```
"Округление: ... = Окр1 availableValue=Окр1_00: 'руб., коп.', Окр1: руб."
```
### modify-parameter — изменить существующий параметр
Находит параметр по имени, добавляет/обновляет свойства.
Shorthand: `"ИмяПараметра [Заголовок] [ключ=значение]... [@флаги]"`. Находит параметр по имени, обновляет указанные свойства.
```
"ПорядокОкругления use=Always"
"ПорядокОкругления [Округление сумм] denyIncompleteValues=true"
"ПериодОтчета [Отчетный период]" # только title
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1 presentation=руб."
"ПорядокОкругления availableValue=Перечисление.Округления.Окр1: руб., Перечисление.Округления.Окр1000: тыс."
"СчетПС value=ПланСчетов.Хозрасчетный.КассаПредприятия"
"Виды value=ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Контрагенты, ПланВидовХарактеристик.ВидыСубконтоХозрасчетные.Договоры"
"Контрагент @hidden @always"
```
`[Заголовок]` опциональный — устанавливает или заменяет `<title>`. Можно вызывать без других kv-пар, чтобы только обновить title.
`availableValue=` добавляет один элемент списка допустимых значений (можно несколько через `;;`). Тип значения определяется автоматически (DesignTimeValue для ссылок).
`availableValue=` **заменяет весь список** допустимых значений (старые удаляются). Формат и кавычки — те же, что в `add-parameter`.
`value=` заменяет значение параметра. Несколько значений через запятую → **список значений** (заменяет все прежние); для запятой внутри значения — кавычки `'...'`.
Флаги `@hidden` / `@always` — те же, что и в `add-parameter`. Идемпотентны.
### rename-parameter — переименовать параметр
@@ -229,15 +264,21 @@ Value — имена ресурсов (как в полях/вычисляемы
Не поддерживает пакетный режим. Value — полный текст запроса или `@path/to/file.sql` (ссылка на внешний файл). Путь разрешается относительно Template.xml, затем CWD.
Когда что: **существенная переработка** (добавить поля, соединения, переписать пакет) → выгрузи запрос через `/skd-info <tpl> -Mode query -Name <набор> -Raw -OutFile file.sql`, отредактируй файл, верни `set-query @file`. `-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат точны (включая многопакетные запросы). **Точечная замена** (переименовать идентификатор, заменить подстроку) → выгрузка не нужна, используй `patch-query` ниже.
### patch-query — точечная замена в тексте запроса
Shorthand: `"старое => новое"`. Заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
Shorthand: `"старое => новое [@once]"`. По умолчанию заменяет все вхождения подстроки. Поддерживает пакетный режим и `-DataSet`.
```
"СубконтоДт1) В => СубконтоКт1) В"
"ЛЕВОЕ СОЕДИНЕНИЕ => ВНУТРЕННЕЕ СОЕДИНЕНИЕ"
"КАК ВТ_СтароеИмя => КАК ВТ_НовоеИмя @once"
```
`@once` — упасть с ошибкой, если в запросе не **ровно одно** вхождение. Защищает от случайных замен в комментариях и однотипных идентификаторах.
Многострочные подстроки поддерживаются.
### set-outputParameter — установить параметр вывода
```
@@ -249,16 +290,27 @@ Shorthand: `"старое => новое"`. Заменяет все вхожде
### set-structure — установить структуру варианта
Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — детальные записи. Заменяет всю структуру. Не поддерживает пакетный режим.
Shorthand: `"Поле1 > Поле2 > details"`. `>` — вложенный уровень группировки, `,` — несколько полей в одном уровне, `details` — детальные записи. **Заменяет всю структуру полностью** (включая Selection/order/filter/conditionalAppearance каждой группы). Для точечной модификации полей группировки с сохранением настроек — используй `modify-structure`. Не поддерживает пакетный режим.
```
"Организация > Номенклатура > details"
"Валюта, НаименованиеБанка, ИНН"
"details"
"СчетМеждународногоУчета @name=ДанныеОтчета"
```
`@name=Имя` — присваивает имя группировке (`<dcsset:name>`). Используется для привязки шаблонов через `groupName`.
### modify-structure — изменить поля группировки существующей группы
Тот же shorthand что и `set-structure`. Находит группу по `@name=`, заменяет только `<groupItems>` (поля группировки). Selection/order/filter/conditionalAppearance/outputParameters группы сохраняются. Без `@name=` — ошибка.
```
"Валюта @name=ДанныеОтчета"
"Валюта, НаименованиеБанка @name=ДанныеОтчета"
"details @name=ДанныеОтчета"
```
### modify-field — изменить существующее поле
Тот же shorthand что и `add-field`. Находит по dataPath, объединяет свойства (непустые переопределяют), сохраняет позицию.
@@ -267,6 +319,23 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
"Цена [Цена USD]: decimal(10,4) @dimension"
```
### set-field-role — установить роль поля
Shorthand: `"<dataPath> [@флаги] [kv=значение]"`. **Полностью заменяет** роль поля. Если в значении только dataPath без флагов/kv — удаляет роль.
```
"Сумма" # снять роль полностью
"СуммаОстаток @balance" # простая балансовая роль
"СуммаНач @balance balanceGroupName=Сумма balanceType=OpeningBalance" # с уточнением
"Контрагент @dimension parentDimension=Группа"
"Период @period" # роль периода
```
Флаги: `@balance`, `@dimension`, `@account`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`.
KV: `balanceGroupName`, `balanceType` (OpeningBalance/ClosingBalance), `parentDimension`, `accountTypeExpression`, `orderType` (Asc/Desc), `expression`, `periodNumber`, `periodType`.
Поддерживает пакетный режим (`;;`).
### modify-filter — изменить существующий фильтр
Тот же shorthand что и `add-filter`. Находит по полю, обновляет оператор/значение/флаги. См. правило для `<use>` ниже.
@@ -294,6 +363,7 @@ Shorthand: `"Поле1 > Поле2 > details"`. `details`/`детали` — д
| `clear-selection` | `*` | Очищает все элементы selection |
| `clear-order` | `*` | Очищает все элементы order |
| `clear-filter` | `*` | Очищает все элементы filter |
| `clear-conditionalAppearance` | `*` | Очищает все правила условного оформления |
## Верификация
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -4
View File
@@ -1,7 +1,7 @@
---
name: skd-info
description: Анализ структуры схемы компоновки данных 1С (СКД) — наборы, поля, параметры, варианты. Используй для понимания отчёта — источник данных (запрос), доступные поля, параметры
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>]
argument-hint: <TemplatePath> [-Mode overview|query|fields|links|calculated|resources|params|variant|templates|trace|full] [-Name <dataset|variant|field|group>] [-Raw]
allowed-tools:
- Bash
- Read
@@ -20,7 +20,8 @@ allowed-tools:
| `Mode` | Режим анализа (по умолчанию `overview`) |
| `Name` | Имя набора (query), поля (fields/calculated/resources/trace), варианта (variant) или группировки/поля (templates) |
| `Batch` | Номер пакета запроса, 0 = все (только query) |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `Raw` | (только query) сырой текст запроса целиком, без заголовков/оглавления/разделителей пакетов. Для выгрузки в `.sql` и возврата через `skd-edit set-query @file` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк; `-Raw` не усекается) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
@@ -31,6 +32,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
```powershell
... -Mode query -Name НоменклатураСЦенами
... -Mode query -Name ДанныеТ13 -Batch 3
... -Mode query -Name ДанныеТ13 -Raw -OutFile query.sql
... -Mode fields -Name КадастроваяСтоимость
... -Mode calculated -Name КоэффициентКи
... -Mode resources -Name СуммаНалога
@@ -45,7 +47,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
| Режим | Без `-Name` | С `-Name` |
|-------|-------------|-----------|
| `overview` | Навигационная карта схемы + подсказки Next | — |
| `query` | — | Текст запроса набора (с оглавлением батчей) |
| `query` | — | Текст запроса набора (с оглавлением батчей); `-Raw` — чистая выгрузка для правки |
| `fields` | Карта: имена полей по наборам | Деталь поля: набор, тип, роль, формат |
| `links` | Все связи наборов | — |
| `calculated` | Карта: имена вычисляемых полей | Выражение + заголовок + ограничения |
@@ -65,7 +67,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-info.ps1" -Temp
3. `query -Name <набор>` — посмотреть текст SQL-запроса
4. `variant -Name <N>` — посмотреть группировки и фильтры варианта
Подробные примеры вывода каждого режима — в `modes-reference.md`.
Переработка запроса (round-trip): `query -Name <набор> -Raw -OutFile q.sql`
правка `q.sql``/skd-edit <tpl> -Operation set-query -Value "@q.sql"`. Флаг
`-Raw` отдаёт запрос целиком без декораций, поэтому выгрузка ↔ возврат
точны (включая многопакетные запросы с временными таблицами).
## Верификация
-246
View File
@@ -1,246 +0,0 @@
# /skd-info — полная справка по режимам
Компактное описание — в [SKILL.md](SKILL.md).
## overview (по умолчанию) — карта схемы
Компактная навигационная карта (10-25 строк). Показывает структуру и подсказывает следующие шаги:
```
=== DCS: ОсновнаяСхемаКомпоновкиДанных (362 lines) ===
Sources: ИсточникДанных1 (Local)
Datasets:
[Query] НоменклатураСЦенами 7 fields, query 40 lines
Calculated: 1
Resources: 1
Templates: 1 templates, 1 group bindings
Params: (none)
Variants:
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
Next:
-Mode query query text
-Mode fields field tables by dataset
-Mode calculated calculated field expressions
-Mode resources resource aggregation
-Mode variant -Name <N> variant structure (1..2)
```
Для DataSetUnion — дерево наборов + связи:
```
Datasets:
[Union] РасчетНалогаНаИмущество 52 fields
├─ [Query] РасчетНалогаНаИмущество 51 fields, query 181 lines
├─ [Query] ДанныеПоКадастровой 29 fields, query 40 lines
├─ [Query] ДанныеПоСреднегодовой 34 fields, query 41 lines
Links: РасчетНалогаНаИмущество -> СостояниеОС (2 fields)
```
Параметры разделяются на видимые/скрытые:
```
Params: 18 (7 visible, 11 hidden): Период, Ответственный, ...
```
## query — текст запроса
`-Name <набор>` — имя DataSet (обязателен если наборов > 1).
Извлекает raw-текст запроса с деэкранированием XML (`&amp;``&`, `&gt;``>`). Для пакетных запросов — оглавление батчей:
```
=== Query: ДанныеТ13 (334 lines, 13 batches) ===
Batch 1: lines 1-8 → ПОМЕСТИТЬ Представления_Периоды
Batch 2: lines 9-26 → ПОМЕСТИТЬ Представления_СотрудникиОрганизации
...
--- Batch 1 ---
ВЫБРАТЬ
ДАТАВРЕМЯ(1, 1, 1) КАК Период
ПОМЕСТИТЬ Представления_Периоды
...
```
Фильтр по номеру батча: `-Batch 3` покажет только 3-й пакет.
## fields — поля наборов данных
Без `-Name` — карта: имена полей по наборам:
```
=== Fields map ===
СостояниеОС [Query] (3): Организация, ОсновноеСредство, ДатаСостояния
РасчетНалогаНаИмущество [Union] (52): ДоляСтоимостиЧислитель, ...
РасчетНалогаНаИмущество [Query] (51): КадастроваяСтоимость, ...
```
С `-Name <поле>` — детали конкретного поля:
```
=== Field: ДатаСостояния "Дата ввода в эксплуатацию" ===
Dataset: СостояниеОС [Query]
Format: ДФ=dd.MM.yyyy
```
Показывает: dataset, title, type, role, useRestriction, format, presentationExpression.
## links — связи наборов данных
```
=== Links (4) ===
РасчетНалогаНаИмущество -> СостояниеОС :
Организация -> Организация
ОсновноеСредство -> ОсновноеСредство
```
Группирует по парам наборов. Показывает поля связи и параметры.
## calculated — вычисляемые поля
Без `-Name` — карта: имена и заголовки:
```
=== Calculated fields (23) ===
ДоляСтоимости "Доля стоимости"
КоэффициентКи "Коэффициент Ки"
...
```
С `-Name <поле>` — полное выражение:
```
=== Calculated: ДоляСтоимости ===
Expression:
ВЫБОР КОГДА ... ТОГДА "1" ИНАЧЕ ... КОНЕЦ
Title: Доля стоимости
Restrict: condition
```
## resources — ресурсы (итоги по группировкам)
Без `-Name` — карта: имена полей, `*` = есть формулы по группировкам:
```
=== Resources (51) ===
НалоговаяБаза
КоэффициентКи *
...
* = has group-level formulas
```
С `-Name <поле>` — формулы агрегации:
```
=== Resource: ДатаСостояния ===
[ОсновноеСредство] ЕстьNull(ДатаСостояния, "")
```
## params — параметры схемы
```
=== Parameters (16) ===
Name Type Default Visible Expression
Период StandardPeriod LastMonth yes -
НачалоПериода DateTime - hidden &Период.ДатаНачала
Организация CatalogRef.Организации null yes -
```
## variant — варианты отчёта
Без `-Name` — список вариантов:
```
=== Variants (2) ===
[1] НоменклатураИЦены "Номенклатура и цены" Table(detail) 3 filters
[2] НоменклатураБезЦен "Номенклатура без цен" Group(detail) 2 filters
```
С `-Name <N|имя>` — структура конкретного варианта:
```
=== Variant [1]: НоменклатураИЦены "Номенклатура и цены" ===
Structure:
Table "Таблица"
├── Columns: [ТипЦен Items]
│ Selection: Auto, Цена
└── Rows: [Номенклатура Items]
Selection: Номенклатура, УИД, Auto
Filter:
[ ] Номенклатура InHierarchy [user]
[ ] ТипЦен Equal
[x] ВАрхиве = false "Исключая скрытые товары"
DataParams: КлючВарианта="НоменклатураИЦены"
Output: style=ЧерноБелый groups=Separately totalsH=None totalsV=None
```
## templates — привязки шаблонов вывода
Три типа привязок: `fieldTemplate` (к полю), `groupTemplate` (к группировке, Header/Footer), `groupHeaderTemplate` (заголовок группы).
Без `-Name` — карта привязок:
```
=== Templates (70 defined: 49 field, 37 group) ===
Field bindings (49): (all trivial)
ОстаточнаяСтоимостьНа0101, ОстаточнаяСтоимостьНа0102, ...
Group bindings (37):
ВидНалоговойБазы
Header -> Макет3 (1 rows, 1 params)
СреднегодоваяСтоимость2019
Footer -> Макет50 (1 rows) spacer
GroupHeader -> Макет40 (3 rows)
```
С `-Name <группировка|поле>` — содержимое шаблонов:
```
=== Templates: СреднегодоваяСтоимость2019 ===
Footer -> Макет50 [1 rows, 1 cells]:
Row 1: (empty)
GroupHeader -> Макет40 [3 rows, 78 cells]:
Row 1: "№ п/п" | "###Группировки1###" | "Инв. номер" | ...
Row 2: "01.01" | "01.02" | ... | "31.12"
Row 3: "1" | "2" | ... | "26"
```
Для field-привязок:
```
=== Field template: ОстаточнаяСтоимостьНа0101 -> Макет4 ===
[1 rows, 1 cells]
Row 1: {ОстаточнаяСтоимостьНа0101}
(all params trivial)
```
**Тривиальность выражений**: `Поле = Поле` и `Поле = Представление(Поле)` считаются тривиальными и НЕ выводятся. Показываются только нетривиальные — когда выражение содержит другое поле, вызов метода, пустую строку и т.д.
## trace — трассировка поля от заголовка до запроса
Ищет поле по dataPath ИЛИ заголовку (включая подстроку) и показывает полную цепочку происхождения за один вызов:
```
=== Trace: КоэффициентКи "Коэффициент Ки" ===
Dataset: (schema-level only, not in dataset fields)
Calculated:
ВЫБОР КОГДА ... ТОГДА 0 ИНАЧЕ ... КОНЕЦ
Operands:
КоличествоМесяцевИспользования -> РасчетНалогаНаИмущество [Query]
КоличествоМесяцевВладения -> РасчетНалогаНаИмущество [Query]
Resource:
[ОсновноеСредство] Сумма(КоэффициентКи)
```
Типичный сценарий: пользователь видит колонку "Коэффициент Ки" в отчёте и спрашивает как она считается. Один вызов `trace` показывает: формулу вычисления, откуда берутся операнды, как агрегируется в ресурс.
## Что не выводится
- XML namespace-декларации
- Обёртки v8:item/v8:lang/v8:content (извлекаем чистый текст)
- userSettingID (GUID-ы пользовательских настроек)
- Дефолтные periodAdditionBegin/End = 0001-01-01
- viewMode
+84 -5
View File
@@ -1,4 +1,4 @@
# skd-info v1.3 — Analyze 1C DCS structure
# skd-info v1.7 — Analyze 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
@@ -10,7 +10,8 @@ param(
[int]$Batch = 0,
[int]$Limit = 150,
[int]$Offset = 0,
[string]$OutFile
[string]$OutFile,
[switch]$Raw
)
$ErrorActionPreference = "Stop"
@@ -333,8 +334,68 @@ for ($i = $pathParts.Count - 1; $i -ge 0; $i--) {
$totalXmlLines = (Get-Content $resolvedPath).Count
function Get-SupportStatusForPath([string]$targetPath) {
try {
$rp = (Resolve-Path $targetPath).Path
$elemUuid = $null
$binPath = $null
# Reads the uuid of the first metadata element in an .xml file (or $null).
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp
$d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $binPath = $cand }
}
if ($elemUuid -and $binPath) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
if (-not $binPath -or -not (Test-Path $binPath)) { return "не на поддержке" }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return "снято с поддержки (правки свободны)" }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$h = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $h.Success) { return "не на поддержке" }
$G = [int]$h.Groups[1].Value
$K = [int]$h.Groups[2].Value
if ($K -eq 0) { return "снято с поддержки (правки свободны)" }
if ($G -eq 1) { return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения" }
if (-not $elemUuid) { return "не на поддержке" }
$u = [regex]::Escape($elemUuid.ToLower())
$best = $null
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
if ($null -eq $best) { return "не на поддержке" }
switch ($best) {
0 { return "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта" }
1 { return "редактируется с сохранением поддержки" }
2 { return "снято с поддержки (правки свободны)" }
}
return "не на поддержке"
} catch { return "не на поддержке" }
}
function Show-Overview {
$lines.Add("=== DCS: $templateName ($totalXmlLines lines) ===")
$lines.Add("Поддержка: $(Get-SupportStatusForPath $TemplatePath)")
$lines.Add("")
# Sources
@@ -655,6 +716,13 @@ function Show-Query {
}
$rawQuery = Unescape-Xml $queryNode.InnerText
# Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip
if ($Raw) {
foreach ($ql in ($rawQuery.Trim() -split "`n")) { $lines.Add($ql.TrimEnd()) }
return
}
$dsNameStr = $targetDs.SelectSingleNode("s:name", $ns).InnerText
# Split into batches
@@ -824,8 +892,14 @@ function Show-Fields {
$roleParts = @()
if ($role) {
foreach ($child in $role.ChildNodes) {
if ($child.NodeType -eq "Element" -and $child.InnerText -eq "true") {
if ($child.NodeType -ne "Element") { continue }
$txt = $child.InnerText.Trim()
if ($txt -eq "true") {
$roleParts += $child.LocalName
} elseif ($txt -eq "false") {
# skip default-false flags
} else {
$roleParts += "$($child.LocalName)=$txt"
}
}
}
@@ -1869,7 +1943,12 @@ $totalLines = $result.Count
# OutFile
if ($OutFile) {
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines((Join-Path (Get-Location) $OutFile), $result, $utf8Bom)
if ([System.IO.Path]::IsPathRooted($OutFile)) {
$outPath = [System.IO.Path]::GetFullPath($OutFile)
} else {
$outPath = [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $OutFile))
}
[System.IO.File]::WriteAllLines($outPath, $result, $utf8Bom)
Write-Host "Written $totalLines lines to $OutFile"
exit 0
}
@@ -1883,7 +1962,7 @@ if ($Offset -gt 0) {
$result = $result[$Offset..($totalLines - 1)]
}
if ($result.Count -gt $Limit) {
if (-not $Raw -and $result.Count -gt $Limit) {
$shown = $result[0..($Limit - 1)]
foreach ($l in $shown) { Write-Host $l }
Write-Host ""
+87 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-info v1.3 — Analyze 1C DCS structure
# skd-info v1.7 — Analyze 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -265,6 +265,74 @@ def build_structure_tree(item_node, prefix, is_last, out_lines):
build_structure_tree(child, f"{prefix}{continuation}", last, out_lines)
def get_support_status_for_path(target_path):
try:
def root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
pass
return None
rp = os.path.abspath(target_path)
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
elem_uuid = root_uuid(rp)
bin_path = None
d = os.path.dirname(rp)
for _ in range(12):
if not d:
break
if not elem_uuid:
elem_uuid = root_uuid(d + ".xml")
if not bin_path:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
bin_path = cand
if elem_uuid and bin_path:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not bin_path or not os.path.exists(bin_path):
return "не на поддержке"
data = open(bin_path, "rb").read()
if len(data) <= 32:
return "снято с поддержки (правки свободны)"
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return "не на поддержке"
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return "снято с поддержки (правки свободны)"
if g == 1:
return "конфигурация read-only (возможность изменения выключена) — правки невозможны без включения"
if not elem_uuid:
return "не на поддержке"
best = None
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
if best is None:
return "не на поддержке"
return {
0: "на замке — прямая правка сломает обновления; дорабатывай через cfe-* либо включи редактирование объекта",
1: "редактируется с сохранением поддержки",
2: "снято с поддержки (правки свободны)",
}.get(best, "не на поддержке")
except Exception:
return "не на поддержке"
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -278,6 +346,7 @@ def main():
parser.add_argument("-Limit", type=int, default=150)
parser.add_argument("-Offset", type=int, default=0)
parser.add_argument("-OutFile", default=None)
parser.add_argument("-Raw", action="store_true")
args = parser.parse_args()
# --- Resolve path ---
@@ -355,6 +424,7 @@ def main():
def show_overview():
lines.append(f"=== DCS: {template_name} ({total_xml_lines} lines) ===")
lines.append(f"Поддержка: {get_support_status_for_path(template_path)}")
lines.append("")
# Sources
@@ -634,6 +704,13 @@ def main():
sys.exit(1)
raw_query = unescape_xml("".join(query_node.itertext()))
# Raw mode: emit verbatim query text only (no headers/TOC/batch split) for round-trip
if args.Raw:
for ql in raw_query.strip().split("\n"):
lines.append(ql.rstrip())
return
ds_name_str = (target_ds.find("s:name", NSMAP).text or "")
# Split into batches
@@ -777,8 +854,15 @@ def main():
role_parts = []
if role is not None:
for child in role:
if isinstance(child.tag, str) and (child.text or "").strip() == "true":
if not isinstance(child.tag, str):
continue
txt = (child.text or "").strip()
if txt == "true":
role_parts.append(localname(child))
elif txt == "false":
pass
else:
role_parts.append(f"{localname(child)}={txt}")
info["role"] = ", ".join(role_parts)
# UseRestriction
@@ -1712,7 +1796,7 @@ def main():
sys.exit(0)
result = result[args.Offset:]
if len(result) > args.Limit:
if not args.Raw and len(result) > args.Limit:
shown = result[:args.Limit]
for line in shown:
print(line)
@@ -1,4 +1,4 @@
# skd-validate v1.1 — Validate 1C DCS structure
# skd-validate v1.2 — Validate 1C DCS structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -438,6 +438,17 @@ if ($script:stopped) { & $finalize; exit 1 }
if ($calcFieldNodes.Count -gt 0) {
$cfOk = $true
$cfSeen = @{}
# Collect totalField dataPaths — an empty calculatedField is legitimate if a
# totalField with the same dataPath provides the expression (real-world
# pattern in vendor ERP/БП reports for fields visible only in totals).
$tfPaths = @{}
foreach ($tf in $totalFieldNodes) {
$tfDp = $tf.SelectSingleNode("s:dataPath", $ns)
if ($tfDp -and $tfDp.InnerText) {
$tfPaths[$tfDp.InnerText] = $true
}
}
foreach ($cf in $calcFieldNodes) {
$dp = $cf.SelectSingleNode("s:dataPath", $ns)
$expr = $cf.SelectSingleNode("s:expression", $ns)
@@ -457,8 +468,15 @@ if ($calcFieldNodes.Count -gt 0) {
}
if (-not $expr -or -not $expr.InnerText.Trim()) {
Report-Error "CalculatedField '$path' has empty expression"
$cfOk = $false
# Empty expression is legitimate in several vendor patterns:
# - totalField with same dataPath provides the calculation
# - groupTemplate uses the field as group name (declarative only)
# - field is referenced only by settingsVariants for grouping
# Surface as warning, not error, to avoid false positives on real
# ERP/БП reports while still flagging the unusual shape.
if (-not $tfPaths.ContainsKey($path)) {
Report-Warn "CalculatedField '$path' has empty expression (declarative-only?)"
}
}
# Warn if collides with a dataset field
@@ -542,14 +560,16 @@ if ($templateNodes.Count -gt 0) {
}
$tName = $nameNode.InnerText
if ($tplSeen.ContainsKey($tName)) {
Report-Error "Duplicate template name: $tName"
$tplOk = $false
# Vendor configs (ERP/БП) ship templates with repeating names — the
# platform identifies them by position/context, not by <name>. Demote
# to warning so the check still surfaces the collision without failing.
Report-Warn "Duplicate template name: $tName (allowed by platform but ambiguous)"
} else {
$tplSeen[$tName] = $true
}
}
if ($tplOk) {
Report-OK "$($templateNodes.Count) template(s): names unique"
Report-OK "$($templateNodes.Count) template(s) found"
}
}
@@ -581,7 +601,8 @@ if ($script:stopped) { & $finalize; exit 1 }
$validComparisonTypes = @(
"Equal","NotEqual","Greater","GreaterOrEqual","Less","LessOrEqual",
"InList","NotInList","InHierarchy","InListByHierarchy",
"InList","NotInList","InHierarchy","NotInHierarchy",
"InListByHierarchy","NotInListByHierarchy",
"Contains","NotContains","BeginsWith","NotBeginsWith",
"Filled","NotFilled"
)
@@ -734,6 +755,176 @@ if ($variantNodes.Count -eq 0) {
}
}
# --- 16. valueType structural checks ---
# Catches broken XDTO that XML/structural checks miss (decimal without xs:,
# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens).
$validTypeQualifier = @{
'xs:decimal' = 'v8:NumberQualifiers'
'xs:string' = 'v8:StringQualifiers'
'xs:dateTime' = 'v8:DateQualifiers'
'xs:boolean' = ''
'v8:StandardPeriod' = ''
'v8:UUID' = ''
'v8:Null' = ''
'v8:Type' = ''
'v8:ValueStorage' = ''
}
$validSign = @('Any', 'Nonnegative', 'Negative')
$validLength = @('Variable', 'Fixed')
$validFractions = @('Date', 'DateTime', 'Time')
# DCS supports composite types: multiple <v8:Type> blocks may share a single
# trailing qualifier block (e.g. xs:string + CatalogRef.X + StringQualifiers).
# So we collect all types and qualifiers per valueType, then check consistency.
$qualifierProducers = @{
'v8:NumberQualifiers' = 'xs:decimal'
'v8:StringQualifiers' = 'xs:string'
'v8:DateQualifiers' = 'xs:dateTime'
}
$valueTypeNodes = $root.SelectNodes("//s:valueType", $ns)
$vtChecked = 0
$vtOk = $true
foreach ($vt in $valueTypeNodes) {
$vtChecked++
$types = @() # list of short type strings; '' marks a ref type
$qualifiers = @() # list of @{ name = 'v8:XQualifiers'; node = $child }
foreach ($child in $vt.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.NamespaceURI -ne 'http://v8.1c.ru/8.1/data/core') { continue }
$localName = $child.LocalName
if ($localName -eq 'Type') {
$t = "$($child.InnerText)".Trim()
if (-not $t) {
Report-Error "valueType: <v8:Type> is empty"
$vtOk = $false
continue
}
if ($t -match '^([A-Za-z][A-Za-z0-9]*):(.+)$') {
$prefix = $Matches[1]
$localT = $Matches[2]
if ($prefix -eq 'xs' -or $prefix -eq 'v8') {
if (-not $validTypeQualifier.ContainsKey($t)) {
Report-Error "valueType: unknown type '$t' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or <prefix>:*Ref.X)"
$vtOk = $false
} else {
$types += $t
}
} else {
$prefixNs = $child.GetNamespaceOfPrefix($prefix)
if ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise/current-config') {
if (-not ($localT -match '^[A-Za-z]+(Ref)?\.')) {
Report-Error "valueType: ref type '$t' must look like '<prefix>:<Kind>.<Name>' (e.g. d5p1:CatalogRef.X)"
$vtOk = $false
} else {
$types += '' # ref — no qualifier needed
}
} elseif ($prefixNs -eq 'http://v8.1c.ru/8.1/data/enterprise') {
# System types: AccumulationRecordType etc. — no qualifiers
if (-not ($localT -match '^[A-Za-z][A-Za-z0-9]*$')) {
Report-Error "valueType: system type '$t' has unexpected local-name shape"
$vtOk = $false
} else {
$types += ''
}
} else {
Report-Error "valueType: type '$t' uses prefix '$prefix' bound to unexpected namespace '$prefixNs'"
$vtOk = $false
}
}
} else {
Report-Error "valueType: type '$t' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)"
$vtOk = $false
}
} elseif ($localName -match 'Qualifiers$') {
$qName = "v8:$localName"
$qualifiers += @{ name = $qName; node = $child }
# Validate qualifier internals
if ($qName -eq 'v8:NumberQualifiers') {
$digits = $child.SelectSingleNode("v8:Digits", $ns)
$frac = $child.SelectSingleNode("v8:FractionDigits", $ns)
$sign = $child.SelectSingleNode("v8:AllowedSign", $ns)
if (-not $digits -or -not ($digits.InnerText -match '^\d+$')) {
Report-Error "v8:NumberQualifiers: <v8:Digits> missing or not a non-negative integer"
$vtOk = $false
}
if (-not $frac -or -not ($frac.InnerText -match '^\d+$')) {
Report-Error "v8:NumberQualifiers: <v8:FractionDigits> missing or not a non-negative integer"
$vtOk = $false
}
if ($sign -and $sign.InnerText -and $sign.InnerText -notin $validSign) {
Report-Error "v8:NumberQualifiers: <v8:AllowedSign>$($sign.InnerText)</v8:AllowedSign> — must be one of: $($validSign -join ', ')"
$vtOk = $false
}
} elseif ($qName -eq 'v8:StringQualifiers') {
$len = $child.SelectSingleNode("v8:Length", $ns)
$al = $child.SelectSingleNode("v8:AllowedLength", $ns)
if (-not $len -or -not ($len.InnerText -match '^\d+$')) {
Report-Error "v8:StringQualifiers: <v8:Length> missing or not a non-negative integer"
$vtOk = $false
}
if ($al -and $al.InnerText -and $al.InnerText -notin $validLength) {
Report-Error "v8:StringQualifiers: <v8:AllowedLength>$($al.InnerText)</v8:AllowedLength> — must be one of: $($validLength -join ', ')"
$vtOk = $false
}
} elseif ($qName -eq 'v8:DateQualifiers') {
$df = $child.SelectSingleNode("v8:DateFractions", $ns)
if ($df -and $df.InnerText -and $df.InnerText -notin $validFractions) {
Report-Error "v8:DateQualifiers: <v8:DateFractions>$($df.InnerText)</v8:DateFractions> — must be one of: $($validFractions -join ', ')"
$vtOk = $false
}
}
}
}
# Cross-check: every qualifier must have a matching scalar type in this valueType
foreach ($q in $qualifiers) {
$producer = $qualifierProducers[$q.name]
if (-not $producer) { continue }
if ($types -notcontains $producer) {
Report-Error "valueType: <$($q.name)> has no matching <v8:Type>$producer</v8:Type> in this valueType"
$vtOk = $false
}
}
}
if ($vtChecked -gt 0 -and $vtOk) {
Report-OK "$vtChecked valueType block(s): structure and qualifiers OK"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- 17. value content checks ---
# Catches literal placeholders ("_") and empty strings in DesignTimeValue refs
# that XDTO would reject at db-load-xml.
$valueNodes = @()
$valueNodes += @($root.SelectNodes("//s:value[@xsi:type]", $ns))
$valueNodes += @($root.SelectNodes("//dcscor:value[@xsi:type]", $ns))
$vChecked = 0
$vOk = $true
foreach ($vn in $valueNodes) {
if (-not $vn) { continue }
$vChecked++
$xsiType = $vn.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance")
$text = $vn.InnerText
if ($xsiType -eq 'dcscor:DesignTimeValue') {
if (-not $text -or $text.Trim() -eq '' -or $text.Trim() -eq '_') {
Report-Error "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '$text'"
$vOk = $false
} elseif (-not ($text -match '^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+')) {
Report-Warn "<value xsi:type=`"dcscor:DesignTimeValue`">$text</value> — doesn't look like a typical ref path"
}
}
}
if ($vChecked -gt 0 -and $vOk) {
Report-OK "$vChecked <value> element(s) with xsi:type: content OK"
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Final output ---
& $finalize
@@ -1,4 +1,4 @@
# skd-validate v1.1 — Validate 1C DCS structure (Python port)
# skd-validate v1.2 — Validate 1C DCS structure (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import os
@@ -434,6 +434,15 @@ if stopped:
if len(calc_field_nodes) > 0:
cf_ok = True
cf_seen = {}
# Collect totalField dataPaths — an empty calculatedField is legitimate if a
# totalField with the same dataPath provides the expression (real-world
# pattern in vendor ERP/БП reports for fields visible only in totals).
tf_paths = set()
for tf in total_field_nodes:
tf_dp = find(tf, "s:dataPath")
if tf_dp is not None and inner_text(tf_dp):
tf_paths.add(inner_text(tf_dp))
for cf in calc_field_nodes:
dp = find(cf, "s:dataPath")
expr = find(cf, "s:expression")
@@ -451,8 +460,14 @@ if len(calc_field_nodes) > 0:
cf_seen[path] = True
if expr is None or not text_of(expr):
report_error(f"CalculatedField '{path}' has empty expression")
cf_ok = False
# Empty expression is legitimate in several vendor patterns:
# - totalField with same dataPath provides the calculation
# - groupTemplate uses the field as group name (declarative only)
# - field is referenced only by settingsVariants for grouping
# Surface as warning, not error, to avoid false positives on real
# ERP/БП reports while still flagging the unusual shape.
if path not in tf_paths:
report_warn(f"CalculatedField '{path}' has empty expression (declarative-only?)")
# Warn if collides with a dataset field
if path in all_field_paths:
@@ -526,12 +541,14 @@ if len(template_nodes) > 0:
continue
t_name = inner_text(name_node)
if t_name in tpl_seen:
report_error(f"Duplicate template name: {t_name}")
tpl_ok = False
# Vendor configs (ERP/БП) ship templates with repeating names — the
# platform identifies them by position/context, not by <name>. Demote
# to warning so the check still surfaces the collision without failing.
report_warn(f"Duplicate template name: {t_name} (allowed by platform but ambiguous)")
else:
tpl_seen[t_name] = True
if tpl_ok:
report_ok(f"{len(template_nodes)} template(s): names unique")
report_ok(f"{len(template_nodes)} template(s) found")
# ── 13. GroupTemplate checks ─────────────────────────────────
@@ -558,7 +575,8 @@ if stopped:
valid_comparison_types = (
"Equal", "NotEqual", "Greater", "GreaterOrEqual", "Less", "LessOrEqual",
"InList", "NotInList", "InHierarchy", "InListByHierarchy",
"InList", "NotInList", "InHierarchy", "NotInHierarchy",
"InListByHierarchy", "NotInListByHierarchy",
"Contains", "NotContains", "BeginsWith", "NotBeginsWith",
"Filled", "NotFilled",
)
@@ -685,6 +703,166 @@ else:
if v_ok:
report_ok(f"{len(variant_nodes)} settingsVariant(s) found")
# ── 16. valueType structural checks ───────────────────────────
# Catches broken XDTO that XML/structural checks miss (decimal without xs:,
# missing qualifiers, mismatched qualifier blocks, unknown sign/length tokens).
import re as _re_vt
_VALID_TYPE_QUALIFIER = {
'xs:decimal': 'v8:NumberQualifiers',
'xs:string': 'v8:StringQualifiers',
'xs:dateTime': 'v8:DateQualifiers',
'xs:boolean': '',
'v8:StandardPeriod': '',
'v8:UUID': '',
'v8:Null': '',
'v8:Type': '',
'v8:ValueStorage': '',
}
_VALID_SIGN = ('Any', 'Nonnegative', 'Negative')
_VALID_LENGTH = ('Variable', 'Fixed')
_VALID_FRACTIONS = ('Date', 'DateTime', 'Time')
_V8_NS_URI = 'http://v8.1c.ru/8.1/data/core'
_CONFIG_NS_URI = 'http://v8.1c.ru/8.1/data/enterprise/current-config'
# DCS supports composite types: multiple <v8:Type> blocks may share a single
# trailing qualifier block (e.g. xs:string + CatalogRef.X + StringQualifiers).
# So we collect all types and qualifiers per valueType, then check consistency.
_QUALIFIER_PRODUCERS = {
'v8:NumberQualifiers': 'xs:decimal',
'v8:StringQualifiers': 'xs:string',
'v8:DateQualifiers': 'xs:dateTime',
}
vt_nodes = find_all(root, "//s:valueType")
vt_checked = 0
vt_ok = True
for vt in vt_nodes:
vt_checked += 1
types = [] # short type strings; '' marks a ref type
qualifiers = [] # list of (qName, node)
for child in vt:
if not isinstance(child.tag, str):
continue
qn = etree.QName(child.tag)
if qn.namespace != _V8_NS_URI:
continue
local = qn.localname
if local == 'Type':
t = (child.text or '').strip()
if not t:
report_error("valueType: <v8:Type> is empty")
vt_ok = False
continue
m = _re_vt.match(r'^([A-Za-z][A-Za-z0-9]*):(.+)$', t)
if not m:
report_error(f"valueType: type '{t}' has no namespace prefix (expected xs:/v8:/d5p1: — e.g. xs:decimal not decimal)")
vt_ok = False
continue
prefix, local_t = m.group(1), m.group(2)
if prefix in ('xs', 'v8'):
if t not in _VALID_TYPE_QUALIFIER:
report_error(f"valueType: unknown type '{t}' (allowed: xs:decimal/xs:string/xs:dateTime/xs:boolean/v8:StandardPeriod or <prefix>:*Ref.X)")
vt_ok = False
else:
types.append(t)
else:
prefix_ns = child.nsmap.get(prefix)
if prefix_ns == _CONFIG_NS_URI:
if not _re_vt.match(r'^[A-Za-z]+(Ref)?\.', local_t):
report_error(f"valueType: ref type '{t}' must look like '<prefix>:<Kind>.<Name>' (e.g. d5p1:CatalogRef.X)")
vt_ok = False
else:
types.append('') # ref — no qualifier needed
elif prefix_ns == 'http://v8.1c.ru/8.1/data/enterprise':
# System types: AccumulationRecordType etc. — no qualifiers
if not _re_vt.match(r'^[A-Za-z][A-Za-z0-9]*$', local_t):
report_error(f"valueType: system type '{t}' has unexpected local-name shape")
vt_ok = False
else:
types.append('')
else:
report_error(f"valueType: type '{t}' uses prefix '{prefix}' bound to unexpected namespace '{prefix_ns}'")
vt_ok = False
elif local.endswith('Qualifiers'):
q_name = f"v8:{local}"
qualifiers.append((q_name, child))
if q_name == 'v8:NumberQualifiers':
digits = find(child, "v8:Digits")
frac = find(child, "v8:FractionDigits")
sign = find(child, "v8:AllowedSign")
if digits is None or not _re_vt.match(r'^\d+$', text_of(digits)):
report_error("v8:NumberQualifiers: <v8:Digits> missing or not a non-negative integer")
vt_ok = False
if frac is None or not _re_vt.match(r'^\d+$', text_of(frac)):
report_error("v8:NumberQualifiers: <v8:FractionDigits> missing or not a non-negative integer")
vt_ok = False
if sign is not None and text_of(sign) and text_of(sign) not in _VALID_SIGN:
report_error(f"v8:NumberQualifiers: <v8:AllowedSign>{text_of(sign)}</v8:AllowedSign> — must be one of: {', '.join(_VALID_SIGN)}")
vt_ok = False
elif q_name == 'v8:StringQualifiers':
length = find(child, "v8:Length")
al = find(child, "v8:AllowedLength")
if length is None or not _re_vt.match(r'^\d+$', text_of(length)):
report_error("v8:StringQualifiers: <v8:Length> missing or not a non-negative integer")
vt_ok = False
if al is not None and text_of(al) and text_of(al) not in _VALID_LENGTH:
report_error(f"v8:StringQualifiers: <v8:AllowedLength>{text_of(al)}</v8:AllowedLength> — must be one of: {', '.join(_VALID_LENGTH)}")
vt_ok = False
elif q_name == 'v8:DateQualifiers':
df = find(child, "v8:DateFractions")
if df is not None and text_of(df) and text_of(df) not in _VALID_FRACTIONS:
report_error(f"v8:DateQualifiers: <v8:DateFractions>{text_of(df)}</v8:DateFractions> — must be one of: {', '.join(_VALID_FRACTIONS)}")
vt_ok = False
# Cross-check: every qualifier must have a matching scalar type in this valueType
for q_name, _ in qualifiers:
producer = _QUALIFIER_PRODUCERS.get(q_name)
if not producer:
continue
if producer not in types:
report_error(f"valueType: <{q_name}> has no matching <v8:Type>{producer}</v8:Type> in this valueType")
vt_ok = False
if vt_checked > 0 and vt_ok:
report_ok(f"{vt_checked} valueType block(s): structure and qualifiers OK")
if stopped:
finalize()
sys.exit(1)
# ── 17. value content checks ──────────────────────────────────
# Catches literal placeholders ('_') and empty strings in DesignTimeValue refs
# that XDTO would reject at db-load-xml.
value_nodes = find_all(root, "//s:value[@xsi:type]") + find_all(root, "//dcscor:value[@xsi:type]")
v_checked = 0
v_ok = True
for vn in value_nodes:
if vn is None:
continue
v_checked += 1
xsi_type = vn.get(XSI_TYPE) or ''
text = vn.text or ''
if xsi_type == 'dcscor:DesignTimeValue':
stripped = text.strip()
if not stripped or stripped == '_':
report_error(f"<value xsi:type=\"dcscor:DesignTimeValue\">{text}</value> — DesignTimeValue must be a reference path (e.g. Перечисление.X.Y), not '{text}'")
v_ok = False
elif not _re_vt.match(r'^[A-Za-zА-Яа-яЁё]+\.[A-Za-zА-Яа-яЁё0-9_]+', stripped):
report_warn(f"<value xsi:type=\"dcscor:DesignTimeValue\">{text}</value> — doesn't look like a typical ref path")
if v_checked > 0 and v_ok:
report_ok(f"{v_checked} <value> element(s) with xsi:type: content OK")
if stopped:
finalize()
sys.exit(1)
# ── Final output ──────────────────────────────────────────────
finalize()

Some files were not shown because too many files have changed in this diff Show More