Compare commits

...

582 Commits

Author SHA1 Message Date
Nick Shirokov 3a7f1c1763 fix(web-test): заполнение data-ячеек безголового грида + устранение мигания чекбокса
Два фикса в fillTableRow на гридах:

1. Безголовые гриды (без .gridHead): правка data-ячейки (число/ссылка)
   молча возвращала filled:[]. В режиме редактирования INPUT рендерится
   в оверлее .inputs ВНЕ .gridBox, поэтому readActiveGridCell не доставал
   colindex через предков → синтетическое имя Колонка{N} не матчилось в
   Tab-цикле. Теперь colindex резолвится по x-координате INPUT относительно
   ячеек тела грида — тем же приёмом, что и шапочная ветка, через единый
   synthHeaderlessColumns.

2. Чекбокс-ячейки: безусловный первичный клик по центру ячейки тогглил
   чекбокс ДО checkbox-логики, которая затем «доправляла» значение обратно
   — лишний клик и видимое мигание (снимаем→ставим) при установке в уже
   текущее значение. Детект чекбокса (findCheckboxAtPoint, читает состояние
   из DOM без режима редактирования) перенесён ДО первичного клика: кликаем
   по иконке только когда текущее состояние != желаемого (0 кликов на
   идемпотентном тоггле). Затрагивало любые таблицы, не только безголовые.

Тест 21-headerless расширен шагами на заполнение data-ячеек (число direct,
ссылка dropdown) и клик по data-колонке (через API, без raw DOM).

Полный регресс web-test: 25 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 12:55:34 +03:00
Nick Shirokov 03d90b6960 docs(web-test): убрать из SKILL.md строку про одиночное значение на поле-списке
Описывала реализацию (auto-routed/value-list field), а не применение. Одиночное
значение через selectValue/fillFields — обычное использование (уже задокументировано);
новое для модели только массив (мультивыбор) — он и остаётся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:53:51 +03:00
Nick Shirokov 842e1d0de9 feat(web-test): fillFields/одиночный selectValue на полях-списках значений + фиксы
Модель может заполнять поля-списки одним значением или массивом через fillFields/
selectValue — раньше это виснет (DLB→форма «Выбор значений», safeClick по surface)
или throw «type dialog» (CB→«Список значений», ложный isTypeDialog). Статически
поле-список не отличить от обычного рефа → детекция поведенческая.

- Рефактор-шов: selectValuesMulti = открытие + dispatchMultiSurface(field,values,
  {formNum,isCloudDD,baseForm}); детекторы detectMultiSurface/isValueListSurface.
- Делегация на УЖЕ открытой форме (без переоткрытия/мигания), асимметрично по пути:
  DLB (fillReferenceField) — по «DLB открыл форму»; CB (одиночный selectValue, до
  isTypeDialog) — по Подбор/СнятьФлажки. Обычный реф (DLB→инлайн / форма «Выбрать»
  без этих кнопок) НЕ делегируется.
- fillFields: массив → selectValue(array); одиночное течёт в существующие
  hasSelect/hasPick ветки (фикс внутри них).
- Фикс clearPool для платформенного пула: Ctrl+A не выделяет все строки → цикл
  построчного удаления (кастомный пул с чекбоксами — через СнятьФлажки).

Регресс 25/25 (обычный реф Контрагент НЕ делегирован). Доп. кейсы в
22-multiselect-listfield. select-value v1.33, fill v1.20.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 11:51:36 +03:00
Nick Shirokov 7b53f28644 perf(web-test): убрать интермиттентный ~30с хэнг при открытии в мультивыборе
В selectValuesMulti перед F4 был лишний re-focus инпута (page.click), который
интермиттентно упирался в Playwright-таймаут actionability (~30с), когда инпут
перекрыт кнопками DLB/CB или .surface, и глотался .catch — съедая 30с до F4.
Убрано: clickElement уже фокусит поле, F4 жмём сразу. Также снят дублирующий
waitForStable после clickElement (он стабилизирует страницу внутри).

Тест 22: 174с → ~76с, все 5 поверхностей зелёные. Открытие F4-ветки ~4с (было 30с+).
Одиночный selectValue не затронут.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:49:44 +03:00
Nick Shirokov 9f4b3449a9 chore(web-test): убрать отладочный console.log из selectValuesMulti
Лог детекта поверхности шумел в exec-выводе при обычном использовании навыка.
Нужная диагностика и так в возврате (selected.values/notSelected); поверхность —
внутренняя деталь, наружу не отдаётся. Чистое удаление строки, логика не менялась.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:26:57 +03:00
Nick Shirokov 619fd77fae docs(web-test): сжать описание мультивыбора selectValue в SKILL.md
Убрано перечисление 4 внутренних поверхностей и «auto-detected gesture» —
протекающая реализация, модели не нужна. Оставлено только то, что нужно
вызывающей модели: массив → мультивыбор, replace, форма возврата
(values/notSelected), формы элементов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:24:32 +03:00
Nick Shirokov fe4443ca97 fix(web-test): уйти от хрупкого имени кнопки СписокСнятьФлажки в мультивыборе
Имя кнопки содержит имя грида (<Список>СнятьФлажки) — хрупко. Константа
MULTI_BTN.uncheckAll → стабильная подстрока «СнятьФлажки»; clickElement и
hasButton матчат по вхождению, поэтому работает независимо от имени списка.
Коллизии с «УстановитьФлажки» нет. Live-проверено: clickElement('СнятьФлажки')
резолвит СписокСнятьФлажки и снимает пометки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:22:30 +03:00
Nick Shirokov bde3a5f89f feat(web-test): мультивыбор в selectValue(field, [...]) — 5 поверхностей
selectValue с массивом значений → выбор нескольких значений в полях типа
«список значений». Всегда replace. selectValuesMulti диспетчеризует 4 жеста
(F4-first детект): checkbox-form (все кандидаты, diff-тоггл), pool-podbor
(промежуточный пул + Подбор, очистка Ctrl+A+Delete), cloud-dropdown (инлайн
.cloudDD, коорд-клик + клик-вне), catalog-multirow (Ctrl-мультивыбор + Выбрать).

Композиция существующего API (clickElement/fillTableRow/closeForm/filterList/
readTable/getFormState); единственный новый низкоуровневый код — readCloudDDScript
(dom/submenu.mjs). Возврат selected:{field, values, notSelected?:[{value,reason}]}
(англ. коды, без артефактов реализации). Имена кнопок — MULTI_BTN (единый
источник детект+клик). Одиночный selectValue не тронут.

Регресс 25/25. Новый tests/web-test/22-multiselect-listfield.test.mjs.
Live: стенд МножественныйВыбор (5 полей: A/B/C/D/E) + Титан (Наименование
компании=A, Статьи=B), значения восстановлены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 20:17:05 +03:00
Nick Shirokov 7c75424153 feat(web-test): поддержка гридов без шапки (headerless grids)
Единый источник деривации колонок для безголовых гридов (без .gridHead):
synthHeaderlessColumns (HEADERLESS_GRID_FN в _shared.mjs) — привязка по
colindex, имена Колонка{N}/(checkbox)/(picture), расщепление комбинированной
марк-ячейки (галка+текст в одной .gridBox) на (checkbox)+Колонка1 через subTarget.

Ветки head ? существующее : synth добавлены в:
- readTableScript, findGridCellScript, scanGridRowsScript,
  findGridHeadCenterCoordsScript (grid.mjs)
- findCellCoordsByFieldsScript, findNextCellCoordsByKeyScript,
  sortFieldKeysByColindexScript, readActiveGridCellScript (grid-edit.mjs)
- сводка таблиц в READ_FORM_FN (_shared.mjs)

Гейт — неизменная истинность head, headed-путь нетронут. Регресс 23/23 зелёный.
Новый tests/web-test/21-headerless.test.mjs. Live на БезшапочнаяТаблица +
МножественныйВыбор: readTable/getFormState/fillTableRow(тоггл (checkbox))/clickElement.

Follow-up: правка data-ячейки на безголовом ValueTable (edit-Tab-loop row-fill).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:42:20 +03:00
Nick Shirokov 08ea3f0806 fix(web-test): клик по строке грида на узких модальных формах подбора
clickElement по строке грида целился в центр `.gridLine`, который на узкой
модальной форме подбора со множеством колонок (≈2775px при окне ~894px) уезжал
за пределы окна → mouse.click мимо строки, выделение не менялось (clickElement
при этом рапортовал успех). Проявлялось на форме множественного выбора (F4).

Общая логика клика вынесена в `ROW_CLICK_POINT_FN` (_shared.mjs): первая видимая
текстовая ячейка строки (skip checkbox/picture, skip tree-toggle, кламп X). Фикс
применён в findClickTargetScript (forms.mjs, row-select) и переиспользован в
scanGridRowsScript (grid.mjs, дедупликация inline-блока — путь selectValue).

Регресс: новый tests/web-test/20-modal-select-row.test.mjs (RED до фикса, GREEN
после). Полный набор — 23/23.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:17:32 +03:00
Nick Shirokov 022ba4e06c feat(web-test): безшапочная таблица для регресса чтения/заполнения/клика
Обработка БезшапочнаяТаблица (подсистема Склад, роль Администратор) с формой,
несущей редактируемую таблицу значений со скрытой шапкой (Header=false):
колонки ссылка (Номенклатура) / число / число / булево-чекбокс, 3 предзаполненные
строки. Покрывает безшапочные гриды для readTable/fillTableRow/clickElement —
цель рефактора deriveGridColumns (сейчас readTable отдаёт hint об отсутствии
gridHead — ожидаемый RED до фазы движка).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:35:26 +03:00
Nick Shirokov 7af630d727 feat(web-test): синтетический стенд множественного выбора (список значений)
Обработка МножественныйВыбор (подсистема Склад, роль Администратор):
- основная форма с 5 полями типа «список значений»: 4 с расширенным
  редактированием множественных значений (тип задан декларативно через
  valueType) + граничное поле без расширенного режима (платформенная
  реализация). Поля Через флажки/Через подбор открывают форму ввода через
  StartChoice; тип берётся из реквизита, выбранные обновляются на месте
  (сохраняя ТипЗначения).
- форма ФормаВводаЗначений — обрезанный порт типовой БСП-формы
  ВводЗначенийСпискомСФлажками (через form-decompile→обрезка→form-compile):
  безшапочная таблица (Header=false) Пометка+Значение, командная панель,
  режим A (предзагрузка кандидатов + флажки, по БыстрыйВыбор типа) vs
  режим B (пул + Подбор). Возвращает только отмеченные; подобранные
  помечаются (ChoiceProcessing).

Воспроизводит поверхности множественного выбора (безшапочный грид,
список значений) для последующей разработки selectValue(field, [...]).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 14:30:00 +03:00
Nick Shirokov cbde49efda chore(repo): нормализовать .gitignore к LF (политика EOL)
Добавляем .gitignore в .gitattributes (eol=lf) и нормализуем три файла:
корневой (был mixed) + .claude/skills/ + web-test/. Их правим вручную,
значит Edit-tool иначе флипал бы EOL. LICENSE намеренно не трогаем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:45:02 +03:00
Nick Shirokov ca83dee8d9 chore(repo): добавить .git-blame-ignore-revs (нормализация EOL)
Чтобы git blame атрибутировал строки содержательным коммитам, а не
механической нормализации 26888a07. Локально включается через
blame.ignoreRevsFile (настроено); GitHub/GitLab учитывают файл сами.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:23:05 +03:00
Nick Shirokov 26888a07d5 chore(repo): нормализация EOL к LF + .gitattributes
Приводим авторский контент (.ps1/.psm1/.py/.mjs/.md/.json, пин .bsl)
к единому LF и закрепляем политикой в .gitattributes. Инструмент правки
всегда пишет LF, поэтому единый LF убирает EOL-шум в диффах, ложные
срабатывания blame и налог на ручную синхронизацию CRLF-файлов.

BOM на .ps1 сохранён (git с eol=lf меняет только CR<->LF, BOM не трогает).
Данные 1С (*.xml) и бинарники под нормализацию не берём.

Гейт: PS-порт 459/459, Python-порт 459/459, web-test E2E 22/22 (с пересборкой стенда).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 14:22:45 +03:00
Nick Shirokov 9eb9571e95 fix(web-test): fillTableRow — ранний выход после заполнения всех полей строки
Tab-цикл не выходил сразу после заполнения последнего поля: ветки заполнения по
составному типу и по примитиву делали continue без проверки «все поля закрыты», и
цикл протабывал остаток строки до wrap-around. На широких строках с объединёнными
колонками (составной субконто) это заметно «бежало» табами до конца строки — холостые
итерации + риск ухода фокуса/создания лишней строки.

- row-fill v1.24: единая проверка в начале итерации `if (every filled) break`.
  Значение последнего поля к этому моменту уже зафиксировано (commit-Tab примитива,
  выбор из формы, либо end-of-row commit для choice-ячейки).
- 05-table: guard на количество строк после составного фила.

Проверено: полный регресс web-test 22/22; вживую на широкой ТЧ с составным субконто —
лишний хвост табов устранён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 13:22:31 +03:00
Nick Shirokov a633520c66 fix(web-test): клик в видимую ячейку строки формы выбора + пер-колоночный/exact матчинг
scanGridRowsScript целился в центр всей .gridLine; на широкой форме выбора (колонок
больше, чем влезает в окно) центр-X уезжает за вьюпорт в оверлей → клик мимо строки →
Enter не выбирает → форма не закрывается → ложное not_selectable «group/folder row».

- dom/grid v1.10: матч по ячейкам (строка: exact→startsWith→includes; объект {col:val}:
  пер-колоночно, AND, предпочтение exact-all), точка клика — первая видимая текстовая
  ячейка строки (не центр); возврат visibleSample/isGroup. scrollIntoView не нужен
  (динсписок держит в DOM только видимые строки).
- forms/select-value v1.25: структурированный search в скан (без склейки значений
  объекта), различение still_open vs реальная группа (isGroup), actionable not_found
  со списком видимых кандидатов.
- tests: широкая (14 колонок, choiceMode) форма выбора Контрагентов + контрагент «Север»
  рядом с «ООО Север» → детерминированный регресс центр-X + exact-preference;
  selectValue (04-selectvalue) и fillTableRow составной ячейки (05-table).

Проверено: живой E2E на реальной форме выбора составного типа, red-green на синтетике,
полный регресс web-test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:40:26 +03:00
Nick Shirokov 38d89af47d fix(web-publish): корректное включение OData через <standardOdata> в default.vrd
Атрибут enableStandardOdata на <point> не распознаётся платформой → OData 404.
Заменено на дочерний <standardOdata enable="true"/> (строчная форма; E2E 8.3.24+8.3.27).
Заглавная <standardOData> из ИТС также даёт 404 — задокументировано в web-spec.md. v1.4.

Co-Authored-By: Pavel V <40602668+PavelVir@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 20:46:28 +03:00
Nick Shirokov 23af4aa6a8 fix(db-*,epf-*): проброс --user/--password в ibcmd-ветках
ibcmd-ветки игнорировали параметры авторизации (-UserName/-Password),
хотя 1cv8-ветки передавали их через /N//P. На защищённой паролем базе
ibcmd-операции падали с «Для выполнения операции требуется аутентификация
в информационной базе».

Затронуты: db-load-cf (config load), db-update (config apply),
db-load-xml + db-load-git (config import + apply), db-dump-xml (config export),
db-dump-cf (config save), epf-dump (export --file), epf-build (import --out).
db-dump-dt/db-load-dt уже пробрасывали — не трогались.
db-create/stub-db-create создают свежую базу — авторизация не нужна.

E2E подтверждено на ERP 2.5.12.73 (ibcmd 8.3.24): без кред — auth-ошибка,
с -UserName/-Password — выгрузка проходит. Паритет PS↔PY, EOL/BOM сохранены.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 18:36:14 +03:00
Nick Shirokov 8e9f9465b6 fix(cfe-borrow): глубокие пути через ТЧ + DefinedType-тоталы при заимствовании формы
На формах со сложными табличными частями (напр. Document.РеализацияТоваровУслуг)
строгий импорт ibcmd падал «Неверный путь к данным» по двум причинам (Designer
/LoadConfigFromFiles это пропускал). Оба корня выверены эталоном Конфигуратора.

1. Глубокие пути через ТЧ: Объект.<ТЧ>.<Колонка>.<Подреквизит>
   (Объект.Товары.Номенклатура.ТипНоменклатуры). Сборщик терял 3-й сегмент, а
   обработка вообще исключала ТЧ-корни → каталог колонки заимствовался пустой
   оболочкой без под-реквизитов. Теперь сборщик хранит SubSubAttr, а общий helper
   Borrow-DeepTargetAttrs/borrow_deep_target_attrs заимствует каталог колонки с
   под-реквизитами (как Designer: Catalog.Номенклатура с ТипНоменклатуры/ЕдиницаИзмерения).

2. Тоталы колонок: Объект.<ТЧ>.Total<Колонка> → «тип колонки не определён». Колонка
   типизирована через DefinedType, а DefinedType заимствовался оболочкой без <Type>.
   Теперь Read-SourceObject добывает <Type> DefinedType, Build-BorrowedObjectXml его
   эмитит (Designer кладёт полный <Type>, напр. xs:decimal Nonnegative).

Регресс cfe-borrow 6/6 + form-bindings на обоих рантаймах; acc-форма без изменений.
Паритет PS↔PY.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 17:50:14 +03:00
Nick Shirokov 330f52c6e6 fix(cfe-init,cfe-borrow): наследовать формат-версию MDClasses от базовой конфигурации
При заимствовании форм из конфигураций старого формата (напр. УТ, весь конфиг 2.13)
расширение получалось разноверсийным: cfe-init хардкодил version="2.17", а cfe-borrow
писал layout-форму версией исходника (2.13). Платформа на строгом импорте
(ibcmd config import --extension) отвергала: «версия формата файла отличается»
(Designer /LoadConfigFromFiles это пропускал — он лояльнее).

Эталон Конфигуратора подтверждает: расширение должно быть ЕДИНОЙ формат-версии,
а заимствованная форма поднимается до версии корня расширения.

- cfe-init: формат-версия MDClasses теперь читается из базового Configuration.xml
  (root @version, дефолт 2.17), а не хардкодится. Формат-версия НЕ функция
  CompatibilityMode (УТ: Compat 8_3_17 + формат 2.13). Переиспользует паттерн
  Detect-FormatVersion, уже принятый в form-*/meta-*/role-*/subsystem-* навыках.
- cfe-borrow: layout-форма (<Form>/<BaseForm>) пишется версией расширения
  ($script:formatVersion / format_version), а не версией исходной формы.

Итог: всё расширение единой версией базы. E2E на свежей УТ-базе (2.13): borrow →
ibcmd import чисто, PS и PY. acc (2.17) без изменений. cfe-init/cfe-borrow 6/6 ×2 рантайма.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:28:48 +03:00
Nick Shirokov 0dc014dc47 fix(cfe-borrow): не затирать существующий Module.bsl при повторном заимствовании формы
Повторный borrow формы записывал пустой Module.bsl безусловно → терялся
пользовательский код, добавленный в модуль формы (аналог IngvarConsulting/unica#4,
воспроизведено: 150→3 байта). Теперь пустой модуль создаётся только если файла ещё
нет; существующий сохраняется ("Preserved existing Module.bsl"). Дополняет
идемпотентность re-borrow (форма-обёртка/uuid уже были закрыты в v1.5).

Тест form-bindings усилен: между первым и вторым заимствованием в модуль пишется
код (writeFile), idempotent:true теперь проверяет его сохранность побайтно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:57:41 +03:00
Nick Shirokov 36c2fd9233 feat(form-validate,cfe-borrow): проверка висячих привязок + идемпотентность re-borrow + добор по Field
Три доработки по форме, полезные модели, использующей навыки.

form-validate v1.7→v1.8 (5a): Check 5 генерализован с одного <DataPath> на все
8 тегов-привязок (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/
MultipleValue*DataPath/RowPicture*). Висячая привязка (корень не в <Attributes>)
теперь ловится при validate, а не всплывает на дорогом db-load. Skip-правила
(companion-элементы, базовые элементы id<1000000 в BaseForm, opaque-формы)
сохранены без изменений. Заодно фикс бага Check 12 в py-порте
(type_invalid → type_error_count, краш на невалидном cfg:-типе в config-контексте).

cfe-borrow v1.4→v1.5:
- #4: borrow_form переиспользует uuid обёртки Forms/<Name>.xml, если файл уже
  существует, вместо генерации нового → повторное заимствование формы
  байт-идемпотентно (агент может ретраить без дрейфа identity).
- #1: collect для -BorrowMainAttribute дополнен сканом <Field>Объект.X</Field>
  (поля фильтров/условного оформления/динсписков) — набор заимствованных
  реквизитов теперь совпадает с Конфигуратором (добавился УдалитьЮрФизЛицо).

Тесты: form-validate/dangling-binding (фикстура broken-dangling-binding,
expectError+stdoutContains); cfe-borrow/form-bindings + idempotent:true.
Регресс 6/6 cfe-borrow + 11/11 form-validate на обоих рантаймах, E2E-load OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:38:39 +03:00
Nick Shirokov 4767800fce fix(cfe-borrow): корректное заимствование формы — все привязки данных + идемпотентность
При заимствовании управляемой формы в расширение скрипт генерировал Form.xml,
который платформа отвергала при загрузке («Неверный путь к данным»), а повторное
заимствование портило объект. Три бага (подтверждены E2E-загрузкой в базу):

1. Висячие привязки. Stripping вырезал только DataPath/TitleDataPath/RowPictureDataPath,
   но не FooterDataPath/HeaderDataPath/MultipleValue*DataPath. Поле с множественным
   выбором (МеткиОбъекта) оставляло висячую привязку на незаимствованный реквизит.
   Введён единый список binding-тегов (выведен по дампам acc/erp/unf, сверен с
   form-compile) + helper Strip-FormBindings/strip_form_bindings; сборщик путей для
   -BorrowMainAttribute расширен на тот же список.
2. Повторное заимствование дублировало реквизиты (нет дедупа против ChildObjects).
3. Повторный enrich дописывал свойства объекта (DescriptionLength и т.п.) внутрь
   каждого <Attribute> (глобальная замена по </ExtendedConfigurationObject>).
   Фикс: guard + якорь к Properties объекта (count=1).

Попутно исправлен PS↔PY паритет form-borrow: PY энтити-кодировал кириллицу в
атрибутах самозакрытых элементов (decode_numeric_entities), порядок extraProps
в PS приведён к PY ([ordered]@{}).

Регресс-кейс tests/skills/cases/cfe-borrow/form-bindings.json (form-compile →
двойной borrow → snapshot/normalizeUuids). Регресс 6/6 PS+PY, cfe-validate OK,
E2E-загрузка в базу успешна на обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:54:13 +03:00
Nick Shirokov c172cf142a docs(v8-project): пример .v8-project.example.json + рекомендуемая структура src/ + ibcmd
- .v8-project.example.json в корне как шаблон (без секретов, коммитится; точное
  имя .v8-project.json остаётся в .gitignore). Версия платформы и configSrc=src\cf
  под рекомендуемую структуру.
- v8-project-guide: раздел «Рекомендуемая структура проекта» (src/cf, src/cfe/<Имя>,
  src/epf/<Имя>, src/erf/<Имя>), упоминание шаблона, configSrc-примеры → src\cf,
  нейтральный раздел про выбор движка 1cv8/ibcmd.
- db-guide: нейтральный раздел «Движок: 1cv8 или ibcmd» (по умолчанию конфигуратор;
  ibcmd — на усмотрение пользователя через -V8Path в задаче или файл ibcmd.exe в v8path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:19:39 +03:00
Nick Shirokov 684bbb5036 test(skills): platform-epf — канонический DSL form-compile (чинит PY-прогон)
Шаг form-compile в platform-epf был написан в «вольном» диалекте: атрибуты и
команды с ключом `id` вместо `name`, кнопка как `{id,type:'button',action}`.
PS-порт это молча глотал (пустые имена / skip с warning), PY-порт падал
(`KeyError: 'name'`). Переписано канонически (как cases/form-compile/commands.json
и build-epf): `name` у атрибутов/команд, кнопка `{button, command}` внутри cmdBar.
Зелено на обоих рантаймах и движках (PY-матрица platform/* → 7/7).

Это была опечатка теста, не баг навыка. Робастность form-compile на некорректном
входе (чистая ошибка вместо raw-traceback в PY / тихого пустого имени в PS) —
отдельная необязательная тема.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:53:37 +03:00
Nick Shirokov d0469c6080 merge: фикс PY-quoting в db-create (1cv8 строка соединения) 2026-06-22 18:11:47 +03:00
Nick Shirokov 32bf72bba9 fix(db-create): PY — убрать встроенные кавычки в строке соединения 1cv8
subprocess.run([exe]+args) на Windows сам экранирует кавычки внутри одного
аргумента, поэтому f'File="{path}"' / f'Srvr="..";Ref=".."' приходили в 1С
покорёженными → CREATEINFOBASE падал «Неверные параметры соединения» (exit 1).
Без встроенных кавычек subprocess сам квотит токен с пробелами, а argv-парсер 1С
снимает внешние кавычки. Предсуществующий баг (на dev), всплыл на --runtime python
движковой матрицы. PS-порт корректен (зелёный прогон) — в нём только бамп версии
для синхронности портов. v1.3 → v1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:11:02 +03:00
Nick Shirokov 55e7d82210 merge: поддержка ibcmd в db-*/epf-*/erf- (опт-ин по имени exe) + движковая матрица в регресс-тестах 2026-06-22 16:13:11 +03:00
Nick Shirokov ceacaa3509 test(skills): движковая матрица 1cv8/ibcmd в интеграционных тестах
Раннер: контекст платформы дорезолвит ibcmd.exe рядом с 1cv8.exe;
тест объявляет `engines: ['1cv8','ibcmd']` → одни и те же шаги прогоняются
на каждом движке ({v8path} подставляется в нужный exe), результаты помечаются
суффиксом [1cv8]/[ibcmd]. ibcmd-проход авто-skip, если ibcmd.exe нет.
Дефолт engines=['1cv8'] — прочие тесты не меняются.

Новые типы шагов: editFile (подстановочная замена) и assertContains
(проверка подстроки) — для round-trip проверок.

platform-config и platform-epf переведены в матрицу. Новый platform-partial:
частичная выгрузка/загрузка объекта с round-trip маркера на обоих движках.

README: раздел про интеграционные тесты, матрицу и типы шагов.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 16:04:16 +03:00
Nick Shirokov 89496f535d docs(db,epf,erf): ibcmd — опт-ин, убрать дубль автоопределения платформы
По итогам обсуждения: ibcmd оставляем на усмотрение опытных пользователей,
не светим в основном потоке инструкций.

- Убраны развёрнутые врезки «Если -V8Path указывает на ibcmd.exe — …»
  во всех SKILL.md. Упоминание остаётся только в описании параметра
  -V8Path (1cv8.exe / ibcmd.exe); ограничения и так enforced скриптом
  (чистая ошибка на серверной базе / -Format Plain / -AllExtensions).
- Заменена неактуальная и дублирующая строка автоопределения платформы
  («Get-ChildItem … Sort -Desc | Select -First 1») — скрипт сам резолвит
  платформу (реестр → числовая сортировка → Program Files [+ x86]),
  модели не нужно дублировать ручной поиск.

Только SKILL.md, скрипты и версии не затронуты. EOL/BOM сохранён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:08:43 +03:00
Nick Shirokov 5bd6ba30cd fix(db,epf): изолированный --data на каждую ibcmd-операцию
ibcmd без --data использует общий каталог автономного сервера в
%LOCALAPPDATA%\standalone-server: он копит реестр/локи, и зависший
процесс (например, ушедший в консольный интерактив при нехватке кред)
держит его лок, блокируя ВСЕ последующие ibcmd-операции «Рабочий каталог
заблокирован процессом N».

Теперь каждая ibcmd-операция получает свой одноразовый --data=<temp> и
удаляет его после: зависший/параллельный вызов лочит только свой каталог,
не накапливается реестр («уже зарегистрирована»). База (--db-path) и
чужой автономный сервер на дефолтном каталоге не затрагиваются.

Реализация: ps1 — --data=$tempDir (создаётся до try, finally чистит на
exit); py — свой ib_data через tempfile.mkdtemp + atexit-очистка (надёжно
на любом sys.exit); stub-db-create — свой ib_data + явная очистка.

Проверено: A/B через навык с живым холдером — операция с общим --data
падает «заблокирован процессом N», навык (свой --data) проходит; очистка
temp-каталогов подтверждена (оба порта); 1cv8-ветки не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 19:01:03 +03:00
Nick Shirokov 8fb3e9421d feat(db-dump-xml,db-load-xml): -AllExtensions через ibcmd
Закрыт последний частичный пробел расширений: -AllExtensions под ibcmd
больше не ошибка, а реальные подкоманды:
- db-dump-xml -AllExtensions → ibcmd infobase config export all-extensions <dir>
- db-load-xml -AllExtensions → ibcmd infobase config import all-extensions <dir>
  (+ цепочка config apply --force при -UpdateDB)

1cv8-ветки без изменений. Версии: db-dump-xml 1.3→1.4, db-load-xml 1.7→1.8.

E2E: база с двумя расширениями → export all-extensions (Ext1/Ext2) →
import all-extensions обратно, оба порта; 1cv8-регресс (-AllExtensions
-Mode Full) цел.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:17:15 +03:00
Nick Shirokov 3f5065221e feat(epf-build,epf-dump): полная сборка/разборка EPF·ERF через ibcmd
Если -V8Path указывает на ibcmd.exe — обработки/отчёты собираются и
разбираются автономным сервером (offline, без запуска платформы), иначе
как прежде через 1cv8 DESIGNER.

- epf-build → ibcmd infobase config import <src-dir> --out=<epf> --db-path
- epf-dump  → ibcmd infobase config export --file=<epf> <dir> --db-path
- stub-db-create: при ibcmd создаёт stub-базу ОДНИМ вызовом
  ibcmd infobase create --db-path --create-database [--import=cfg --apply
  --force] вместо трёх стартов 1cv8 (CREATEINFOBASE/Load/Update). --force
  обязателен: иначе apply уходит в интерактивный [y/n] и отменяется.

Только файловые базы (серверные/-Format Plain под ibcmd → чистая ошибка).
1cv8-ветки без изменений. Версии: epf-build/epf-dump 1.1→1.2,
stub-db-create 1.0→1.1.

E2E: dump (.epf→XML), build без ref-типов, build СО ссылочными типами
через auto-stub на ibcmd (валидность подтверждена обратной разборкой),
1cv8-регресс — всё зелёное; оба порта.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:09:23 +03:00
Nick Shirokov 27b9d539e0 feat(db): ibcmd для частичной выгрузки/загрузки из исходников
Закрыты частичные режимы, ранее дававшие «use 1cv8»:
- db-dump-xml Mode=Partial → ibcmd config export objects <имена>
  --out=<dir> (выгрузка указанных объектов конфигурации);
- db-load-xml Mode=Partial / -Files / -ListFile → ibcmd config import
  files <отн.пути> --base-dir=<ConfigDir> (--base-dir обязателен);
- db-load-git → добавлена ibcmd-поддержка (раньше отсутствовала):
  config import files <изменённые из git> --base-dir=<ConfigDir>,
  с цепочкой config apply --force при -UpdateDB; DryRun не трогает движок.

Несовместимое под ibcmd по-прежнему даёт чистую ошибку: -Format Plain,
-AllExtensions, db-dump-xml Mode UpdateInfo. 1cv8-ветки без изменений.
Версии: db-dump-xml 1.2→1.3, db-load-xml 1.6→1.7, db-load-git 1.5→1.6.

E2E: export objects (только указанные объекты), import files round-trip
(+apply), db-load-git staged→import files+apply, DryRun, error-кейсы,
1cv8-регресс Partial — всё зелёное.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 16:33:07 +03:00
Nick Shirokov 4102b7005a feat(db): тиражирование ibcmd на create/cf/update/xml (по имени exe в -V8Path)
Распространение пилота dt-пары на остальные применимые навыки. Если
-V8Path указывает на ibcmd.exe — операция идёт через автономный сервер
(offline, без запуска платформы), иначе как прежде через 1cv8 DESIGNER.

Маппинг (только файловые базы --db-path):
- db-create  → infobase create --create-database [--restore=dt|--load=cf --apply]
- db-dump-cf → infobase config save
- db-load-cf → infobase config load
- db-update  → infobase config apply --force (--force обязателен: без него
  ibcmd уходит в интерактивный [y/n] и в неинтерактиве отменяет, exit 102)
- db-dump-xml→ infobase config export (иерархический, Mode Full/Changes)
- db-load-xml→ infobase config import (+цепочка config apply при -UpdateDB)

Несовместимое под ibcmd даёт понятную ошибку с указанием на 1cv8:
серверные базы, -AllExtensions, -Format Plain, Mode Partial/UpdateInfo,
Files/ListFile. 1cv8-ветки без изменений. Версии 1.1→1.2 (xml-load 1.5→1.6).

E2E цепочка через ibcmd: create→dump-cf→load-cf→update→dump-xml→
load-xml+UpdateDB — всё exit 0; 1cv8-регресс (create/load-cf/update) цел.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:37:19 +03:00
Nick Shirokov 11bab7669d feat(db-dump-dt,db-load-dt): пилот ibcmd (движок по имени exe в -V8Path)
Если -V8Path указывает на ibcmd.exe — операция выполняется через утилиту
автономного сервера (offline, без запуска платформы), иначе как прежде
через 1cv8 DESIGNER. Выбор движка неявный (sniff имени exe), без новых
параметров; в реестре .v8-project.json пользователь прописывает v8path
= путь к ibcmd.exe.

Пилот на dt-паре:
- dump:    ibcmd infobase dump --db-path=<base> [--user][--password] <dt>
- restore: ibcmd infobase restore --db-path=<base> [--create-database
  если нет 1Cv8.1CD] [--user][--password] <dt>
Только файловые базы (серверные под ibcmd → понятная ошибка: нужны креды
СУБД, которых нет в реестре). Вывод ibcmd (UTF-8) захватывается из
stdout/stderr (нет /Out-файла). 1cv8-ветка без изменений. Версия 1.1→1.2.

E2E: dump 115281б, restore round-trip (свежий каталог +--create-database,
overwrite существующей без флага), 1cv8-регресс, server+ibcmd→ошибка.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:18:41 +03:00
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
839 changed files with 142383 additions and 75388 deletions
+1 -1
View File
@@ -1 +1 @@
__pycache__/
__pycache__/
+60 -60
View File
@@ -1,60 +1,60 @@
---
name: cf-edit
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /cf-edit — редактирование конфигурации 1С
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Operation` | Операция (см. таблицу) |
| `Value` | Значение для операции (batch через `;;`) |
| `DefinitionFile` | JSON-файл с массивом операций |
| `NoValidate` | Пропустить авто-валидацию |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
```
## Операции
| Операция | Формат Value | Описание |
|----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
## Примеры
```powershell
# Изменить версию и поставщика
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
# Добавить объекты
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Удалить объект
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
# Роли по умолчанию
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
```
---
name: cf-edit
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /cf-edit — редактирование конфигурации 1С
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Operation` | Операция (см. таблицу) |
| `Value` | Значение для операции (batch через `;;`) |
| `DefinitionFile` | JSON-файл с массивом операций |
| `NoValidate` | Пропустить авто-валидацию |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
```
## Операции
| Операция | Формат Value | Описание |
|----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
## Примеры
```powershell
# Изменить версию и поставщика
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
# Добавить объекты
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Удалить объект
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
# Роли по умолчанию
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
```
+150 -150
View File
@@ -1,150 +1,150 @@
# cf-edit — справочник операций
## modify-property
Свойства для редактирования:
### Скалярные
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
### LocalString (многоязычные)
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
### Enum
| Свойство | Допустимые значения |
|----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` |
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
### Ref
`DefaultLanguage` — значение вида `Language.Русский`
### Формат batch
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
## add-childObject / remove-childObject
Формат: `Type.Name` — XML-тип и имя объекта через точку.
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
## add-defaultRole / remove-defaultRole / set-defaultRoles
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
`set-defaultRoles` полностью заменяет список ролей.
## set-panels
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
**Запись** — одна из:
- Строка-алиас (одна панель в этом слоте)
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
**Алиасы панелей:**
| Алиас | Панель |
|-------|--------|
| `sections` | Панель разделов |
| `open` | Панель открытых |
| `favorites` | Панель избранного |
| `history` | Панель истории |
| `functions` | Панель функций текущего раздела |
**Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
**Пример** (DefinitionFile):
```json
[
{
"operation": "set-panels",
"value": {
"top": ["open"],
"left": ["sections"],
"right": [{ "group": ["favorites", "history"] }],
"bottom": ["functions"]
}
}
]
```
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
## set-home-page
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
`value` — объект:
| Ключ | Канонич. (XML) | Описание |
|------|----------------|----------|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
| `left` | `LeftColumn` | массив записей форм |
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
**Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
**Пример:**
```json
[
{
"operation": "set-home-page",
"value": {
"template": "TwoColumnsVariableWidth",
"left": [
"CommonForm.НачалоРаботы",
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
{
"form": "CommonForm.РабочийСтолОператора",
"visibility": false,
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
}
],
"right": [
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
]
}
}
]
```
## DefinitionFile (JSON)
```json
[
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
]
```
# cf-edit — справочник операций
## modify-property
Свойства для редактирования:
### Скалярные
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
### LocalString (многоязычные)
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
### Enum
| Свойство | Допустимые значения |
|----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` |
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
### Ref
`DefaultLanguage` — значение вида `Language.Русский`
### Формат batch
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
## add-childObject / remove-childObject
Формат: `Type.Name` — XML-тип и имя объекта через точку.
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
## add-defaultRole / remove-defaultRole / set-defaultRoles
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
`set-defaultRoles` полностью заменяет список ролей.
## set-panels
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
**Запись** — одна из:
- Строка-алиас (одна панель в этом слоте)
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
**Алиасы панелей:**
| Алиас | Панель |
|-------|--------|
| `sections` | Панель разделов |
| `open` | Панель открытых |
| `favorites` | Панель избранного |
| `history` | Панель истории |
| `functions` | Панель функций текущего раздела |
**Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
**Пример** (DefinitionFile):
```json
[
{
"operation": "set-panels",
"value": {
"top": ["open"],
"left": ["sections"],
"right": [{ "group": ["favorites", "history"] }],
"bottom": ["functions"]
}
}
]
```
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
## set-home-page
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
`value` — объект:
| Ключ | Канонич. (XML) | Описание |
|------|----------------|----------|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
| `left` | `LeftColumn` | массив записей форм |
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
**Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
**Пример:**
```json
[
{
"operation": "set-home-page",
"value": {
"template": "TwoColumnsVariableWidth",
"left": [
"CommonForm.НачалоРаботы",
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
{
"form": "CommonForm.РабочийСтолОператора",
"visibility": false,
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
}
],
"right": [
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
]
}
}
]
```
## DefinitionFile (JSON)
```json
[
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
{ "operation": "add-defaultRole", "value": "ПолныеПрава" }
]
```
File diff suppressed because it is too large Load Diff
+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()
+54 -54
View File
@@ -1,54 +1,54 @@
---
name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-info — Структура конфигурации 1С
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Mode` | Режим: `overview` (default), `brief`, `full` |
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
```
## Три режима
| Режим | Что показывает |
|---|---|
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
## Примеры
```powershell
# Обзор пустой конфигурации
... -ConfigPath src
# Краткая сводка реальной конфигурации
... -ConfigPath src -Mode brief
# Полная информация
... -ConfigPath src -Mode full
# С пагинацией
... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page
```
---
name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-info — Структура конфигурации 1С
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Mode` | Режим: `overview` (default), `brief`, `full` |
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
```
## Три режима
| Режим | Что показывает |
|---|---|
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
## Примеры
```powershell
# Обзор пустой конфигурации
... -ConfigPath src
# Краткая сводка реальной конфигурации
... -ConfigPath src -Mode brief
# Полная информация
... -ConfigPath src -Mode full
# С пагинацией
... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page
```
File diff suppressed because it is too large Load Diff
+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}")
+49 -49
View File
@@ -1,49 +1,49 @@
---
name: cf-init
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-init — Создание пустой конфигурации 1С
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `Name` | Имя конфигурации (обязат.) |
| `Synonym` | Синоним (= Name если не указан) |
| `OutputDir` | Каталог для создания (default: `src`) |
| `Version` | Версия конфигурации |
| `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
```
## Примеры
```powershell
# Базовая конфигурация
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
# С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Другой режим совместимости
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
```
## Верификация
```
/cf-init TestConfig -OutputDir test-tmp/cf
/cf-info test-tmp/cf — проверить созданное
/cf-validate test-tmp/cf — валидировать
```
---
name: cf-init
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-init — Создание пустой конфигурации 1С
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
## Параметры и команда
| Параметр | Описание |
|----------|----------|
| `Name` | Имя конфигурации (обязат.) |
| `Synonym` | Синоним (= Name если не указан) |
| `OutputDir` | Каталог для создания (default: `src`) |
| `Version` | Версия конфигурации |
| `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
```
## Примеры
```powershell
# Базовая конфигурация
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
# С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Другой режим совместимости
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
```
## Верификация
```
/cf-init TestConfig -OutputDir test-tmp/cf
/cf-info test-tmp/cf — проверить созданное
/cf-validate test-tmp/cf — валидировать
```
+249 -249
View File
@@ -1,249 +1,249 @@
# cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$OutputDir = "src",
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir
}
# --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile"
exit 1
}
# --- Generate UUIDs ---
$uuidCfg = [guid]::NewGuid().ToString()
$uuidLang = [guid]::NewGuid().ToString()
# 7 ContainedObject ObjectIds
$co1 = [guid]::NewGuid().ToString()
$co2 = [guid]::NewGuid().ToString()
$co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities ---
$mobileFuncs = @(
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
)
$mobileXml = ""
foreach ($mf in $mobileFuncs) {
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
}
# --- Synonym XML ---
$synonymXml = ""
if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
}
# --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- Configuration.xml ---
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Configuration uuid="$uuidCfg">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<xr:ObjectId>$co1</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<xr:ObjectId>$co2</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<xr:ObjectId>$co3</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<xr:ObjectId>$co4</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<xr:ObjectId>$co5</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<xr:ObjectId>$co6</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<xr:ObjectId>$co7</xr:ObjectId>
</xr:ContainedObject>
</InternalInfo>
<Properties>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Synonym>$synonymXml</Synonym>
<Comment/>
<NamePrefix/>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes>
<ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/>
<Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version>
<UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
<AdditionalFullTextSearchDictionaries/>
<CommonSettingsStorage/>
<ReportsUserSettingsStorage/>
<ReportsVariantsStorage/>
<FormDataSettingsStorage/>
<DynamicListsUserSettingsStorage/>
<URLExternalDataStorage/>
<Content/>
<DefaultReportForm/>
<DefaultReportVariantForm/>
<DefaultReportSettingsForm/>
<DefaultReportAppearanceTemplate/>
<DefaultDynamicListSettingsForm/>
<DefaultSearchForm/>
<DefaultDataHistoryChangeHistoryForm/>
<DefaultDataHistoryVersionDataForm/>
<DefaultDataHistoryVersionDifferencesForm/>
<DefaultCollaborationSystemUsersChoiceForm/>
<RequiredMobileApplicationPermissions/>
<UsedMobileApplicationFunctionalities>$mobileXml
</UsedMobileApplicationFunctionalities>
<StandaloneConfigurationRestrictionRoles/>
<MobileApplicationURLs/>
<AllowedIncomingShareRequestTypes/>
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
<DefaultInterface/>
<DefaultStyle/>
<DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/>
<DetailedInformation/>
<Copyright/>
<VendorInformationAddress/>
<ConfigurationInformationAddress/>
<DataLockControlMode>Managed</DataLockControlMode>
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
<ModalityUseMode>DontUse</ModalityUseMode>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<DefaultConstantsForm/>
</Properties>
<ChildObjects>
<Language>Русский</Language>
</ChildObjects>
</Configuration>
</MetaDataObject>
"@
# --- Languages/Русский.xml ---
$langXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Language uuid="$uuidLang">
<Properties>
<Name>Русский</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Русский</v8:content>
</v8:item>
</Synonym>
<Comment/>
<LanguageCode>ru</LanguageCode>
</Properties>
</Language>
</MetaDataObject>
"@
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
# via panelDef but not placed by default. Without this file the web client renders
# section icons without labels (icon-only mode).
$openPanelInst = [guid]::NewGuid().ToString()
$sectionsPanelInst = [guid]::NewGuid().ToString()
$caiXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
<top>
<panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
</panel>
</top>
<left>
<panel id="$sectionsPanelInst">
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
</panel>
</left>
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
$extDir = Join-Path $OutputDir "Ext"
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
# --- Output ---
Write-Host "[OK] Создана конфигурация: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
Write-Host " Ext/CAI: $caiFile"
# cf-init v1.2 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$OutputDir = "src",
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir
}
# --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile"
exit 1
}
# --- Generate UUIDs ---
$uuidCfg = [guid]::NewGuid().ToString()
$uuidLang = [guid]::NewGuid().ToString()
# 7 ContainedObject ObjectIds
$co1 = [guid]::NewGuid().ToString()
$co2 = [guid]::NewGuid().ToString()
$co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString()
# --- Mobile functionalities ---
$mobileFuncs = @(
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
@("OSBackup","true"), @("ApplicationUsageStatistics","false"),
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
)
$mobileXml = ""
foreach ($mf in $mobileFuncs) {
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
}
# --- Synonym XML ---
$synonymXml = ""
if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
}
# --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- Configuration.xml ---
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Configuration uuid="$uuidCfg">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<xr:ObjectId>$co1</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<xr:ObjectId>$co2</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<xr:ObjectId>$co3</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<xr:ObjectId>$co4</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<xr:ObjectId>$co5</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<xr:ObjectId>$co6</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<xr:ObjectId>$co7</xr:ObjectId>
</xr:ContainedObject>
</InternalInfo>
<Properties>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Synonym>$synonymXml</Synonym>
<Comment/>
<NamePrefix/>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes>
<ScriptVariant>Russian</ScriptVariant>
<DefaultRoles/>
<Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version>
<UpdateCatalogAddress/>
<IncludeHelpInContents>false</IncludeHelpInContents>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
<AdditionalFullTextSearchDictionaries/>
<CommonSettingsStorage/>
<ReportsUserSettingsStorage/>
<ReportsVariantsStorage/>
<FormDataSettingsStorage/>
<DynamicListsUserSettingsStorage/>
<URLExternalDataStorage/>
<Content/>
<DefaultReportForm/>
<DefaultReportVariantForm/>
<DefaultReportSettingsForm/>
<DefaultReportAppearanceTemplate/>
<DefaultDynamicListSettingsForm/>
<DefaultSearchForm/>
<DefaultDataHistoryChangeHistoryForm/>
<DefaultDataHistoryVersionDataForm/>
<DefaultDataHistoryVersionDifferencesForm/>
<DefaultCollaborationSystemUsersChoiceForm/>
<RequiredMobileApplicationPermissions/>
<UsedMobileApplicationFunctionalities>$mobileXml
</UsedMobileApplicationFunctionalities>
<StandaloneConfigurationRestrictionRoles/>
<MobileApplicationURLs/>
<AllowedIncomingShareRequestTypes/>
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>
<DefaultInterface/>
<DefaultStyle/>
<DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/>
<DetailedInformation/>
<Copyright/>
<VendorInformationAddress/>
<ConfigurationInformationAddress/>
<DataLockControlMode>Managed</DataLockControlMode>
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
<ModalityUseMode>DontUse</ModalityUseMode>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<DefaultConstantsForm/>
</Properties>
<ChildObjects>
<Language>Русский</Language>
</ChildObjects>
</Configuration>
</MetaDataObject>
"@
# --- Languages/Русский.xml ---
$langXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Language uuid="$uuidLang">
<Properties>
<Name>Русский</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Русский</v8:content>
</v8:item>
</Synonym>
<Comment/>
<LanguageCode>ru</LanguageCode>
</Properties>
</Language>
</MetaDataObject>
"@
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
# Open panel on top, Sections panel on left; Functions/Favorites/History declared
# via panelDef but not placed by default. Without this file the web client renders
# section icons without labels (icon-only mode).
$openPanelInst = [guid]::NewGuid().ToString()
$sectionsPanelInst = [guid]::NewGuid().ToString()
$caiXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
<top>
<panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
</panel>
</top>
<left>
<panel id="$sectionsPanelInst">
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
</panel>
</left>
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
$extDir = Join-Path $OutputDir "Ext"
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc)
# --- Output ---
Write-Host "[OK] Создана конфигурация: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
Write-Host " Ext/CAI: $caiFile"
+29 -29
View File
@@ -1,29 +1,29 @@
---
name: cf-validate
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-validate — валидация конфигурации 1С
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
```
---
name: cf-validate
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /cf-validate — валидация конфигурации 1С
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
```
File diff suppressed because it is too large Load Diff
+101 -101
View File
@@ -1,101 +1,101 @@
---
name: cfe-borrow
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-borrow — Заимствование объектов из конфигурации
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
## Предусловие
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры
| Параметр | Описание |
|----------|----------|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
| `Object` | Что заимствовать (обязат.), batch через `;;` |
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
## Формат -Object
- `Catalog.Контрагенты` — справочник
- `CommonModule.РаботаСФайлами` — общий модуль
- `Document.РеализацияТоваров` — документ
- `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
Создаётся:
1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
4. **Регистрация**`<Form>` в ChildObjects родительского объекта
### Заимствование основного реквизита формы (-BorrowMainAttribute)
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
**Два режима**:
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
**Типовой сценарий** (добавление реквизита + вывод на форму):
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
2. `/meta-edit` — добавить новый реквизит в объект расширения
3. `/form-edit` — вывести реквизит на заимствованную форму
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
```
## Примеры
```powershell
# Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
# Заимствовать форму с ВСЕМИ реквизитами объекта
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
```
## Верификация
```
/cfe-validate <ExtensionPath>
```
---
name: cfe-borrow
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-borrow — Заимствование объектов из конфигурации
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
## Предусловие
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры
| Параметр | Описание |
|----------|----------|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
| `Object` | Что заимствовать (обязат.), batch через `;;` |
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
## Формат -Object
- `Catalog.Контрагенты` — справочник
- `CommonModule.РаботаСФайлами` — общий модуль
- `Document.РеализацияТоваров` — документ
- `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
Создаётся:
1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
4. **Регистрация**`<Form>` в ChildObjects родительского объекта
### Заимствование основного реквизита формы (-BorrowMainAttribute)
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
**Два режима**:
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
**Типовой сценарий** (добавление реквизита + вывод на форму):
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
2. `/meta-edit` — добавить новый реквизит в объект расширения
3. `/form-edit` — вывести реквизит на заимствованную форму
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
```
## Примеры
```powershell
# Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
# Заимствовать форму с ВСЕМИ реквизитами объекта
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
```
## Верификация
```
/cfe-validate <ExtensionPath>
```
File diff suppressed because it is too large Load Diff
+202 -113
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
def strip_form_bindings(xml, keep_objekt):
"""Strip data-binding tags whose root attribute isn't borrowed.
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
for tag in FORM_BINDING_DATA_TAGS:
if keep_objekt:
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
else:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
for tag in FORM_BINDING_PICTURE_TAGS:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
return xml
def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs
back to literal so PS↔PY output matches. Named entities (&amp; &lt; ...) are left intact."""
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
return s
def localname(el):
return etree.QName(el.tag).localname
@@ -462,6 +492,13 @@ def main():
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
if prop_node is not None:
src_props[prop_name] = (prop_node.text or "").strip()
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if type_name == "DefinedType":
type_node = props_node.find(f"{{{MD_NS}}}Type")
if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
@@ -533,6 +570,10 @@ def main():
prop_val = source_props.get(prop_name, "false")
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}")
lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS:
@@ -644,7 +685,26 @@ def main():
first_level = {}
deep_paths = []
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content):
# Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
for tag in FORM_BINDING_DATA_TAGS:
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
if len(segments) >= 2:
seg1 = segments[1]
if seg1 in STANDARD_FIELDS:
continue
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
@@ -655,22 +715,14 @@ def main():
seg1 = segments[1]
if seg1 in STANDARD_FIELDS:
continue
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1})
# Also collect from TitleDataPath
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Deduplicate deep paths
seen = set()
unique_deep = []
for dp in deep_paths:
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}"
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
if key not in seen:
seen.add(key)
unique_deep.append(dp)
@@ -941,26 +993,40 @@ def main():
# Step 3: Build the adopted content and insert into main object XML
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Generate full object XML with attributes and TS
content_parts = []
for attr in src_attrs:
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t")
content_parts.append(attr_xml)
for ts in src_ts:
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
content_parts.append(ts_xml)
adopted_content = "\n".join(content_parts).rstrip()
# Read existing object XML and inject
# Read existing object XML (needed for dedup + enrichment)
with open(obj_file, "r", encoding="utf-8-sig") as fh:
obj_content = fh.read()
# Inject extra properties after ExtendedConfigurationObject
# Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set()
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
# Generate full object XML with attributes and TS
content_parts = []
for attr in insert_attrs:
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
for ts in insert_ts:
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
adopted_content = "\n".join(content_parts).rstrip()
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
if extra_props:
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
obj_props_block = m_props.group(1) if m_props else ""
props_xml = ""
for p_name, p_val in extra_props.items():
if f"<{p_name}>" in obj_props_block:
continue
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}")
if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content
if adopted_content:
@@ -1012,79 +1078,93 @@ def main():
# Step 5: Handle deep paths (Form mode only)
if mode == "Form" and deep_paths:
# Filter out deep paths where ObjectAttr is a TabularSection
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names]
if real_deep:
info(f" Processing {len(real_deep)} deep path(s)...")
# Group by ObjectAttr -> target catalog
deep_by_attr = {}
for dp in real_deep:
if dp["ObjectAttr"] not in deep_by_attr:
deep_by_attr[dp["ObjectAttr"]] = []
# Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
deep_by_attr = {}
for dp in deep_paths:
if dp["ObjectAttr"] in ts_names:
continue
deep_by_attr.setdefault(dp["ObjectAttr"], [])
if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
if deep_by_attr:
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
for attr_name, sub_attr_names in deep_by_attr.items():
# Find the attribute's type to determine target catalog
attr_info = None
for a in src_attrs:
if a["Name"] == attr_name:
attr_info = a
break
attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
if not attr_info:
continue
# Extract catalog name from type: cfg:CatalogRef.XXX
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
if not cat_match:
continue
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
target_type_name = cat_match.group(1)
target_obj_name = cat_match.group(2)
# Ensure target is borrowed
if not test_object_borrowed(target_type_name, target_obj_name):
t_src = read_source_object(target_type_name, target_obj_name)
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
# Resolve sub-attributes in target catalog
sub_names = {sn: True for sn in sub_attr_names}
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
if sub_resolved["Attributes"]:
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
# Collect and borrow ref types from deep attributes
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
sub_ref_types = collect_reference_types(sub_type_xmls)
for srt in sub_ref_types:
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
# Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
ts_deep_by_col = {}
for dp in deep_paths:
if dp["ObjectAttr"] not in ts_names:
continue
if not dp.get("SubSubAttr"):
continue
if dp["SubSubAttr"] in STANDARD_FIELDS:
continue
k = (dp["ObjectAttr"], dp["SubAttr"])
ts_deep_by_col.setdefault(k, [])
if dp["SubSubAttr"] not in ts_deep_by_col[k]:
ts_deep_by_col[k].append(dp["SubSubAttr"])
if ts_deep_by_col:
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
if not ts_info:
continue
col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
if not col_info:
continue
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
if not cat_match:
continue
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
info(" Main attribute borrowing complete")
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
if not test_object_borrowed(target_type_name, target_obj_name):
t_src = read_source_object(target_type_name, target_obj_name)
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
save_text_bom(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
sub_names = {sn: True for sn in sub_attr_names}
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
if sub_resolved["Attributes"]:
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
sub_ref_types = collect_reference_types(sub_type_xmls)
for srt in sub_ref_types:
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
dir_name = CHILD_TYPE_DIR_MAP[type_name]
@@ -1100,8 +1180,22 @@ def main():
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
src_form_content = fh.read()
# 3. Generate form metadata XML
new_form_uuid = new_guid()
# 3. Generate form metadata XML.
# If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
new_form_uuid = ""
if os.path.isfile(existing_wrapper):
try:
existing_root = etree.parse(existing_wrapper).getroot()
for c in existing_root:
if isinstance(c.tag, str) and localname(c) == "Form":
new_form_uuid = c.get("uuid", "") or ""
break
except Exception:
new_form_uuid = ""
if not new_form_uuid:
new_form_uuid = new_guid()
form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
@@ -1131,7 +1225,10 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", format_version)
# Borrowed form uses the extension's format version (not the source form's) — keeps the
# extension uniform; otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
form_version = format_version
src_auto_cmd = None
form_props = []
@@ -1149,25 +1246,21 @@ def main():
continue
if not reached_visual:
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
form_props.append(etree.tostring(fc, encoding="unicode"))
form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = ""
if src_auto_cmd is not None:
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode")
auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension)
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons
if borrow_main_attr:
# Keep only Объект.* DataPaths
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
else:
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
# Strip data-binding tags whose root attribute isn't borrowed
auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
# ChildItems: copy full tree, clean up base-config references
child_items_xml = ""
@@ -1178,20 +1271,12 @@ def main():
break
if src_child_items is not None:
child_items_xml = etree.tostring(src_child_items, encoding="unicode")
child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip DataPath / TitleDataPath / RowPictureDataPath
if borrow_main_attr:
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
else:
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
# Strip data-binding tags whose root attribute isn't borrowed
child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX)
@@ -1428,12 +1513,16 @@ def main():
save_text_bom(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl
# 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
module_dir = os.path.join(form_xml_dir, "Form")
os.makedirs(module_dir, exist_ok=True)
module_bsl_file = os.path.join(module_dir, "Module.bsl")
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
if os.path.isfile(module_bsl_file):
info(" Preserved existing Module.bsl")
else:
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects
register_form_in_object(type_name, obj_name, form_name)
+57 -57
View File
@@ -1,57 +1,57 @@
---
name: cfe-diff
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-diff — Анализ расширения конфигурации
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
```
## Mode A — обзор расширения
Для каждого объекта показывает:
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
Для каждой формы заимствованного объекта показывается:
- `(borrowed)` / `(own)` — заимствованная или собственная форма
- callType-события формы и элементов
- callType на командах
## Mode B — проверка переноса
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
Статусы:
- `[TRANSFERRED]` — код найден в конфигурации
- `[NOT_TRANSFERRED]` — код не найден
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
## Примеры
```powershell
# Обзор — что изменено в расширении
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
# Проверка переноса — все ли #Вставка перенесены
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
```
---
name: cfe-diff
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-diff — Анализ расширения конфигурации
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ConfigPath` | Путь к конфигурации (обязат.) | — |
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
```
## Mode A — обзор расширения
Для каждого объекта показывает:
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм
Для каждой формы заимствованного объекта показывается:
- `(borrowed)` / `(own)` — заимствованная или собственная форма
- callType-события формы и элементов
- callType на командах
## Mode B — проверка переноса
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
Статусы:
- `[TRANSFERRED]` — код найден в конфигурации
- `[NOT_TRANSFERRED]` — код не найден
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
## Примеры
```powershell
# Обзор — что изменено в расширении
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
# Проверка переноса — все ли #Вставка перенесены
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
```
+471 -471
View File
@@ -1,471 +1,471 @@
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ExtensionPath,
[Parameter(Mandatory)]
[string]$ConfigPath,
[ValidateSet("A","B")]
[string]$Mode = "A"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
}
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
}
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
# --- Type -> directory mapping ---
$childTypeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes"
}
# --- Parse extension Configuration.xml ---
$extDoc = New-Object System.Xml.XmlDocument
$extDoc.PreserveWhitespace = $false
$extDoc.Load($extCfg)
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
Write-Host " NamePrefix: $namePrefix"
Write-Host ""
# --- Collect ChildObjects ---
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
if (-not $childObjNode) {
Write-Host "[WARN] No ChildObjects in extension"
exit 0
}
$objects = @()
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -eq "Language") { continue }
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
}
if ($objects.Count -eq 0) {
Write-Host "No objects (besides Language) in extension."
exit 0
}
# --- Helper: check if object is borrowed ---
function Get-ObjectInfo {
param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
$dirName = $childTypeDirMap[$objType]
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($objFile)
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
$info = @{
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
File = $objFile
Exists = $true
Type = $objType
Name = $objName
DirName = $dirName
ObjElement = $objEl
ObjNs = $objNs
}
return $info
}
# --- Helper: find .bsl files for object ---
function Get-BslFiles {
param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
$dirName = $childTypeDirMap[$objType]
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
if (-not (Test-Path $objDir -PathType Container)) { return @() }
$bslFiles = @()
$extDir = Join-Path $objDir "Ext"
if (Test-Path $extDir) {
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
foreach ($item in $items) { $bslFiles += $item.FullName }
}
# Forms
$formsDir = Join-Path $objDir "Forms"
if (Test-Path $formsDir) {
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
}
return $bslFiles
}
# --- Helper: parse interceptors from .bsl ---
function Get-Interceptors {
param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$interceptors = @()
$i = 0
while ($i -lt $lines.Count) {
$line = $lines[$i].Trim()
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
$type = $Matches[1]
$method = $Matches[2]
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
}
$i++
}
return $interceptors
}
# --- Helper: extract #Вставка blocks from .bsl ---
function Get-InsertionBlocks {
param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$blocks = @()
$inBlock = $false
$blockLines = @()
$startLine = 0
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].Trim()
if ($line -eq "#Вставка") {
$inBlock = $true
$blockLines = @()
$startLine = $i + 1
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
$inBlock = $false
$blocks += @{
StartLine = $startLine
EndLine = $i + 1
Code = ($blockLines -join "`n").Trim()
File = $bslPath
}
} elseif ($inBlock) {
$blockLines += $lines[$i]
}
}
return $blocks
}
# --- Helper: analyze form for callType events and commands ---
function Get-FormInterceptors {
param([string]$formXmlPath)
if (-not (Test-Path $formXmlPath)) { return $null }
$formDoc = New-Object System.Xml.XmlDocument
$formDoc.PreserveWhitespace = $false
try { $formDoc.Load($formXmlPath) } catch { return $null }
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$fRoot = $formDoc.DocumentElement
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
$isBorrowed = ($baseForm -ne $null)
$interceptors = @()
# Form-level events with callType
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
if ($eventsNode) {
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
$ct = $evt.GetAttribute("callType")
if ($ct) {
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
}
}
}
# Element-level events with callType (scan all elements recursively)
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
if ($childItems) {
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
$elName = $evtNode.GetAttribute("name")
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
$ct = $evt.GetAttribute("callType")
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
}
}
}
# Commands with callType on Action
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
$cmdName = $cmd.GetAttribute("name")
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
$ct = $action.GetAttribute("callType")
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
}
}
return @{
IsBorrowed = $isBorrowed
Interceptors = $interceptors
}
}
# ============================================================
# MODE A: Extension overview
# ============================================================
if ($Mode -eq "A") {
$borrowedList = @()
$ownList = @()
foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info) {
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
continue
}
if (-not $info.Exists) {
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
continue
}
if ($info.Borrowed) {
$borrowedList += $obj
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
# Find .bsl files and interceptors
$bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) {
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
$interceptors = Get-Interceptors $bsl
if ($interceptors.Count -gt 0) {
foreach ($ic in $interceptors) {
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
}
} else {
Write-Host " $relPath (no interceptors)"
}
}
# Check for own attributes/forms in ChildObjects
if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) {
$ownAttrs = 0
$ownForms = 0
$ownTS = 0
$borrowedItems = 0
$formNames = @()
foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
if ($cProps) {
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
if ($cOb -and $cOb.InnerText -eq "Adopted") {
$borrowedItems++
continue
}
}
switch ($c.LocalName) {
"Attribute" { $ownAttrs++ }
"TabularSection" { $ownTS++ }
"Form" { $formNames += $c.InnerText; $ownForms++ }
}
}
$parts = @()
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
if ($parts.Count -gt 0) {
Write-Host " ChildObjects: $($parts -join ', ')"
}
# Analyze forms
$borrowedFormCount = 0
$ownFormCount = 0
foreach ($fn in $formNames) {
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
$fi = Get-FormInterceptors $formXmlPath
if (-not $fi) {
Write-Host " Form.$fn (?)"
continue
}
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
if ($fi.Interceptors.Count -gt 0) {
Write-Host " Form.$fn ($formTag):"
foreach ($ic in $fi.Interceptors) {
Write-Host " $ic"
}
} else {
Write-Host " Form.$fn ($formTag)"
}
}
}
}
} else {
$ownList += $obj
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
# Brief info for own objects
if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) {
$attrs = 0; $forms = 0; $ts = 0
foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
switch ($c.LocalName) {
"Attribute" { $attrs++ }
"TabularSection" { $ts++ }
"Form" { $forms++ }
}
}
$parts = @()
if ($attrs -gt 0) { $parts += "$attrs attrs" }
if ($ts -gt 0) { $parts += "$ts TS" }
if ($forms -gt 0) { $parts += "$forms forms" }
if ($parts.Count -gt 0) {
Write-Host " $($parts -join ', ')"
}
}
}
}
}
Write-Host ""
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
}
# ============================================================
# MODE B: Transfer check
# ============================================================
if ($Mode -eq "B") {
$transferred = 0
$notTransferred = 0
$needsReview = 0
foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
# Find .bsl files with &ИзменениеИКонтроль
$bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) {
$interceptors = Get-Interceptors $bsl
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
if ($macInterceptors.Count -eq 0) { continue }
foreach ($ic in $macInterceptors) {
$methodName = $ic.Method
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
# Find #Вставка blocks in this file
$insertBlocks = Get-InsertionBlocks $bsl
if ($insertBlocks.Count -eq 0) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
$needsReview++
continue
}
# Find corresponding module in config
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
$dirName = $childTypeDirMap[$obj.Type]
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
if (-not (Test-Path $configBsl)) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
$needsReview++
continue
}
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
$allTransferred = $true
foreach ($block in $insertBlocks) {
$code = $block.Code
if (-not $code) { continue }
# Normalize whitespace for comparison
$codeNorm = $code -replace '\s+', ' '
$configNorm = $configContent -replace '\s+', ' '
if ($configNorm.Contains($codeNorm)) {
# Found in config
} else {
$allTransferred = $false
}
}
if ($allTransferred) {
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
$transferred++
} else {
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
$notTransferred++
}
}
}
}
Write-Host ""
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
}
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$ExtensionPath,
[Parameter(Mandatory)]
[string]$ConfigPath,
[ValidateSet("A","B")]
[string]$Mode = "A"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
}
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
}
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
$extCfg = Join-Path $ExtensionPath "Configuration.xml"
$srcCfg = Join-Path $ConfigPath "Configuration.xml"
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
# --- Type -> directory mapping ---
$childTypeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes"
}
# --- Parse extension Configuration.xml ---
$extDoc = New-Object System.Xml.XmlDocument
$extDoc.PreserveWhitespace = $false
$extDoc.Load($extCfg)
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
Write-Host " NamePrefix: $namePrefix"
Write-Host ""
# --- Collect ChildObjects ---
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
if (-not $childObjNode) {
Write-Host "[WARN] No ChildObjects in extension"
exit 0
}
$objects = @()
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -eq "Language") { continue }
$objects += @{ Type = $child.LocalName; Name = $child.InnerText }
}
if ($objects.Count -eq 0) {
Write-Host "No objects (besides Language) in extension."
exit 0
}
# --- Helper: check if object is borrowed ---
function Get-ObjectInfo {
param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
$dirName = $childTypeDirMap[$objType]
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false
$doc.Load($objFile)
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break }
}
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
$info = @{
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
File = $objFile
Exists = $true
Type = $objType
Name = $objName
DirName = $dirName
ObjElement = $objEl
ObjNs = $objNs
}
return $info
}
# --- Helper: find .bsl files for object ---
function Get-BslFiles {
param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
$dirName = $childTypeDirMap[$objType]
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
if (-not (Test-Path $objDir -PathType Container)) { return @() }
$bslFiles = @()
$extDir = Join-Path $objDir "Ext"
if (Test-Path $extDir) {
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
foreach ($item in $items) { $bslFiles += $item.FullName }
}
# Forms
$formsDir = Join-Path $objDir "Forms"
if (Test-Path $formsDir) {
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
}
return $bslFiles
}
# --- Helper: parse interceptors from .bsl ---
function Get-Interceptors {
param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$interceptors = @()
$i = 0
while ($i -lt $lines.Count) {
$line = $lines[$i].Trim()
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
$type = $Matches[1]
$method = $Matches[2]
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
}
$i++
}
return $interceptors
}
# --- Helper: extract #Вставка blocks from .bsl ---
function Get-InsertionBlocks {
param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$blocks = @()
$inBlock = $false
$blockLines = @()
$startLine = 0
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].Trim()
if ($line -eq "#Вставка") {
$inBlock = $true
$blockLines = @()
$startLine = $i + 1
} elseif ($line -eq "#КонецВставки" -and $inBlock) {
$inBlock = $false
$blocks += @{
StartLine = $startLine
EndLine = $i + 1
Code = ($blockLines -join "`n").Trim()
File = $bslPath
}
} elseif ($inBlock) {
$blockLines += $lines[$i]
}
}
return $blocks
}
# --- Helper: analyze form for callType events and commands ---
function Get-FormInterceptors {
param([string]$formXmlPath)
if (-not (Test-Path $formXmlPath)) { return $null }
$formDoc = New-Object System.Xml.XmlDocument
$formDoc.PreserveWhitespace = $false
try { $formDoc.Load($formXmlPath) } catch { return $null }
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$fRoot = $formDoc.DocumentElement
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
$isBorrowed = ($baseForm -ne $null)
$interceptors = @()
# Form-level events with callType
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
if ($eventsNode) {
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
$ct = $evt.GetAttribute("callType")
if ($ct) {
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
}
}
}
# Element-level events with callType (scan all elements recursively)
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
if ($childItems) {
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
$elName = $evtNode.GetAttribute("name")
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
$ct = $evt.GetAttribute("callType")
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
}
}
}
# Commands with callType on Action
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
$cmdName = $cmd.GetAttribute("name")
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
$ct = $action.GetAttribute("callType")
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
}
}
return @{
IsBorrowed = $isBorrowed
Interceptors = $interceptors
}
}
# ============================================================
# MODE A: Extension overview
# ============================================================
if ($Mode -eq "A") {
$borrowedList = @()
$ownList = @()
foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info) {
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
continue
}
if (-not $info.Exists) {
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
continue
}
if ($info.Borrowed) {
$borrowedList += $obj
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
# Find .bsl files and interceptors
$bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) {
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
$interceptors = Get-Interceptors $bsl
if ($interceptors.Count -gt 0) {
foreach ($ic in $interceptors) {
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
}
} else {
Write-Host " $relPath (no interceptors)"
}
}
# Check for own attributes/forms in ChildObjects
if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) {
$ownAttrs = 0
$ownForms = 0
$ownTS = 0
$borrowedItems = 0
$formNames = @()
foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
if ($cProps) {
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
if ($cOb -and $cOb.InnerText -eq "Adopted") {
$borrowedItems++
continue
}
}
switch ($c.LocalName) {
"Attribute" { $ownAttrs++ }
"TabularSection" { $ownTS++ }
"Form" { $formNames += $c.InnerText; $ownForms++ }
}
}
$parts = @()
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
if ($parts.Count -gt 0) {
Write-Host " ChildObjects: $($parts -join ', ')"
}
# Analyze forms
$borrowedFormCount = 0
$ownFormCount = 0
foreach ($fn in $formNames) {
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
$fi = Get-FormInterceptors $formXmlPath
if (-not $fi) {
Write-Host " Form.$fn (?)"
continue
}
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
if ($fi.Interceptors.Count -gt 0) {
Write-Host " Form.$fn ($formTag):"
foreach ($ic in $fi.Interceptors) {
Write-Host " $ic"
}
} else {
Write-Host " Form.$fn ($formTag)"
}
}
}
}
} else {
$ownList += $obj
Write-Host " [OWN] $($obj.Type).$($obj.Name)"
# Brief info for own objects
if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) {
$attrs = 0; $forms = 0; $ts = 0
foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue }
switch ($c.LocalName) {
"Attribute" { $attrs++ }
"TabularSection" { $ts++ }
"Form" { $forms++ }
}
}
$parts = @()
if ($attrs -gt 0) { $parts += "$attrs attrs" }
if ($ts -gt 0) { $parts += "$ts TS" }
if ($forms -gt 0) { $parts += "$forms forms" }
if ($parts.Count -gt 0) {
Write-Host " $($parts -join ', ')"
}
}
}
}
}
Write-Host ""
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
}
# ============================================================
# MODE B: Transfer check
# ============================================================
if ($Mode -eq "B") {
$transferred = 0
$notTransferred = 0
$needsReview = 0
foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
# Find .bsl files with &ИзменениеИКонтроль
$bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) {
$interceptors = Get-Interceptors $bsl
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
if ($macInterceptors.Count -eq 0) { continue }
foreach ($ic in $macInterceptors) {
$methodName = $ic.Method
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
# Find #Вставка blocks in this file
$insertBlocks = Get-InsertionBlocks $bsl
if ($insertBlocks.Count -eq 0) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
$needsReview++
continue
}
# Find corresponding module in config
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
$dirName = $childTypeDirMap[$obj.Type]
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
if (-not (Test-Path $configBsl)) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
$needsReview++
continue
}
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
$allTransferred = $true
foreach ($block in $insertBlocks) {
$code = $block.Code
if (-not $code) { continue }
# Normalize whitespace for comparison
$codeNorm = $code -replace '\s+', ' '
$configNorm = $configContent -replace '\s+', ' '
if ($configNorm.Contains($codeNorm)) {
# Found in config
} else {
$allTransferred = $false
}
}
if ($allTransferred) {
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
$transferred++
} else {
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
$notTransferred++
}
}
}
}
Write-Host ""
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
}
+71 -71
View File
@@ -1,71 +1,71 @@
---
name: cfe-init
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-init — Создание расширения конфигурации 1С
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
## Подготовка
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `Name` | Имя расширения (обязат.) | — |
| `Synonym` | Синоним | = Name |
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
| `OutputDir` | Каталог для создания | `src` |
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
| `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
```
## Примеры
```powershell
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
# Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
# Расширение-доработка с версией
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
# Без роли, с явным префиксом
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
```
## Верификация
```
/cfe-validate <OutputDir>
```
---
name: cfe-init
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-init — Создание расширения конфигурации 1С
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
## Подготовка
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `Name` | Имя расширения (обязат.) | — |
| `Synonym` | Синоним | = Name |
| `NamePrefix` | Префикс собственных объектов | = Name + "_" |
| `OutputDir` | Каталог для создания | `src` |
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
| `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
```
## Примеры
```powershell
# Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
# Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
# Расширение-доработка с версией
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
# Без роли, с явным префиксом
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
```
## Верификация
```
/cfe-validate <OutputDir>
```
+279 -270
View File
@@ -1,270 +1,279 @@
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$NamePrefix,
[string]$OutputDir = "src",
[ValidateSet("Patch","Customization","AddOn")]
[string]$Purpose = "Customization",
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24",
[string]$ConfigPath,
[switch]$NoRole
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Default NamePrefix ---
if (-not $NamePrefix) {
$NamePrefix = "${Name}_"
}
# --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir
}
# --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile"
exit 1
}
# --- Resolve ConfigPath ---
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
if ($ConfigPath) {
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
}
if (Test-Path $ConfigPath -PathType Container) {
$candidate = Join-Path $ConfigPath "Configuration.xml"
if (Test-Path $candidate) { $ConfigPath = $candidate }
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
}
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
# 3a. Read Language UUID from base config
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
if (Test-Path $baseLangFile) {
$baseLangDoc = New-Object System.Xml.XmlDocument
$baseLangDoc.PreserveWhitespace = $false
$baseLangDoc.Load($baseLangFile)
$langEl = $null
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
}
if ($langEl) {
$baseLangUuid = $langEl.GetAttribute("uuid")
Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
} else {
Write-Host "[WARN] No <Language> element in $baseLangFile"
}
} else {
Write-Host "[WARN] Base config language not found: $baseLangFile"
}
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
$baseCfgDoc = New-Object System.Xml.XmlDocument
$baseCfgDoc.PreserveWhitespace = $false
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
if ($compatNode -and $compatNode.InnerText) {
$CompatibilityMode = $compatNode.InnerText.Trim()
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
} else {
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
}
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
if ($ifcNode -and $ifcNode.InnerText) {
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
} else {
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
}
} else {
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
}
# --- Generate UUIDs ---
$uuidCfg = [guid]::NewGuid().ToString()
$uuidLang = [guid]::NewGuid().ToString()
$uuidRole = [guid]::NewGuid().ToString()
# 7 ContainedObject ObjectIds
$co1 = [guid]::NewGuid().ToString()
$co2 = [guid]::NewGuid().ToString()
$co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString()
# --- Synonym XML ---
$synonymXml = ""
if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
}
# --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- Role name ---
$roleName = "${NamePrefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
$defaultRolesXml = ""
if (-not $NoRole) {
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
}
# --- ChildObjects ---
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
if (-not $NoRole) {
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
}
$childObjectsXml += "`r`n`t`t"
# --- Configuration.xml ---
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Configuration uuid="$uuidCfg">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<xr:ObjectId>$co1</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<xr:ObjectId>$co2</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<xr:ObjectId>$co3</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<xr:ObjectId>$co4</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<xr:ObjectId>$co5</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<xr:ObjectId>$co6</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<xr:ObjectId>$co7</xr:ObjectId>
</xr:ContainedObject>
</InternalInfo>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Synonym>$synonymXml</Synonym>
<Comment/>
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes>
<ScriptVariant>Russian</ScriptVariant>
<DefaultRoles>$defaultRolesXml</DefaultRoles>
<Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version>
<DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/>
<DetailedInformation/>
<Copyright/>
<VendorInformationAddress/>
<ConfigurationInformationAddress/>
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
</Properties>
<ChildObjects>$childObjectsXml</ChildObjects>
</Configuration>
</MetaDataObject>
"@
# --- Languages/Русский.xml (adopted format) ---
$langXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Language uuid="$uuidLang">
<InternalInfo/>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>Русский</Name>
<Comment/>
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
<LanguageCode>ru</LanguageCode>
</Properties>
</Language>
</MetaDataObject>
"@
# --- Role XML ---
$roleXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Role uuid="$uuidRole">
<Properties>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
<Synonym/>
<Comment/>
</Properties>
</Role>
</MetaDataObject>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
# --- Role ---
if (-not $NoRole) {
$roleDir = Join-Path $OutputDir "Roles"
if (-not (Test-Path $roleDir)) {
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
}
$roleFile = Join-Path $roleDir "$roleName.xml"
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
}
# --- Output ---
Write-Host "[OK] Создано расширение: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Назначение: $Purpose"
Write-Host " Префикс: $NamePrefix"
Write-Host " Совместимость: $CompatibilityMode"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
if (-not $NoRole) {
Write-Host " Role: $roleFile"
}
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$NamePrefix,
[string]$OutputDir = "src",
[ValidateSet("Patch","Customization","AddOn")]
[string]$Purpose = "Customization",
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24",
[string]$ConfigPath,
[switch]$NoRole
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Default NamePrefix ---
if (-not $NamePrefix) {
$NamePrefix = "${Name}_"
}
# --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir
}
# --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile"
exit 1
}
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
$formatVersion = "2.17"
# --- Resolve ConfigPath ---
$baseLangUuid = "00000000-0000-0000-0000-000000000000"
if ($ConfigPath) {
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath
}
if (Test-Path $ConfigPath -PathType Container) {
$candidate = Join-Path $ConfigPath "Configuration.xml"
if (Test-Path $candidate) { $ConfigPath = $candidate }
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
}
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
# 3a. Read Language UUID from base config
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
if (Test-Path $baseLangFile) {
$baseLangDoc = New-Object System.Xml.XmlDocument
$baseLangDoc.PreserveWhitespace = $false
$baseLangDoc.Load($baseLangFile)
$langEl = $null
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
}
if ($langEl) {
$baseLangUuid = $langEl.GetAttribute("uuid")
Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
} else {
Write-Host "[WARN] No <Language> element in $baseLangFile"
}
} else {
Write-Host "[WARN] Base config language not found: $baseLangFile"
}
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
$baseCfgDoc = New-Object System.Xml.XmlDocument
$baseCfgDoc.PreserveWhitespace = $false
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
if ($fmtVer) {
$formatVersion = $fmtVer
Write-Host "[INFO] Base config format version: $formatVersion"
}
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
if ($compatNode -and $compatNode.InnerText) {
$CompatibilityMode = $compatNode.InnerText.Trim()
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
} else {
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
}
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
if ($ifcNode -and $ifcNode.InnerText) {
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
} else {
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
}
} else {
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
}
# --- Generate UUIDs ---
$uuidCfg = [guid]::NewGuid().ToString()
$uuidLang = [guid]::NewGuid().ToString()
$uuidRole = [guid]::NewGuid().ToString()
# 7 ContainedObject ObjectIds
$co1 = [guid]::NewGuid().ToString()
$co2 = [guid]::NewGuid().ToString()
$co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString()
# --- Synonym XML ---
$synonymXml = ""
if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
}
# --- Optional properties ---
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" }
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- Role name ---
$roleName = "${NamePrefix}ОсновнаяРоль"
# --- DefaultRoles XML ---
$defaultRolesXml = ""
if (-not $NoRole) {
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t"
}
# --- ChildObjects ---
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
if (-not $NoRole) {
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
}
$childObjectsXml += "`r`n`t`t"
# --- Configuration.xml ---
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Configuration uuid="$uuidCfg">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<xr:ObjectId>$co1</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<xr:ObjectId>$co2</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<xr:ObjectId>$co3</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<xr:ObjectId>$co4</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<xr:ObjectId>$co5</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<xr:ObjectId>$co6</xr:ObjectId>
</xr:ContainedObject>
<xr:ContainedObject>
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<xr:ObjectId>$co7</xr:ObjectId>
</xr:ContainedObject>
</InternalInfo>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Synonym>$synonymXml</Synonym>
<Comment/>
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultRunMode>ManagedApplication</DefaultRunMode>
<UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes>
<ScriptVariant>Russian</ScriptVariant>
<DefaultRoles>$defaultRolesXml</DefaultRoles>
<Vendor>$vendorXml</Vendor>
<Version>$versionXml</Version>
<DefaultLanguage>Language.Русский</DefaultLanguage>
<BriefInformation/>
<DetailedInformation/>
<Copyright/>
<VendorInformationAddress/>
<ConfigurationInformationAddress/>
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
</Properties>
<ChildObjects>$childObjectsXml</ChildObjects>
</Configuration>
</MetaDataObject>
"@
# --- Languages/Русский.xml (adopted format) ---
$langXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Language uuid="$uuidLang">
<InternalInfo/>
<Properties>
<ObjectBelonging>Adopted</ObjectBelonging>
<Name>Русский</Name>
<Comment/>
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
<LanguageCode>ru</LanguageCode>
</Properties>
</Language>
</MetaDataObject>
"@
# --- Role XML ---
$roleXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="$formatVersion">
<Role uuid="$uuidRole">
<Properties>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
<Synonym/>
<Comment/>
</Properties>
</Role>
</MetaDataObject>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc)
$langFile = Join-Path $langDir "Русский.xml"
[System.IO.File]::WriteAllText($langFile, $langXml, $enc)
# --- Role ---
if (-not $NoRole) {
$roleDir = Join-Path $OutputDir "Roles"
if (-not (Test-Path $roleDir)) {
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
}
$roleFile = Join-Path $roleDir "$roleName.xml"
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc)
}
# --- Output ---
Write-Host "[OK] Создано расширение: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Назначение: $Purpose"
Write-Host " Префикс: $NamePrefix"
Write-Host " Совместимость: $CompatibilityMode"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
if (-not $NoRole) {
Write-Host " Role: $roleFile"
}
+12 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE)
# cfe-init v1.2 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid
@@ -50,6 +50,10 @@ def main():
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1)
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
format_version = "2.17"
# --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath:
@@ -88,6 +92,10 @@ def main():
try:
base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot()
fmt_ver = base_cfg_root.get("version")
if fmt_ver:
format_version = fmt_ver
print(f"[INFO] Base config format version: {format_version}")
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text:
@@ -155,7 +163,7 @@ def main():
\t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
@@ -190,7 +198,7 @@ def main():
# --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/>
\t\t<Properties>
@@ -205,7 +213,7 @@ def main():
# --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">
\t<Role uuid="{uuid_role}">
\t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name>
+78 -78
View File
@@ -1,78 +1,78 @@
---
name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — |
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
## Формат ModulePath
| ModulePath | Файл |
|------------|------|
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
Аналогично для Report, DataProcessor, InformationRegister и других типов.
## Типы перехвата
| InterceptorType | Декоратор | Назначение |
|-----------------|-----------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода |
| `After` | `&После` | Код после вызова оригинального метода |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Перехват &Перед на сервере
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват &После на клиенте
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
# ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
```
## Генерируемый код (Before)
```bsl
&НаСервере
&Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода
КонецПроцедуры
```
---
name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — |
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
## Формат ModulePath
| ModulePath | Файл |
|------------|------|
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
Аналогично для Report, DataProcessor, InformationRegister и других типов.
## Типы перехвата
| InterceptorType | Декоратор | Назначение |
|-----------------|-----------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода |
| `After` | `&После` | Код после вызова оригинального метода |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Перехват &Перед на сервере
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват &После на клиенте
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
# ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
```
## Генерируемый код (Before)
```bsl
&НаСервере
&Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода
КонецПроцедуры
```
+29 -29
View File
@@ -1,29 +1,29 @@
---
name: cfe-validate
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-validate — валидация расширения конфигурации (CFE)
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
```
---
name: cfe-validate
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /cfe-validate — валидация расширения конфигурации (CFE)
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
```
File diff suppressed because it is too large Load Diff
+70 -78
View File
@@ -1,78 +1,70 @@
---
name: db-create
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name>
allowed-tools:
- Bash
- Read
- Write
- Glob
- AskUserQuestion
---
# /db-create — Создание информационной базы
Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`.
## Usage
```
/db-create <path> — файловая база по указанному пути
/db-create <server>/<name> — серверная база
/db-create — интерактивно
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
После создания базы предложи зарегистрировать через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
| `-AddToList` | нет | Добавить в список баз 1С |
| `-ListName <имя>` | нет | Имя базы в списке |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После создания
1. Прочитай лог-файл и покажи результат
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
## Примеры
```powershell
# Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
---
name: db-create
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name>
allowed-tools:
- Bash
- Read
- Write
- Glob
- AskUserQuestion
---
# /db-create — Создание информационной базы
Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`.
## Usage
```
/db-create <path> — файловая база по указанному пути
/db-create <server>/<name> — серверная база
/db-create — интерактивно
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
После создания базы предложи зарегистрировать через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Путь к файловой базе |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
| `-AddToList` | нет | Добавить в список баз 1С |
| `-ListName <имя>` | нет | Имя базы в списке |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После создания
Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
## Примеры
```powershell
# Создать файловую базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
# Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
# Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
+220 -163
View File
@@ -1,163 +1,220 @@
# db-create v1.0 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Создание информационной базы 1С
.DESCRIPTION
Создаёт новую информационную базу 1С (файловую или серверную).
Поддерживает создание из шаблона и добавление в список баз.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt)
.PARAMETER AddToList
Добавить в список баз 1С
.PARAMETER ListName
Имя базы в списке
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
.EXAMPLE
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
#>
[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]$UseTemplate,
[Parameter(Mandatory=$false)]
[switch]$AddToList,
[Parameter(Mandatory=$false)]
[string]$ListName
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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 template ---
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
# --- Build arguments ---
$arguments = @("CREATEINFOBASE")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
} else {
$arguments += "File=`"$InfoBasePath`""
}
# --- Template ---
if ($UseTemplate) {
$arguments += "/UseTemplate", "`"$UseTemplate`""
}
# --- Add to list ---
if ($AddToList) {
if ($ListName) {
$arguments += "/AddToList", "`"$ListName`""
} else {
$arguments += "/AddToList"
}
}
# --- Output ---
$outFile = Join-Path $tempDir "create_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) {
if ($InfoBaseServer -and $InfoBaseRef) {
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
} else {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
}
} else {
Write-Host "Error creating 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
}
}
# db-create v1.4 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Создание информационной базы 1С
.DESCRIPTION
Создаёт новую информационную базу 1С (файловую или серверную).
Поддерживает создание из шаблона и добавление в список баз.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt)
.PARAMETER AddToList
Добавить в список баз 1С
.PARAMETER ListName
Имя базы в списке
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
.EXAMPLE
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
#>
[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]$UseTemplate,
[Parameter(Mandatory=$false)]
[switch]$AddToList,
[Parameter(Mandatory=$false)]
[string]$ListName
)
$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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate template ---
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
if ($UseTemplate) {
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
$arguments += "--restore=$UseTemplate"
} else {
$arguments += "--load=$UseTemplate", "--apply"
}
}
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("CREATEINFOBASE")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
} else {
$arguments += "File=`"$InfoBasePath`""
}
# --- Template ---
if ($UseTemplate) {
$arguments += "/UseTemplate", "`"$UseTemplate`""
}
# --- Add to list ---
if ($AddToList) {
if ($ListName) {
$arguments += "/AddToList", "`"$ListName`""
} else {
$arguments += "/AddToList"
}
}
# --- Output ---
$outFile = Join-Path $tempDir "create_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) {
if ($InfoBaseServer -and $InfoBaseRef) {
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green
} else {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
}
} else {
Write-Host "Error creating 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
}
}
+75 -9
View File
@@ -1,29 +1,65 @@
#!/usr/bin/env python3
# db-create v1.0 — Create 1C information base
# db-create v1.4 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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)
@@ -47,9 +83,14 @@ def main():
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
@@ -58,6 +99,29 @@ def main():
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
if args.UseTemplate:
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
arguments.append(f"--restore={args.UseTemplate}")
else:
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"Information base created successfully: {args.InfoBasePath}")
else:
print(f"Error creating information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
@@ -67,9 +131,11 @@ def main():
arguments = ["CREATEINFOBASE"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
# No embedded quotes: subprocess quotes the whole token; 1C's argv parser
# strips outer quotes. Inner quotes get escaped by list2cmdline and break parsing.
arguments.append(f'Srvr={args.InfoBaseServer};Ref={args.InfoBaseRef}')
else:
arguments.append(f'File="{args.InfoBasePath}"')
arguments.append(f'File={args.InfoBasePath}')
# --- Template ---
if args.UseTemplate:
+68 -79
View File
@@ -1,79 +1,68 @@
---
name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-cf — Выгрузка конфигурации в CF-файл
Выгружает конфигурацию информационной базы в бинарный CF-файл.
## Usage
```
/db-dump-cf [database] [output.cf]
/db-dump-cf dev config.cf
/db-dump-cf — база по умолчанию, файл config.cf
```
## Параметры подключения
Прочитай `.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-cf.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После выполнения
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога.
## Примеры
```powershell
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
---
name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-cf — Выгрузка конфигурации в CF-файл
Выгружает конфигурацию информационной базы в бинарный CF-файл.
## Usage
```
/db-dump-cf [database] [output.cf]
/db-dump-cf dev config.cf
/db-dump-cf — база по умолчанию, файл config.cf
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Примеры
```powershell
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
+224 -166
View File
@@ -1,166 +1,224 @@
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Выгрузка конфигурации 1С в CF-файл
.DESCRIPTION
Выгружает конфигурацию информационной базы в бинарный CF-файл.
Поддерживает выгрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному CF-файлу
.PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
#>
[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,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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_cf_$(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 += "/DumpCfg", "`"$OutputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_cf_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 "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping configuration (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
}
}
# db-dump-cf v1.4 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Выгрузка конфигурации 1С в CF-файл
.DESCRIPTION
Выгружает конфигурацию информационной базы в бинарный CF-файл.
Поддерживает выгрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному CF-файлу
.PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
#>
[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,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
)
$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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$OutputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/DumpCfg", "`"$OutputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_cf_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 "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error dumping configuration (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
}
}
@@ -1,29 +1,65 @@
#!/usr/bin/env python3
# db-dump-cf v1.0 — Dump 1C configuration to CF file
# db-dump-cf v1.4 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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)
@@ -49,9 +85,14 @@ def main():
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
@@ -60,6 +101,34 @@ def main():
if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
+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` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.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,197 @@
# db-dump-dt v1.3 — 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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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 {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$OutputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
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 ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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,184 @@
#!/usr/bin/env python3
# db-dump-dt v1.3 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif 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)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
else:
print(f"Error dumping information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- 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()
+90 -97
View File
@@ -1,97 +1,90 @@
---
name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-xml — Выгрузка конфигурации в XML
Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo.
## Usage
```
/db-dump-xml [database] [outputDir]
/db-dump-xml dev src/config
/db-dump-xml dev src/config -Mode Full
/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
```
## Параметры подключения
Прочитай `.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`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог для выгрузки |
| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` |
| `-Objects <список>` | для Partial | Имена объектов через запятую |
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Режимы выгрузки
| Режим | Описание |
|-------|----------|
| `Full` | Полная выгрузка — все объекты конфигурации |
| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) |
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
## Примеры
```powershell
# Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Инкрементальная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
---
name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-xml — Выгрузка конфигурации в XML
Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo.
## Usage
```
/db-dump-xml [database] [outputDir]
/db-dump-xml dev src/config
/db-dump-xml dev src/config -Mode Full
/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог для выгрузки |
| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` |
| `-Objects <список>` | для Partial | Имена объектов через запятую |
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Режимы выгрузки
| Режим | Описание |
|-------|----------|
| `Full` | Полная выгрузка — все объекты конфигурации |
| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) |
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
## Примеры
```powershell
# Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Инкрементальная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
+293 -223
View File
@@ -1,224 +1,294 @@
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# db-dump-xml v1.6 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы
.DESCRIPTION
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
- Full: полная выгрузка всей конфигурации
- Changes: инкрементальная выгрузка изменённых объектов
- Partial: выгрузка конкретных объектов из списка
- UpdateInfo: обновление только ConfigDumpInfo.xml
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог для выгрузки конфигурации
.PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
.PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial)
.PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
#>
[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]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "Changes",
[Parameter(Mandatory=$false)]
[string]$Objects,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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 Partial mode ---
if ($Mode -eq "Partial" -and -not $Objects) {
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
exit 1
}
# --- Create output dir if needed ---
if (-not (Test-Path $ConfigDir)) {
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
Write-Host "Created output directory: $ConfigDir"
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(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 += "/DumpConfigToFiles", "`"$ConfigDir`""
$arguments += "-Format", $Format
switch ($Mode) {
"Full" {
Write-Host "Executing full configuration dump..."
}
"Changes" {
Write-Host "Executing incremental configuration dump..."
$arguments += "-update"
$arguments += "-force"
}
"Partial" {
Write-Host "Executing partial configuration dump..."
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$listFile = Join-Path $tempDir "dump_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
$arguments += "-listFile", "`"$listFile`""
Write-Host "Objects to dump: $($objectList.Count)"
foreach ($obj in $objectList) { Write-Host " $obj" }
}
"UpdateInfo" {
Write-Host "Updating ConfigDumpInfo.xml..."
$arguments += "-configDumpInfoOnly"
}
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir"
} else {
Write-Host "Error dumping configuration (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
}
}
<#
.SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы
.DESCRIPTION
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
- Full: полная выгрузка всей конфигурации
- Changes: инкрементальная выгрузка изменённых объектов
- Partial: выгрузка конкретных объектов из списка
- UpdateInfo: обновление только ConfigDumpInfo.xml
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог для выгрузки конфигурации
.PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
.PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial)
.PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
#>
[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]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "Changes",
[Parameter(Mandatory=$false)]
[string]$Objects,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
)
$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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate Partial mode ---
if ($Mode -eq "Partial" -and -not $Objects) {
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
exit 1
}
# --- Create output dir if needed ---
if (-not (Test-Path $ConfigDir)) {
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
Write-Host "Created output directory: $ConfigDir"
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "UpdateInfo") {
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
exit 1
} elseif ($Mode -eq "Partial") {
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$arguments = @("infobase", "config", "export", "objects") + $objList
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
} else {
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/DumpConfigToFiles", "`"$ConfigDir`""
$arguments += "-Format", $Format
switch ($Mode) {
"Full" {
Write-Host "Executing full configuration dump..."
}
"Changes" {
Write-Host "Executing incremental configuration dump..."
$arguments += "-update"
$arguments += "-force"
}
"Partial" {
Write-Host "Executing partial configuration dump..."
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$listFile = Join-Path $tempDir "dump_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
$arguments += "-listFile", "`"$listFile`""
Write-Host "Objects to dump: $($objectList.Count)"
foreach ($obj in $objectList) { Write-Host " $obj" }
}
"UpdateInfo" {
Write-Host "Updating ConfigDumpInfo.xml..."
$arguments += "-configDumpInfoOnly"
}
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir"
} else {
Write-Host "Error dumping configuration (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
}
}
@@ -1,34 +1,68 @@
#!/usr/bin/env python3
# db-dump-xml v1.0 — Dump 1C configuration to XML files
# db-dump-xml v1.6 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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
@@ -65,9 +99,14 @@ def main():
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
@@ -81,6 +120,46 @@ def main():
os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}")
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "UpdateInfo":
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
sys.exit(1)
elif args.Mode == "Partial":
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
arguments = ["infobase", "config", "export", "objects"] + obj_list
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"Configuration exported successfully to: {args.ConfigDir}")
else:
print(f"Error exporting configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
+158 -158
View File
@@ -1,158 +1,158 @@
---
name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]"
allowed-tools:
- Read
- Write
- Glob
- AskUserQuestion
---
# /db-list — Управление реестром баз данных
Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git.
## Usage
```
/db-list — показать список баз
/db-list add — добавить базу (интерактивно)
/db-list remove <id> — удалить базу из реестра
/db-list show <id|alias> — подробности по базе
```
## Формат `.v8-project.json`
Файл размещается в корне проекта (рядом с `.git/`).
```json
{
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
"databases": [
{
"id": "dev",
"name": "Разработка",
"type": "file",
"path": "C:\\Bases\\MyApp_Dev",
"user": "Admin",
"password": "",
"aliases": ["dev", "разработка"],
"branches": ["dev", "develop", "feature/*"],
"configSrc": "C:\\WS\\myapp\\cfsrc"
},
{
"id": "test",
"name": "Тестовая",
"type": "server",
"server": "srv01",
"ref": "MyApp_Test",
"user": "Admin",
"password": "123",
"aliases": ["test", "тест"]
}
],
"default": "dev"
}
```
### Поля корневого объекта
| Поле | Тип | Описание |
|------|-----|----------|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
### Поля объекта базы данных
| Поле | Тип | Обязательное | Описание |
|------|-----|:------------:|----------|
| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
| `name` | string | да | Человекочитаемое имя |
| `type` | `"file"` / `"server"` | да | Тип подключения |
| `path` | string | для file | Путь к каталогу файловой базы |
| `server` | string | для server | Адрес сервера 1С |
| `ref` | string | для server | Имя базы на сервере |
| `user` | string | нет | Имя пользователя 1С |
| `password` | string | нет | Пароль |
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
## Алгоритм разрешения базы данных
Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке:
1. По `id` (точное совпадение)
2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой»)
3. По `name` (нечёткое совпадение с учётом морфологии и регистра)
3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`:
- Точное совпадение: ветка `dev``"branches": ["dev"]`
- Glob-паттерн: ветка `release/2.1``"branches": ["release/*"]`
4. Если ветка не совпала — используй `default`
5. Если не найдено или неоднозначно — спроси пользователя
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
### Автоопределение платформы
Если `v8path` не задан в конфиге:
```powershell
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
```
## Операции
### Показать список баз
Прочитай `.v8-project.json`, выведи таблицу:
```
ID Имя Тип Путь/Сервер По умолч.
dev Разработка file C:\Bases\MyApp_Dev ✓
test Тестовая server srv01/MyApp_Test
```
### Добавить базу
Спроси у пользователя через AskUserQuestion:
- id, name, type (file/server)
- path (для file) или server + ref (для server)
- user, password (необязательно)
- aliases, branches (необязательно)
Добавь в массив `databases`. Если это первая база — установи как `default`.
### Удалить базу
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
### Подробности по базе
Выведи все поля конкретной базы.
## Формирование строки подключения
Для использования в шаблонах команд других навыков:
**Файловая база:**
```
/F "<path>"
```
**Серверная база:**
```
/S "<server>/<ref>"
```
**Аутентификация** (добавляется если user задан):
```
/N"<user>" /P"<password>"
```
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
---
name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]"
allowed-tools:
- Read
- Write
- Glob
- AskUserQuestion
---
# /db-list — Управление реестром баз данных
Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git.
## Usage
```
/db-list — показать список баз
/db-list add — добавить базу (интерактивно)
/db-list remove <id> — удалить базу из реестра
/db-list show <id|alias> — подробности по базе
```
## Формат `.v8-project.json`
Файл размещается в корне проекта (рядом с `.git/`).
```json
{
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
"databases": [
{
"id": "dev",
"name": "Разработка",
"type": "file",
"path": "C:\\Bases\\MyApp_Dev",
"user": "Admin",
"password": "",
"aliases": ["dev", "разработка"],
"branches": ["dev", "develop", "feature/*"],
"configSrc": "C:\\WS\\myapp\\cfsrc"
},
{
"id": "test",
"name": "Тестовая",
"type": "server",
"server": "srv01",
"ref": "MyApp_Test",
"user": "Admin",
"password": "123",
"aliases": ["test", "тест"]
}
],
"default": "dev"
}
```
### Поля корневого объекта
| Поле | Тип | Описание |
|------|-----|----------|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
### Поля объекта базы данных
| Поле | Тип | Обязательное | Описание |
|------|-----|:------------:|----------|
| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
| `name` | string | да | Человекочитаемое имя |
| `type` | `"file"` / `"server"` | да | Тип подключения |
| `path` | string | для file | Путь к каталогу файловой базы |
| `server` | string | для server | Адрес сервера 1С |
| `ref` | string | для server | Имя базы на сервере |
| `user` | string | нет | Имя пользователя 1С |
| `password` | string | нет | Пароль |
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
## Алгоритм разрешения базы данных
Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке:
1. По `id` (точное совпадение)
2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой»)
3. По `name` (нечёткое совпадение с учётом морфологии и регистра)
3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`:
- Точное совпадение: ветка `dev``"branches": ["dev"]`
- Glob-паттерн: ветка `release/2.1``"branches": ["release/*"]`
4. Если ветка не совпала — используй `default`
5. Если не найдено или неоднозначно — спроси пользователя
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
### Автоопределение платформы
Если `v8path` не задан в конфиге:
```powershell
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
```
## Операции
### Показать список баз
Прочитай `.v8-project.json`, выведи таблицу:
```
ID Имя Тип Путь/Сервер По умолч.
dev Разработка file C:\Bases\MyApp_Dev ✓
test Тестовая server srv01/MyApp_Test
```
### Добавить базу
Спроси у пользователя через AskUserQuestion:
- id, name, type (file/server)
- path (для file) или server + ref (для server)
- user, password (необязательно)
- aliases, branches (необязательно)
Добавь в массив `databases`. Если это первая база — установи как `default`.
### Удалить базу
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
### Подробности по базе
Выведи все поля конкретной базы.
## Формирование строки подключения
Для использования в шаблонах команд других навыков:
**Файловая база:**
```
/F "<path>"
```
**Серверная база:**
```
/S "<server>/<ref>"
```
**Аутентификация** (добавляется если user задан):
```
/N"<user>" /P"<password>"
```
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
+73 -81
View File
@@ -1,81 +1,73 @@
---
name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-cf — Загрузка конфигурации из CF-файла
Загружает конфигурацию из бинарного CF-файла в информационную базу.
## Usage
```
/db-load-cf <input.cf> [database]
/db-load-cf config.cf dev
```
> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения
Прочитай `.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-cf.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение |
| `-AllExtensions` | нет | Загрузить все расширения из архива |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После выполнения
1. Прочитай лог-файл и покажи результат
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
## Примеры
```powershell
# Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
---
name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-cf — Загрузка конфигурации из CF-файла
Загружает конфигурацию из бинарного CF-файла в информационную базу.
## Usage
```
/db-load-cf <input.cf> [database]
/db-load-cf config.cf dev
```
> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение |
| `-AllExtensions` | нет | Загрузить все расширения из архива |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
**Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
## Примеры
```powershell
# Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
+224 -166
View File
@@ -1,166 +1,224 @@
# db-load-cf v1.0 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка конфигурации 1С из CF-файла
.DESCRIPTION
Загружает конфигурацию из бинарного CF-файла в информационную базу.
Поддерживает загрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к CF-файлу для загрузки
.PARAMETER Extension
Загрузить как расширение
.PARAMETER AllExtensions
Загрузить все расширения из архива
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
#>
[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)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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_cf_$(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 += "/LoadCfg", "`"$InputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_cf_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 "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (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
}
}
# db-load-cf v1.4 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка конфигурации 1С из CF-файла
.DESCRIPTION
Загружает конфигурацию из бинарного CF-файла в информационную базу.
Поддерживает загрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к CF-файлу для загрузки
.PARAMETER Extension
Загрузить как расширение
.PARAMETER AllExtensions
Загрузить все расширения из архива
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
#>
[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)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
)
$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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$InputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/LoadCfg", "`"$InputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_cf_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 "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (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
}
}
@@ -1,29 +1,65 @@
#!/usr/bin/env python3
# db-load-cf v1.0 — Load 1C configuration from CF file
# db-load-cf v1.4 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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)
@@ -49,9 +85,14 @@ def main():
args = parser.parse_args()
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
@@ -60,6 +101,34 @@ def main():
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
+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` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.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,213 @@
# db-load-dt v1.3 — 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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-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 {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$InputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
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 ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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,192 @@
#!/usr/bin/env python3
# db-load-dt v1.3 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif 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)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
arguments.append("--create-database")
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"Information base restored successfully from: {args.InputFile}")
else:
print(f"Error restoring information base (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- 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()
+77 -78
View File
@@ -1,78 +1,77 @@
---
name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-git — Загрузка изменений из Git
Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу.
## Usage
```
/db-load-git [database]
/db-load-git dev — все незафиксированные изменения
/db-load-git dev -Source Staged — только staged
/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD"
/db-load-git dev -DryRun — только показать что будет загружено
```
## Параметры подключения
Прочитай `.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`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
1. Показать список загруженных файлов и результат из лога
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
---
name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-git — Загрузка изменений из Git
Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу.
## Usage
```
/db-load-git [database]
/db-load-git dev — все незафиксированные изменения
/db-load-git dev -Source Staged — только staged
/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD"
/db-load-git dev -DryRun — только показать что будет загружено
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
+445 -359
View File
@@ -1,359 +1,445 @@
# db-load-git v1.3 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка изменений из Git в базу 1С
.DESCRIPTION
Определяет изменённые файлы конфигурации по данным Git и выполняет
частичную загрузку в информационную базу.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-выгрузки конфигурации (git-репозиторий)
.PARAMETER Source
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
.PARAMETER CommitRange
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
.PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER DryRun
Только показать что будет загружено (без загрузки)
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
#>
[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]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("All", "Staged", "Unstaged", "Commit")]
[string]$Source = "All",
[Parameter(Mandatory=$false)]
[string]$CommitRange,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$DryRun,
[Parameter(Mandatory=$false)]
[switch]$UpdateDB
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile {
param([string]$RelativePath)
$parts = $RelativePath -split '[\\/]'
if ($parts.Count -ge 2) {
return "$($parts[0])/$($parts[1]).xml"
}
return $null
}
# --- Resolve V8Path (skip if DryRun) ---
if (-not $DryRun) {
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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 (skip if DryRun) ---
if (-not $DryRun) {
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
}
# --- Validate config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
exit 1
}
# --- Validate Commit mode ---
if ($Source -eq "Commit" -and -not $CommitRange) {
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
exit 1
}
# --- Check git ---
try {
$null = git --version 2>&1
} catch {
Write-Host "Error: git not found in PATH" -ForegroundColor Red
exit 1
}
# --- Get changed files from Git ---
$changedFiles = @()
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
$configDirNormalized = $ConfigDir.Replace('\', '/')
Push-Location $ConfigDir
try {
switch ($Source) {
"Staged" {
Write-Host "Getting staged changes..."
$raw = git diff --cached --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
"Unstaged" {
Write-Host "Getting unstaged changes..."
$raw = git diff --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
"Commit" {
Write-Host "Getting changes from $CommitRange..."
$raw = git diff --name-only --relative $CommitRange 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
"All" {
Write-Host "Getting all uncommitted changes..."
$raw = git diff --cached --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git diff --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
}
} finally {
Pop-Location
}
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
if ($changedFiles.Count -eq 0) {
Write-Host "No changes found"
exit 0
}
Write-Host "Git changes detected: $($changedFiles.Count) files"
# --- Filter and map to config files ---
$configFiles = @()
foreach ($file in $changedFiles) {
$file = $file.Trim().Replace('\', '/')
if ([string]::IsNullOrWhiteSpace($file)) { continue }
# Skip service files
if ($file -eq "ConfigDumpInfo.xml") { continue }
$fullPath = Join-Path $ConfigDir $file
if ($file -match '\.xml$') {
# XML file — add directly if exists
if (Test-Path $fullPath) {
if ($configFiles -notcontains $file) {
$configFiles += $file
}
}
}
else {
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
if ($objectXml) {
$fullXmlPath = Join-Path $ConfigDir $objectXml
if (Test-Path $fullXmlPath) {
if ($configFiles -notcontains $objectXml) {
$configFiles += $objectXml
}
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
$configFiles += $file
}
# Add all files from Ext/ directory of the object
$parts = $file -split '[\\/]'
if ($parts.Count -ge 2) {
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
if (Test-Path $extDir) {
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
if ($configFiles -notcontains $extRelPath) {
$configFiles += $extRelPath
}
}
}
}
}
}
}
}
if ($configFiles.Count -eq 0) {
Write-Host "No configuration files found in changes"
exit 0
}
Write-Host "Files for loading: $($configFiles.Count)"
foreach ($f in $configFiles) { Write-Host " $f" }
# --- DryRun: stop here ---
if ($DryRun) {
Write-Host ""
Write-Host "DryRun mode - no changes applied"
exit 0
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
# --- Write list file (UTF-8 with BOM) ---
$listFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
# --- 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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
$arguments += "-listFile", "`"$listFile`""
$arguments += "-Format", $Format
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host ""
Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
Write-Host ""
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (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
}
}
# db-load-git v1.8 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка изменений из Git в базу 1С
.DESCRIPTION
Определяет изменённые файлы конфигурации по данным Git и выполняет
частичную загрузку в информационную базу.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-выгрузки конфигурации (git-репозиторий)
.PARAMETER Source
Источник изменений: All, Staged, Unstaged, Commit (по умолчанию All)
.PARAMETER CommitRange
Диапазон коммитов (для Source=Commit), напр. HEAD~3..HEAD
.PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER DryRun
Только показать что будет загружено (без загрузки)
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source Commit -CommitRange "HEAD~3..HEAD"
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
#>
[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]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("All", "Staged", "Unstaged", "Commit")]
[string]$Source = "All",
[Parameter(Mandatory=$false)]
[string]$CommitRange,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$DryRun,
[Parameter(Mandatory=$false)]
[switch]$UpdateDB
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile {
param([string]$RelativePath)
$parts = $RelativePath -split '[\\/]'
if ($parts.Count -ge 2) {
return "$($parts[0])/$($parts[1]).xml"
}
return $null
}
# --- 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) {
$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
}
}
# --- Detect engine + validate connection (skip if DryRun) ---
$engine = "1cv8"
if (-not $DryRun) {
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
}
# --- Validate config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
exit 1
}
# --- Validate Commit mode ---
if ($Source -eq "Commit" -and -not $CommitRange) {
Write-Host "Error: -CommitRange required for Source=Commit" -ForegroundColor Red
exit 1
}
# --- Check git ---
try {
$null = git --version 2>&1
} catch {
Write-Host "Error: git not found in PATH" -ForegroundColor Red
exit 1
}
# --- Get changed files from Git ---
$changedFiles = @()
$ConfigDir = (Resolve-Path $ConfigDir).Path.TrimEnd('\')
$configDirNormalized = $ConfigDir.Replace('\', '/')
Push-Location $ConfigDir
try {
switch ($Source) {
"Staged" {
Write-Host "Getting staged changes..."
$raw = git diff --cached --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
"Unstaged" {
Write-Host "Getting unstaged changes..."
$raw = git diff --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
"Commit" {
Write-Host "Getting changes from $CommitRange..."
$raw = git diff --name-only --relative $CommitRange 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
"All" {
Write-Host "Getting all uncommitted changes..."
$raw = git diff --cached --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git diff --name-only --relative 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
$raw = git ls-files --others --exclude-standard 2>&1
if ($LASTEXITCODE -eq 0) { $changedFiles += $raw }
}
}
} finally {
Pop-Location
}
$changedFiles = $changedFiles | Where-Object { $_ -is [string] -and -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique
if ($changedFiles.Count -eq 0) {
Write-Host "No changes found"
exit 0
}
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 (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
if ($file -match '\.xml$') {
# XML file — add directly if exists
if (Test-Path $fullPath) {
if ($configFiles -notcontains $file) {
$configFiles += $file
}
}
}
else {
# Non-XML (BSL, HTML, etc.) — map to parent object XML + include all Ext/ files
$objectXml = Get-ObjectXmlFromSubFile -RelativePath $file
if ($objectXml) {
$fullXmlPath = Join-Path $ConfigDir $objectXml
if (Test-Path $fullXmlPath) {
if ($configFiles -notcontains $objectXml) {
$configFiles += $objectXml
}
if ((Test-Path $fullPath) -and $configFiles -notcontains $file) {
$configFiles += $file
}
# Add all files from Ext/ directory of the object
$parts = $file -split '[\\/]'
if ($parts.Count -ge 2) {
$extDir = Join-Path (Join-Path $ConfigDir $parts[0]) "$($parts[1])\Ext"
if (Test-Path $extDir) {
Get-ChildItem -Path $extDir -Recurse -File | ForEach-Object {
$extRelPath = $_.FullName.Replace("$ConfigDir\", '').Replace('\', '/')
if ($configFiles -notcontains $extRelPath) {
$configFiles += $extRelPath
}
}
}
}
}
}
}
}
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
}
Write-Host "Files for loading: $($configFiles.Count)"
foreach ($f in $configFiles) { Write-Host " $f" }
# --- DryRun: stop here ---
if ($DryRun) {
Write-Host ""
Write-Host "DryRun mode - no changes applied"
exit 0
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_git_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; import specific files) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
Write-Host "Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $configFiles
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$applyOut = & $V8Path @applyArgs 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Write list file (UTF-8 with BOM) ---
$listFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
# --- 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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
$arguments += "-listFile", "`"$listFile`""
$arguments += "-Format", $Format
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
# --- Execute ---
Write-Host ""
Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
# --- Result ---
Write-Host ""
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (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
}
}
+113 -11
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
# db-load-git v1.3 — Load Git changes into 1C database
# db-load-git v1.8 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
import glob
import json
import os
import random
import re
@@ -13,23 +15,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
@@ -93,9 +126,15 @@ def main():
if not args.DryRun:
v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) ---
# --- Detect engine + validate connection (skip if DryRun) ---
engine = "1cv8"
if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
@@ -146,14 +185,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 +230,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)
@@ -205,6 +255,58 @@ def main():
os.makedirs(temp_dir, exist_ok=True)
try:
if engine == "ibcmd":
# --- ibcmd branch (file infobase only; import specific files) ---
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + config_files
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode != 0:
print(f"Error loading changes (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Changes loaded successfully ({len(config_files)} files)")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
ar = subprocess.run([v8path] + apply_args, capture_output=True, encoding="utf-8", errors="replace")
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f:
+101 -109
View File
@@ -1,109 +1,101 @@
---
name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-xml — Загрузка конфигурации из XML
Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку.
## Usage
```
/db-load-xml <configDir> [database]
/db-load-xml src/config dev
/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
```
> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения
Прочитай `.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`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-исходников |
| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` |
| `-Files <список>` | для Partial | Относительные пути файлов через запятую |
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Режимы загрузки
| Режим | Описание |
|-------|----------|
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML |
| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) |
### Формат файла списка (listFile)
Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**:
```
Catalogs/Номенклатура.xml
Catalogs/Номенклатура/Ext/ObjectModule.bsl
Documents/Заказ.xml
Documents/Заказ/Forms/ФормаДокумента.xml
```
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## После выполнения
1. Прочитай лог и покажи результат
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Полная загрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
---
name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-xml — Загрузка конфигурации из XML
Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку.
## Usage
```
/db-load-xml <configDir> [database]
/db-load-xml src/config dev
/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
```
> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-исходников |
| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` |
| `-Files <список>` | для Partial | Относительные пути файлов через запятую |
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Режимы загрузки
| Режим | Описание |
|-------|----------|
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML |
| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) |
### Формат файла списка (listFile)
Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**:
```
Catalogs/Номенклатура.xml
Catalogs/Номенклатура/Ext/ObjectModule.bsl
Documents/Заказ.xml
Documents/Заказ/Forms/ФормаДокумента.xml
```
## После выполнения
Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
## Примеры
```powershell
# Полная загрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
+388 -279
View File
@@ -1,279 +1,388 @@
# db-load-xml v1.3 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка конфигурации 1С из XML-файлов
.DESCRIPTION
Загружает конфигурацию в информационную базу из XML-файлов.
Поддерживает полную и частичную загрузку.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-исходников конфигурации
.PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full)
.PARAMETER Files
Относительные пути файлов через запятую (для режима Partial)
.PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
.PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
#>
[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]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Partial")]
[string]$Mode = "Full",
[Parameter(Mandatory=$false)]
[string]$Files,
[Parameter(Mandatory=$false)]
[string]$ListFile,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[switch]$StrictLog
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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 config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
exit 1
}
# --- Validate Partial mode ---
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_xml_$(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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
if ($Mode -eq "Full") {
Write-Host "Executing full configuration load..."
} else {
Write-Host "Executing partial configuration load..."
# Build list file
$generatedListFile = $null
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
} 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" }
}
$arguments += "-listFile", "`"$generatedListFile`""
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
}
$arguments += "-Format", $Format
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_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
# --- Read log ---
$logContent = $null
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
}
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
# db-load-xml v1.10 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Загрузка конфигурации 1С из XML-файлов
.DESCRIPTION
Загружает конфигурацию в информационную базу из XML-файлов.
Поддерживает полную и частичную загрузку.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-исходников конфигурации
.PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full)
.PARAMETER Files
Относительные пути файлов через запятую (для режима Partial)
.PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
.PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
#>
[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]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Partial")]
[string]$Mode = "Full",
[Parameter(Mandatory=$false)]
[string]$Files,
[Parameter(Mandatory=$false)]
[string]$ListFile,
[Parameter(Mandatory=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[switch]$StrictLog
)
$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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
exit 1
}
# --- Validate Partial mode ---
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
# partial: import specific files (relative to ConfigDir)
$fileList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} elseif ($Files) {
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
if ($fileList.Count -eq 0) {
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $fileList
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
if ($UpdateDB) {
$applyArgs = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($UserName) { $applyArgs += "--user=$UserName" }
if ($Password) { $applyArgs += "--password=$Password" }
$applyArgs += "--data=$tempDir"
Write-Host "Running: ibcmd $($applyArgs -join ' ')"
$applyOut = & $V8Path @applyArgs 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/LoadConfigFromFiles", "`"$ConfigDir`""
if ($Mode -eq "Full") {
Write-Host "Executing full configuration load..."
} else {
Write-Host "Executing partial configuration load..."
# Build list file
$rawList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} else {
$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"
}
$arguments += "-Format", $Format
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_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
# --- Read log ---
$logContent = $null
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
}
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
}
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+140 -19
View File
@@ -1,34 +1,68 @@
#!/usr/bin/env python3
# db-load-xml v1.3 — Load 1C configuration from XML files
# db-load-xml v1.10 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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
@@ -73,8 +107,14 @@ def main():
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
@@ -88,6 +128,77 @@ def main():
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1)
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "Partial" or args.Files or args.ListFile:
# partial: import specific files (relative to ConfigDir)
if args.ListFile:
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
with open(args.ListFile, encoding="utf-8-sig") as f:
file_list = [ln.strip() for ln in f if ln.strip()]
elif args.Files:
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
else:
file_list = []
if not file_list:
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + file_list
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode != 0:
print(f"Error loading configuration from files (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
print(f"Configuration loaded successfully from: {args.ConfigDir}")
if result.stdout:
print(result.stdout)
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
ar = subprocess.run([v8path] + apply_args, capture_output=True, encoding="utf-8", errors="replace")
exit_code = ar.returncode
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
if ar.stdout:
print(ar.stdout)
if ar.stderr:
print(ar.stderr, file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
@@ -114,23 +225,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")
+76 -76
View File
@@ -1,76 +1,76 @@
---
name: db-run
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-run — Запуск 1С:Предприятие
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
## Usage
```
/db-run [database]
/db-run dev
/db-run dev /Execute process.epf
/db-run dev /C "параметр запуска"
```
## Параметры подключения
Прочитай `.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-run.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
| `-CParam <строка>` | нет | Параметр запуска (/C) |
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Важно
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
## Примеры
```powershell
# Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
```
---
name: db-run
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-run — Запуск 1С:Предприятие
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
## Usage
```
/db-run [database]
/db-run dev
/db-run dev /Execute process.epf
/db-run dev /C "параметр запуска"
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
| `-CParam <строка>` | нет | Параметр запуска (/C) |
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Важно
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
## Примеры
```powershell
# Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
```
+170 -145
View File
@@ -1,145 +1,170 @@
# db-run v1.0 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Запуск 1С:Предприятие
.DESCRIPTION
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
Запуск в фоне не ждёт завершения процесса.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER Execute
Путь к внешней обработке для запуска
.PARAMETER CParam
Параметр запуска (/C)
.PARAMETER URL
Навигационная ссылка (e1cib/...)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
#>
[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=$false)]
[string]$Execute,
[Parameter(Mandatory=$false)]
[string]$CParam,
[Parameter(Mandatory=$false)]
[string]$URL
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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
}
# --- Build arguments as single string ---
# Note: Start-Process without -NoNewWindow uses ShellExecute.
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
# re-joins elements. Single string avoids this.
$argString = "ENTERPRISE"
if ($InfoBaseServer -and $InfoBaseRef) {
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
} else {
$argString += " /F `"$InfoBasePath`""
}
if ($UserName) { $argString += " /N`"$UserName`"" }
if ($Password) { $argString += " /P`"$Password`"" }
# --- Optional params ---
if ($Execute) {
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
if ($ext -eq ".erf") {
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
$Execute = ""
}
}
if ($Execute) {
$argString += " /Execute `"$Execute`""
}
if ($CParam) {
$argString += " /C `"$CParam`""
}
if ($URL) {
$argString += " /URL `"$URL`""
}
$argString += " /DisableStartupDialogs"
# --- Execute (background, no wait) ---
Write-Host "Running: 1cv8.exe $argString"
Start-Process -FilePath $V8Path -ArgumentList $argString
Write-Host "1C:Enterprise launched" -ForegroundColor Green
# db-run v1.1 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Запуск 1С:Предприятие
.DESCRIPTION
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
Запуск в фоне не ждёт завершения процесса.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER Execute
Путь к внешней обработке для запуска
.PARAMETER CParam
Параметр запуска (/C)
.PARAMETER URL
Навигационная ссылка (e1cib/...)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
#>
[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=$false)]
[string]$Execute,
[Parameter(Mandatory=$false)]
[string]$CParam,
[Parameter(Mandatory=$false)]
[string]$URL
)
$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
}
# --- Build arguments as single string ---
# Note: Start-Process without -NoNewWindow uses ShellExecute.
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
# re-joins elements. Single string avoids this.
$argString = "ENTERPRISE"
if ($InfoBaseServer -and $InfoBaseRef) {
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
} else {
$argString += " /F `"$InfoBasePath`""
}
if ($UserName) { $argString += " /N`"$UserName`"" }
if ($Password) { $argString += " /P`"$Password`"" }
# --- Optional params ---
if ($Execute) {
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
if ($ext -eq ".erf") {
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
$Execute = ""
}
}
if ($Execute) {
$argString += " /Execute `"$Execute`""
}
if ($CParam) {
$argString += " /C `"$CParam`""
}
if ($URL) {
$argString += " /URL `"$URL`""
}
$argString += " /DisableStartupDialogs"
# --- Execute (background, no wait) ---
Write-Host "Running: 1cv8.exe $argString"
Start-Process -FilePath $V8Path -ArgumentList $argString
Write-Host "1C:Enterprise launched" -ForegroundColor Green
+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)
+86 -93
View File
@@ -1,93 +1,86 @@
---
name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-update — Обновление конфигурации БД
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
## Usage
```
/db-update [database]
/db-update dev
/db-update dev -Dynamic+
```
## Параметры подключения
Прочитай `.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-update.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-Extension <имя>` | нет | Обновить расширение |
| `-AllExtensions` | нет | Обновить все расширения |
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Фоновое обновление (серверная база)
| Параметр | Описание |
|----------|----------|
| `-BackgroundStart` | Начать фоновое обновление |
| `-BackgroundFinish` | Дождаться окончания |
| `-BackgroundCancel` | Отменить |
| `-BackgroundSuspend` | Приостановить |
| `-BackgroundResume` | Возобновить |
## Коды возврата
| Код | Описание |
|-----|----------|
| 0 | Успешно |
| 1 | Ошибка (см. лог) |
## Предупреждения
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
## Примеры
```powershell
# Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
---
name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-update — Обновление конфигурации БД
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
## Usage
```
/db-update [database]
/db-update dev
/db-update dev -Dynamic+
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-Extension <имя>` | нет | Обновить расширение |
| `-AllExtensions` | нет | Обновить все расширения |
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Фоновое обновление (серверная база)
| Параметр | Описание |
|----------|----------|
| `-BackgroundStart` | Начать фоновое обновление |
| `-BackgroundFinish` | Дождаться окончания |
| `-BackgroundCancel` | Отменить |
| `-BackgroundSuspend` | Приостановить |
| `-BackgroundResume` | Возобновить |
## Предупреждения
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
## Примеры
```powershell
# Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
+243 -184
View File
@@ -1,184 +1,243 @@
# db-update v1.0 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Обновление конфигурации базы данных 1С
.DESCRIPTION
Применяет изменения основной конфигурации к конфигурации базы данных.
Поддерживает динамическое обновление, обновление расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER Extension
Имя расширения для обновления
.PARAMETER AllExtensions
Обновить все расширения
.PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить
.PARAMETER Server
Обновление на стороне сервера
.PARAMETER WarningsAsErrors
Предупреждения считать ошибками
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
#>
[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=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("+", "-")]
[string]$Dynamic,
[Parameter(Mandatory=$false)]
[switch]$Server,
[Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_update_$(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 += "/UpdateDBCfg"
# --- Options ---
if ($Dynamic) {
$arguments += "-Dynamic$Dynamic"
}
if ($Server) {
$arguments += "-Server"
}
if ($WarningsAsErrors) {
$arguments += "-WarningsAsErrors"
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "update_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 "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (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
}
}
# db-update v1.4 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Обновление конфигурации базы данных 1С
.DESCRIPTION
Применяет изменения основной конфигурации к конфигурации базы данных.
Поддерживает динамическое обновление, обновление расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER Extension
Имя расширения для обновления
.PARAMETER AllExtensions
Обновить все расширения
.PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить
.PARAMETER Server
Обновление на стороне сервера
.PARAMETER WarningsAsErrors
Предупреждения считать ошибками
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
#>
[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=$false)]
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[ValidateSet("+", "-")]
[string]$Dynamic,
[Parameter(Mandatory=$false)]
[switch]$Server,
[Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors
)
$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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/UpdateDBCfg"
# --- Options ---
if ($Dynamic) {
$arguments += "-Dynamic$Dynamic"
}
if ($Server) {
$arguments += "-Server"
}
if ($WarningsAsErrors) {
$arguments += "-WarningsAsErrors"
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "update_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 "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (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
}
}
+80 -7
View File
@@ -1,29 +1,65 @@
#!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration
# db-update v1.4 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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)
@@ -52,11 +88,48 @@ def main():
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.Dynamic == "+":
arguments.append("--dynamic=auto")
elif args.Dynamic == "-":
arguments.append("--dynamic=disable")
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
+196 -196
View File
@@ -1,196 +1,196 @@
---
name: epf-bsp-add-command
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools:
- Read
- Edit
- Glob
- Grep
---
# /epf-bsp-add-command — Добавление команды БСП
Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик.
Предварительно обработка должна быть инициализирована через `/epf-bsp-init`.
## Usage
```
/epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|-----------------------|--------------------------------------------|
| ProcessorName | да | — | Имя обработки |
| Идентификатор | да | — | Внутреннее имя команды (латиница) |
| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) |
| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя |
| SrcDir | нет | `src` | Каталог исходников |
## Маппинг типов команд
Пользователь может указать тип в свободной форме:
| Пользователь пишет | ТипКоманды |
|---------------------------------------|-----------------------------------------------------|
| открыть форму, форма | `ТипКомандыОткрытиеФормы()` |
| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` |
| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` |
| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` |
| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` |
Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`:
| Вид обработки (из кода) | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон добавления команды
Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`:
```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'");
НоваяКоманда.Идентификатор = "{{Идентификатор}}";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь;
```
Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также:
```bsl
НоваяКоманда.Модификатор = "ПечатьMXL";
```
Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`.
## Шаблоны обработчиков
### ВызовСерверногоМетода — если обработчик уже есть
Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`:
```bsl
ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
```
### ВызовСерверногоМетода — если обработчика нет
Для глобальных обработок (без `ОбъектыНазначения`):
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
Для назначаемых обработок (с `ОбъектыНазначения`):
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
### ПечатнаяФорма — если процедура Печать уже есть
Добавь блок перед `КонецПроцедуры`:
```bsl
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли;
```
### ПечатнаяФорма — если процедуры Печать нет
```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли;
КонецПроцедуры
```
### ВызовКлиентскогоМетода
Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`):
Для глобальных обработок:
```bsl
&НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
Для назначаемых обработок:
```bsl
&НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
Если процедура уже есть — добавь ветку `ИначеЕсли`.
## Инструкции
1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init`
3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`)
4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации`
5. Добавь обработчик:
- Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс`
- Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`)
6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры
7. Используй табы для отступов
## Пример
Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"`
В `СведенияОВнешнейОбработке()` перед `Возврат` добавится:
```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'");
НоваяКоманда.Идентификатор = "ЗаказПокупателя";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь;
```
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
---
name: epf-bsp-add-command
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools:
- Read
- Edit
- Glob
- Grep
---
# /epf-bsp-add-command — Добавление команды БСП
Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик.
Предварительно обработка должна быть инициализирована через `/epf-bsp-init`.
## Usage
```
/epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|-----------------------|--------------------------------------------|
| ProcessorName | да | — | Имя обработки |
| Идентификатор | да | — | Внутреннее имя команды (латиница) |
| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) |
| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя |
| SrcDir | нет | `src` | Каталог исходников |
## Маппинг типов команд
Пользователь может указать тип в свободной форме:
| Пользователь пишет | ТипКоманды |
|---------------------------------------|-----------------------------------------------------|
| открыть форму, форма | `ТипКомандыОткрытиеФормы()` |
| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` |
| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` |
| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` |
| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` |
Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`:
| Вид обработки (из кода) | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон добавления команды
Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`:
```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'");
НоваяКоманда.Идентификатор = "{{Идентификатор}}";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь;
```
Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также:
```bsl
НоваяКоманда.Модификатор = "ПечатьMXL";
```
Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`.
## Шаблоны обработчиков
### ВызовСерверногоМетода — если обработчик уже есть
Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`:
```bsl
ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
```
### ВызовСерверногоМетода — если обработчика нет
Для глобальных обработок (без `ОбъектыНазначения`):
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
Для назначаемых обработок (с `ОбъектыНазначения`):
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
### ПечатнаяФорма — если процедура Печать уже есть
Добавь блок перед `КонецПроцедуры`:
```bsl
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли;
```
### ПечатнаяФорма — если процедуры Печать нет
```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли;
КонецПроцедуры
```
### ВызовКлиентскогоМетода
Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`):
Для глобальных обработок:
```bsl
&НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
Для назначаемых обработок:
```bsl
&НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}}
КонецЕсли;
КонецПроцедуры
```
Если процедура уже есть — добавь ветку `ИначеЕсли`.
## Инструкции
1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init`
3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`)
4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации`
5. Добавь обработчик:
- Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс`
- Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`)
6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры
7. Используй табы для отступов
## Пример
Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"`
В `СведенияОВнешнейОбработке()` перед `Возврат` добавится:
```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'");
НоваяКоманда.Идентификатор = "ЗаказПокупателя";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь;
```
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
+208 -208
View File
@@ -1,208 +1,208 @@
---
name: epf-bsp-init
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид>
allowed-tools:
- Read
- Edit
- Glob
- Grep
---
# /epf-bsp-init — Регистрация обработки в БСП
Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП.
## Usage
```
/epf-bsp-init <ProcessorName> <Вид> [Назначение...]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|---------------------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) |
| Вид | да | — | Вид обработки (см. маппинг ниже) |
| Назначение | * | — | Объекты метаданных для назначаемых видов |
| SrcDir | нет | `src` | Каталог исходников |
\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов.
## Маппинг вида обработки
Пользователь может указать вид в свободной форме. Определи нужный по контексту:
| Пользователь пишет | Вид | API-метод |
|-------------------------------------------|----------------------------|----------------------------------------------|
| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` |
| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` |
| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` |
| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` |
| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` |
| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` |
## Тип команды по умолчанию
| Вид | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон: СведенияОВнешнейОбработке
Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции.
```bsl
Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}};
ПараметрыРегистрации.Версия = "1.0";
{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь;
{{СЕКЦИЯ_МОДИФИКАТОР}}
Возврат ПараметрыРегистрации;
КонецФункции
```
### Подстановки
- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида
- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию
### Условные секции
**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект:
```bsl
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
```
Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`).
Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой.
**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма:
```bsl
НоваяКоманда.Модификатор = "ПечатьMXL";
```
Для остальных видов — удалить вместе с пустой строкой.
## Шаблоны серверных обработчиков
Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`.
### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация
КонецПроцедуры
```
### Для ПечатнаяФорма
```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация
КонецПроцедуры
```
### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода)
Если пользователь явно выбрал серверный метод вместо открытия формы:
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация
КонецПроцедуры
```
Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`.
## Инструкции
1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Прочитай файл
3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй
4. Если файл не найден — предложи сначала вызвать `/epf-init`
5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти`
6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области
7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции
8. Используй табы для отступов (как в исходном файле)
## Пример
Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату`
Результат в `ObjectModule.bsl`:
```bsl
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
ПараметрыРегистрации.Версия = "1.0";
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь;
НоваяКоманда.Модификатор = "ПечатьMXL";
Возврат ПараметрыРегистрации;
КонецФункции
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
```
## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Собрать EPF: `/epf-build`
---
name: epf-bsp-init
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид>
allowed-tools:
- Read
- Edit
- Glob
- Grep
---
# /epf-bsp-init — Регистрация обработки в БСП
Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП.
## Usage
```
/epf-bsp-init <ProcessorName> <Вид> [Назначение...]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|---------------------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) |
| Вид | да | — | Вид обработки (см. маппинг ниже) |
| Назначение | * | — | Объекты метаданных для назначаемых видов |
| SrcDir | нет | `src` | Каталог исходников |
\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов.
## Маппинг вида обработки
Пользователь может указать вид в свободной форме. Определи нужный по контексту:
| Пользователь пишет | Вид | API-метод |
|-------------------------------------------|----------------------------|----------------------------------------------|
| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` |
| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` |
| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` |
| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` |
| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` |
| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` |
## Тип команды по умолчанию
| Вид | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон: СведенияОВнешнейОбработке
Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции.
```bsl
Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}};
ПараметрыРегистрации.Версия = "1.0";
{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь;
{{СЕКЦИЯ_МОДИФИКАТОР}}
Возврат ПараметрыРегистрации;
КонецФункции
```
### Подстановки
- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида
- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию
### Условные секции
**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект:
```bsl
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
```
Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`).
Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой.
**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма:
```bsl
НоваяКоманда.Модификатор = "ПечатьMXL";
```
Для остальных видов — удалить вместе с пустой строкой.
## Шаблоны серверных обработчиков
Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`.
### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация
КонецПроцедуры
```
### Для ПечатнаяФорма
```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация
КонецПроцедуры
```
### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода)
Если пользователь явно выбрал серверный метод вместо открытия формы:
```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация
КонецПроцедуры
```
Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`.
## Инструкции
1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Прочитай файл
3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй
4. Если файл не найден — предложи сначала вызвать `/epf-init`
5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти`
6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области
7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции
8. Используй табы для отступов (как в исходном файле)
## Пример
Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату`
Результат в `ObjectModule.bsl`:
```bsl
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
ПараметрыРегистрации.Версия = "1.0";
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь;
НоваяКоманда.Модификатор = "ПечатьMXL";
Возврат ПараметрыРегистрации;
КонецФункции
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация
КонецПроцедуры
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
```
## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Собрать EPF: `/epf-build`
+69 -69
View File
@@ -1,69 +1,69 @@
---
name: epf-build
description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников
argument-hint: <ProcessorName>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /epf-build — Сборка обработки
## Usage
```
/epf-build <ProcessorName> [SrcDir] [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|--------------------------------------|
| ProcessorName | да | — | Имя обработки (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Примеры
```powershell
# Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
```
---
name: epf-build
description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников
argument-hint: <ProcessorName>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /epf-build — Сборка обработки
## Usage
```
/epf-build <ProcessorName> [SrcDir] [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|--------------------------------------|
| ProcessorName | да | — | Имя обработки (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Примеры
```powershell
# Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
```
+225 -173
View File
@@ -1,173 +1,225 @@
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников
.DESCRIPTION
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
Общий скрипт для epf-build и erf-build.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER SourceFile
Путь к корневому XML-файлу исходников
.PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
#>
[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]$SourceFile,
[Parameter(Mandatory=$true)]
[string]$OutputFile
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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
}
# --- Auto-create stub database if no connection specified ---
$autoCreatedBase = $null
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$sourceDir = Split-Path $SourceFile -Parent
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
Write-Host "No database specified. Creating temporary stub database..."
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
if ($stubProc.ExitCode -ne 0) {
Write-Host "Error: failed to create stub database" -ForegroundColor Red
exit 1
}
$InfoBasePath = $autoBasePath
$autoCreatedBase = $autoBasePath
}
# --- Validate source file ---
if (-not (Test-Path $SourceFile)) {
Write-Host "Error: source file not found: $SourceFile" -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 "epf_build_$(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 += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "build_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 "Build completed successfully: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error building (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
}
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
# epf-build v1.4 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников
.DESCRIPTION
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
Общий скрипт для epf-build и erf-build.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER SourceFile
Путь к корневому XML-файлу исходников
.PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
#>
[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]$SourceFile,
[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
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)" -ForegroundColor Red
exit 1
}
# --- Auto-create stub database if no connection specified ---
$autoCreatedBase = $null
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$sourceDir = Split-Path $SourceFile -Parent
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
Write-Host "No database specified. Creating temporary stub database..."
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`""
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru
if ($stubProc.ExitCode -ne 0) {
Write-Host "Error: failed to create stub database" -ForegroundColor Red
exit 1
}
$InfoBasePath = $autoBasePath
$autoCreatedBase = $autoBasePath
}
# --- Validate source file ---
if (-not (Test-Path $SourceFile)) {
Write-Host "Error: source file not found: $SourceFile" -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 "epf_build_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: build EPF/ERF via config import --out ---
$srcDir = Split-Path $SourceFile -Parent
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "build_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 "Build completed successfully: $OutputFile" -ForegroundColor Green
} else {
Write-Host "Error building (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
}
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
+68 -7
View File
@@ -1,34 +1,68 @@
#!/usr/bin/env python3
# epf-build v1.0 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.4 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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
@@ -51,6 +85,10 @@ def main():
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
if engine == "ibcmd" and args.InfoBaseServer and args.InfoBaseRef:
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)", file=sys.stderr)
sys.exit(1)
# --- Auto-create stub database if no connection specified ---
auto_created_base = None
@@ -84,6 +122,29 @@ def main():
os.makedirs(temp_dir, exist_ok=True)
try:
if engine == "ibcmd":
# --- ibcmd branch: build EPF/ERF via config import --out ---
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"External data processor/report built successfully: {args.OutputFile}")
else:
print(f"Error building external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Build arguments ---
arguments = ["DESIGNER"]
@@ -1,4 +1,4 @@
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.2 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1252,6 +1252,29 @@ $propsXml </Properties>$childObjLine
}
}
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($stubEngine -eq "ibcmd") {
Write-Host "Creating infobase (ibcmd): $TempBasePath"
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$ibOut = & $V8Path @ibArgs 2>&1
$ibRc = $LASTEXITCODE
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
if ($ibRc -ne 0) {
if ($ibOut) { Write-Host ($ibOut | Out-String) }
Write-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
Write-Host "[OK] Stub database created: $TempBasePath"
Write-Host $TempBasePath
exit 0
}
# --- 5. Create infobase ---
Write-Host "Creating infobase: $TempBasePath"
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# stub-db-create v1.0 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.2 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -1034,6 +1034,32 @@ def main():
if register_columns:
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
# Stub via ibcmd (one call: create [--import --apply])
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
if stub_engine == "ibcmd":
import shutil
print(f'Creating infobase (ibcmd): {temp_base}')
ib_data = tempfile.mkdtemp(prefix="stub_data_")
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
if has_ref_types:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
result = subprocess.run(ib_args, capture_output=True, encoding='utf-8', errors='replace')
shutil.rmtree(ib_data, ignore_errors=True)
if result.returncode != 0:
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
if has_ref_types:
import shutil
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
print(f'[OK] Stub database created: {temp_base}')
print(temp_base)
sys.exit(0)
# Create infobase
print(f'Creating infobase: {temp_base}')
result = subprocess.run(
+69 -69
View File
@@ -1,69 +1,69 @@
---
name: epf-dump
description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла
argument-hint: <EpfFile>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /epf-dump — Разборка обработки
## Usage
```
/epf-dump <EpfFile> [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------|
| EpfFile | да | — | Путь к EPF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
## Примеры
```powershell
# Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
```
---
name: epf-dump
description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла
argument-hint: <EpfFile>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /epf-dump — Разборка обработки
## Usage
```
/epf-dump <EpfFile> [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------|
| EpfFile | да | — | Путь к EPF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
## Примеры
```powershell
# Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
```
+224 -167
View File
@@ -1,167 +1,224 @@
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники
.DESCRIPTION
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
Общий скрипт для epf-dump и erf-dump.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к EPF/ERF-файлу
.PARAMETER OutputDir
Каталог для выгрузки исходников
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
#>
[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=$true)]
[string]$OutputDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve V8Path ---
if (-not $V8Path) {
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
} elseif (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 database connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
exit 1
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_dump_$(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 += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
$arguments += "-Format", $Format
# --- Output ---
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully to: $OutputDir" -ForegroundColor Green
} else {
Write-Host "Error dumping (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
}
}
# epf-dump v1.4 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<#
.SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники
.DESCRIPTION
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
Общий скрипт для epf-dump и erf-dump.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к EPF/ERF-файлу
.PARAMETER OutputDir
Каталог для выгрузки исходников
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
#>
[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=$true)]
[string]$OutputDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
)
$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 database connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: dump EPF/ERF via config export --file ---
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$output = & $V8Path @arguments 2>&1
$exitCode = $LASTEXITCODE
if ($exitCode -eq 0) {
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
} else {
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
$arguments += "-Format", $Format
# --- Output ---
$outFile = Join-Path $tempDir "dump_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 "Dump completed successfully to: $OutputDir" -ForegroundColor Green
} else {
Write-Host "Error dumping (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
}
}
+71 -7
View File
@@ -1,34 +1,68 @@
#!/usr/bin/env python3
# epf-dump v1.0 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.4 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
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
@@ -57,12 +91,20 @@ def main():
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Validate database connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
sys.exit(1)
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
@@ -78,6 +120,28 @@ def main():
os.makedirs(temp_dir, exist_ok=True)
try:
if engine == "ibcmd":
# --- ibcmd branch: dump EPF/ERF via config export --file ---
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
result = subprocess.run([v8path] + arguments, capture_output=True, encoding="utf-8", errors="replace")
if result.returncode == 0:
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
else:
print(f"Error dumping external data processor/report (code: {result.returncode})", file=sys.stderr)
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
sys.exit(result.returncode)
# --- Build arguments ---
arguments = ["DESIGNER"]
+41 -41
View File
@@ -1,41 +1,41 @@
---
name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /epf-init — Создание новой обработки
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
## Usage
```
/epf-init <Name> [Synonym] [SrcDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
```
## Дальнейшие шаги
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать EPF: `/epf-build`
---
name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /epf-init — Создание новой обработки
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
## Usage
```
/epf-init <Name> [Synonym] [SrcDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"]
```
## Дальнейшие шаги
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать EPF: `/epf-build`
+90 -90
View File
@@ -1,90 +1,90 @@
# epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<ExternalDataProcessor uuid="$uuid1">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
<xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
<xr:TypeId>$uuid3</xr:TypeId>
<xr:ValueId>$uuid4</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>$Name</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<DefaultForm/>
<AuxiliaryForm/>
</Properties>
<ChildObjects/>
</ExternalDataProcessor>
</MetaDataObject>
"@
$rootFile = Join-Path $SrcDir "$Name.xml"
$processorDir = Join-Path $SrcDir $Name
if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile"
exit 1
}
if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
}
$extDir = Join-Path $processorDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# --- Модуль объекта ---
$moduleBsl = @"
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создана обработка: $rootFile"
Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath"
# epf-init v1.1 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$SrcDir = "src"
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<ExternalDataProcessor uuid="$uuid1">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
<xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
<xr:TypeId>$uuid3</xr:TypeId>
<xr:ValueId>$uuid4</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>$Name</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<DefaultForm/>
<AuxiliaryForm/>
</Properties>
<ChildObjects/>
</ExternalDataProcessor>
</MetaDataObject>
"@
$rootFile = Join-Path $SrcDir "$Name.xml"
$processorDir = Join-Path $SrcDir $Name
if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile"
exit 1
}
if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
}
$extDir = Join-Path $processorDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# --- Модуль объекта ---
$moduleBsl = @"
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создана обработка: $rootFile"
Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath"
+30 -30
View File
@@ -1,30 +1,30 @@
---
name: epf-validate
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /epf-validate — валидация внешней обработки (EPF)
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
```
---
name: epf-validate
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /epf-validate — валидация внешней обработки (EPF)
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
```
File diff suppressed because it is too large Load Diff
+71 -71
View File
@@ -1,71 +1,71 @@
---
name: erf-build
description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников
argument-hint: <ReportName>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /erf-build — Сборка отчёта
## Usage
```
/erf-build <ReportName> [SrcDir] [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|--------------------------------------|
| ReportName | да | — | Имя отчёта (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
Используй общий скрипт из epf-build:
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Примеры
```powershell
# Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
```
---
name: erf-build
description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников
argument-hint: <ReportName>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /erf-build — Сборка отчёта
## Usage
```
/erf-build <ReportName> [SrcDir] [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|--------------------------------------|
| ReportName | да | — | Имя отчёта (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
Используй общий скрипт из epf-build:
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Примеры
```powershell
# Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
```
+71 -71
View File
@@ -1,71 +1,71 @@
---
name: erf-dump
description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла
argument-hint: <ErfFile>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /erf-dump — Разборка отчёта
## Usage
```
/erf-dump <ErfFile> [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------|
| ErfFile | да | — | Путь к ERF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1`
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
Используй общий скрипт из epf-dump:
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
## Примеры
```powershell
# Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
```
---
name: erf-dump
description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла
argument-hint: <ErfFile>
allowed-tools:
- Bash
- Read
- Glob
- Grep
---
# /erf-dump — Разборка отчёта
## Usage
```
/erf-dump <ErfFile> [OutDir]
```
| Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------|
| ErfFile | да | — | Путь к ERF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
Используй общий скрипт из epf-dump:
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
## Примеры
```powershell
# Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
```
+42 -42
View File
@@ -1,42 +1,42 @@
---
name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /erf-init — Создание нового отчёта
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
## Usage
```
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
```
## Дальнейшие шаги
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать ERF: `/erf-build`
---
name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /erf-init — Создание нового отчёта
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
## Usage
```
/erf-init <Name> [Synonym] [SrcDir] [--with-skd]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD]
```
## Дальнейшие шаги
- Добавить форму: `/form-add`
- Добавить макет: `/template-add`
- Добавить справку: `/help-add`
- Собрать ERF: `/erf-build`
+180 -180
View File
@@ -1,180 +1,180 @@
# erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$SrcDir = "src",
[switch]$WithSKD
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
# --- Формируем Properties ---
$mainDCSValue = ""
$childObjectsContent = ""
if ($WithSKD) {
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
$childObjectsContent = @"
<Template>ОсновнаяСхемаКомпоновкиДанных</Template>
"@
}
$mainDCSElement = if ($mainDCSValue) {
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
} else {
"<MainDataCompositionSchema/>"
}
$childObjectsXml = if ($childObjectsContent) {
"<ChildObjects>$childObjectsContent</ChildObjects>"
} else {
"<ChildObjects/>"
}
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<ExternalReport uuid="$uuid1">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
<xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
<xr:TypeId>$uuid3</xr:TypeId>
<xr:ValueId>$uuid4</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>$Name</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<DefaultForm/>
<AuxiliaryForm/>
$mainDCSElement
<DefaultSettingsForm/>
<AuxiliarySettingsForm/>
<DefaultVariantForm/>
<VariantsStorage/>
<SettingsStorage/>
</Properties>
$childObjectsXml
</ExternalReport>
</MetaDataObject>
"@
$rootFile = Join-Path $SrcDir "$Name.xml"
$reportDir = Join-Path $SrcDir $Name
if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile"
exit 1
}
if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
}
$extDir = Join-Path $reportDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# --- Модуль объекта ---
$moduleBsl = @"
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создан отчёт: $rootFile"
Write-Host " Каталог: $reportDir"
Write-Host " Модуль: $modulePath"
# --- СКД-макет (если --WithSKD) ---
if ($WithSKD) {
$templatesDir = Join-Path $reportDir "Templates"
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
$skdMetaPath = Join-Path $templatesDir "$skdName.xml"
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
$skdUuid = [guid]::NewGuid().ToString()
$skdMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Template uuid="$skdUuid">
<Properties>
<Name>$skdName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основная схема компоновки данных</v8:content>
</v8:item>
</Synonym>
<Comment/>
<TemplateType>DataCompositionSchema</TemplateType>
</Properties>
</Template>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
$skdContent = @"
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
</DataCompositionSchema>
"@
$skdFilePath = Join-Path $skdExtDir "Template.xml"
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath"
}
# erf-init v1.1 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
[string]$Name,
[string]$Synonym = $Name,
[string]$SrcDir = "src",
[switch]$WithSKD
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$uuid1 = [guid]::NewGuid().ToString()
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
# --- Формируем Properties ---
$mainDCSValue = ""
$childObjectsContent = ""
if ($WithSKD) {
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
$childObjectsContent = @"
<Template>ОсновнаяСхемаКомпоновкиДанных</Template>
"@
}
$mainDCSElement = if ($mainDCSValue) {
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
} else {
"<MainDataCompositionSchema/>"
}
$childObjectsXml = if ($childObjectsContent) {
"<ChildObjects>$childObjectsContent</ChildObjects>"
} else {
"<ChildObjects/>"
}
$xml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<ExternalReport uuid="$uuid1">
<InternalInfo>
<xr:ContainedObject>
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
<xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject>
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
<xr:TypeId>$uuid3</xr:TypeId>
<xr:ValueId>$uuid4</xr:ValueId>
</xr:GeneratedType>
</InternalInfo>
<Properties>
<Name>$Name</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
</v8:item>
</Synonym>
<Comment/>
<DefaultForm/>
<AuxiliaryForm/>
$mainDCSElement
<DefaultSettingsForm/>
<AuxiliarySettingsForm/>
<DefaultVariantForm/>
<VariantsStorage/>
<SettingsStorage/>
</Properties>
$childObjectsXml
</ExternalReport>
</MetaDataObject>
"@
$rootFile = Join-Path $SrcDir "$Name.xml"
$reportDir = Join-Path $SrcDir $Name
if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile"
exit 1
}
if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
}
$extDir = Join-Path $reportDir "Ext"
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc)
# --- Модуль объекта ---
$moduleBsl = @"
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создан отчёт: $rootFile"
Write-Host " Каталог: $reportDir"
Write-Host " Модуль: $modulePath"
# --- СКД-макет (если --WithSKD) ---
if ($WithSKD) {
$templatesDir = Join-Path $reportDir "Templates"
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
$skdMetaPath = Join-Path $templatesDir "$skdName.xml"
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
$skdUuid = [guid]::NewGuid().ToString()
$skdMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">
<Template uuid="$skdUuid">
<Properties>
<Name>$skdName</Name>
<Synonym>
<v8:item>
<v8:lang>ru</v8:lang>
<v8:content>Основная схема компоновки данных</v8:content>
</v8:item>
</Synonym>
<Comment/>
<TemplateType>DataCompositionSchema</TemplateType>
</Properties>
</Template>
</MetaDataObject>
"@
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc)
$skdContent = @"
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
</DataCompositionSchema>
"@
$skdFilePath = Join-Path $skdExtDir "Template.xml"
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc)
Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath"
}
+32 -32
View File
@@ -1,32 +1,32 @@
---
name: erf-validate
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /erf-validate — валидация внешнего отчёта (ERF)
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
```
---
name: erf-validate
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools:
- Bash
- Read
- Glob
---
# /erf-validate — валидация внешнего отчёта (ERF)
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
```
+71 -71
View File
@@ -1,71 +1,71 @@
---
name: form-add
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /form-add — Добавление формы к объекту конфигурации
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
## Usage
```
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-------------|:------------:|--------------|----------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
| FormName | да | — | Имя формы (ФормаДокумента) |
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
| Synonym | нет | = FormName | Синоним формы |
| --set-default | нет | авто | Установить как форму по умолчанию |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
```
## Purpose — назначение формы
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|---------|-------------------------|-------------------|---------------------|
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
## Примеры
```
# Форма документа
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
# Форма списка каталога
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
# Форма записи регистра сведений
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
# Форма выбора с синонимом
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
# Установить как форму по умолчанию
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
```
## Workflow
1. `/form-add` — создать каркас формы
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
3. `/form-validate` — проверить корректность
4. `/form-info` — проанализировать результат
---
name: form-add
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools:
- Bash
- Read
- Write
- Edit
- Glob
- Grep
---
# /form-add — Добавление формы к объекту конфигурации
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
## Usage
```
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-------------|:------------:|--------------|----------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
| FormName | да | — | Имя формы (ФормаДокумента) |
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
| Synonym | нет | = FormName | Синоним формы |
| --set-default | нет | авто | Установить как форму по умолчанию |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
```
## Purpose — назначение формы
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|---------|-------------------------|-------------------|---------------------|
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
## Примеры
```
# Форма документа
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
# Форма списка каталога
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
# Форма записи регистра сведений
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
# Форма выбора с синонимом
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
# Установить как форму по умолчанию
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
```
## Workflow
1. `/form-add` — создать каркас формы
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
3. `/form-validate` — проверить корректность
4. `/form-info` — проанализировать результат
+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)
File diff suppressed because it is too large Load Diff
@@ -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
+142 -142
View File
@@ -1,142 +1,142 @@
---
name: form-edit
description: Добавление элементов, реквизитов и команд в существующую управляемую форму 1С. Используй когда нужно точечно модифицировать готовую форму
argument-hint: <FormPath> <JsonPath>
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /form-edit — Редактирование формы
Добавляет элементы, реквизиты и/или команды в существующий Form.xml. Автоматически выделяет ID из правильного пула, генерирует companion-элементы (ContextMenu, ExtendedTooltip, и др.) и обработчики событий.
## Использование
```
/form-edit <FormPath> <JsonPath>
```
## Параметры
| Параметр | Обязательный | Описание |
|-----------|:------------:|----------------------------------|
| FormPath | да | Путь к существующему Form.xml |
| JsonPath | да | Путь к JSON с описанием добавлений |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>"
```
## JSON формат
```json
{
"into": "ГруппаШапка",
"after": "Контрагент",
"elements": [
{ "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] }
],
"attributes": [
{ "name": "СуммаИтого", "type": "decimal(15,2)" }
],
"commands": [
{ "name": "Рассчитать", "action": "РассчитатьОбработка" }
]
}
```
### Расширения (extension-формы)
Для заимствованных форм (с `<BaseForm>`) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции:
```json
{
"formEvents": [
{ "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" },
{ "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" }
],
"elementEvents": [
{ "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" }
],
"commands": [
{ "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" },
{ "name": "Запрос", "actions": [
{ "callType": "Before", "handler": "Расш1_ЗапросПеред" },
{ "callType": "After", "handler": "Расш1_ЗапросПосле" }
]}
],
"elements": [
{ "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] }
]
}
```
### Позиционирование элементов
| Ключ | По умолчанию | Описание |
|------|-------------|----------|
| `into` | корневой ChildItems | Имя группы/таблицы/страницы, куда вставлять |
| `after` | в конец | Имя элемента, после которого вставлять |
### Типы элементов
Те же DSL-ключи, что в `/form-compile`:
| Ключ | XML тег | Companions |
|------|---------|------------|
| `input` | InputField | ContextMenu, ExtendedTooltip |
| `check` | CheckBoxField | ContextMenu, ExtendedTooltip |
| `label` | LabelDecoration | ContextMenu, ExtendedTooltip |
| `labelField` | LabelField | ContextMenu, ExtendedTooltip |
| `group` | UsualGroup | ExtendedTooltip |
| `table` | Table | ContextMenu, AutoCommandBar, Search*, ViewStatus* |
| `pages` | Pages | ExtendedTooltip |
| `page` | Page | ExtendedTooltip |
| `button` | Button | ExtendedTooltip |
Группы и таблицы поддерживают `children`/`columns` для вложенных элементов.
### Кнопки: command и stdCommand
- `"command": "ИмяКоманды"``Form.Command.ИмяКоманды`
- `"stdCommand": "Close"``Form.StandardCommand.Close`
- `"stdCommand": "Товары.Add"``Form.Item.Товары.StandardCommand.Add` (стандартная команда элемента)
### Допустимые события (`on`)
Компилятор предупреждает об ошибках в именах событий. Основные:
- **input**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Clearing`, `AutoComplete`, `TextEditEnd`
- **check**: `OnChange`
- **table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `BeforeAddRow`, `BeforeDeleteRow`, `OnActivateRow`
- **label/picture**: `Click`, `URLProcessing`
- **pages**: `OnCurrentPageChange`
- **button**: `Click`
### Система типов (для attributes)
`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной).
### Секции расширений
| Секция | Назначение |
|--------|-----------|
| `formEvents` | События уровня формы с `callType` (Before/After/Override) |
| `elementEvents` | События на существующих элементах заимствованной формы |
| `callType` на `commands` | callType на Action команды |
| `callType` на `on` | callType на событиях новых элементов (объектный формат) |
Все extension-секции опциональны — без них навык работает как с обычными формами.
## Workflow
1. `/form-info` — посмотреть текущую структуру формы
2. Создать JSON с описанием добавлений
3. `/form-edit` — добавить в форму
4. `/form-validate` — проверить корректность
5. `/form-info` — убедиться что добавилось правильно
---
name: form-edit
description: Добавление элементов, реквизитов и команд в существующую управляемую форму 1С. Используй когда нужно точечно модифицировать готовую форму
argument-hint: <FormPath> <JsonPath>
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /form-edit — Редактирование формы
Добавляет элементы, реквизиты и/или команды в существующий Form.xml. Автоматически выделяет ID из правильного пула, генерирует companion-элементы (ContextMenu, ExtendedTooltip, и др.) и обработчики событий.
## Использование
```
/form-edit <FormPath> <JsonPath>
```
## Параметры
| Параметр | Обязательный | Описание |
|-----------|:------------:|----------------------------------|
| FormPath | да | Путь к существующему Form.xml |
| JsonPath | да | Путь к JSON с описанием добавлений |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-edit.ps1" -FormPath "<путь>" -JsonPath "<путь>"
```
## JSON формат
```json
{
"into": "ГруппаШапка",
"after": "Контрагент",
"elements": [
{ "input": "Склад", "path": "Объект.Склад", "on": ["OnChange"] }
],
"attributes": [
{ "name": "СуммаИтого", "type": "decimal(15,2)" }
],
"commands": [
{ "name": "Рассчитать", "action": "РассчитатьОбработка" }
]
}
```
### Расширения (extension-формы)
Для заимствованных форм (с `<BaseForm>`) автоматически активируется extension-режим: ID начинаются с 1000000+. Доступны дополнительные секции:
```json
{
"formEvents": [
{ "name": "OnCreateAtServer", "handler": "Расш1_ПриСозданииПосле", "callType": "After" },
{ "name": "OnOpen", "handler": "Расш1_ПриОткрытии", "callType": "Before" }
],
"elementEvents": [
{ "element": "Банк", "name": "OnChange", "handler": "Расш1_БанкПриИзменении", "callType": "Before" }
],
"commands": [
{ "name": "Подбор", "action": "Расш1_ПодборПосле", "callType": "After" },
{ "name": "Запрос", "actions": [
{ "callType": "Before", "handler": "Расш1_ЗапросПеред" },
{ "callType": "After", "handler": "Расш1_ЗапросПосле" }
]}
],
"elements": [
{ "input": "Поле", "path": "Объект.Поле", "on": [{ "event": "OnChange", "callType": "After" }] }
]
}
```
### Позиционирование элементов
| Ключ | По умолчанию | Описание |
|------|-------------|----------|
| `into` | корневой ChildItems | Имя группы/таблицы/страницы, куда вставлять |
| `after` | в конец | Имя элемента, после которого вставлять |
### Типы элементов
Те же DSL-ключи, что в `/form-compile`:
| Ключ | XML тег | Companions |
|------|---------|------------|
| `input` | InputField | ContextMenu, ExtendedTooltip |
| `check` | CheckBoxField | ContextMenu, ExtendedTooltip |
| `label` | LabelDecoration | ContextMenu, ExtendedTooltip |
| `labelField` | LabelField | ContextMenu, ExtendedTooltip |
| `group` | UsualGroup | ExtendedTooltip |
| `table` | Table | ContextMenu, AutoCommandBar, Search*, ViewStatus* |
| `pages` | Pages | ExtendedTooltip |
| `page` | Page | ExtendedTooltip |
| `button` | Button | ExtendedTooltip |
Группы и таблицы поддерживают `children`/`columns` для вложенных элементов.
### Кнопки: command и stdCommand
- `"command": "ИмяКоманды"``Form.Command.ИмяКоманды`
- `"stdCommand": "Close"``Form.StandardCommand.Close`
- `"stdCommand": "Товары.Add"``Form.Item.Товары.StandardCommand.Add` (стандартная команда элемента)
### Допустимые события (`on`)
Компилятор предупреждает об ошибках в именах событий. Основные:
- **input**: `OnChange`, `StartChoice`, `ChoiceProcessing`, `Clearing`, `AutoComplete`, `TextEditEnd`
- **check**: `OnChange`
- **table**: `OnStartEdit`, `OnEditEnd`, `OnChange`, `Selection`, `BeforeAddRow`, `BeforeDeleteRow`, `OnActivateRow`
- **label/picture**: `Click`, `URLProcessing`
- **pages**: `OnCurrentPageChange`
- **button**: `Click`
### Система типов (для attributes)
`string`, `string(100)`, `decimal(15,2)`, `boolean`, `date`, `dateTime`, `CatalogRef.XXX`, `DocumentObject.XXX`, `ValueTable`, `DynamicList`, `Type1 | Type2` (составной).
### Секции расширений
| Секция | Назначение |
|--------|-----------|
| `formEvents` | События уровня формы с `callType` (Before/After/Override) |
| `elementEvents` | События на существующих элементах заимствованной формы |
| `callType` на `commands` | callType на Action команды |
| `callType` на `on` | callType на событиях новых элементов (объектный формат) |
Все extension-секции опциональны — без них навык работает как с обычными формами.
## Workflow
1. `/form-info` — посмотреть текущую структуру формы
2. Создать JSON с описанием добавлений
3. `/form-edit` — добавить в форму
4. `/form-validate` — проверить корректность
5. `/form-info` — убедиться что добавилось правильно
File diff suppressed because it is too large Load Diff
+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:
+30 -30
View File
@@ -1,30 +1,30 @@
---
name: form-info
description: Анализ структуры управляемой формы 1С (Form.xml) — элементы, реквизиты, команды, события. Используй для понимания формы — при написании модуля формы, анализе обработчиков и элементов
argument-hint: <FormPath>
allowed-tools:
- Bash
- Read
- Glob
---
# /form-info — Компактная сводка формы
Читает Form.xml и выводит дерево элементов, реквизиты с типами, команды, события. Заменяет чтение тысяч строк XML.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>"
```
## Параметры
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| FormPath | да | Путь к файлу Form.xml |
| Expand | нет | Раскрыть свёрнутую секцию по имени или title, `*` — все |
| Limit | нет | Макс. строк (по умолчанию 150) |
| Offset | нет | Пропустить N строк (пагинация) |
Вывод самодокументирован. `[Group:AH]`/`[Group:AV]` = AlwaysHorizontal/AlwaysVertical.
---
name: form-info
description: Анализ структуры управляемой формы 1С (Form.xml) — элементы, реквизиты, команды, события. Используй для понимания формы — при написании модуля формы, анализе обработчиков и элементов
argument-hint: <FormPath>
allowed-tools:
- Bash
- Read
- Glob
---
# /form-info — Компактная сводка формы
Читает Form.xml и выводит дерево элементов, реквизиты с типами, команды, события. Заменяет чтение тысяч строк XML.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-info.ps1" -FormPath "<путь к Form.xml>"
```
## Параметры
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| FormPath | да | Путь к файлу Form.xml |
| Expand | нет | Раскрыть свёрнутую секцию по имени или title, `*` — все |
| Limit | нет | Макс. строк (по умолчанию 150) |
| Offset | нет | Пропустить N строк (пагинация) |
Вывод самодокументирован. `[Group:AH]`/`[Group:AV]` = AlwaysHorizontal/AlwaysVertical.
+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 = [

Some files were not shown because too many files have changed in this diff Show More