Compare commits

..
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 8925bd5875 fix(cfe-borrow): ChildObjects и InternalInfo у контейнерных типов (#56)
Заимствованные DataProcessor, Report, DocumentJournal, HTTPService,
WebService, Subsystem и др. выводились без <ChildObjects/> — платформа
отвергала расширение при загрузке исходников («ожидаемое ChildObjects»).
Признак теперь берётся у объекта-источника, список типов остаётся
страховкой и дополнен недостающими.

Следом всплывал второй отказ — «Отсутствует внутренняя информация
(узел InternalInfo)» для Sequence, FilterCriterion, SettingsStorage:
этих типов не было в карте генерируемых типов. Добавлены они, а также
IntegrationService и WSReference.

Проверено реальной загрузкой на 8.3.24 и 8.3.27: синтетический стенд
(9 типов) и заимствование из acc/ut в BP_DEMO и UT_DEMO.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 17:56:01 +03:00
Nick ShirokovandClaude Opus 5 f552def817 fix(xdto-compile): версия формата из Configuration.xml вместо хардкода 2.17
Раундтрип по 1173 пакетам XDTO трёх конфигураций дал 760 совпадений и 413
расхождений — все ровно по две строки и все на УТ: штамп версии. Оригинал
2.20, наш вывод 2.17. Содержательных расхождений нет ни одного.

xdto-compile единственный из навыков, пишущих XML внутрь конфигурации, не
определял версию формата. Причина не в XDTO: волна авто-детекта прошла
2026-04-06 (d1550864) и накрыла существовавшие тогда навыки, а xdto-compile
появился 2026-07-25 — соглашение к нему просто не применили.

Добавлен Detect-FormatVersion / detect_format_version — дословная копия из
остальных навыков (включая сегодняшнюю правку про символы вместо байтов),
разрешение пути на вызывающей стороне.

Аудит остальных навыков: литеральный хардкод остался только в epf-init и
erf-init (автономные обработки, конфигурации рядом нет — дефолт законен) и в
тестовой заглушке stub-db-create.ps1.

Проверка: XDTO 1173/1173 без расхождений (было 760), сюита xdto 43/43 на
обоих портах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 16:21:22 +03:00
Nick ShirokovandClaude Opus 5 fae22435d4 fix(cfe-borrow,form-*,interface-edit,meta-compile,role-compile,subsystem-compile,template-add): длина среза Configuration.xml
Detect-FormatVersion читает префикс Configuration.xml как
ReadAllText(...).Substring(0, Min(2000, (Get-Item).Length)). Размер файла — в
БАЙТАХ, Substring считает СИМВОЛЫ: на кириллице байт больше, и если файл
короче 2000 символов, длина среза выходит за строку и навык падает
исключением. Проявлялось на маленьких конфигурациях — например на фикстуре
EPF внутри конфигурации на поддержке.

Функция расходится копиями по навыкам, поэтому правка одинаковая в восьми:
длина берётся по самой строке. В help-add так было изначально; в
meta-compile рядом (Detect-CompatibilityMode) уже стоял верный вариант.

py-порты иммунны: там f.read(2000) в текстовом режиме — читает символы и не
бросает исключение. Зеркалить нечего, версии выровнены по обоим портам.

Заодно tests/skills/verify-snapshots.mjs: outputPath кейса читался только из
params, тогда как runner.mjs берёт его с верхнего уровня — из-за расхождения
харнесс искал результат не там, где навык его написал, и это маскировало
падение как «выход не создан».

Проверка: 1С-сертификация mxl-compile 13/13 (кейс guard-allow-external падал
с начала кампании), полная сюита 630/630 на PowerShell и 627+3 skipped на python.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:35:17 +03:00
Nick ShirokovandClaude Opus 5 919d49fe14 fix(form-edit,meta-edit,mxl-compile,role-compile,subsystem-*): кавычки в тексте не экранируем
Правило платформы единое и подтверждено дважды. По корпусу трёх конфигураций:
92142 сырых кавычки в тексте элементов и НИ ОДНОЙ &quot;; в условиях RLS —
16334 сырых против нуля экранированных (при этом &amp; платформа пишет, то есть
амперсанд экранируется, а кавычка нет). Загрузка на стенде: оба варианта
принимаются, но выгружает платформа сырую кавычку — то есть &quot; не ошибка,
а лишний шум в роундтрипе.

Решение было принято раньше в form-compile (там оно и записано комментарием) и
в skd-compile/skd-edit, но шесть навыков из него выпали. Приведены к общему виду:
где Esc-Xml использовался только для текста — функция стала текстовой; в meta-edit,
где она нужна и для атрибута, текстовые места переведены на существующий
Esc-XmlText. Атрибуты нигде не затронуты: там экранирование кавычек обязательно.

Дрейф эталонов — три строки, все условия RLS; role-info и role-validate строят
фикстуры прогоном role-compile (кросс-навыковый пересъём).

Проверка: полная сюита 630/630 на PowerShell и 627+3 skipped на python;
1С-сертификация role-compile 9/9, subsystem-compile 9/9, subsystem-edit 6/6,
form-edit 6/6, meta-edit 16/16. В mxl-compile кейс guard-allow-external падает
и до правки — не связан.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 15:08:55 +03:00
Nick ShirokovandClaude Opus 5 6963df6b99 fix(meta-compile,meta-decompile): полиморфный DataPathField и экранирование кавычек в тексте
Первый полный прогон по всем поддерживаемым типам трёх конфигураций
(41366 объектов) вскрыл два дефекта, живущих только в корпусе 2.17.

DataPathField в блоке характеристик полиморфно: обычно -1 («не задано»),
но в 8 случаях содержит путь к полю. Обе стороны жёстко приводили значение
к [int], из-за чего декомпиляция всего объекта падала — 8 документов БП не
разбирались вовсе. Теперь число остаётся числом, а путь проходит через
Expand-CharField/Shorten-CharField, как соседние путевые поля.

Текст элемента экранировался атрибутной функцией: кавычка превращалась в
&quot;, тогда как платформа держит её в тексте как есть (наименование
«Транспортные средства, зарегистрированные в системе "Платон"» в ERP).
В компиляторе для этого давно есть Esc-XmlText/esc_xml_text — переведены
все 128 мест эмиссии текста в обоих портах. Замена строго ослабляющая:
атрибуты не затронуты, экранирование & < > сохранено.

Проверка: три документа БП с путевым DataPathField 3/3, сюита meta-compile
76/76 на обоих портах. Полный прогон корпуса идёт частями.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 13:49:25 +03:00
Nick ShirokovandClaude Opus 5 b174ca659d fix(meta-decompile): предопределённый элемент с разделителем в значении
Сокращённая запись предопределённого элемента — "(Код) Имя [Наименование]".
Компилятор читает код как [^)]*, а имя как \S+, поэтому значение, содержащее
собственные разделители грамматики, разбор ломает: код "114 (108)" даёт
"(114 (108)) Код108", регулярка не сходится, и элемент теряет и имя, и код —
в XML уходят пустые <Name/> и <Code/>.

Декомпилятор теперь проверяет однозначность и при конфликте отдаёт объектную
форму {name, code, description}, которую компилятор поддерживает давно.
Конфликтом считаются ')' или ':' в коде, пробел/скобка/':' в имени, скобки
в наименовании.

Затронуто по 6 элементов в БП и в ERP (вычеты НДФЛ с кодами вида "117 (109)").
Пробел давний: эмиссия predefined помечена в WORKFLOW как черновая, а на УТ
таких кодов нет — вскрылось только на корпусе 2.17.

Проверка: справочники acc+erp+ut 2159/2159 (было 2157), JSON декомпилятора
побайтово совпадает у PS и py, сюиты meta-decompile 3/3 на обоих портах и
meta-compile 76/76.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 12:28:54 +03:00
Nick ShirokovandClaude Opus 5 a305c1bc89 fix(meta-decompile): регистрочувствительное сравнение с дефолтом
Аудит всех 198 сравнений в декомпиляторе после того, как ловушка PS
(-eq/-ne регистронезависимы) сработала трижды подряд: синоним метода,
RootURL и синоним шаблона теряли значение, отличавшееся от дефолта
только регистром.

Сравнения разделены на два класса. Рискованный — где дефолт ВЫВОДИТСЯ ИЗ
ДАННЫХ (имя, синоним, обработчик): там регистр реально гуляет. Такие
сравнения синонимов уже были регистрочувствительными с прошлой кампании;
оставались три — обработчик метода против "ИмяШаблона+ИмяМетода", состав
UsePurposes и список InputByString. Исправлены.

Add-EnumProp (центральный хелпер сравнения с дефолтом) тоже переведён на
-cne: на текущем корпусе это no-op, платформа пишет enum канонически, но
снимает латентную ловушку.

Инлайновые сравнения с enum-литералами (~150 шт.) оставлены как есть:
там дефолт — фиксированная константа формата, а не данные, и расхождение
по регистру означало бы неканоничный enum от платформы, чего не бывает.

Проверка (изменений нет ни на одном корпусе): сервисы ERP+БП 46/46,
сервисы УТ 25/25, справочники УТ 519/519, tier-1 УТ 1282/1283,
справочники acc+erp+ut 2157/2159, сюиты meta-compile 76/76 и
meta-decompile 3/3. Два расхождения в справочниках — давний пробел по
предопределённым элементам, воспроизводится и на коде до правки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:59:33 +03:00
Nick ShirokovandClaude Opus 5 05beeca7d3 fix(meta-compile,meta-decompile): многоязычные синонимы и регистр RootURL в сервисах
Прогон сервисов на конфигурациях формата 2.17 (ERP + БП, 46 объектов) вскрыл
то, чего не было в УТ: 25 совпадений из 46.

ERP двуязычна, и синоним шаблона, метода, операции и параметра приезжает
объектом {ru,en}. Компилятор интерполировал его в строку — в XML попадал
литерал "@{ru=Версия; en=Version}" вместо пары языковых элементов. Значение
теперь передаётся в эмиттер как есть, он и так умеет обе формы.

RootURL сравнивался с дефолтом (имя в нижнем регистре) регистронезависимо,
поэтому MobileAppReceiptScanner считался дефолтным и терял регистр при
регенерации. Сравнение сделано регистрочувствительным — тот же класс ошибки,
что уже ловили на синонимах методов.

Спека meta-dsl-spec дополнена: объектные формы шаблонов, методов, операций и
параметров; xdtoPackages как список; descriptorFileName; dataLockControlMode;
нотация Кларка для типов из своего пространства имён; Use в ReuseSessions.

Проверка: сервисы ERP+БП 46/46, сервисы УТ 25/25, сюита 76/76 на обоих
портах, 1С-сертификация кейса с многоязычным синонимом пройдена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:47:02 +03:00
Nick ShirokovandClaude Opus 5 87f97e986a feat(meta-decompile,meta-compile): поддержка WebService и полнота его формата
meta-decompile не знал тип WebService — 18 сервисов УТ не проходили раундтрип.
Добавлен разбор (оба порта): namespace, состав XDTO-пакетов, дескриптор,
операции с параметрами.

Компилятор поддерживал тип поверхностно; раундтрип на реальных сервисах
показал, чего не хватало:

- XDTOPackages эмитился скаляром, а это СПИСОК элементов: ссылка на пакет
  конфигурации (xr:MDObjectRef) либо URI внешнего пространства имён
  (xs:string). Presentation пуст, CheckState 0 — 19/19 по корпусу;
- DescriptorFileName не эмитился вовсе, хотя есть у всех 18 сервисов и НЕ
  выводится из имени (DMILService -> dmil.1cws) — нужен явный ключ;
- DataLockControlMode не эмитился (Managed у всех 192 операций);
- Comment не эмитился ни у сервиса, ни у операций и параметров;
- типы из собственного пространства имён (81 случай) писались без локального
  xmlns. В DSL задаются нотацией Кларка "{uri}ИмяТипа", компилятор объявляет
  xmlns сам — как это делает платформа.

Nillable захватывается явно у операций и параметров: по корпусу значения
смешанные (операции 103/89, параметры 128/395), дефолт угадать нельзя.

Порядок операций и параметров приведён к порядку DSL в обоих портах — PS шёл
в порядке хеш-таблицы, py сортировал.

Проверка: 25 сервисов УТ (18 WebService + 7 HTTPService) 25/25 без
расхождений; JSON декомпилятора побайтово совпадает у PS и py на всех 25;
сюита 76/76 на обоих портах; 1С-сертификация обоих кейсов пройдена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 11:40:10 +03:00
Nick ShirokovandClaude Opus 5 01c45581ef feat(meta-decompile): поддержка HTTPService + починка его компиляции
meta-decompile не знал тип HTTPService, поэтому 7 сервисов УТ не проходили
раундтрип вовсе. Добавлен разбор: rootURL, reuseSessions, sessionMaxAge и
дерево urlTemplates -> methods.

Раундтрип на реальных сервисах вскрыл три пробела компилятора:

1. ReuseSessions: в allowlist были только DontUse и AutoUse, а платформа
   пишет ещё и Use (4 объекта в корпусе) — компиляция падала с ошибкой.
2. Обработчик метода выводился по формуле ИмяШаблона+ИмяМетода; в реальных
   конфигурациях он произвольный (УдаленныйВызовМетодаЧерезТелоЗапроса).
   Метод получил объектную форму {httpMethod, handler, synonym, comment}
   рядом со строчным сокращением "только HTTP-метод".
3. Comment не эмитился ни у объекта, ни у шаблонов и методов, хотя платформа
   пишет тег всегда.

Порядок шаблонов и методов приведён к порядку DSL в обоих портах: PS шёл в
порядке хеш-таблицы, py сортировал — снэпшоты бы разъехались между портами.

В декомпиляторе сравнение синонима с авто-выводом сделано регистрочувствительным
(-ceq): "Post" против "post" считалось совпадением, и синоним терялся.

Проверка: HTTP-сервисы УТ 7/7 без расхождений (было 0 — тип не поддерживался),
сюита 76/76 на обоих портах, 1С-сертификация кейса пройдена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 21:20:44 +03:00
Nick ShirokovandClaude Opus 5 dac265995e fix(meta-compile): LineNumberLength только объектам с хранением в БД
Раундтрип по обработкам и отчётам УТ (7568 объектов) показал 265 лишних
строк: мы эмитили LineNumberLength каждой табличной части, а платформа
пишет его только объектам, у которых есть таблица в базе.

Проверено на двух конфигурациях независимо: у обработок 0 тегов из 235+257
табличных частей, у отчётов 0 из 106+8; у справочников, документов, ПВХ,
планов обмена и бизнес-процессов тег стоит поголовно. Свойство задаёт
разрядность физического номера строки в таблице БД — у обработок и отчётов
таблиц нет, хранить нечего.

Исключены DataProcessor и Report, а также ExternalDataProcessor и
ExternalReport: внешние обработки той же природы, и при сборке EPF на
формате 2.20 мы бы писали тег, которого платформа не пишет.

Кейс format-220-props дополнен обработкой с табличной частью: у неё тега
нет, у справочника в той же конфигурации LineNumberLength=9.

Проверка: УТ tier-2 7543/7543 без расхождений (было 71 diff),
сюита 76/76 на обоих портах, 1С-сертификация на 8.3.27 пройдена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:58:31 +03:00
Nick ShirokovandClaude Opus 5 4b6c3faa1d fix(meta-compile): строка вида "Документ.Имя" перестала молча становиться ссылкой
Значением параметра выбора и значения заполнения бывает ЗНАЧЕНИЕ — пустая
ссылка, предопределённый элемент или значение перечисления. Объект метаданных
значением быть не может, поэтому двухчастное "Тип.Имя" — это строка.

Правило уже существовало, но только для перечислений ("Enum.X" не значение);
для остальных корней его не было, и строка "Документ.РеализацияТоваровУслуг"
превращалась в xr:DesignTimeRef с переводом имени на английский — молчаливая
подмена смысла: вместо текста получалась ссылка на реальный документ.

Проверка по корпусу (acc+erp+ut): 5520 значений в ChoiceParameters и 8623 в
FillValue — двухчастных с типом метаданных НЕТ ни одного. Прощающий ввод не
пострадал: "Справочник.Номенклатура.ПустаяСсылка" -> Catalog.Номенклатура.EmptyRef,
"Перечисление.X.Y" -> Enum.X.EnumValue.Y работают как раньше (закреплено кейсом).

Проверка: УТ tier-1 1283 -> 1282 совпадения, справочники 519/519,
сюита 76/76 на обоих портах, 1С-сертификация кейса пройдена.
Остаётся 1 расхождение — план обмена без Ext/Content.xml (хвостовая аномалия
конфигурации: на одной БП все четыре платформы файл пишут).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:30:51 +03:00
Nick ShirokovandClaude Opus 5 3d5c9e5bee fix(meta-compile,meta-decompile): не-дефолтный TypeReductionMode измерения и nil в FixedArray
Раундтрип по документам и регистрам УТ (1283 объекта) вскрыл два пробела.

TypeReductionMode измерения РС: декомпилятор значение захватывал, но парсер
измерения в компиляторе не переносил ключ дальше, и любое отклонение от
дефолта молча заменялось на TransformValues. Так терялось третье значение
свойства — DeleteData («Удалять данные»); всего у свойства три режима:
Преобразовывать значения (дефолт), Удалять данные, Запрещать.
Синтетика на матрице платформ: DeleteData принимается и переживает роундтрип
на 8.3.25, 8.3.26 и 8.3.27 — порог тот же, что у самого свойства (2.18).

nil-элемент внутри FixedArray (<v8:Value xsi:nil="true"/>) декомпилировался
пустой строкой, компилятор эмитил xs:string. Теперь это JSON null в обе
стороны. Конструкция не новая — есть уже в дампе 8.3.20.

Спеки: у TypeReductionMode перечислены все три значения с названиями из
конфигуратора; версия появления исправлена с 2.20 на 2.18.

Проверка: УТ tier-1 1283 объекта -> 1280 совпадений (было 1273),
сюита meta-compile 76/76 на обоих портах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 20:03:28 +03:00
Nick ShirokovandClaude Opus 5 75e861da6e fix(meta-decompile,meta-compile): пустая ссылка в ChoiceParameters теряла тип
Раундтрип по справочникам УТ 11.5.27 вскрыл: <app:value xsi:type="xr:DesignTimeRef"/>
с пустым содержимым декомпилировался в "value": "", и компилятор законно
эмитил свой дефолт xs:string — тип ссылки терялся.

Конвенция для этого случая уже была (маркер {emptyRef: true} у fillValue),
пробел был только в ChoiceParameters. Декомпилятор теперь ставит маркер,
компилятор его понимает — и в скалярном значении, и внутри FixedArray.

Форма редкая, поэтому прежние кампании её не поймали: в acc она встречается
1 раз, в erp — ни разу, в УТ — 7 раз.

Is-EmptyRef в PS явно отсекает коллекции: у массива $v.emptyRef разворачивается
в свойства элементов (member enumeration), и массив с одним таким элементом
схлопывал FixedArray в скаляр. В py-порте isinstance(v, dict) такого не допускает —
расхождение поймано снэпшотом.

Проверка: справочники УТ 519/519 без расхождений (было 519 diff),
сюита meta-compile 76/76 на обоих портах, 1С-сертификация кейса пройдена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 19:25:24 +03:00
Nick ShirokovandClaude Opus 5 2067778ba3 docs(db-guide): разрешение ссылочных типов при разборе EPF/ERF
Замер на реквизите CatalogRef (8.3.24): в базе с подходящей конфигурацией
конфигуратор и ibcmd дают одинаковый результат — имя типа
(cfg:CatalogRef.Валюты). Движок начинает влиять, только когда база не та:
ibcmd оставляет идентификатор типа (<v8:TypeId>), 1cv8 подменяет тип на
xs:string с квалификаторами строки.

Годится только имя типа: оно резолвится по имени и собирается в любой
конфигурации с одноимённым объектом. Идентификатор привязан к исходной
конфигурации — в чужой базе сборка проходит без ошибки, но uuid остаётся
висячим. Оба деградированных варианта молчаливы, поэтому разбирать EPF/ERF
нужно только в базе с подходящей конфигурацией.

Инструкции навыков epf-dump/erf-dump намеренно не меняются: ограничение там
уже сформулировано, а объяснение «что будет, если разобрать в пустой базе»
только провоцирует так делать.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 17:38:13 +03:00
Nick ShirokovandClaude Opus 5 b92aad519c docs(specs,db-guide): лестница версий формата 2.17-2.18-2.19-2.20
Спеки утверждали, что за 2.17 (8.3.20-8.3.24) сразу идёт 2.20 (8.3.27+),
и приписывали версии 2.20 всю дельту. Промежуточные версии существуют:
8.3.25 пишет 2.18, 8.3.26 - 2.19.

Атрибуция изменений исправлена по замеру одной конфигурации, выгруженной
четырьмя платформами: TypeReductionMode и TextToSpeech - 2.18, отказ ролей
писать право, равное setForNewObjects, - 2.19, LineNumberLength - 2.20.

Два утверждения о «дельте 2.20» в 1c-configuration-spec § 7 были ошибочны и
удалены. Сжатая шапка xmlns оказалась артефактом стороннего сериализатора в
эталонном дампе, а не форматом: платформа во всех версиях пишет полную шапку.
Стиль пустых элементов признаком версии не является - он различается и между
дампами одной версии.

Добавлено: ConfigurationExtensionCompatibilityMode платформа при выгрузке
подставляет свой собственный даже при неизменной конфигурации; три свойства
форм из 2.18 - изменение дефолта эмиссии, а не новые свойства (8.3.24
принимает их и сохраняет при роундтрипе, в отличие от TypeReductionMode).

В db-guide зафиксировано побайтовое совпадение выгрузок конфигуратором и
ibcmd - с оговоркой, что на разбор EPF/ERF это не переносится: там результат
определяется базой разбора, а не движком.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 17:38:13 +03:00
Nick ShirokovandClaude Opus 5 2e5289a88f fix(meta-compile,meta-validate,cf-init,*-validate): промежуточные версии формата 2.18/2.19
Версия формата выгрузки идёт лестницей, а не скачком 2.17 -> 2.20:
8.3.20-8.3.24 -> 2.17, 8.3.25 -> 2.18, 8.3.26 -> 2.19, 8.3.27 -> 2.20.
Прежняя дельта мерилась через две ступени и приписала версии 2.20 всё,
что появилось по дороге.

Замер на одной и той же конфигурации, выгруженной четырьмя платформами:
- 2.18: TypeReductionMode (4793 файла), TextToSpeech, 3 редких свойства форм;
- 2.19: новых тегов нет, роли перешли на omit-on-default;
- 2.20: только LineNumberLength.

meta-compile: единый гейт isFormat220 управлял обоими свойствами, поэтому на
проекте 2.18/2.19 TypeReductionMode не эмитился, хотя платформа этих версий
его пишет — роундтрип разъезжался. Порог расщеплён: >=2.18 для
TypeReductionMode, >=2.20 для LineNumberLength.

meta-validate: реестр versionedProps объявлял TypeReductionMode свойством
2.20, из-за чего проверка 18 давала ложную ошибку на корректном файле
2.18/2.19. Исправлено на 2.18.

Валидаторы (meta/form/cf/cfe/epf) считали 2.18 и 2.19 неизвестными версиями
и предупреждали «Unusual version»; cf-init не давал отскаффолдить
конфигурацию под 8.3.25/8.3.26. Обе версии впущены.

Тесты: фикстура empty-config-218, кейс meta-compile на границу свойств
(TypeReductionMode есть, LineNumberLength нет), два кейса meta-validate на
законность штампов 2.18/2.19; кейс error-220-props-in-217 теперь проверяет
оба сообщения с разными порогами. Кейс 2.18 проверен загрузкой в живую
8.3.25 через verify-snapshots.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 16:28:50 +03:00
Nick ShirokovandClaude Opus 5 ecd289fe11 docs(v8-project-guide): ibcmd на headless Linux/macOS и природа файлового ограничения
Ограничение «только файловые базы» — свойство обвязки навыков, а не утилиты:
ibcmd умеет клиент-серверные базы, но подключается к СУБД напрямую и требует
реквизитов, которых навыки не запрашивают. Плюс добавлен headless-сценарий для
Linux/macOS с примером v8path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 19:15:54 +03:00
Nick ShirokovandClaude Opus 5 1563973636 fix(db-run,stub-db-create): нормализация путей и пропущенные бампы версий
Хвосты после разбора: db-run и stub-db-create остались единственными,
кто не прощал обрамляющие кавычки и хвостовой разделитель в путях.
Плюс db-create правился в коммите паритета без бампа версии, а раннер —
без бампа своего заголовка.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:56:07 +03:00
Nick ShirokovandClaude Opus 5 4eea147619 docs(db-guide): что печатает навык и как читается вывод платформы
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:38:10 +03:00
Nick ShirokovandClaude Opus 5 0de74ba6d1 test(db-create,epf-build): вывод платформы, кодировки, путь с пробелом
Кейсы, разведённые по портам из-за расхождения вывода, слиты обратно —
это и есть приёмочный признак паритета. Цепочка epf-build разделена на
два кейса: успешный запуск временной базы теперь молчит, поэтому её
командная строка проверяется на падающем фейке.

Новое: чтение вывода платформы в UTF-8 и cp866, отсутствие блока при
молчащей платформе, форма токена для пути с пробелом, отказ до запуска
при отсутствующей базе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:38:10 +03:00
Nick ShirokovandClaude Opus 5 58d4426bc9 fix(db-*,epf-*): единый контракт вывода платформы и квотирования
Порты расходились в трёх местах, и каждое проявлялось только в редком
случае — то есть там, где цена ошибки максимальна.

1. Вывод. PS наследовал консоль (текст платформы попадал в поток без
   метки и в непредсказуемой позиции), PY захватывал его и не печатал
   вовсе — аварийное сообщение мимо /Out терялось. Теперь оба порта
   захватывают вывод и печатают его отдельным блоком «Вывод платформы»,
   только если он непуст: молчащий успех остаётся молчаливым.

2. Кодировка. PS декодировал вывод ibcmd как cp866, тогда как ibcmd
   пишет UTF-8 (проверено на 8.3.24, 8.3.27, 8.5) — русские сообщения
   приходили крякозябрами. PY использовал text=True, то есть локальную
   кодовую страницу. Теперь оба декодируют UTF-8 строго, с фолбэком на
   cp866 для аварийного текста 1cv8.

3. Квотирование. PY не работал с путём к базе, содержащим пробел, — ни
   на Windows, ни на macOS: 1С ждёт кавычки внутри значения
   (File="путь"), а subprocess квотирует токен целиком. PS работал,
   потому что вклеивал кавычки сам. Теперь обе версии строят токены
   одинаково; на Windows PY передаёт готовую командную строку.

Плюс валидация ввода: путевые параметры прощают обрамляющие кавычки,
пробелы по краям и хвостовой разделитель, а навыки, требующие готовую
базу, проверяют её наличие до запуска.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 22:37:57 +03:00
Nick ShirokovandClaude Opus 5 c0b4f3fb3b test(runner): гейт кейса по ОС (osOnly)
Фейк платформы, написанный как .cmd, на macOS не исполняется ни одним
портом — такие кейсы падали на маке вместо пропуска. runtimeOnly для
этого не годится: ограничение не по порту, а по ОС.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:26:29 +03:00
Nick ShirokovandClaude Opus 5 f6165c8d35 docs(db-guide,v8-project-guide): форма списка доп. аргументов
Список пишется одной строкой через запятую — так же, как -Objects/-Files;
отмечено ограничение: значение с запятой внутри не поддерживается.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:15:49 +03:00
Nick ShirokovandClaude Opus 5 478d6acfa2 test(runner): проверки stdout работают и в негативных кейсах
expect.stdoutContains/stdoutNotContains жили в ветке успеха, поэтому у
кейса с expectError проверялся только ненулевой код возврата, а строки
не смотрелись вовсе. Пятнадцать кейсов (включая xdto-validate,
meta-validate, form-validate, meta-remove) были зелёными вхолостую —
у facet-conflicts текст навыка успел разойтись с ожиданием.

Плюс case-level "cwd": "workDir" — кейсу может понадобиться, чтобы
навык стартовал внутри рабочего каталога (фикстура .v8-project.json).

Кейсы на доп. аргументы платформы: pass-through 1cv8 и ibcmd, источник
из реестра проекта, цепочка epf-build → stub, конфликт ключа,
позиционный токен ibcmd, чужой движок, маскирование секрета.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:15:40 +03:00
Nick ShirokovandClaude Opus 5 761e7b8613 fix(db-*,epf-*): список доп. аргументов — строкой через запятую
powershell.exe -File (именно так вызываются навыки) не умеет биндить
массив: значения через пробел уходят в позиционные параметры, а список
через запятую приезжает одним склеенным токеном. Поэтому параметр
принимает список в конвенции репозитория (как -Objects/-Files) и
разбирается внутри; нативный вызов массивом продолжает работать.

То же разбиение добавлено в py-порт — чтобы одна и та же строка вызова
работала в обоих. Значение с запятой внутри не поддерживается.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 21:15:25 +03:00
Nick ShirokovandClaude Opus 5 545b32a55f docs(db-guide,v8-project-guide): дополнительные аргументы платформы
Разбор escape hatch для вызывающего: два параметра по движкам, ключи
v8args/ibcmdargs в реестре проекта, что отклоняется и почему,
маскирование секретов и известные пределы разбора (/Psecret, /@).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 20:44:24 +03:00
Nick ShirokovandClaude Opus 5 efee7f8f2b feat(db-*,epf-*): передача дополнительных аргументов в 1cv8 и ibcmd
Набор аргументов платформы был закрыт: общий ключ запуска (например
/UseHwLicenses+ на машине с аппаратной лицензией) передать было нельзя,
и сборка на автоматически созданной временной базе падала с «Не найдена
лицензия».

Добавлен escape hatch — по параметру на движок, плюс зеркальные ключи
в .v8-project.json (v8args / ibcmdargs) для машинно-специфичных флагов:

- -AdditionalV8Arguments  → 1cv8.exe, ключи вида /Key
- -AdditionalIbcmdArguments → ibcmd, ключи вида --key=value

Аргументы уходят во все запуски платформы, которые делает навык:
epf-build без базы прогоняет CREATEINFOBASE, /LoadConfigFromFiles,
/UpdateDBCfg и саму сборку — ключ получает каждый.

До запуска отклоняются: аргумент, которым управляет сам скрипт (режим,
подключение, /Out, пакетная операция), позиционный токен для ibcmd и
параметр «не своего» движка. Значения секрето-опасных ключей (/P, /UC,
--password, --token) в логе маскируются.

Порядок источников: .v8-project.json, затем параметр. Поведение без
новых параметров не меняется.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 20:44:15 +03:00
Nick ShirokovandClaude Opus 5 d0e81a1715 fix(web-test): состояние группы по контролу сворачивания, а не по вёрстке содержимого
На боевой форме вскрылись случаи, которые прежнее правило не покрывало. Замеры показали,
что любая эвристика «по первому сиблингу за заголовком» нежизнеспособна:

- служебная обёртка .logicGroupContainer пишется двумя способами (у таблицы
  <дочерний>#group_div, у вложенной группы <дочерний>_div) — знали только первый;
- display самой обёртки плавает: block до первого тогла, none после;
- первые узлы группы могут быть скрыты своей логикой, а видимое содержимое идёт дальше
  по цепочке — раскрытая группа читалась как свёрнутая, и постусловие роняло успешный клик.

Теперь состояние берётся из контрола сворачивания: у варианта «картинка» — кадр gx спрайта
hideshow у каретки (полярность обратна дереву в dom/grid.mjs), у варианта «гиперссылка»
каретки нет, там принадлежность по отступу — дети группы смещены глубже её заголовка,
свободный сосед стоит на уровне заголовка. База отсчёта — левый край блока заголовка
(каретка сдвигает текст вправо), обход ограничен: у последней свёрнутой группы границы за
ней нет, и первый видимый узел нашёлся через 107 сиблингов в чужой ветке формы.

Клик по заголовку группы теперь сперва скроллит цель в вид: цель кликается по координатам,
и ниже вьюпорта клик молча не доходил.

Ответ на клик отдаёт clicked.group (техническое имя) и clicked.title (текущий заголовок):
заголовок ключом быть не может — он повторяется между блоками формы и меняется при
раскрытии (CollapsedRepresentationTitle), после чего клик по прежнему тексту не находит
элемент. При смене заголовка hint говорит, чем кликать дальше.

Фикстура: вложенная группа первым ребёнком, «скрыт первый узел» в двух вариантах контрола,
группа с меняющимся заголовком в конце формы (уезжает за вьюпорт). Каждый кейс проверен на
красноту без своей правки.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:12:07 +03:00
Nick ShirokovandClaude Opus 5 006e64405c fix(web-test): readTable разбирает сгруппированную шапку по «Группа / Колонка»
1С кладёт заголовок группы колонок и её листья в ОДНУ строку шапки, различая их
только высотой и шириной. Колонка заводилась на каждый бокс с текстом, а строка
ключуется по имени — поэтому одноимённые листья соседних групп («План» под «Цена»
и под «Количество») схлопывались в один ключ и склеивались через " / ", а
заголовки групп становились пустыми колонками.

Заголовок группы отличается от настоящей широкой колонки над узкими (паттерн
«Исполнитель» над «Срок»/«Выполнена») одним надёжным признаком: его colindex не
встречается ни в одной ячейке тела. По нему листья переименовываются в
«Группа / Лист» — той же конвенцией, что уже применяет читалка табличного
документа, — а заголовок из колонок убирается. Разворот объединённой шапки
(«Субконто 1/2/3») не задет: под ним боксов-листьев в шапке нет.

Заодно у пути записи заголовок ячейки резолвился x-сканом шапки и под группой
отдавал «Цена» всем трём листьям — fillTableRow по имени из readTable отвечал
notFilled. Теперь имя берётся из общей модели, как в чтении и клике.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 10:12:56 +03:00
Nick ShirokovandClaude Opus 5 ba27f2218f fix(web-test): фактическое состояние свёрнутой группы и попадание клика по заголовку
Группа, содержимое которой — таблица, отдавала collapsed:false в обоих состояниях,
а свернуть её обратно было нельзя. Два независимых дефекта:

1. Первым сиблингом за <base>#title_div платформа кладёт пустую служебную обёртку
   <дочерний>#group_div.logicGroupContainer (display:block, height:0), а контент идёт
   дальше по сиблингам — внутри обёртки его нет. Чтение display первого сиблинга
   давало collapsed:false и свёрнутой, и развёрнутой группе.
2. <base>#title_text растягивается по ширине содержимого (173px свёрнута → 1295px
   развёрнута), кликабелен только вложенный label шириной по тексту. Клик в
   геометрический центр попадал в пустоту: раскрыть удавалось, свернуть — нет.

GROUP_STATE_FN обходит сиблинги по id-префиксу обёртки (префикс обрывает обход на
чужих узлах — свободный элемент между группами по-прежнему не путает определение).
TEXT_CLICK_POINT_FN целится во вложенный текст с клампом влево, как rowClickPoint.
Обе — общие для getFormState().groups[] и резолвера цели клика.

Клик по заголовку, не изменивший состояние, теперь бросает ошибку вместо
toggled:true — идемпотентность {expand} не затронута, при нечитаемом collapsed
проверка пропускается.

Фикстура: свёрнутая группа с таблицей (прежние содержали только декорации, поэтому
регресс был зелёным) и растянутая гиперссылка с Сообщить в обработчике — у декораций
промаха нет, их внутренний узел тянется вместе с контейнером.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 20:54:23 +03:00
Nick ShirokovandClaude Opus 5 5472e03417 docs(form-compile): согласовать порядок вызова с form-add
Раздел Workflow описывал порядок «сначала компиляция, потом form-add», тогда как
form-add/SKILL.md и все тестовые кейсы задают обратный: каркас, затем наполнение.
Модель получала разный ответ в зависимости от того, какой навык прочитала первым.

Оба порядка дают одинаковый результат, но начинать с form-add надёжнее: при неверном
пути к объекту он сообщает об этом сразу, тогда как компиляция создаст каталоги по
указанному пути и отчитается об успехе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 17:28:41 +03:00
Nick ShirokovandClaude Opus 5 05856abe25 docs(xdto-guide,xdto-decompile): вернуть рецепт версионной копии исполнителю
Предыдущая правка гайда изложила копирование пакета пошагово в повелительном
наклонении, и стало неясно, кому эти шаги адресованы: гайд построен на том, что
задачу ставят словами, а шаги делает агент. Пользователь мог прочитать это как
работу, которую надо сделать самому.

В гайде теперь сказано, что происходит и чего достаточно назвать в задаче.
Сами шаги и острая кромка (менять targetNamespace вместе с объявлением xmlns,
не заменять все вхождения строки) — в SKILL.md навыка выгрузки, там же, где
описан путь «выгрузить → поправить → собрать».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:36:38 +03:00
Nick ShirokovandClaude Opus 5 32455a64b1 docs(xdto-guide): рецепт версионной копии пакета вместо описания результата
Раздел «Новая версия пакета» описывал, что получается, но не как это сделать,
и из-за этого выглядел местом, требующим отдельного флага компилятора. Рутины
там на самом деле немного: выгрузить схему, поменять в шапке targetNamespace
и связанное объявление xmlns, собрать под новым именем — имя и синоним задаются
флагами, править их внутри схемы не нужно.

Названа острая кромка: заменять все вхождения URI строкой нельзя — пострадает
импорт пространства имён, для которого старый URI является префиксом.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:33:27 +03:00
Nick ShirokovandClaude Opus 5 64b461930b docs(xdto-dsl-spec): отображение fixed и отсутствовавшее зеркало xdto:fixed
Таблица соответствий утверждала «@nillable, @default, @fixed — те же имена».
Для fixed это неверно: в модели признак и значение разнесены, fixed="V" из XSD
превращается в fixed="true" + default="V". Компилятор был написан ровно по этой
строке — неверная строка спеки воспроизвелась в коде буквально и дожила до
первой проверки платформой.

Плюс в таблице аннотаций не было xdto:fixed, хотя компилятор его принимает:
зеркало нужно для fixed="false" при заданном default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:25:47 +03:00
Nick ShirokovandClaude Opus 5 79beba2d1f fix(xdto-compile,xdto-validate): признак фиксированного значения без самого значения
В модели XDTO fixed — булев признак, а значение лежит в default; в XML-схеме
fixed="V" совмещает и признак, и значение. Компилятор оба идиома принимал, но
не проверял принятое: зеркало xdto:fixed="true" без default собиралось молча
в пакет, который платформа отвергает («Отсутствует фиксированное значение
свойства»). Прощающий ввод был сделан наполовину.

xdto-validate v1.1 — два ERROR: значение попало в признак (fixed не булев)
и признак без значения. Формулировка второго повторяет платформенную дословно,
чтобы отказ загрузки и наш вывод читались как одно и то же.

xdto-compile v1.1 — то же условие предупреждением на сборке, то есть на шаг
раньше db-update, где починить дешевле.

Кейсы: оба идиома плюс только default и атрибут (загружается в базу);
зеркало без значения — проверка диагностики, из платформенной верификации
исключено штатным skipPlatformVerify, пакет невалиден by design.

Правило откалибровано корпусом (760 пакетов, оба рантайма): 0 ложных
срабатываний, состав предупреждений не изменился. Round-trip остался
760/760 байт-в-байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:25:38 +03:00
Nick ShirokovandClaude Opus 5 20d86ae10f docs(xdto-guide): примеры с названным объектом работы, три новых сценария
Формулировка задачи может быть общей, но объект работы нужно назвать: без пути
к файлу или имени пакета агент останавливается и просит уточнений вместо работы.
Проверено прогоном триггеров: тот же запрос без якоря и с якорем даёт разный
исход. Отсюда абзац «что стоит назвать в задаче» и переформулировка примеров,
где объект не назывался.

Сняты два неудачных примера: «создай по нему документы» (вторая половина не про
XDTO) и симптом без якоря, дублирующий соседний сильный пример.

Добавлены сценарии, которых не было: инвентаризация незнакомой конфигурации,
обратные ссылки перед правкой (в прозе упоминались, примера не было) и проверка
перед загрузкой — у навыка проверки не было ни одного примера.

Уточнения по тексту:
- импорт пространств имён самой платформы пакетами объявлять не нужно;
- штатный экспорт XML-схемы даёт невалидную XSD и для неквалифицированной формы
  элементов, не только теряет nillable;
- обещание round-trip подкреплено измерением на корпусе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 15:48:29 +03:00
Nick ShirokovandClaude Opus 5 5d5a1bc36a fix(xdto): три дефекта, найденных платформенной верификацией снэпшотов
verify-snapshots загружает результат каждого кейса в 1С. Раньше навыки xdto
через него не проходили вовсе; первый прогон дал 5 из 9. Ни корпусная сверка,
ни валидатор такого не ловили: корпус состоит из заведомо валидных пакетов,
а синтетические кейсы до сих пор в базу не грузились.

1. fixed. В модели XDTO это булев флаг, значение лежит в default; в XSD наоборот —
   fixed="V" несёт значение. Компилятор писал значение прямо в fixed, и платформа
   отвергала пакет («Отсутствует фиксированное значение свойства»). Перевод сделан
   в обе стороны; по принципу прощающего ввода принимается и модельная форма через
   зеркало xdto:fixed. Отображение выведено по корпусу: fixed встречается только
   вместе с default, значений всего два.

2. Импорт на несуществующий пакет платформа отвергает («xdto-package-3.3 …
   не определен»), а у нас проверки не было. Добавлена ошибка валидатора и,
   что важнее, предупреждение прямо на сборке — отказ при db-update дешевле
   поймать на шаг раньше. Правило пришлось калибровать корпусом: сначала оно
   дало 67 ложных срабатываний на платформенных пространствах имён, их список
   выведен и исключён.

3. localName проверяется как NCName — фикстура с пробелом в имени была негодной,
   заменена на реалистичный дефис (name="alpha_3" localName="alpha-3").

Харнесс получил skipPlatformVerify с обязательной причиной: результат
set-namespace невалиден by design, операция намеренно оставляет висящий импорт
у зависящего пакета.

Итог: 9/9 компилятора, 9/10 + 1 осознанный пропуск у edit, round-trip 760/760,
валидатор 0 ложных, 40 тестов на обоих рантаймах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:51:08 +03:00
Nick ShirokovandClaude Opus 5 3eb805f7b0 docs(xdto-guide): примеры задачами, а не синтаксисом команд
Гайд адресован неподготовленному читателю, а основной сценарий — задача
в произвольной форме или её часть внутри большей. Синтаксис вызовов такого
читателя скорее отпугнёт, к тому же он уже описан в SKILL.md каждого навыка
и в гайде дублировался.

Каждый сценарий теперь начинается с того, как задачу формулируют словами
(«сформируй платёжку в формате клиент-банка», «обращение к Смена.Сотрудник
возвращает что-то бесструктурное»), дальше — что за этим происходит и на что
обратить внимание. За точным синтаксисом — ссылки на SKILL.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:13:48 +03:00
Nick ShirokovandClaude Opus 5 8232cbeaaa test(verify-snapshots): поддержка caseFiles и навыков xdto
Харнесс платформенной верификации не знал про caseFiles — механизм файлового
входа кейса, добавленный в runner.mjs. Та же функция перенесена сюда,
xdto-compile и xdto-edit добавлены в список проверяемых навыков.

Первый прогон отвергает 4 кейса из 9 — разбор в debug/xdto/FINDINGS.md §15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:12:02 +03:00
Nick ShirokovandClaude Opus 5 49d7204385 docs(xdto): пользовательский гайд и группа в README
Из семейств гайды есть у cf, cfe, db, epf, form, meta, role, skd, web —
у XDTO не было. Гайд построен вокруг задач, а не вокруг навыков: написать код
заполнения, разобрать входящий XML, добавить пакет по схеме контрагента,
поправить существующий, выпустить новую версию, отдать схему наружу,
разобраться с «бесструктурным» свойством.

Группа добавлена в таблицу README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:06:26 +03:00
Nick ShirokovandClaude Opus 5 5fd952a796 docs(xdto-dsl-spec): синхронизировать с правкой уплощения xs:choice
Спека описывала уплощение как «вложенный выбор варианта не сохраняется»,
не упоминая, что ветки теперь становятся необязательными — а это и есть
суть правки: иначе «одно из двух» превращалось в «оба обязательны»
и тип нельзя было заполнить.

Заодно в таблицу аннотаций добавлен xdto:declareNs, который был реализован
и описан в справочнике навыка, но в спеку не попал.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 14:00:25 +03:00
Nick ShirokovandClaude Opus 5 817ae0fea7 fix(xdto-info): рецепты создания по факту вида типа
Рецепт для вложенного объекта учил окольной форме там, где она не нужна.
Именованный тип берётся так же, как корневой — ФабрикаXDTO.Тип(ns, имя);
через Свойства.Получить(...).Тип идут только к анонимному, у которого имени
нет. Теперь строка выдаётся по факту: для именованного одна форма, для
анонимного другая, и обе с настоящими именами из пакета.

Убрано утверждение «Узел = ФабрикаXDTO.Создать(ТипУзла, Значение)» для типа
со значением элемента. По синтакс-помощнику Создать(<Тип>, <Значение>)
принимает ТипЗначенияXDTO, а такой узел — объектный тип, то есть форма была
просто неверной. Вместо неё проверяемый факт: значение лежит в свойстве
__content.

Зато для типа значения эта форма как раз корректна, а рецепта там не было
вовсе — добавлен.

Попутно: строка-заглушка «(раскрыт выше)» создавалась без новых ключей, и
py-порт падал с KeyError там, где PowerShell молча возвращает $null на
отсутствующем свойстве. Ключи добавлены, доступ переведён на .get().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:52:52 +03:00
Nick ShirokovandClaude Opus 5 6486f433de fix(xdto-compile,xdto-info,xdto-edit): правки по итогам прогона на субагентах
Четырём субагентам выданы реалистичные задачи по песочнице, навыки в
формулировках не назывались. Разбор — в debug/xdto/FINDINGS.md, §13.

ГЛАВНОЕ — дефект компилятора. При уплощении вложенного xs:choice ветки
оставались обязательными: схема «самовывоз ИЛИ адрес доставки» давала пакет,
требующий заполнить оба, и ни один реальный документ в него не ложился.
Компилятор предупреждал о потере выбора, но молчал о последствии, а валидатор
показывал «0 ошибок, 0 предупреждений» — структурно пакет корректен,
семантически мёртв. Теперь ветки становятся необязательными (единственное
уплощение, оставляющее тип заполнимым), предупреждение называет их поимённо.
Корневой xs:choice не затронут: он отображается в ordered="false".

xdto-edit: -Value "@файл" по конвенции skd-edit — на кавычках при инлайновой
передаче XSD споткнулись двое агентов из четырёх, причём сырой LoadXml уводил
чинить схему вместо транспорта. При сбое разбора теперь понятное сообщение.

xdto-info: поиск, законно ничего не нашедший, падал throw'ом со стектрейсом и
читался как поломка инструмента — теперь строка и exit 1. Блок «Создание»
покрывал только корневой тип, хотя вся реальная работа в XDTO — вложенные и
анонимные типы; добавлены рецепты по факту наличия. Отсутствие раздела «Точки
входа» было неоднозначным — теперь явная строка. Новый -RequiredOnly даёт
скелет «заполни обязательное»: необязательный объект уходит вместе с поддеревом.
По умолчанию выключен — иначе список читался бы как полный.

xdto-validate: предупреждение про anyType описывало историю («платформа заменяет
при импорте»), хотя в файле уже зафиксирован anyType; сначала состояние, потом
происхождение.

Проверено: 40 тестов на обоих рантаймах, round-trip 760/760, валидатор
0 ложных срабатываний на корпусе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:44:46 +03:00
Nick ShirokovandClaude Opus 5 1a1bbbac6f refactor(xdto-info): легенда обозначений в выводе, а не в инструкции
Инструкция несла 14-строчный пример вывода — то самое, что модель увидит,
запустив навык, но читаемое при каждой загрузке инструкции. Та же логика,
по которой из xdto-validate убран каталог проверок.

Легенда при этом нужна: ← Имя, [значение элемента], · Пакет из вывода сами
не читаются. Поэтому она переехала в вывод и печатается только для тех
обозначений, которые в нём реально встретились — на плоском типе легенды
нет вовсе. В самом навыке уже был такой прецедент: режим списка пакетов
поясняет свои колонки прямо в выводе.

Инструкция сократилась с 99 до 77 строк.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:05:15 +03:00
Nick ShirokovandClaude Opus 5 105ba67cb5 docs(xdto): триггеры не обещают того, чего навыки не делают
xdto-info объявлял себя средством «для написания кода» и «для разбора
входящего XML» — он не делает ни того ни другого, а даёт структуру, чтобы
это написал вызывающий. Формулировка приведена к принятой в семействе:
meta-info говорит «как подготовительный шаг при написании запросов и кода».

xdto-compile тем же оборотом обещал «разбор внешнего XML-формата», хотя
собирает пакет; заменено на «под внешний XML-формат».

xdto-decompile претендовал на «отредактировать существующий пакет» —
это роль xdto-edit, и ровно то противоречие, что было устранено в теле
инструкций. Теперь триггер описывает свою настоящую нишу: получить схему,
чтобы переработать целиком, отдать контрагенту или перенести пакет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 13:00:42 +03:00
Nick ShirokovandClaude Opus 5 1d49c16a67 docs(xdto): устранить противоречие «как править пакет» между навыками
На один и тот же вопрос четыре инструкции отвечали по-разному: decompile
объявлял себя «основным способом менять пакет», edit сам себя занижал
до варианта для маленьких пакетов, compile про edit не упоминал вовсе,
а workflow валидатора его не знал. Модель получала бы разный ответ
в зависимости от того, на какой файл попала, причём два из них уводили
от единственного навыка, созданного ровно для этой задачи.

Единое правило проведено через все четыре: точечная правка — xdto-edit;
переработка схемы целиком или знакомство с ней — decompile → compile.
Заодно в decompile добавлена развилка на xdto-info, чтобы разграничить
«нужна схема» и «нужна сводка для кода».

Мелочи: грамматика в xdto-edit, служебное значение -Mode auto убрано
из таблицы параметров (пользователь его не пишет), описан флаг [до N].

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:58:07 +03:00
Nick ShirokovandClaude Opus 5 a146fc1467 fix(xdto-edit): диагностика без привязки к харнессу и к «всему комплекту»
Сообщение называло каталог .claude/skills, хотя проект портируется в
.cursor/skills, .codex/skills, .gemini/skills и другие — путь теперь
вычисляется от расположения скрипта и потому верен на любой порт-ветке.

И предлагало копировать весь набор навыков, хотя нужны ровно два соседа:
xdto-decompile и xdto-compile. Теперь называются только недостающие.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:50:31 +03:00
Nick ShirokovandClaude Opus 5 679f820e52 fix(xdto-edit): преflight-проверка соседних навыков
xdto-edit — единственный навык с жёсткой зависимостью: без xdto-decompile
и xdto-compile модельные операции не работают. Остальные пять межнавыковых
вызовов в репозитории ведут только к *-validate, необязательному post-шагу
с деградацией в [SKIP], так что это отступление, а не следование практике.

Раньше отсутствие соседа обнаруживалось на середине правки. Теперь комплектность
проверяется до начала работы, с указанием, что навыки ставятся комплектом.
Операции над объектом метаданных (rename, set-synonym, set-comment) соседей
не требуют и работают в одиночку — проверка их не блокирует.

В SKILL.md зависимость намеренно не описана: это раздуло бы инструкцию и подало
бы исключение как допустимую практику. Причины, по которым не сделана копия
(конвертер — скрипт, а не библиотека; вторая реализация разошлась бы, чему есть
прямая улика в learning_meta_edit_emitter_ports; гарантия байт-точности держится
на тождестве кода) записаны в debug/xdto/FINDINGS.md — там, где их увидит тот,
кто соберётся «починить» это дублированием.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:43:57 +03:00
Nick ShirokovandClaude Opus 5 318abe3fc9 feat(xdto-edit): точечная правка пакета без чтения всей схемы
Навык нужен ровно для одного: не втаскивать в контекст мегабайтную схему ради
одного поля. Чистота диффа тут ни при чём — она уже обеспечена round-trip'ом
(замер: правка двух вещей через decompile→compile даёт 3 изменённые строки из 241).

Поэтому edit не заводит второй эмиттер, а строится поверх round-trip'а: пакет
выгружается в XSD, операция применяется к схеме, пакет собирается обратно
компилятором. Байт-точность для нетронутого достаётся даром, а смена namespace
перегенерирует все объявления префиксов сама — в EnterpriseData_1_20_2 их 5280.
На лишний шаг (загрузка XSD в DOM и пересохранение) заведён отдельный харнесс:
холостая правка не меняет ни байта на всех 760 пакетах.

Операции: add/replace/remove-property, add/remove-type, add-enum, add-import,
rename, set-synonym, set-comment, set-namespace. Содержимое — всегда фрагмент
XSD, тем же языком, что в компиляторе; отдельных -MinOccurs нет, свойство
меняется целиком через replace-property. Адресация точкой, путь заходит внутрь
встроенных типов.

rename трогает три места (объект метаданных, имена файла и каталога, регистрацию
в Configuration.xml). set-namespace правит свой пакет и перечисляет зависящие,
но не меняет их: при версионировании они и должны смотреть на прежнее
пространство имён. После правки автоматически запускается xdto-validate.

Проверено загрузкой в базу 8.3.24: add-property, add-enum и set-namespace
переживают db-load-xml + db-update.

Попутные ловушки портирования (детали — debug/xdto/FINDINGS.md): пустой элемент
в lxml ложен, из-за чего "or"-цепочка создавала бы вторую частицу в типе
с пустой sequence; диапазон [Ѐ-ӿԀ-ӿ] валиден в .NET и не компилируется в Python.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:29:28 +03:00
Nick ShirokovandClaude Opus 5 a4a55cf883 feat(xdto-info): структура пакета и типа в терминах 1С
Навык отвечает на вопрос «что присвоить и что обязательно», а не показывает
модель как есть. Типы переведены в нотацию 1С с учётом ограничений
(xs:decimal + totalDigits → Число(18,2)), псевдонимы развёрнуты со стрелкой
на исходное имя, кратность вынесена во флаги, для перечислимых типов выводятся
допустимые литералы. Различие атрибут/элемент в таблице свойств не показывается:
в коде 1С обращение одинаковое.

Флаг ставится на обязательные, хотя в модели XDTO умолчание обратное. Причина —
соседний meta-info, где непомеченный реквизит необязательный: один значок,
означающий в двух навыках противоположное, сам по себе источник ошибок.

Режимы: список пакетов конфигурации, состав пакета с точками входа, структура
типа с разузлованием на -Depth и used-by. Разузлование идёт через границы
пакетов с пометкой источника, анонимные типы раскрываются всегда, циклы
обрываются. Пакет адресуется путём, именем или namespace — последнее потому,
что модель приходит к задаче от строки ФабрикаXDTO.Тип(ns, имя), а не от имени
пакета в конфигурации.

Попутно закрыт баг паритета во всех четырёх py-портах: платформа допускает
в targetNamespace произвольную строку (в БП есть пакет с кириллическим
«ДопФайлУниверсальный»), .NET такое принимает, а libxml2 отвергает как
невалидный URI. Добавлено узкое отступление на восстанавливающий разбор —
только для этой ошибки, чтобы валидатор не перестал замечать битый XML.
Обнаружено это только потому, что корпус впервые прогнан на Python: раньше
все 760 гонялись лишь на PowerShell. Теперь 760/760 на обоих рантаймах.

Сортировка в PS переведена на ординальную: Sort-Object сортирует по культуре,
sorted() в Python — по кодам, и на смешанных латиница/кириллица имена
расходились бы.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 12:05:08 +03:00
Nick ShirokovandClaude Opus 5 bd4a082259 docs(xdto-decompile,xdto-validate,xdto-compile): убрать дублирующие разделы «Верификация»
У декомпилятора и валидатора раздел дословно повторял блок примеров и таблицу
параметров строкой выше, у компилятора — шаг 3 из «Типичного workflow».
Конкретная форма команды перенесена в этот шаг, разделы убраны.

Раздел полезен там, где отсылает к другим навыкам (как в role-compile),
и есть лишь у 14 навыков из 75 — обязательной конвенцией не является.
Инструкции стали короче на 30 строк без потери содержания.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:50:52 +03:00
Nick ShirokovandClaude Opus 5 0aa9342407 docs(xdto-compile,xdto-decompile,xdto-validate): обязательность параметров и умолчания
В таблицах параметров не было видно, что обязательно, а что нет, и какие
значения подставляются по умолчанию. Добавлена колонка «Обязательный»
(по образцу form-edit), умолчания расписаны, отмечена взаимоисключающая
пара -XsdPath/-Xsd, псевдонимы -Path и поведение без -Force.

Сверил документацию с поведением скриптов: хеш-таблица в -Synonym работает
только в PS-порте, в Python её нет — из инструкции убрана, многоязычный
синоним задаётся блоком xs:appinfo, который поддерживают оба порта.

Убран последний след нашего обсуждения формата («отдельного DSL нет» —
читателю не с чем сравнивать), добавлено, где посмотреть уже собранные
пакеты, и точное имя файла справочника аннотаций.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 10:46:59 +03:00
Nick ShirokovandClaude Opus 5 7ca6dfa6b2 fix(xdto-compile,xdto-validate): не терять конструкции молча; вычитать инструкции
xdto-compile терял свойства без единого слова: на реалистичной чужой схеме
из шести объявленных доезжало одно. Вложенные xs:sequence/xs:choice теперь
уплощаются (модель хранит плоский список), xs:all трактуется как
последовательность, xs:group и xs:attributeGroup раскрываются по ссылке —
и о каждом приближении навык пишет предупреждение. Молчаливая потеря — тот же
класс дефекта, что мы ловим у платформы, лечится так же: сообщением, не отказом.

xdto-validate получил проверки на грабли, найденные при разработке: порядок
элементов верхнего уровня (платформа отвергает пакет, не называя причины),
конфликты объявлений (name+ref, type+вложенный тип, тип без разновидности),
несовпадение рода базового типа, дубли имён свойств.

Новые правила прогнаны по всем 760 пакетам выгрузок: всё, что породила
платформа, валидно по определению, поэтому каждая ошибка там — ошибка правила.
Первый прогон дал 7, и все три класса оказались реальным поведением платформы:
length вместе с minLength/maxLength встречается, два пакета делят один
targetNamespace (Envelope и SOAP_Envelope_1_1 в БП), form="Text" называется
не только __content. Правила понижены до предупреждений либо сняты. Заодно
убран шум: предупреждение о неиспользуемом import срабатывало на четверти
корпуса — теперь только вместе с anyType, где оно и означает проблему.
Итог: 0 ошибок на корпусе, предупреждений 53 вместо 242.

Инструкции переписаны под читателя-исполнителя: убраны детали реализации
и наши мерки, каталог проверок валидатора (его вывод самодостаточен),
локальные пути в примерах заменены нейтральными. Таблица соответствий
XSD и справочник аннотаций вынесены в xdto-compile/xsd-reference.md.

Round-trip 760/760 сохранён, паритет PS/PY сохранён.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:48:48 +03:00
Nick ShirokovandClaude Opus 5 d05aef54b4 feat(xdto-compile,xdto-decompile,xdto-validate): пакеты XDTO из XML-схемы
Три навыка для работы с пакетами XDTO. Формат описания — обычная XSD,
своего DSL нет: рутину снимает конвертер (локальные объявления префиксов
dNpM на каждой ссылке, инвертированная кратность lowerBound/upperBound,
фасеты атрибутами вместо дочерних элементов, обязательный порядок
элементов верхнего уровня). То, чего XSD выразить не может — nillable
у атрибута, qualified у свойства, «атрибут записан явно» — едет
атрибутами из пространства имён модели XDTO по правилу «то же имя,
что в Package.bin». Свойства объекта метаданных живут в xs:appinfo,
поэтому пара decompile → compile замыкается без потерь.

Инвариант bin → xsd → bin проверен побайтово на 760 пакетах выгрузок
Бухгалтерии и ERP 8.3.24 (харнесс debug/xdto/roundtrip-corpus.mjs).
Сборка из рукописной XSD проверена загрузкой в базу 8.3.24 — именно
она вскрыла обязательный порядок import→property→valueType→objectType,
невидимый для корпусной сверки: все выгрузки уже канонические.

xdto-validate ловит два класса тихих дефектов, которые платформа не
диагностирует: подмену неразрешённого чужого типа на xs:anyType при
импорте XML-схемы и nillable у свойства-атрибута, теряемый экспортом
схемы в Конфигураторе.

Тесты: 18 снэпшот-кейсов на синтетических схемах (типовые конфигурации
в репозиторий не тащим), паритет PS↔PY на общих эталонах. Раннер
получил caseFiles — копирование файлов кейса в workDir для навыков
с файловым, а не JSON входом.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 21:21:57 +03:00
Nick ShirokovandClaude Opus 5 d544071b1e feat(meta-validate): версионные свойства и диапазон LineNumberLength
Две проверки, обе про формат 2.20.

Проверка 18 — реестр versionedProps «тег → минимальная версия формата». Если
свойство присутствует в файле со слишком старым штампом, при сборке на платформе
той версии оно будет молча отброшено: платформа рапортует успех (exit 0), а
свойство теряется — проверено экспериментально на 8.3.24. Реестр расширяется
одной строкой на свойство и служит заделом под 2.21 (8.5) и последующие: он же
подсказывает, что конструкция требует более нового формата.

Проверка 19 — LineNumberLength вне диапазона 5..9 (границы из документации 1С).

Компаратор версий числовой по компонентам: строковое сравнение дало бы
"2.9" > "2.17".

Кейсы: error-lnl-out-of-range, error-220-props-in-217. Регресс 25/25 ps1+py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:54:20 +03:00
Nick ShirokovandClaude Opus 5 10ca8ac873 fix(cf-init,cf-edit): версия формата выгрузки — параметр и наследование
Версию формата задаёт ПЛАТФОРМА выгрузки, а не режим совместимости: одна и та же
БП с режимом Version8_3_24 даёт 2.17 на платформе 8.3.24 и 2.20 на 8.3.27.

- cf-init: параметр -FormatVersion (2.17|2.20|2.21, дефолт 2.17 — читается всеми
  платформами). Конфигурация создаётся с нуля, наследовать не от чего; выводить
  версию из CompatibilityMode было бы неверно. Без параметра нельзя было собрать
  2.20-проект — в том числе для тестовых фикстур.
- cf-edit: шаблон Ext/HomePageWorkArea.xml нёс жёстко вписанный version="2.17",
  то есть в 2.20-конфигурации создавал файл чужой версии. Теперь наследует версию
  из редактируемого Configuration.xml (образец — cfe-init, который так уже умеет).

epf-init/erf-init/cfe-init не трогаем: у первых двух наследовать не от чего
(внешние объекты живут вне конфигурации), cfe-init уже наследует от базовой
конфигурации корректно.

Регресс cf-init 6/6, cf-edit 12/12 — ps1 и py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:54:20 +03:00
Nick ShirokovandClaude Opus 5 068928646d feat(meta-compile,meta-decompile): свойства формата 2.20 (платформа 8.3.27)
Дельта формата 2.17→2.20 содержит три безусловных свойства, которых компилятор
не эмитил. Все три пишутся ТОЛЬКО при формате >= 2.20 (Detect-FormatVersion),
поэтому 2.17-проекты не меняются: полная сюита зелёная, ни один существующий
снэпшот не сдвинулся.

- xr:TypeReductionMode — каждому стандартному реквизиту, после CreateOnInput.
  TransformValues, кроме Owner → Deny (правило проверено против выгрузки acc:
  9 из 9 реквизитов совпали, включая Owner).
- TypeReductionMode — измерениям регистра СВЕДЕНИЙ (у прочих семейств и у
  реквизитов/ресурсов платформа его не пишет).
- LineNumberLength — табличным частям, последним в Properties.

LineNumberLength — прикладная возможность 8.3.27 (5..9 → до 999 999 999 строк
вместо 99 999), поэтому получил полноценный DSL-ключ и описание в spec §5.2.
Его дефолт зависит НЕ от версии формата, а от режима совместимости на момент
создания ТЧ (<=8_3_26 → 5, >=8_3_27 → 9) — платформа фиксирует значение и позже
не пересчитывает, поэтому в одной конфигурации соседствуют ТЧ с 5 и 9. Отсюда
новая Detect-CompatibilityMode: читает CompatibilityMode из Configuration.xml
(префикс 64 КБ — тег лежит на ~11-12 КБ, существующим 2000 байт не хватает).

Декомпилятор: TypeReductionMode захватывается только при отклонении от правила
(компилятор выводит его сам), LineNumberLength — всегда при наличии тега:
выводить его дефолт значило бы дублировать логику компилятора с риском разойтись.

Компараторы версий числовые по компонентам — строковое сравнение неверно
("2.9" > "2.17" лексикографически).

Тест-инфра: setup-фикстуры empty-config-220 и empty-config-220-compat24
(строятся тем же cf-init), два кейса — по одному на каждую ось.

Проверка: роундтрип реального 2.20-документа БП (АвансовыйОтчет, 7 ТЧ) —
по новым тегам 0 расхождений, значения и позиции совпали; остаточный хвост
52/39 идентичен такому же на 2.17, то есть пред-существующий. Сюита 570/570
ps1, 567+3 skipped py, ps1==py. 1С-сертификация обоих кейсов на 8.3.27 ✓.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 17:53:54 +03:00
Nick ShirokovandClaude Opus 5 769b4d3dbd docs(tests): описать все поля тест-кейса в README
Таблица «Все поля кейса» отставала от раннера: не были описаны idempotent,
runtimeOnly, skipValidation, а expect ограничивался упоминанием files/
stdoutContains/stdoutNotContains — preserves и структура preRun не
документировались вовсе.

Из-за таких пробелов формат кейса приходится выяснять по коду — а это ровно
тот способ, который однажды дал 9 кейсов meta-edit с несуществующим ключом:
тесты зелёные, навык no-op, снэпшот фиксирует исходник.

Добавлено (сверено с runner.mjs и с реальными кейсами):
- idempotent, runtimeOnly, skipValidation в основную таблицу;
- таблица ключей expect + вложенная таблица preserves (file/bom/eol/encoding/
  finalNewline/noCR13) с пометкой, что preserves и эталон дополняют друг друга:
  первый следит за байтовым стилем, второй за структурой;
- формы шагов preRun (прогон навыка и writeFile).

editFile намеренно не описан — это шаг интеграционных тестов, не preRun кейса.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:35:02 +03:00
Nick ShirokovandClaude Opus 5 3b5444e69d test(runner): строгий режим снэпшотов — отсутствие эталона не проходит молча
compareSnapshot при отсутствии каталога эталона возвращал {match:true,
reason:'no snapshot (skipped)'}, причём reason никуда не выводился. Кейс без
эталона был молча зелёным, а «намеренно нет» и «эталон потерялся / не создан
при добавлении кейса» — неразличимы. README закреплял это как штатное
(«совпадает со snapshot (если есть)»).

Теперь эталон обязателен везде, кроме expectError, readonly external: и явного
opt-out. Диагностика — на месте кейса, с готовой командой; сводной статистики
не добавляем (вне контекста она ничего не сообщает).

- noSnapshot: "<причина>" — легальный пропуск. Причина обязательна: отключение
  сверки должно стоить автору формулировки, а ревьюеру быть видно в diff'е;
  осмысленность причины рантайм проверить не может. true/"" → падение.
- Нет эталона и нет opt-out → падение с рецептом (команда --update-snapshots
  либо подсказка объявить noSnapshot).
- Мёртвый эталон (noSnapshot + существующий каталог) → падение: не сверяется,
  но выглядит покрытием.
- updateSnapshot пропускает кейсы с noSnapshot — иначе --update-snapshots сам
  порождал бы противоречие. Опечатка в имени поля fail-safe: opt-out не
  сработает, кейс упадёт как «эталон отсутствует».
- Диагностика вынесена в общий snapshotErrors() — обе ветки (runCase /
  runCaseAsync) больше не дублируют логику.

Размечены 3 кейса meta-validate: навык только читает и печатает, эталон
зафиксировал бы выход preRun (meta-compile), а не проверяемого навыка.

Проверка: до разметки сюита падала ровно на этих 3 кейсах (независимое
подтверждение аудита). Негативные сценарии проверены все пять: потерянный
эталон, мёртвый эталон, noSnapshot без причины, update на opt-out кейсе
(не создаёт), update на обычном (создаёт байт-в-байт прежний).
Полная сюита 566/566 ps1; python 563 passed + 3 skipped — идентично HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 15:25:20 +03:00
Nick ShirokovandClaude Opus 5 b194834f2b test: снэпшоты для roundtrip-crlf-preserve (cf-edit, meta-edit, subsystem-edit)
Кейсы проверяли только БАЙТОВЫЙ стиль файла (expect.preserves: BOM/CRLF/
encoding/finalNewline/noCR13) и что валидатор не ругнулся. Что в CRLF-файл
записан КОРРЕКТНЫЙ XML, не проверял никто: снэпшота не было, а
compareSnapshot при отсутствии эталона молча возвращает pass.

Снэпшот ортогонален preserves — сравнивается нормализованное содержимое
(структура), preserves остаётся на байтовых характеристиках. Дублирования нет.

Регресс 12/12, 20/20, 6/6 — ps1 и py. 1С-сертификация 3/3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:40:27 +03:00
Nick ShirokovandClaude Opus 5 387f10edf0 feat(meta-validate): проверка формы MDObjectRef-ссылок
Значения xsi:type="xr:MDObjectRef" не проверялись вообще. Ошибка «тип ссылки
вместо объекта метаданных» обнаруживалась только платформой при загрузке
(«Неизвестный объект метаданных»), причём в логе, а не в коде возврата.

Проверка 17 по первому сегменту пути (переиспользован $validTypes +
$structuralOnlyTypes):
- сегмент оканчивается на Ref → Error: вида метаданных с таким именем
  не существует, ссылка гарантированно нерабочая; в тексте подсказана
  исправленная форма;
- неизвестный сегмент без Ref → Warn (список видов может быть неполон).

Ловит дефект статически, без платформы, независимо от происхождения файла.

Кейс error-mdobjectref-type-form + фикстура. Регресс 23/23 ps1+py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:27:14 +03:00
Nick ShirokovandClaude Opus 5 eb1a2ed8c1 fix(meta-edit): нормализация MDObjectRef в Owners/BasedOn/RegisterRecords/References
Та же дыра, что и в meta-compile, но здесь нормализации не было вообще —
Normalize-MDObjectRef отсутствовала как функция. set-owners "CatalogRef.Валюты"
(или modify.properties.Owners) записывал неверную ссылку молча.

- Перенесена мапа корней + Normalize-MDObjectRef (зеркало meta-compile).
- В complexPropertyMap добавлены флаги mdref/root; нормализация подключена
  в Add-/Remove-/Set-ComplexPropertyItem рядом с существующим expand.
  Покрывает Owners, RegisterRecords, BasedOn, RegisteredDocuments.
- References графы журнала документов — эмитились напрямую, тоже нормализуются.

Инструкция навыка не менялась: SKILL.md и json-dsl.md уже показывают
каноническую форму Catalog.Контрагенты.

Кейс modify-property-mdobjectref (документированный путь modify.properties).
Регресс 20/20 ps1+py, 1С-сертификация снэпшота пройдена.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:27:14 +03:00
Nick ShirokovandClaude Opus 5 66d45d3654 fix(meta-compile): нормализация MDObjectRef — CatalogRef./русская запись → Catalog.
MDObjectRef ссылается на ОБЪЕКТ метаданных (Catalog.Валюты), а не на тип ссылки.
Эмиттеры пропускали значение как есть, если в нём была точка, поэтому
"CatalogRef.Валюты" доходил до XML без изменений → при загрузке конфигурации
платформа отвечала «Неизвестный объект метаданных».

Инструкция вела в баг сама: reference/catalog.md документировал
owners: ["CatalogRef.Контрагенты"]. Тестами не ловилось — все кейсы
использовали каноническую форму.

- Normalize-MDObjectRef расширена ссылочными формами (англ. *Ref + рус. *Ссылка);
  вида метаданных, оканчивающегося на Ref, не существует → схлопывание однозначно.
  В ТИПАХ реквизитов запись CatalogRef.X верна — там мапа не применяется.
- Добавлен параметр defaultRoot (голое имя без точки), инлайн-подстановка
  "Catalog.$ownerRef" в owners убрана — логика теперь в одном месте.
- Нормализация применена в 4 местах, где её не было: owners, basedOn,
  registerRecords, baseCalculationTypes.
- Кейс catalog-inputbystring-datalock переведён на неканонический вход:
  снэпшот не изменился ни на байт — прямое доказательство нормализации.

Регресс 73/73 ps1+py, полная сюита 566/566. Живая проверка на 8.3.27:
подчинённый справочник с owners CatalogRef./СправочникСсылка. грузится чисто.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:26:55 +03:00
Nick ShirokovandClaude Opus 4.8 e01688e764 fix(form-add): идемпотентная регистрация <Form>/<Template> в ChildObjects
form-add и template-add вставляли запись в ChildObjects безусловно.
Если форма/макет уже зарегистрированы (например, form-compile
регистрирует <Form>, не создавая файл метаданных, а затем вызывается
form-add) — возникал дубль <Form>/<Template>, ломавший валидацию.

Приведено к идемпотентной модели, уже применённой в form-compile и
meta-compile: перед вставкой ищем существующую запись по имени; при
наличии — пропускаем и печатаем "Already registered ... (skipped
duplicate)". Зеркально в ps1 и py, регрессионный тест-кейс.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:28:16 +03:00
Nick ShirokovandClaude Opus 4.8 b892379202 fix(skd-compile): авто-Auto для осей таблицы, диаграмм и объектных групп
Оси таблицы (columns/rows), точки/серии диаграмм и объектные группировки без
явного selection получали пустой пивот молча — ресурсы не попадали в ячейки
пересечения. Теперь при отсутствии ключа selection/order эмитится
SelectedItemAuto/OrderItemAuto (как строковый shorthand и как ручное добавление
оси в Конфигураторе). Пустой [] уважается как «явно ничего».

skd-decompile теперь эмитит [] для отсутствующих selection/order на осях,
группах и диаграммах — decompile→compile round-trip остаётся бит-в-бит (иначе
compile впаял бы Auto на боевых узлах без выбора, напр. ветках use=false).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:05:22 +03:00
Nick ShirokovandClaude Opus 4.8 8826a88427 fix(web-test): заголовок формы в состоянии + formHasField по массиву fields
Два задокументированных ассерта бросали ВСЕГДА, то есть были мертвы:

- formTitle читал state.title, которого не заполнял никто: getFormStateScript
  собирал форму без заголовка. Единственным носителем оставалась панель
  открытых окон (activeTab), а она отключается в настройках 1С — на такой базе
  заголовок был недоступен ничем.
- formHasField читал state.fields[name], хотя fields — массив объектов
  {name, value, …}. На массиве это всегда undefined.

getFormState теперь отдаёт title. Берётся он из шапки самой формы: заголовок
лежит в атрибуте (title у .toplineBoxTitle, data-title у родителя), сам элемент
пустой — поэтому поиском по тексту он и не находился.

Выбор шапки — не «первая видимая»: при открытом всплывающем окне видимы ДВЕ,
родителя и окна, и наивное правило отдавало заголовок родителя — правдоподобный
неверный ответ, при котором тест «окно выбора открылось» зеленел бы по
документу. Приоритет взят тот же, что уже отлажен для крестика закрытия в
closeCrossScript: плавающее окно ps<N> с наибольшим индексом → собственная шапка
формы → и только потом панель открытых окон. Привязка к id, а не к тексту — не
ломается на другой локали. Панель осталась последним звеном: она отключаема, а
при всплывающем окне ещё и показывает родителя.

Диагностика раннера (resetState) тоже переведена на title с прежним activeTab
как запасным.

formHasField ищет по массиву и перечисляет доступные имена в ошибке (раньше
Object.keys по массиву давал индексы). formTitle отличает «заголовок недоступен»
(title === null) от несовпадения.

Почему не поймали раньше: из 12 ассертов сюита вызывала 8, и оба сломанных были
среди четырёх невызываемых. Теперь все четыре задействованы на настоящем выводе
getFormState — formTitle/formHasField/noErrors в 12-formstate (включая случай
всплывающего окна), tableRowCount в 09-filter. Отдельного юнит-теста намеренно
нет: состояние для него пришлось бы писать руками, а именно неверное
представление о форме состояния и породило оба дефекта.

Доки приведены к массиву: примеры вида s.fields['X']?.value в regress.md и в
спеке заменены на fields.find(f => f.name === 'X').

Проверено: заголовок на списке, форме элемента и всплывающем окне; каскад
разведён по значениям (шапка выигрывает у панели, при пустой шапке — откат);
позитив и негатив всех четырёх ассертов на реальном состоянии формы; полный
регресс 29/29 до и после, file/name/status идентичны.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:51:12 +03:00
Nick ShirokovandClaude Opus 4.8 9b65dccd8a fix(web-test): не отдавать управление, пока список ещё ищет
filterList отчитывался успехом, пока динамический список ещё выполнял поиск:
readTable возвращал предыдущие строки, а следующий клик по строке попадал в
чужой объект. Отличить такой результат от верного нельзя — сценарий проходит
зелёным по не тому документу.

Дыра оказалась не в filterList, а в общем ожидании. waitForStable следил только
за .loadingImage/.waitCurtain/.progressBar и счётчиком полей ввода — ни то, ни
другое не меняется при перерисовке строк списка. Замер на реальной базе: за весь
поиск старый признак isLoading не сработал НИ РАЗУ.

При этом 1С всё это время показывает над списком информбар «Поиск...» — тот же
.stateWindowSupportSurface, который движок уже отдаёт в errors.stateText. Читать
умели, ждать — нет.

waitForStable теперь считает видимый маркер занятости признаком «не готово»:
счётчик стабильности сбрасывается, дедлайн продлевается, пока маркер виден, но
не дольше BUSY_MAX_WAIT (60 с) — реальный поиск на боевом списке идёт десятки
секунд, а зависшая операция всё равно завершает ожидание.

Маркеры сопоставляются ПО ТЕКСТУ (Поиск/Ожид/Searching/Please wait), а не по
факту наличия информбара: тот же носитель несёт терминальные сообщения отчётов
(«Отчет не сформирован», «Не установлено значение параметра»), и ожидание их
исчезновения вешало бы каждый отчёт до таймаута.

Радиус общий, а не точечный в filterList: маркер — индикатор длительной операции
вообще, тот же класс гонки достижим из clickElement и openCommand. Частное
лечение для кнопок (CDP-монитор в click-form) уже есть; второй костыль сделал бы
третий неизбежным. CDP-монитор как гейт не годится отдельно: он считает
готовностью паузу 300 мс без запросов, а при фоновой операции с периодическим
опросом такие паузы штатны.

Проверено на реальной базе: текст информбара — «Поиск...», виден t=6.7..13.2 с,
опрос, идентичный гейтовому, увидел занятость дважды (isLoading — ноль раз);
полный регресс 29/29 до и после, file/name/status идентичны, суммарное время
879.9 → 859.0 с, отчёты не зависли.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:24:01 +03:00
Nick ShirokovandClaude Opus 4.8 6256c48c05 docs(web-test): в regress.md оставить поведение, убрать механику резолва
Инструкция навыка описывает использование: конфиг и хуки берутся из корня
сьюта при любом переданном пути, разные сьюты в одном прогоне отвергаются.
Маркеры подъёма и ограничители (.git / .v8-project.json / cwd) — контракт
реализации, их место в спеке; читатель инструкции о них не спросит.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 15:01:45 +03:00
Nick ShirokovandClaude Opus 4.8 90d8263a05 fix(web-test): резолвить корень сьюта подъёмом вверх, а не от переданного пути
Конфиг и хуки резолвились строго от каталога первого позиционного пути, поэтому
запуск подкаталога сьюта был невозможен: `test tests/app/00-smoke/` падал с
«No URL provided and no webtest.config.mjs found» — хотя спека прямо обещает
запуск подкаталога («Фильтр по пути с CLI»).

Опаснее отказа по URL были два молчаливых следствия: при `--url=` прогон
подкаталога терял `_hooks.mjs` и ехал по неподготовленному стенду без единого
предупреждения, а `_allure/` не находился. Плюс `file:` в отчёте считался от
переданного пути, из-за чего один и тот же тест получал разный ID в зависимости
от способа запуска и рвал историю Allure/JUnit.

Введён корень сьюта: подъём от каталога пути до первого `webtest.config.mjs`
ИЛИ `_hooks.mjs` (конфиг необязателен — сьют только с хуками иначе снова терял
бы подготовку), с ограничением подъёма каталогом `.git`/`.v8-project.json`, а
при их отсутствии — cwd. Граница ничего не выбирает, только останавливает, так
что ложная граница даёт «корень не найден», а не чужой корень. От найденного
корня берутся все пять ролей: конфиг, хуки, каталог отчёта, пути в отчёте,
`_allure/`.

Попутно: пути из разных сьютов в одном прогоне теперь отвергаются (раньше
молча выигрывал первый путь, и сьют B ехал по подготовке сьюта A); найденный
корень печатается в шапке; отсутствие корня — предупреждение в stderr;
диагностика говорит про корень сьюта, а не только про URL.

Проверено: 12/12 офлайн-кейсов резолвера; полный регресс 29/29 до и после —
`file`/`name`/`status` идентичны; `_suite-root/nested/` (сценарий, который
падал) проходит с подхваченными конфигом и хуками; `_hang/` 6/6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 14:41:16 +03:00
Nick ShirokovandClaude Opus 4.8 9c7010a49e test(platform-dump-modes): покрыть db-dump-xml Full/Changes/UpdateInfo на реальной 1С
Постусловие непустого каталога валидировалось только на Full/Partial. Новый
1cv8-тест гоняет Changes (в существующий дамп) и UpdateInfo (в свежий каталог) →
подтверждает, что режимы дают реальный выход и постусловие не даёт ложного
падения. UpdateInfo проверяется ассертом наличия ConfigDumpInfo.xml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:31:45 +03:00
Nick ShirokovandClaude Opus 4.8 094a8bea81 fix(db-*,epf-*): точная редактура секретов по значению вместо regex по токену
Прежняя маскировка (^/N|/P по токену) на *nix цепляла путь, начинающийся с
заглавной /N или /P (напр. /Projects, /Numbers) — косметическая пере-маскировка
в строке Running. Заменено на редактуру конкретных значений (пароль/пользователь)
через литеральную замену: секрет скрывается везде, где встречается, а похожие на
флаг пути не трогаются. 11 навыков, оба порта.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:28:27 +03:00
Nick ShirokovandClaude Opus 4.8 06485d216b fix(epf-dump): постусловие выходного каталога + маскировка учётных данных
Пропущенный при разборе #49-52 навык того же класса: epf-dump разбирает EPF/ERF
через платформу в каталог XML. Успех определялся только по коду возврата (ложный
успех при пустом выходе), а строка Running светила /P<пароль> (1cv8) и --password=
(ibcmd). Добавлены postcondition непустого OutputDir и маскировка, обе ветки,
оба порта. Покрывает и erf-dump (общий скрипт).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:15:34 +03:00
Nick ShirokovandClaude Opus 4.8 00cd0f3f5f fix(db-load,db-update): диагностика аномального кода — только факты, без догадки о причине
Убрана спекулятивная гипотеза причины (headless/GUI/лицензия) из сообщения:
краш сигналом/exception может быть вызван чем угодно, а зашитая догадка
заякоривает модель-координатора на неверном диагнозе. Оставлены только факты
(сигнал/exception-код, признак аномального завершения), следствие (ИБ может
быть несогласованна) и нейтральное действие (verify before retrying).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 20:02:42 +03:00
Nick ShirokovandClaude Opus 4.8 28f3410463 test(build-cfe): актуализировать под cfe-patch-method v2 (source-aware)
cfe-patch-method стал source-aware: читает оригинал метода из -ConfigPath.
Тест не передавал -ConfigPath и опирался на пустой ObjectModule источника →
шаг перехвата падал «Не указан -ConfigPath». Добавлен seed-шаг с процедурой
ПриЗаписи в исходный модуль + -ConfigPath в вызов перехватчика.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:50:06 +03:00
Nick ShirokovandClaude Opus 4.8 d3fe8eb010 fix(db-*,epf-build): маскировать учётные данные в строке Running
Навыки печатали полную командную строку платформы, включая /P<пароль> (1cv8)
и --password= (ibcmd), в диагностику. Добавлен per-token маскер (/N, /P,
--user=, --password= → ***); привязка к началу токена не трогает пути.
Оба порта, обе ветки движка, все 9 навыков с параметрами подключения.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 19:10:04 +03:00
Nick ShirokovandClaude Opus 4.8 b8e141e7ce test(skills): регресс ложного успеха через fake-платформу + гейт runtimeOnly
Кейсы с .cmd-заглушкой платформы (Start-Process исполняет .cmd) проверяют, что
db-create/db-run/db-dump-cf не рапортуют успех, когда платформа вышла с 0/умерла
без артефакта. Гейт runtimeOnly пропускает кейс на несовместимом порту (py
list-exec не запускает .cmd) — гоняются под powershell на Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:52:25 +03:00
Nick ShirokovandClaude Opus 4.8 4b6ffc595e fix(db-load,db-update): расшифровывать аномальный код завершения платформы
Мутирующие навыки не производят одиночный артефакт, но при крахе платформы
(нет GUI-сессии/лицензии) возвращали голый код вроде -11. Добавлен аннотатор:
POSIX-сигнал (напр. -11 → SIGSEGV) и Windows exception-код (напр. 0xC0000005) →
внятное сообщение с предупреждением о возможной несогласованности ИБ. Без
ожидания фоновых процессов и без ps-скрейпинга.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:52:25 +03:00
Nick ShirokovandClaude Opus 4.8 2804355fba fix(db-dump,epf-build): подтверждать выходной артефакт перед рапортом об успехе
Тот же класс ложного успеха, что и в db-create: exit 0 без реального результата.
Добавлен postcondition на выходной артефакт — файл ненулевого размера для
db-dump-cf/db-dump-dt/epf-build (покрывает и erf-build через общий скрипт),
непустой каталог для db-dump-xml. Обе ветки движка (1cv8 и ibcmd).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:52:06 +03:00
Nick ShirokovandClaude Opus 4.8 b3f8e832a7 fix(db-create): подтверждать создание 1Cv8.1CD перед рапортом об успехе
Код возврата launcher-а сам по себе не доказывает, что файловая ИБ создана:
в неблагоприятной среде платформа может вернуть 0, не создав ничего. Добавлен
postcondition — для файловой ИБ проверяется наличие ненулевого 1Cv8.1CD (обе
ветки: 1cv8 и ibcmd); при его отсутствии — честная ошибка и ненулевой код.
Серверная ИБ не проверяется (нет файла для stat).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:52:06 +03:00
Nick ShirokovandClaude Opus 4.8 e85fc538f6 fix(db-run): проверять ранний выход процесса, возвращать PID, маскировать секреты
Раньше db-run безусловно печатал «launched» сразу после запуска, не отличая
успешный фоновый старт от мгновенного падения (нет дисплея/лицензии). Теперь
короткое контрольное окно ловит ранний выход → ненулевой код без «launched»,
иначе печатается PID. Строка Running маскирует /N и /P (не светить пароль);
маскировка привязана к границе токена, чтобы не портить путь с сегментом /N|/P.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 18:51:50 +03:00
Nick ShirokovandClaude Opus 4.8 89a0081403 fix(ci): вычислять RUNTIME_REQUIREMENTS в bash, а не в env-тернаре (#48)
Прошлый коммit сломал build-ports.yml: GHA-выражение с бэктиками/двоеточием/
кавычками в env: ломало парсинг workflow (run failed, 0s). Переношу вычисление
строки в bash-шаг по matrix.runtime — надёжно к спецсимволам.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:55:41 +03:00
Nick ShirokovandClaude Opus 4.8 0898675169 docs(build): requirements.txt + py-only зависимости в порт-README (#48)
Добавлен requirements.txt (lxml/Pillow/psutil) — единый pip-манифест py-рантайма.
build-ports.yml копирует его в build только для python-сборок (в PS-порты не попадает)
+ добавлен в paths-триггер. Порт-README: блок «Требования» стал runtime-условным
(плейсхолдер {{RUNTIME_REQUIREMENTS}}) — py-вариант даёт `pip install -r requirements.txt`,
PS-вариант больше не упоминает python-deps. Заявленный минимум исправлен 3.10+ → 3.9+
(все скрипты компилируются на 3.9.6, регресс зелёный). Main README: команда установки
в py-секцию.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:53:36 +03:00
Nick ShirokovandClaude Opus 4.8 bf86210816 docs(meta-compile): пометить ConfigDumpInfo.xml как платформенный (#45)
ConfigDumpInfo.xml — служебный файл версий выгруженных объектов, управляемый
платформой (для инкрементальной ВЫГРУЗКИ, db-dump-xml Changes). При загрузке не
используется; некорректные записи в нём только помешали бы. configVersion
вычисляет только платформа — руками не сгенерировать. meta-compile его намеренно
не трогает: посылка #45 неверна.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 17:21:32 +03:00
Nick ShirokovandClaude Opus 4.8 57e99d144e fix(skills): round-trip сохранение EOL/BOM/encoding в py edit-портах (#44/#46/#47)
Py-порты (lxml) при точечном редактировании существующего 1С-XML переписывали
весь файл: CRLF→LF, encoding="UTF-8"→"utf-8", добавляли финальный перенос,
плодили литерал &#13; (сериализация \r из tail'ов). Результат — широкий шумовой
diff и скрытый лишний текст-узел при exit 0 и зелёной валидации. PS1-порты
(XmlDocument) багу не подвержены — эталон.

Фикс во всех 13 round-trip re-serialize py-навыках: перед записью детектится
стиль существующего файла (BOM / EOL / регистр encoding / финальный перенос) и
восстанавливается при сохранении; переносы канонизируются к LF (убирает &#13;),
затем приводятся к EOL источника. Новый файл (путь не существует) → прежнее
поведение, снапшоты не двигаются. Навыки: cf-edit, meta-edit, meta-remove,
interface-edit, subsystem-edit, skd-edit, form-edit, form-add, help-add,
template-add, template-remove, form-remove, cfe-borrow. Версии py+ps1 подняты
синхронно.

Harness (tests/skills/runner.mjs): снята маска &#13; в normalizeXmlContent
(порты её больше не порождают → гвардия ловит регресс); добавлен expect.preserves
— raw-байтовая проверка BOM/EOL/encoding/финального переноса/отсутствия &#13;
в обход нормализации. Регрессионные round-trip кейсы на CRLF+BOM+UTF-8 фикстурах
для cf-edit/meta-edit/subsystem-edit.

Верификация: py 556/556, ps1 556/556; платформа 1С 8.3.24 (verify-snapshots)
cf-edit 12/12, meta-edit/subsystem-edit round-trip загружаются; негатив-тест
подтверждает, что harness ловит дефект на старом коде.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 16:53:00 +03:00
Nick ShirokovandClaude Opus 4.8 fbdf07e18a fix(meta-edit): modify-property Type — структурный дескриптор типа, guard от порчи
Корневой modify-property Type у ПВХ/Константы расплющивал структурный <Type>
(<v8:Type> + квалификаторы) в скалярный текст, а meta-validate это пропускал.

meta-edit (v1.21): modify-property Type перестраивает дескриптор через готовый
build_value_type_xml (составной тип, квалификаторы, ref-типы); прочие структурные
свойства с дочерними узлами → ошибка до записи файла вместо тихой порчи.

meta-validate (v1.10): корневой <Type> со скалярным текстом без <v8:Type>/<v8:TypeSet>
теперь ошибка (был false negative).

Порты PS1/PY синхронны. Регрессионные кейсы: modify-property-type-pvh (структурный
Type + ref), error-scalar-root-type (детект порчи).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 15:47:59 +03:00
Nick ShirokovandClaude Opus 4.8 6318018bc1 feat(cfe-patch-method): прозрачность пустых строк и комментариев при ресинке (v2.5)
Косметика вендора (пустые строки, строки-комментарии) больше не ломает классификацию:
- пустая строка между якорем и уже-перенесённым кодом → раньше ДУБЛЬ, теперь ПЕРЕНЕСЕНО;
- пустая/комментарий у якоря → раньше ложный конфликт, теперь переякоривание.

Три шага разведены:
- размещение якоря — сначала точно (комментарии/пустые включены, держит позицию
  вставки относительно стабильного комментария), затем fallback по значимым строкам;
- поглощение — по значимым строкам (пустые/комментарии перешагиваем); вставку из
  одних комментариев/пустых не поглощаем;
- вывод тела — всегда v2 дословно, все комментарии/пустые нового оригинала сохраняются.

Пограничный случай (комментарий разработчика у поглощённого кода): значимый код
поглощаем, осиротевший комментарий — строкой ⚠ в отчёте, не роняя в конфликт.

Новые helper: Test-Significant/Get-SignificantProjection (+ py). Верхняя сверка
АКТУАЛЕН остаётся точной (любой diff → перепись тела в v2). Зеркально ps1↔py,
+3 кейса (blankskew/comment-stable/orphan-comment), 22/22 на обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 14:35:56 +03:00
Nick ShirokovandClaude Opus 4.8 912128d24f feat(cfe-patch-method): распознавание неактуальных правок при актуализации (v2.4)
Правка, перенесённая вендором в основную конфигурацию, больше не дублируется
и не уходит в ложный конфликт:
- вставка, чей код уже в новом оригинале → раньше ДУБЛЬ, теперь снимается;
- удаление, чей блок уже вырезан → раньше ложный КОНФЛИКТ, теперь снимается.

Обесценивание — свойство операции. Новый статус метода ПЕРЕНЕСЕНО В ОСНОВНУЮ,
когда поглощены все правки (перехватчик можно удалить); при частичном —
АКТУАЛИЗИРОВАН со счётчиками «правок сохранено: N, перенесено в основную: M».
Существующий счётчик «перенесено правок» переименован в «правок сохранено»,
чтобы «перенесено» осталось за поглощением базой. -Check не роняет exit,
если единственное расхождение — перенесённые правки.

Детекция на существующих примитивах (Find-UniqueRun + новые Test-RunAt/
Test-DeleteAbsorbed), только по точному совпадению. Зеркально ps1↔py,
+3 кейса (transferred-insert/delete/partial), 19/19 на обоих рантаймах.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 13:56:34 +03:00
Nick ShirokovandClaude Opus 4.8 4b6ced87c4 feat(cfe-patch-method): conflict.md — обрамление вставки якорями + нумерация (v2.3)
- непереносимый блок показывается в контексте (строки-до/#Вставка/строки-после),
  вместо раздельных списков «после:/перед:» + «Блок:»
- нумерация конфликтов: ### Конфликт №N в conflict.md и // [РЕСИНК-КОНФЛИКТ №N]
  над припаркованным блоком в .bsl — сопоставление один-к-одному при нескольких
  конфликтах в одном методе
- локатор в модуле — по метке №N (grep-стабильно), без номеров строк
- зеркально в .py, снэпшот resync-conflict обновлён

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 12:54:55 +03:00
Nick ShirokovandClaude Opus 4.8 1f8d611a00 docs(cfe-patch-method): SKILL.md — актуализация description и терминологии
- description: «Генерация и актуализация…», без потери триггер-слов
- Типы перехвата: применимость к процедурам/функциям вынесена в колонку
- Две секции актуализации слиты в одну, телеграф переписан ровным тоном
- «КФ» → «конфигурация-источник» (единый термин)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 22:00:18 +03:00
Nick ShirokovandClaude Opus 4.8 f89cdf9eff docs(cfe-patch-method): SKILL.md — убрать дублирующее вывод, детализировать маркеры
Принцип: в SKILL.md — только то, чего модель НЕ видит в выводе/результате.

- Убрано: «контекст/сигнатура/обрамление определяются автоматически» (параметров
  для них нет, результат виден); секция «Что переносится из оригинала» целиком
  (всё видно в сгенерированном коде); устаревшие имена файлов воркспейса.
- Добавлено: раздел «Маркеры #Вставка/#Удаление» — синтаксис и семантика, которые
  модель пишет сама и из копии тела не считает (удаляемые строки остаются между
  маркерами, 0-я колонка, unmarked = дословно оригинал = суть контроля, пример).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:17:18 +03:00
Nick ShirokovandClaude Opus 4.8 dcce32faed refactor(cfe-patch-method): эргономика вывода по итогам dogfood
По результатам прогона субагентом реального сценария (адаптация метода Бухгалтерии
+ рефакторинг оригинала → конфликт):

- SKILL.md: убрана протёкшая и УСТАРЕВШАЯ реализация — раздел про merge-воркспейс
  называл файлы merged.bsl/diff.txt, которых больше нет; раздел «Проверка/актуализация
  пачкой» дублировал рантайм-вывод. Оставлено только решенческое (режимы, область,
  зона ответственности, зачем проактивно).
- conflict.md: к каждой неразмещённой вставке добавлена привязка к якорю (после/перед из
  local) и подсказка «куда переносить» (якорь вынесен/отрефакторен → ищи в диффе новый
  вызов, размещай пост-обработкой) + напоминание сохранить BOM. Диагноз дрейфа якоря
  теперь виден, не нужно грепать вручную.
- Согласование числительных в итог-строках (было «1 конфликтов»).

Паритет ps1<->py, 16 кейсов зелёные в обоих.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 21:07:41 +03:00
Nick ShirokovandClaude Opus 4.8 9aaa3a1c1e feat(cfe-patch-method): батч -Check/-Actualize контролируемых методов (v2.2)
После обновления КФ контролируемые методы (&ИзменениеИКонтроль) молча уезжают в
рассинхрон — платформа при загрузке не ругается, ошибка лишь в рантайме. Добавлены
два явных режима по всему расширению (или -ModulePath/-MethodName для сужения):

- -Check — отчёт: какие методы дрейфнули (ДРЕЙФ/КОНФЛИКТ/МЕТОД-ИСЧЕЗ), актуальные
  числом; ничего не пишет; exit 1 при наличии дрейфа.
- -Actualize — чинит пачкой: авто-перенос + merge-воркспейс на конфликтах.

Одиночный ресинк вынесен в общую функцию resync_one (report_only), одиночный путь и
батч используют её. Зона ответственности узкая — только тело &ИзменениеИКонтроль.

Merge-воркспейс переработан: тонкий index.md (список конфликтов + пути к .bsl расширения)
+ подпапка на метод (conflict.md с блоком/диффом + base/local/remote), без общей портянки.

cfe-validate: крошка-указатель [INFO] при наличии контролируемых методов -> /cfe-patch-method -Check.

Тесты: +check-clean/check-drift/actualize-batch. 16 кейсов, оба рантайма зелёные,
байтовый паритет ps1<->py. verify-snapshots (реальная 1С) Windows ps+py — 16/16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 20:37:58 +03:00
Nick ShirokovandClaude Opus 4.8 8399f13e5a fix(tests): verify-snapshots preRun writeFile создаёт родительские папки
Реплей preRun-шага writeFile в verify-snapshots.mjs писал файл без mkdir -p
(в runner.mjs фикс уже был). Кейсы, пишущие в ext/ (cfe-borrow общего модуля
не создаёт ext/.../Ext/), падали с ENOENT. Зеркалит фикс из runner.mjs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:57:35 +03:00
Nick ShirokovandClaude Opus 4.8 39fc1b977d feat(cfe-patch-method): устойчивый якорь ресинка, прозрачность, merge-имена файлов
Упрочнение актуализации &ИзменениеИКонтроль (v2.1):

- Якорь вставки теперь двусторонний (контекст до+после, окно 3) с расширением:
  Тир A — уникальная смежная пара; Тир B — одиночная уникальность before/after.
  Срезает ложные конфликты, когда строка перед вставкой generic/повторяется
  (пустая, КонецЦикла;, КонецЕсли; и т.п.), сохраняя безопасность (не уверены → конфликт).
- Прозрачность: на [АКТУАЛИЗИРОВАН] и ЧАСТИЧНО печатается сводка перенесённого.
- Файлы-версии переименованы в конвенцию git-mergetool: base/local/remote/merged
  (+diff base->remote) вместо v1/v2/current; merged.bsl добавлен. Комментарий в
  модуле и SKILL.md обновлены.

Тесты: +resync-reanchor (generic-строка -> авто), resync-conflict переделан на
настоящий конфликт (окружающий блок исчез). 13 кейсов, оба рантайма зелёные,
байтовый паритет ps1<->py включая merge-воркспейс.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 17:16:11 +03:00
Nick ShirokovandClaude Opus 4.8 a125464caa feat(cfe-patch-method): source-aware перехватчик + ресинк ИзменениеИКонтроль
Навык переписан в v2.0: вместо шаблона-заглушки читает оригинал метода из
конфигурации-источника и генерирует корректный каркас.

Генерация:
- новый -ConfigPath (опционален, если ModulePath — путь к файлу .bsl модуля);
- наследование директивы контекста, полной сигнатуры, обрамляющих #Если и
  #Область (в исходном порядке; регион переиспользуется, если уже есть);
- тип Instead (&Вместо с ПродолжитьВызов); гвард: Before/After только для процедур;
- ModAndControl копирует всё тело оригинала;
- воздух (пустые строки) вокруг структурных границ и между методами;
- имя с суффиксом типа только при коллизии;
- убраны -Context/-IsFunction (выводятся из оригинала).

Актуализация (повторный ModAndControl): предок восстанавливается из маркеров,
однозначные правки #Вставка/#Удаление переносятся авто, спорные — [РЕСИНК-КОНФЛИКТ]
плюс файлы-версии v1/v2/current/diff. Статусы АКТУАЛЕН/АКТУАЛИЗИРОВАН/ЧАСТИЧНО.

Паритет ps1<->py (raw-кириллица в .py). Тесты: 12 кейсов, оба рантайма зелёные.
runner.mjs: шаг writeFile теперь делает mkdir -p.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:31:53 +03:00
Nick ShirokovandClaude Opus 4.8 fc2323f321 feat(web-test): всплывающие (popup) группы — behavior, состояние, клик
Popup-группа надёжно отличается от сворачиваемой по DOM-маркеру панели
<base>#panel_div (+ #CloseBtn). В getFormState().groups она помечается
behavior:'popup', а её collapsed берётся из display панели (закрыта =
collapsed:true), не из инлайн-сиблинга (у popup содержимое в отдельном
слое, а не под mainGroup).

clickElement по заголовку popup и открывает, и закрывает — тот же
словарь {expand}/{toggle}, что и у сворачиваемых. После открытия
содержимое панели становится читаемым в getFormState (fields/
hyperlinks/texts). Тест 25-decoration-form покрывает popup; SKILL
дополнен. Полный регресс 29 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:18:18 +03:00
Nick ShirokovandClaude Opus 4.8 bd86ece90e feat(web-test): чтение и раскрытие сворачиваемых групп формы
getFormState().groups → [{name, title, collapsed}] для сворачиваемых
групп (оба варианта ControlRepresentation: заголовок-гиперссылка и
картинка-каретка #titleBtn). Обычные несворачиваемые группы не
попадают. Состояние — по display первого контент-сиблинга за #title_div
(переживает свободные элементы между группами: при обходе Form.xml дети
группы идут до следующего сиблинга).

clickElement(title, {expand}/{expand:false}/{toggle}) раскрывает/
сворачивает группу — единый словарь с грид-узлами/деревьями, клик по
#titleBtn (вариант «картинка») или заголовку-гиперссылке. Новый
kind:'formGroup' в findClickTargetScript + хендлер click-group.mjs.

Фикстура СтраницаНастроек расширена вариантами A/B + негатив (обычная
группа) + стресс-привязка (свободный элемент между группами); тест
25-decoration-form покрывает чтение и expand/collapse/toggle. Полный
регресс 29 passed.

Popup-группы: содержимое в отдельном слое, состояние пока не читается
надёжно (follow-up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:56:55 +03:00
Nick ShirokovandClaude Opus 4.8 86cddc8ec3 docs(form-compile): задокументировать controlRepresentation для свёрнутых групп
Свойство «Отображение управления» (TitleHyperlink/Picture) уже эмитилось
через generic-скаляры, но не было в таблице свойств группы SKILL.md;
в спеке значилось неверное `Picture | Text`. Добавлен фокус-кейс с обоими
литералами, снапшот верифицирован загрузкой в 1С 8.3.24.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 12:21:12 +03:00
Nick ShirokovandClaude Opus 4.8 9856fd5180 fix(web-test): детектировать форму-декорацию без полей ввода
Страницы настроек (напр. «Администрирование → Интернет-поддержка и
сервисы») собраны из гиперссылок, frameButton и сворачиваемых групп —
без единого input.editInput / textarea / a.press. detectForm/detectForms
считали такую форму отсутствующей → getFormState = {form:null,
formCount:0}, навык её не видел.

Расширен союзный селектор детекции (.staticTextHyper/.frameButton/
.checkbox/.radio/.tumblerItem/.grid). detectForm двухуровневый: обычные
формы выбираются по редактируемым контролам (поведение не меняется), по
расширенному счёту — только когда у формы нет ни одного поля ввода.
form0 (рабочий стол) по-прежнему исключён фильтром n>0.

Регресс: обработка-фикстура СтраницаНастроек (форма без командной панели:
гиперссылка + сворачиваемая группа) в подсистеме Администрирование +
тест 25-decoration-form. Полный набор 29 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 22:03:13 +03:00
Nick ShirokovandClaude Opus 4.8 37f39a5022 docs(mxl): внести DSL-спеку в навык, убрать ссылку на docs/ (#40)
SKILL.md навыков ссылались на docs/mxl-dsl-spec.md, которого нет
в порт-ветках — агент искал документацию внутри папки навыка.

- mxl-compile: спека внесена как reference/dsl-spec.md (self-contained),
  SKILL.md ссылается на неё относительно навыка + добавлен empty в правила
- mxl-decompile: спеку не тянет (нужна только для авторинга JSON),
  убраны описания внутренностей скрипта — оставлено «как применять»

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:42:22 +03:00
Nick ShirokovandClaude Opus 4.8 637dd6d0eb test(hooks): портируемый REPO/CORPUS в run.mjs
REPO выводится из расположения самого файла (import.meta.url), а не хардкодом
'C:/WS/tasks/skills'. На POSIX 'C:/…'-строка не абсолютна, и resolve(cwd, path) в
support-guard/skill-suggester склеивал её в удвоенный несуществующий путь — из-за чего
8 кейсов ложно «падали» на Mac (guard/suggester не находили фикстуры). Production-логика
корректна; чинится только тестовая обвязка. CORPUS теперь env-переопределяем
(CC_1C_CFSRC), дефолт прежний; acc/erp-кейсы под existsSync → SKIP без корпуса.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:12:42 +03:00
Nick ShirokovandClaude Opus 4.8 ddc1641176 fix(support-guard): не блокировать автономные внешние обработки/отчёты (#39)
При поиске корня конфигурации guard поднимался по дереву вверх и «проскакивал»
собственный корень автономной внешней обработки/отчёта (ExternalDataProcessor /
ExternalReport), лежащей внутри дерева выгрузки конфигурации. Если у охватывающей
конфигурации выключена возможность изменения (G=1), внешний объект ложно
блокировался как «объект типовой конфигурации на поддержке», а info-навыки
выводили нерелевантную строку «Поддержка: конфигурация read-only».

Теперь climb останавливается на границе автономного объекта: если целевой файл или
встреченный по пути <каталог>.xml имеет корень ExternalDataProcessor/ExternalReport,
подъём прекращается и объект не привязывается к конфигурации. Корень внешнего объекта
всегда глубже Configuration.xml, поэтому встречается первым — регрессии для обычных
объектов конфигурации нет.

Синхронно во всех копиях guard-а (навыки автономны): хук support-state.mjs
(decideSupport + findConfigRoot), 16 мутаторов (Assert-EditAllowed), 5 info-навыков
и meta-info (Get-SupportStatusForPath / Get-ObjectSupportStatus) — ps1 и py. Для
info-навыков строка «Поддержка:» для внешнего объекта опускается.

Тесты: hooks/test/run.mjs — секция внешней границы (G=1 + встроенная EPF);
tests/skills — кейсы mxl-compile (guard пропускает) и mxl-info (строка опущена).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 19:46:54 +03:00
Nick ShirokovandClaude Opus 4.8 e7b5df4d50 docs(web-test): убрать из инструкции описание фикса вместо использования
Добавленный абзац сообщал, что имя колонки из readTable годится для клика и
заполнения. Читатель инструкции в обратном и не сомневался — это описание нашей
правки, а не способа пользоваться навыком. Нумерация «Субконто Дт 1/2/3» видна в
выводе readTable и без предупреждения.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:14:27 +03:00
Nick ShirokovandClaude Opus 4.8 7696a8b3ee fix(web-test): единая модель колонок грида — colindex первым, геометрия запасной
Резолверов колонок было пять, все независимые, и каждый ломался по-своему:
readTable (геометрия X + Y-подряды), clickElement (геометрия X, без Y),
поиск строки {кол: знач} (геометрия X, без Y и fixed-гарда), filterList
(порядковый индекс шапки), fillTableRow (colindex — единственный целый).

Механика поломки (снята живьём на списке задач ERP): шапка «Исполнитель»
широкая (x 1085..1515) и накрывает «Срок» (1085..1251) и «Выполнена»
(1251..1515). Ячейка «Исполнитель» имеет центр 1300 → приписывается к группе
«Выполнена» → та получает лишний под-ряд → срабатывает эвристика «объединённая
шапка» → фантомные «Выполнена 1/2», а значения соседей склеиваются через ' / '.

Теперь COLUMN_MODEL_FN (dom/_shared.mjs) — единственный источник правды:
buildColumnModel / columnForCell / cellForColumn / resolveColumnByName. Идентичность
колонки — colindex (собственный id колонки в 1С, есть и на шапке, и на ячейке);
геометрия работает только там, где своей шапки у ячейки нет — под-ряды
объединённой шапки («Субконто Дт» над тремя ячейками). Путь записи пришёл к этому
решению раньше (grid-edit.mjs: «reliable across merged headers») — остальные
выровнены по нему.

Следствие: имя колонки из readTable теперь годится для клика/заполнения/фильтра —
раньше readTable отдавал «Субконто Дт 2», а клик про такое имя не знал.

Попутно закрыт второй дефект: безымянная picture-колонка определялась по ПЕРВОЙ
строке, а picField над Boolean не рисует картинку при Ложь → колонка пропадала из
columns целиком. Модель сэмплит до 10 строк и ищет ячейку по colindex.

Проверено:
- 24-multirow-header (стенд, оба паттерна ERP) — зелёный; до правки красный;
- клик проверяется по факту (DOM select+focus), а не по эху clicked.column:
  до правки клик по «Срок» молча жал «Исполнитель 2» и рапортовал успех;
- живьём на ERP: список задач — 9 честных колонок вместо фантомов, значения на
  местах; форма операции (шапка 2 этажа, строка 3 под-ряда) — «Субконто Дт/Кт 1..3»
  сохранены, ничего не поехало;
- полный регресс 28/28.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:14:27 +03:00
Nick ShirokovandClaude Opus 4.8 96b38d9f91 test(web-test): стенд с двухэтажной шапкой + красный тест резолвинга колонок
Обработка МногострочнаяШапка воспроизводит два паттерна, снятых живьём с ERP:

1. паттерн «Задачи» — широкая колонка «Исполнитель» (x 705..1206) над парой узких
   «Срок» (705..956) и «Выполнена» (956..1206). У каждой ячейки есть шапка со своим
   colindex → верный ответ однозначен, но матчинг по центру x его не находит.
2. паттерн «Операция» — шапка только у группы «Субконто» (showInHeader:false у детей,
   как «Субконто Дт» в ERP), ячеек три без своих шапок → разворот в «Субконто 1/2/3»
   правилен и должен пережить правку.

Тест 24-multirow-header покрывает чтение, клик и заполнение. Сейчас КРАСНЫЙ — фиксирует
дефект до правки:

  columns: [... "Исполнитель 1","Исполнитель 2","Исполнитель 3","Срок","Выполнена" ...]
  row0:    "Исполнитель 2": "Срок 1 / Выполнена 1"   ← значения склеены в чужую колонку
           "Срок": ""   "Выполнена": ""              ← свои колонки пусты

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:14:27 +03:00
Nick ShirokovandClaude Opus 4.8 ad8a2e5cab feat(web-test): readTable отдаёт состояние строки по ведущей иконке
readTable матчил только спрайты pictureCollection (именованные pic-колонки), а
ведущая иконка состояния приходит из convertPicture?url=e1csys/<dir>/<file>.zip&gx=N
и молча дропалась. Проверить «помечен на удаление» можно было только выводом
колонки через «Настроить список» в каждом тесте.

Строка списка объектов теперь отдаёт плоские булевы _deleted / _posted /
_predefined / _completed / _started / _finished и сырьё _rowPic для диагностики.

Ключ словаря — ПОЛНЫЙ путь спрайта, не имя файла: basic/folder.zip и
accnt/folder.zip — разные файлы с одинаковым именем и разной раскладкой gx
(в basic gx=1 элемент, в accnt gx=1 предопределённый).

Отсутствие булева значит «не знаю», а не false: ось неприменима либо кадр не
расшифрован. Дефолт false отвергнут — врал бы молча в зелёную сторону.

Пути и раскладки сняты живьём на ERP; у Task/BusinessProcess раскладка сверена с
данными списка. Кадры 4/5 basic/folder.zip (второе измерение — иерархия
элементов) расшифрованы по байтовому равенству кадров: gx4 ≡ gx1, gx5 ≡ gx3.

Стенд: документы в заданных состояниях + помеченный элемент справочника +
безымянная picture-колонка ПЕРЕД значком состояния — воспроизводит ловушку
«первый .gridBoxImg не тот» (проверено подменой на наивный экстрактор).

Тест 22-row-state; полный регресс 27/27.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 20:59:17 +03:00
Nick ShirokovandClaude Opus 4.8 115a966f0f feat(web-test): status реально пингует сервер, а не верит файлу сессии
cmdStatus проверял только наличие .browser-session.json, поэтому после
падения/перезагрузки оставшийся файл читался как живая сессия (ложное
ok:true). Сервер уже отдаёт реальную живость через GET /status
(browser.isConnected()) — CLI теперь им пользуется:

- ok:true/ready:true только если сервер ответил connected:true (exit 0);
- server-unreachable (сервер мёртв, файл остался) → exit 1 + самоочистка
  stale-файла; browser-disconnected → exit 1;
- контракт кода возврата сохранён: exit 0 = живая готовая сессия.
- SKILL.md: ждать готовности поллингом status (exit 0), а не ловлей
  stdout долгоживущего start.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 15:06:37 +03:00
Nick ShirokovandClaude Opus 4.8 b81a7504ce feat(web-test): гард недоступных элементов + метка disabled в getFormState
clickElement/fillFields/selectValue бросали ложный успех при действии над
недоступным (disabled) контролом — в 1С это no-op. Причина: резолвер цели
клика не смотрел признак недоступности, который ридер getFormState уже знал.

- резолвер клика снимает disabled (кнопки/frameButton/флажок/тумблер/поле),
  clickElement бросает `"X" is disabled` вместо тихого no-op;
- fillFields и selectValue тоже бросают на недоступном поле/флажке/ссылке;
- getFormState помечает disabled у frameButton, флажка, переключателя и
  тумблера (раньше был только у a.press-кнопок и полей ввода);
- стенд: обработка ПроверкаДоступности с парами доступный/недоступный по всем
  типам контролов (в подсистеме Администрирование) + тест 23-availability;
- SKILL.md: заметка про disabled у getFormState и throw у clickElement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 13:53:33 +03:00
Nick ShirokovandClaude Opus 4.8 4a778cb3b1 style(web-test): сообщения стартовых блокеров на английском
Диагностика движка везде английская (session.mjs, test.mjs); сообщения про
нехватку лицензии и требуемую авторизацию из коммита 2a71e6c9 остались на
русском — разнобой. Приведено к общему языку; русским остаётся только
цитируемый текст платформы (это данные, а не наш текст). Детект и check.mjs
завязаны на английские части строк, поведение не меняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:16:23 +03:00
Nick ShirokovandClaude Opus 4.8 bb98d3c240 feat(web-test): closeForm закрывает форму каскадом, кнопка подтверждения по смыслу
closeForm умел только Escape. На реальном стенде Escape не закрывает ни
модальную форму, ни даже обычный список — форма оставалась открытой, и
resetState считал контекст грязным.

- Каскад закрытия (closeCrossScript в dom/forms.mjs): Escape → крестик
  плавающего окна (ps<N>, модалка поверх формы) → крестик страницы формы
  (VW_page<N>, работает и при скрытой панели вкладок) → крестик активной
  вкладки (класс select, не первый попавшийся .openedClose). Порядок и якоря
  замерены живьём. Крестик формы бьёт вкладочный, потому что панель открытых
  может быть выключена настройкой.
- nothingToClose: когда Escape не помог и крестика нет нигде — платформа сама
  говорит, что поверхность не закрывается, то есть это рабочий стол. Этот сигнал
  и питает «чисто» в resetState, без базового снимка и списков-исключений.
- Семантика подтверждения по смыслу вопроса (pickConfirmationLabel): 1С теми же
  кнопками «Да/Нет» задаёт разные вопросы. «Сохранить изменения?» → save?Да:Нет
  (как было); «Закрыть согласование?» → Да=закрыть (иначе save:false жал «Нет» =
  «остаться», и модалка утекала в следующий тест). Решение по вопросительному
  предложению; неизвестная формулировка → прежнее поведение.
- Ответственность DOM: поиск крестика вынесен в dom/forms.mjs (генератор скрипта
  для page.evaluate), close.mjs его только зовёт. Диагностика приведена к
  английскому, как остальной модуль; русским остаётся лишь цитата платформы.

Проверено на свежей базе пилота: модалка «Комментарий и согласование» →
closeForm({save:false}) = closed:true, answered:Да, viaCross:true, formCount 2→1
(критерий пилота); стопка документ→список→стол разбирается насквозь и
останавливается на столе. Полный регресс 25/25, closeForm зовёт каждый тест.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:15:32 +03:00
Nick ShirokovandClaude Opus 4.8 8ccd562625 fix(web-test): resetState замечает провал и не считает рабочий стол грязью
resetState молча сдавался: крутил 10 попыток closeForm, игнорировал их
результат и ничего не возвращал. Контекст с чужой открытой формой уходил в
пул как «чистый», следующий тест кликал в него (заявка пилота: Сц.3 роняет
форму → Сц.5 падает на чужой).

- resetState возвращает вердикт {clean, attempts, form, title, modal}. «Чисто»
  определяется не как «форм нет» (form == null — верно только для пустого
  стола), а как «закрывать нечего»: closeForm.nothingToClose. Замерено на
  стенде пилота — рабочий стол там это form=5, formCount=3, openForms=[5,6,7],
  и старое правило объявляло бы чистый контекст грязным ПОСЛЕ КАЖДОГО теста
  (clean:false за 8.7с). Теперь clean:true за 0.9с, одна итерация вместо десяти.
- resetOrAbort читает вердикт: не clean → !-строка с именем оставшейся формы +
  abortContext. Ровно логика, уже работавшая для пробоя дедлайна.
- Диагностика буферизуется и печатается ПОД строкой своего теста: cleanup идёт
  до записи результата, поэтому раньше !-строки вставали над тестом и
  приписывались предыдущему (на этом купилась и сама сессия).
- Ранний выход по closed:false НЕ вводим: A/B на живом наборе показал, что
  грязная «Приходная накладная» отдаёт closed:false на первой попытке и
  закрывается на следующей — выход прерывал бы контекст зря.

Проверено: контракт стабом 5/5; на стенде пилота ложное срабатывание снято
(8.8с→0.9с), стопка форм разбирается до стола; полный регресс 25/25, ноль
строк «not clean» (было две — «Тестовые ошибки»).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 11:15:02 +03:00
Nick ShirokovandClaude Opus 4.8 2a71e6c980 feat(web-test): ловить диалог авторизации + чистая диагностика в start/run
Второй вход в ту же ловушку: публикация без пользователя (нет Usr= в vrd).
Клиент показывает диалог «Пользователь/Пароль/Войти», ввод учётных данных
движок не поддерживает — но вместо ошибки он ждал 67.7с, а затем closeModals()
жал Escape и диалог ИСЧЕЗАЛ. Оставалась пустая страница, объявленная здоровым
стартом, и дальше та же ложь про «режим отображения панели».

Разведка изменила постановку в двух местах:

1. Улику затирал сам движок. Замерено: одного Escape достаточно, чтобы форма
   логина пропала. Поэтому детект обязан отработать ДО closeModals.
2. Комментарий «страница логина легитимна» оказался фикцией: войти руками через
   start было невозможно и раньше — движок сам уничтожал форму. Значит падать
   тут ничего не ломает, и развилка «в раннере падать, в интерактиве нет»
   отпадает. Комментарий переписан, чтобы следующий читатель не поверил ему
   больше, чем коду.

Хуже, чем с лицензией: на диалоге авторизации сеанс 1С УСПЕВАЕТ создаться и
держит лицензию впустую. Поэтому connect() освобождает его (disconnect) перед
тем, как ошибка уйдёт наверх: cmdStart зовёт connect без catch, run.mjs не
оборачивает команду, а убийство процесса лицензию не освобождает.

- session.mjs (v1.19→v1.20): +1 якорь #authWindow в тот же предикат, своё
  сообщение с рецептом (-UserName → Usr=/Pwd=), очистка в connect().
- start.mjs (v1.0→v1.1), run.mjs (v1.0→v1.1): стартовый блокер — это диагноз,
  а не крах. Три строки и код 1 вместо стека, который указывал внутрь
  session.mjs и читался бы как поломка движка (модель пошла бы чинить не то).

Проверено вживую:
- шаг 0 (до кода): #authWindow не существует на здоровых стартах — ни в клиенте,
  ни при загрузке; проверено и на bpdemo с автологином (19.5с, опрос 100мс);
- позитивный контроль на bpdemo-auth (BP_DEMO без Usr=): бросок за 2.7с вместо
  67.7с, слот убран из реестра, лицензия вернулась — 6/6;
- то же через connect(): сообщение, браузер закрыт, сеанс освобождён;
- негативный контроль: полный регресс 25/25, ноль ложных срабатываний; start на
  bpdemo (автологин) поднимается штатно;
- попутно на испорченном прогоне (сам съел лицензию соседней сессией) детект
  отработал в раннере на НАСТОЯЩЕМ дефиците: два мультиконтекстных теста упали
  с внятной причиной, остальные 23 прошли — прогон поехал дальше.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:14:13 +03:00
Nick ShirokovandClaude Opus 4.8 9bec6171d3 feat(web-test): ловить стартовый блокер 1С вместо ложного диагноза
Когда у 1С нет свободной лицензии, клиент рисует блокирующий стартовый диалог
ВМЕСТО приложения. Движок этого не видел: ждал свой маркер инициализации 60с,
молча проваливался в waitForTimeout(5000) и ВОЗВРАЩАЛ УСПЕХ. Слот оставался без
seanceId, а первое же обращение выдавало заведомую ложь:

  navigateSection: "Склад" not found. Section panel is in icon-only mode…

Поймано на себе: спайки утекли сеансами → упёрлись в лимит → час гонялись за
несуществующим «режимом отображения панели».

Конфигом это не лечится: число свободных лицензий — не свойство стенда. Замеры
дали 3, потом 1, потому что на той же машине работала параллельная сессия со
своими сеансами. Предсказать нельзя — надо ловить.

- waitForClientOrStartupBlock: один waitForFunction ждёт, что наступит раньше —
  клиент (#themesCell_theme_0) или видимый стартовый диалог (#messageBoxText).
  Бросает ТОЛЬКО по положительной улике: отсутствие клиента уликой не является
  (страница логина легитимна, поведение там не изменилось). Якоря — id, текст
  платформы лишь цитируется в сообщение, поэтому смена локали детект не сломает.
  Перед обвинением — переподтверждение через 600мс (защита от мигания при
  отрисовке оболочки).
- openAndSettle убирает третью копию тех же трёх строк и делает обязательное:
  при блокировке слот НЕ должен пережить ошибку. Он регистрируется до ожидания,
  а ensureContext в раннере — это `if (hasContext(name)) return`, так что битый
  слот молча обслуживал бы все следующие тесты диалогом отказа.
- test.mjs: дефолтный контекст поднимается во внешнем try, у которого только
  finally, а run.mjs не оборачивает cmdTest — бросок оттуда дал бы голый стек и
  НИКАКОГО отчёта. Теперь: строка, отчёт, освобождение сеансов, выход 1.
- Кнопки диалога не нажимаем сознательно: его автозапуск по обратному отсчёту
  может завершить чужой сеанс на общей машине. Причина зафиксирована в тексте
  ошибки, чтобы это не «починили» кликом.

Проверено вживую:
- шаг 0 (до кода): на здоровом старте #messageBoxText не существует вовсе — ни
  в загруженном клиенте, ни в момент загрузки (опрос 100мс);
- позитивный контроль на НАСТОЯЩЕМ отказе: бросает за 1.1–13.5с вместо 66,
  цитирует диалог и список сеансов, слот из реестра убран — 6/6;
- негативный контроль: полный регресс 25/25, ноль ложных срабатываний;
- путь connect() (exec/run/start) — клиент поднимается штатно.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 20:11:56 +03:00
Nick ShirokovandClaude Opus 4.8 57c101ef3c feat(web-test): бюджеты дедлайнов в конфиг + пробой сброса прерывает контекст
Числа были зашиты в код и подобраны на лёгком синтетическом стенде. На тяжёлом
прикладном решении тот же resetState честно идёт дольше — и упирался бы в чужой
дефолт без возможности его поднять.

- `deadlines: {...}` в webtest.config.mjs переопределяет любой бюджет поштучно.
  Неизвестный ключ или неположительное значение — ошибка до старта прогона:
  опечатка в имени означала бы, что переопределение молча не действует.
- Пробой resetState теперь ПРЕРЫВАЕТ контекст, а не пишет строку и едет дальше.
  После неудавшегося сброса состояние UI неизвестно, и переиспользование слота
  утекало бы грязным состоянием в следующий тест — худший исход плохо подобранного
  бюджета: тихий дрейф вместо видимой ошибки. Теперь слишком тесный бюджет стоит
  перезапуска контекста, но никогда — неверного результата теста.

Проверено: опечатка ключа → внятная ошибка с перечнем допустимых; deadlines
{resetState:1} → пробой виден строкой, контекст прерван, следующий тест зелёный.
check.mjs 6/6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 18:03:10 +03:00
Nick ShirokovandClaude Opus 4.8 9352d28008 test(web-test): обвязка check.mjs для фикстуры _hang + не ограничивать prepare
check.mjs спавнит раннер дочерним процессом и превращает «читать глазами
два условия» в 0/1. Проверяет шесть вещей: раннер завершился за 90с (а не
завис — это и есть суть), вердикт hang, контекст прерван с успешным logout,
следующий тест зелёный (лицензия вернулась), результат зависшего теста попал
в отчёт (инкрементальная запись), код выхода 1. Отдельный код 2 — стенд не
поднят: у фикстуры нет своих хуков, и «нет стенда» не должно выглядеть как
поломка механики.

Заодно фикс собственной регрессии: hooks.prepare был обёрнут в bounded(),
который ГЛОТАЕТ ошибку — упавшая пересборка стенда молча пропускалась бы, и
вместо одной внятной ошибки прогон вываливал бы экран непонятных падений.
Плюс бюджет 120с обрезал бы легитимно долгую пересборку большой базы.
Возвращено к голому await: prepare честно долгий, а его падение обязано быть
фатальным.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:50:08 +03:00
Nick ShirokovandClaude Opus 4.8 dc8afb402b docs(web-test): описать фикстуру _hang и коды выхода в README набора
Фикстура лежит в репозитории, но README о ней не упоминал — а её нельзя
использовать «по интуиции»: ожидаемый результат `1 passed, 1 failed` с кодом
выхода 1, где красный тест означает успех.

Записано: как запускать, какие два условия читать в выводе, когда гонять
(правки пути очистки и жизненного цикла, обновление Playwright — механика
стоит на замеренном поведении библиотеки), чего она стоит (лицензия +
перезапуск браузера в tab-режиме) и чего НЕ ловит (молчаливую поломку logout,
если 1С уйдёт на куки; полумёртвый CDP).

Заодно: коды выхода 2/3 из --global-timeout и актуальный счёт тестов (21→25).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:44:00 +03:00
Nick ShirokovandClaude Opus 4.8 037d000f20 feat(web-test): таймаут теста реально прерывает зависший await
Прогон вставал намертво на зависшем Playwright-действии (~29 мин, без движения),
при --format=allure отчёт терялся целиком — результаты писались только в конце.

Promise.race с таймером был и раньше, но не лечил: race не отменяет t.fn (промис
отменить нельзя), а весь путь после него шёл без единого таймаута. Паттерн
`try { await x } catch {}` ловит reject, но не «никогда не завершится» — а
page.evaluate не имеет таймаута в принципе, поэтому заблокированный JS-поток
рендерера вешал раннер навсегда.

Что сделано:

1. Прерывание. При таймауте движок опрашивает контекст (probeContext) и при
   вердикте hang/browser-dead уничтожает зависшее: abortContext закрывает
   страницу с runBeforeUnload:false, повисший await отваливается «Target closed»,
   следующий тест поднимает контекст лениво. Зависший тест не ретраится.

2. Диагноз в отчёте. hang (браузер жив, рендерер не отвечает) против slow
   (просто не уложился → поднять timeout). Разделитель — асимметрия пробников:
   вызов в browser-процесс отвечает за 1 мс при мёртвом рендерере, evaluate — нет.

3. Лицензии. Штатный logout идёт fetch-ем изнутри страницы и на зависшей
   странице невозможен. abortContext шлёт POST /e1cib/logout из Node: замерено —
   сеанс опознаётся seanceId в URL, кук у клиента нет вообще, после запроса
   клиент пишет «сеанс был завершен». Каскад: node → page → соседняя страница.

4. Дедлайны на весь путь очистки (deadline.mjs) — пробой печатается строкой,
   молча зависнуть больше нельзя. Попутно: disconnect слал logout по зависшей
   странице дважды (модульный `page` после multi-context ветки).

5. Инкрементальный Allure: результат теста пишется сразу по его завершении —
   зависание больше не уничтожает уже собранное.

6. --global-timeout: потолок на прогон, работает и внутри зависшего теста
   (неразрешённый промис не блокирует event loop). Коды выхода: 2 — потолок
   сработал, 3 — зависло само сворачивание. Внешний watchdog больше не нужен.

Проверено вживую на стенде: фикстура tests/web-test/_hang (заблокированный
JS-поток) — падение за 17.6с с verdict: hang вместо вечного зависания,
следующий тест зелёный, оба результата в allure-results; --global-timeout
срабатывает посреди зависания и сохраняет отчёт. Полный регресс 25 тестов:
пробоев дедлайнов ноль.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:37:18 +03:00
Nick ShirokovandClaude Opus 4.8 3889c7279f fix(web-test): скриншоты падений доезжают до Allure-отчёта
Вложение «Screenshot on failure» весило 0 B у всех красных тестов —
две независимых причины, маскирующие друг друга.

1. slugify сохранял кириллицу в именах артефактов. Allure CLI молча не
   находит вложение с не-ASCII именем: пишет "size": 0 без ссылки на файл
   (JAVA_OPTS с file.encoding/sun.jnu.encoding не помогает). Теперь slugify
   транслитерирует кириллицу и схлопывает остальное не-ASCII в дефис.
   Чинит и видео — оно использовало то же имя.

2. Скриншот 1С-ошибки писался в фиксированный <навык>/error-shot.png:
   вне reportDir (репортер аттачит по basename → мёртвая ссылка) и одним
   именем на весь прогон (каждый следующий тест перетирал предыдущий).
   exec-context получил setErrorShotDir + уникальные имена; раннер
   направляет их в reportDir. Дефолт для интерактивных exec/run не менялся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:36:57 +03:00
Nick ShirokovandClaude Opus 4.8 3f1168b975 feat(web-test): управление пулом контекстов/лицензий в раннере
Раннер регресса теперь держит пул 1С-сеансов сам, вместо накопления контекстов
между тестами и ручного закрытия в хуках. Три необязательных поля webtest.config.mjs
(без них поведение прежнее):
- maxContexts     — потолок одновременно живых сеансов (null = без лимита);
- contextPolicy   — 'reuse' (держать открытыми в пределах лимита) | 'strict'
                    (закрывать non-pinned контексты теста сразу после него);
- pinnedContexts  — не вытесняются LRU (default = [defaultContext]; [] делает
                    default вытесняемым на тесном стенде).

Перед setup каждого теста LRU-вытеснение освобождает слот под нужды теста;
уже открытые нужные контексты переиспользуются. Default больше не вечно-pinned.
Исчерпание пула даёт внятную ошибку вместо маскирующего «Browser not connected».

- new: cli/test-runner/context-pool.mjs — чистый планировщик planEviction + LRU.
- cli/commands/test.mjs (v1.4): парсинг/валидация полей, вытеснение с фолбэк-парковкой
  на нужный контекст (нельзя закрыть единственный активный), strict-закрытие, LRU-трекинг.
- Доки: regression-spec (§7/§8/глоссарий), regression-guide (рецепт), regress.md.
- Регресс дай-фудит фичу: 14-multi-context-routing роутит в 3-й контекст c,
  15-multi-context-handover проверяет вытеснение c на границе; конфиг maxContexts:2.

Юнит-краёв планировщика — в debug/ (gitignored). Live-регресс: 25/25 зелёных.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:57:56 +03:00
Nick ShirokovandClaude Opus 4.8 9e1341b80b docs(meta-dsl-spec): актуализировать список видов объектов (23->37) + ФункциональнаяОпция в синонимах
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:38:19 +03:00
Nick ShirokovandClaude Opus 4.8 e473d6c7a9 feat(tests): verify-snapshots — --v8path + авто-детект как в навыках db-*
Приоритет резолва платформы приведён к паритету с resolve_v8path навыков:
--v8path (явный параметр) → .v8-project.json → авто-поиск. Авто-детект теперь
зеркалит py/ps1: Windows — Program Files[ (x86)]\1cv8\*\bin\1cv8.exe, *nix —
/opt/1cv8/*/1cv8, максимальная версия числовой сортировкой (versionKey отбрасывает
хвост bin). --v8path .../ibcmd даёт чистый способ гонять цикл через ibcmd без
правки конфига.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:33:28 +03:00
Nick ShirokovandClaude Opus 4.8 a862e913c8 fix(tests): verify-snapshots — honor v8path на исполняемый файл (ibcmd)
loadV8Context трактовал v8path только как каталог: путь на файл (.../ibcmd)
не резолвился и МОЛЧА подменялся авто-детектом обратно на 1cv8 (Platform-строка
печатала 1cv8 при заданном ibcmd). Теперь: v8path-файл используется как есть →
db-* навыки сами выбирают движок по basename, verify умеет гонять цикл через
ibcmd. Явно заданный неразрешимый путь → null (ошибка), а не тихая подмена
платформой. Авто-детект только при пустом v8path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:30:04 +03:00
Nick ShirokovandClaude Opus 4.8 abb3580d60 fix(tests): verify-snapshots резолвит платформу 1С на macOS/*nix
loadV8Context() гейткипил по наличию 1cv8.exe → на маке (/opt/1cv8/<ver>/1cv8,
без .exe) возвращал null → «1C platform not found» даже при валидном v8path.
Резолв исполняемого файла теперь по ОС (V8_EXE), проверяет и <ver>/ и <ver>/bin/,
плюс auto-detect по /opt/1cv8/* с числовой сортировкой версий. Windows-путь
(bin/1cv8.exe) не тронут.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:54:41 +03:00
Nick ShirokovandClaude Opus 4.8 c178e04f92 feat(meta-edit): add-predefined — предопределённые элементы (Фаза 2.3, v1.19)
Первая операция meta-edit над отдельным файлом Ext/Predefined.xml. add-predefined
добавляет предопределённые (Catalog, ChartOfCharacteristicTypes): строка
"(Код) Имя [Наименование]" или объект {name,code,description,isFolder,childItems}
(дерево групп). Тип кода (String/Number) — из <CodeType> объекта; xsiType по типу;
version из корня. Inline + JSON. Порт эмиттера Resolve-PredefItem/Build-PredefItemXml
из meta-compile. ps1+py.

ИНВАРИАНТ: существующие <Item id=GUID> сохраняются побайтово (текстовый append),
новые получают свежий id — id существующих сущностей не меняются. check-uuid-invariant.mjs
расширен проверкой сохранения id предопределённых при add.

Cert: структура БАЙТ-В-БАЙТ = meta-compile (ps1 и py, id нормализован); GUID-сохранение
при добавлении к существующим; load+UpdateDBCfg в 1С успешны. meta-edit 17/17 ps1+py.
Отложено: remove/set-predefined (вложенное удаление), ПланСчетов/ПланРасчёта (свои грамматики).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:08:52 +03:00
Nick ShirokovandClaude Opus 4.8 870203622a test(meta-edit): guard-инвариант сохранения uuid существующих сущностей
check-uuid-invariant.mjs: компилирует объект (Catalog + реквизиты + ТЧ), фиксирует ВСЕ
uuid (тип-элемент + GeneratedType TypeId/ValueId + реквизиты/ТЧ), применяет широкую
правку (rename + смена типа + структурные свойства + свойства объекта + ТЧ + add + remove),
проверяет что uuid существующих сущностей ЦЕЛЫ (удалённый — ушёл). Оба рантаймa.

Закрывает пробел: снапшот-тесты нормализуют uuid позиционно и НЕ ловят перегенерацию id.
Инвариант критичен (смена uuid рвёт ссылки/данные/состояние поддержки) и станет защитой
для будущих операций над предопределёнными (их <Item id=GUID> тоже нельзя перегенерировать).
Проверено: 20 uuid, ps1+py OK. Запуск после правок meta-edit (как check-enum-drift.mjs).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:51:41 +03:00
Nick ShirokovandClaude Opus 4.8 7a9dc229c7 docs(meta-edit): DataLockFields/RegisteredDocuments в справочнике свойств-списков (Фаза 2.4)
Таблица свойств-списков + inline add-/remove-/set- операции для DataLockFields
(разворот короткого имени реквизита) и RegisteredDocuments. Usage-формулировки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:35:45 +03:00
Nick ShirokovandClaude Opus 4.8 2ff7c271fb feat(meta-edit): свойства-списки DataLockFields + RegisteredDocuments (Фаза 2.4, v1.18)
Расширены complex-свойства (были Owners/RegisterRecords/BasedOn/InputByString):
+ DataLockFields (поля блокировки данных, с разворотом короткого имени реквизита
  в полный путь через Expand-DataPath — как у meta-compile) + RegisteredDocuments
  (регистрируемые документы журнала). JSON modify.properties + inline add-/remove-/set-.
Флаг expand в complexPropertyMap разворачивает пути (Add/Remove/Set-ComplexProperty).
ps1+py зеркально.

Cert декомпиляцией: DataLockFields/RegisteredDocuments БАЙТ-В-БАЙТ = meta-compile (ps1).
verify-snapshots --case (Catalog+DocumentJournal, реальные объекты через preRun) —
грузятся в 1С. meta-edit 16/16 ps1+py.

NB: py-lxml сериализует \r в tail как &#13; (пре-существующее для всех complex-свойств,
валидно — загрузка в 1С подтверждена). EOL-конвенция py(LF)/ps1(CRLF) — отдельная тема.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:34:08 +03:00
Nick ShirokovandClaude Opus 4.8 b8a6a80d9a docs(meta-edit): формулировки инструкции от применения, не от реализации
По фидбэку: инструкция навыка — о том КАК ПРИМЕНЯТЬ, а не как устроено внутри.
Убраны детали реализации: «в канонической позиции», «создаётся/заменяется» механика,
расшифровка флагов через внутренние XML-свойства (req→FillChecking=ShowError и т.п.),
«вложенная XML-структура» → «свойства-списки». Поведение описано в пользовательских
терминах (можно задать даже если не выставлено; опечатка → ошибка; короткие имена в путях).
+ строка-подсказка в SKILL.md про структурные свойства (обнаружимость возможности).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:03:41 +03:00
Nick ShirokovandClaude Opus 4.8 356258e9c1 docs(meta-edit): самодостаточные таблицы structural modify-ключей (Фаза 2 Шаг 5)
child-operations.md: компактная таблица структурных свойств реквизита (Format/EditFormat/
ToolTip/ChoiceForm/MinValue/MaxValue/FillValue/LinkByType/ChoiceParameterLinks/
ChoiceParameters) + поведение create-if-missing/ошибка на опечатке. properties-reference.md:
modify-property create-if-missing + типо-гард. Без отсылок к meta-compile (автономность),
формат — таблицы.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:49:07 +03:00
Nick ShirokovandClaude Opus 4.8 0d02159a34 feat(meta-edit): структурное свойство FillValue явный (Фаза 2 Шаг 3b-2b, v1.17)
modify-attribute/-dimension/-resource: FillValue с явным значением — порт Emit-FillValue
+ Resolve-FillValueSpec/Get-FillTypeCategory/Expand-FillShortRef/fillBool-таблицы в
meta-edit. Тип реквизита извлекается из XML (<Type>/<v8:Type>, Get-AttrTypeStrFromXml) —
по нему категоризация значения. Маркеры {nil}/{emptyRef}; bool→xs:boolean, число→
xs:decimal, ref-путь/короткая ссылка→xr:DesignTimeRef (EmptyRef разворачивается по типу),
строка→xs:string. Esc-XmlText (&<> без ") для паритета текста FillValue.

Завершает Шаг 3: все структурные свойства реквизита (Format/EditFormat/ToolTip/ChoiceForm/
MinValue/MaxValue/LinkByType/ChoiceParameterLinks/ChoiceParameters/FillValue) доступны в
modify, БАЙТ-В-БАЙТ = meta-compile (ps1 и py), платформенный cert в 1С.

Cert декомпиляцией: fillValue на 4 типах реквизита = meta-compile. verify-snapshots
--case modify-attribute-structural (FillValue EmptyRef+decimal) грузится в 1С.
meta-edit 14/14 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:47:07 +03:00
Nick ShirokovandClaude Opus 4.8 39dc3fc1e7 feat(meta-edit): структурное свойство ChoiceParameters + фикс namespace app (Фаза 2 Шаг 3b-2a, v1.16)
modify-attribute/-dimension/-resource: ChoiceParameters ([{name, type?, value?}]) —
порт Emit-ChoiceParameters + полный кластер fill-ref машинерии в meta-edit
(fillRefRoots/fillRefKindRoot/fillEmptyRefWords/accountTypeValues, Normalize-FillRef,
Normalize-ChoiceValue/T, Expand-ChoiceRefValue, ConvertFrom-ChParamShorthand,
ConvertTo-ChScalar, Format-FillNum). Значение → app:value с xsi:type (bool/decimal/
DesignTimeRef); массив → v8:FixedArray/v8:Value; type разворачивает голые ref-имена
(EnumRef.X + "Оптовая" → Enum.X.EnumValue.Оптовая).

КРИТИЧНО: Import-Fragment (ps1+py) теперь объявляет xmlns:app + xmlns:ent — иначе
app:item/app:value падали бы "undeclared prefix". app/ent объявлены в корне 1С-файлов
→ вставка без per-element xmlns.

Cert декомпиляцией: выход meta-edit БАЙТ-В-БАЙТ = meta-compile (ps1 и py; включая
v8:FixedArray и DesignTimeRef-разворот). verify-snapshots --case modify-attribute-structural
(расширен ChoiceParameters bool+EmptyRef) грузится в 1С. meta-edit 14/14 ps1+py.

Остаток Шага 3: fillValue явный (нужна экстракция типа реквизита из XML).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:41:52 +03:00
Nick ShirokovandClaude Opus 4.8 41df320153 feat(meta-edit): структурные свойства LinkByType/ChoiceParameterLinks (Фаза 2 Шаг 3b-1, v1.15)
modify-attribute/-dimension/-resource: LinkByType ({dataPath,linkItem}) и
ChoiceParameterLinks ([{name,dataPath,valueChange}]) — порт эмиттеров Emit-LinkByType/
Emit-ChoiceParameterLinks в meta-edit (используют xr: namespace, уже в Import-Fragment;
app не нужен). Портированы вспомогательные Expand-DataPath (+Resolve-StdAttrEn на
существующих reserved-картах), Get-ChElProp, ConvertFrom-ChLinkShorthand. Прощающий
ввод путей (короткое имя реквизита → полный путь).

Cert декомпиляцией: выход meta-edit БАЙТ-В-БАЙТ совпадает с meta-compile для того же
ввода (ps1 и py; py-lxml пишет LF vs ps1 CRLF — нормализуется раннером/git).
verify-snapshots --case modify-attribute-structural (расширен) грузится в 1С.
meta-edit 14/14 ps1+py.

Остаток Шага 3: fillValue явный + choiceParameters (fill-ref машинерия + фикс app-namespace).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:34:04 +03:00
Nick ShirokovandClaude Opus 4.8 caea1b1049 feat(meta-edit): структурные свойства MinValue/MaxValue (Фаза 2 Шаг 3a, v1.14)
modify-attribute/-dimension/-resource: типизированные MinValue/MaxValue (порт
Emit-MinMaxValue — nil / xs:string / xs:decimal; xsi уже объявлен в Import-Fragment,
app-namespace не нужен). Хелпер Build-MinMaxValueXml + 2 ветки switch, replace-or-create
через Set-AttrPropertyElement. ps1+py.

Cert: verify-snapshots --case modify-attribute-structural (расширен Number-реквизитом
Сумма с MinValue=0/MaxValue=1000000) — грузится в 1С. meta-edit 14/14 ps1+py, byte-паритет.

Остаток Шага 3 (fillValue явный, choiceParameters/choiceParameterLinks/linkByType) —
тяжёлый порт fill-ref машинерии + фикс namespace app в Import-Fragment; карта
зависимостей в плане.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:39:37 +03:00
Nick ShirokovandClaude Opus 4.8 c31cff5ada feat(meta-edit): структурные свойства реквизита Format/EditFormat/ToolTip/ChoiceForm (Фаза 2 Шаг 2, v1.13)
modify-attribute/-dimension/-resource теперь умеют задавать структурные свойства
реквизита (не только скаляры): Format/EditFormat/ToolTip (ML-строки через
существующий Build-MLTextXml) + ChoiceForm. Ветки-диспетчеры в switch перед default,
по образцу ветки type (replace-or-create). Отсутствующее свойство создаётся в
канонической позиции (Insert-PropertyInOrder из Шага 1). Ключи — PascalCase
XML-имена (консистентно с существующей modify-конвенцией CodeLength/Indexing).

Побочный фикс: Set-AttrPropertyElement (ps1) через InsertBefore+RemoveChild вместо
InsertAfter+Remove-NodeWithWhitespace — последний склеивал (</PasswordMode><Format>),
т.к. в XmlDocument ведущий whitespace — отдельный узел; py (tail-модель) был корректен.

Cert: verify-snapshots --case modify-attribute-structural — Format/EditFormat/ToolTip/
ChoiceForm (с реальной формой через form-add preRun) грузятся в 1С. meta-edit 14/14
ps1+py; byte-паритет ps1==py (после нормализации раннера).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:34:47 +03:00
Nick ShirokovandClaude Opus 4.8 0e68421f50 feat(meta-edit): create-if-missing свойств + типо-гард (Фаза 2 Шаг 1, v1.12)
modify-property и default-ветка modify-attribute/-dimension/-resource раньше при
отсутствии элемента свойства делали тихий Warn+no-op — правка молча терялась
(модель думала, что применилось). Теперь: известное отсутствующее свойство
СОЗДАЁТСЯ (в канонической позиции по attrPropOrder для реквизитов, append для
объектных — порядок 1С терпит, cert Шаг 0); неизвестное имя (опечатка) → внятная
ошибка exit≠0. Наборы известных свойств — union по корпусу acc+erp 8.3.24
(knownObjectProps 133, knownChildProps 40); attrPropOrder — из Build-AttributeFragment.
Новый хелпер Insert-PropertyInOrder. ps1+py зеркально.

Cert: Шаг 0 (навыками db-*) — 1С грузит и применяет объект со свойством вне
канонической позиции → append безопасен. Load-cert create-if-missing выхода —
LoadConfigFromFiles+UpdateDBCfg успешны. verify-snapshots --skill meta-edit 9/9.

Тесты: error-modify-property-typo + error-modify-attribute-typo (expectError) +
integration meta-edit-create-if-missing (compile→editFile удалить→правка→assert).
meta-edit 13/13 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:23:07 +03:00
Nick ShirokovandClaude Opus 4.8 9b5908c3b2 feat(meta-validate): базовая проверка нераспознанных валидных типов + прощающий ввод плоских объектов (Фаза 1.4, v1.9)
20 валидных типов метаданных без глубоких правил (Subsystem/Role/CommonForm/
CommonCommand/CommandGroup/CommonAttribute/CommonTemplate/CommonPicture/
SessionParameter/SettingsStorage/FilterCriterion/FunctionalOption/
FunctionalOptionsParameter/Language/Style/StyleItem/WSReference/XDTOPackage/
DocumentNumerator/Sequence) раньше падали как "Unrecognized" + exit 1 — ложная
ошибка на валидном объекте. Теперь для них базовая структурная проверка
(root/uuid + Name-идентификатор) с ранним выходом, без type-specific правил.
Имена типов взяты из LocalName реальных файлов корпуса. По-настоящему неизвестный
тип по-прежнему отвергается.

Побочно: прощающий ввод для плоских объектов (один .xml без папки —
SessionParameter, CommonAttribute, DefinedType, ...) — раньше не находились по
голому имени Dir/Name (fallback покрывал только папочные Dir/Name/Name.xml),
добавлено дописывание .xml.

Тесты (фикстуры не самописные): valid-sessionparameter-basic (meta-compile preRun) +
real-erp-sessionparameter (external, скип на Mac) + error-unknown-type (реальный
каталог корпуса с переименованным корневым тип-элементом). meta-validate 21/21 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:05:51 +03:00
Nick ShirokovandClaude Opus 4.8 b9ed48ccf2 feat(meta-validate): проверка существования ссылочных типов (Фаза 1.2, v1.8)
Check 16: все <v8:Type> вида CatalogRef.X / DocumentRef / EnumRef / DefinedType /
ChartOf*Ref / BusinessProcessRef / ExchangePlanRef / TaskRef проверяются на наличие
объекта в конфигурации (Dir/X.xml или папка Dir/X).

WARN-уровень, не error: ложное срабатывание на частичных выгрузках хуже пропуска.
Расширения (CFE) пропускаются по маркеру ConfigurationExtensionPurpose — их типы
ссылаются на объекты базовой конфигурации, отсутствующие в выгрузке расширения.
Дедуп по ref-ключу; маппинг тип→каталог согласован с TYPE_TO_DIR (verify-snapshots).

Тесты: valid-reftype-resolves (self-ref, -Detailed, OK) + warn-reftype-missing
(WARN, exit 0). meta-validate 18/18 ps1+py. Анти-дрейф OK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 20:41:53 +03:00
Nick ShirokovandClaude Opus 4.8 f8cd7dd681 feat(meta-validate): валидация команд объекта — группа + правило параметра (Фаза 1.1, v1.7)
Check 15: команды в ChildObjects — Group обязателен и валиден (allowlist групп
раздела/формы + CommandGroup.<Имя>); секционная группа (NavigationPanel*/ActionsPanel*)
несовместима с commandParameterType; UUID/Name команды. Зеркало правил meta-compile v1.65
(команды раньше не валидировались вообще → пустой/неверный Group уходил в load-ошибку).
ps1+py идентично.

Тесты: valid-catalog-commands (preRun, группы раздела+формы) + 2 expectError-fixture
(пустая группа; секц.группа+параметр). meta-validate 16/16 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 20:15:57 +03:00
Nick ShirokovandClaude Opus 4.8 ca1c80e164 fix(meta-validate,meta-edit): sync HierarchyType allowlist + анти-дрейф тул (Фаза 0)
Продублированные enum-allowlist-ы meta-validate/meta-edit дрейфнули от meta-compile:
HierarchyType содержал фантом HierarchyItemsOnly вместо HierarchyOfItems (кампания
enum-allowlist подтвердила: ItemsOnly не существует). Валидатор давал неверный вердикт.
Сведено к meta-compile (ps1+py обоих навыков).

tests/skills/check-enum-drift.mjs — гард от повторного дрейфа: сверяет validEnumValues
meta-compile (авторитет) с valid_property_values (validate) и validEnumValues (edit),
выход 1 при расхождении значений. Навыки остаются автономными (allowlist-ы копируются),
тул ловит рассинхрон. Сейчас: OK, дрейфа нет.

meta-validate v1.6, meta-edit v1.11. Тесты 13/13 + 11/11 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 20:05:59 +03:00
Nick ShirokovandClaude Opus 4.8 700ede1df7 feat(meta-compile): валидация category у CommandGroup (v1.65)
Category добавлена в validEnumValues (NavigationPanel/ActionsPanel/FormCommandBar/
FormNavigationPanel) — опечатка даёт ошибку со списком валидных, как у прочих
перечислений. Симметрично валидации group у команд. Зеркало ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:20:54 +03:00
Nick ShirokovandClaude Opus 4.8 95b25b79a1 docs(meta-compile): значения category у CommandGroup
CommandGroup.category — допустимые значения (из upload/meta/CommandGroups):
NavigationPanel/ActionsPanel (командный интерфейс раздела) или FormCommandBar/
FormNavigationPanel (командный интерфейс формы). + связь с command.group=CommandGroup.<Имя>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:18:17 +03:00
Nick ShirokovandClaude Opus 4.8 5d877973a4 docs(meta-compile): терминология групп команд — «раздела/формы» вместо «секционные»
«Секционные» — выдуманный термин, в 1С его нет. Группы противопоставляются как
командный интерфейс РАЗДЕЛА (панель навигации/действий) и ФОРМЫ. Поправлены
reference/blocks.md, тексты ошибок и комментарии (ps1+py). Поведение не изменилось
(имена переменных внутренние; expectError-подстроки на месте).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:15:02 +03:00
Nick ShirokovandClaude Opus 4.8 9ad9c12c9b feat(meta-compile): группы команд — прощающий ввод, allowlist, валидация (v1.64)
Emit-Command: резолв группы (Resolve-CommandGroup) — русские подписи групп
(«Панель навигации.Важное» → NavigationPanelImportant и т.д.), `ГруппаКоманд.X`
→ `CommandGroup.X`. Закрыты два пробела валидации (найдены при 1С-cert):
- пустая/опущенная group → ошибка с подсказкой (список валидных групп), а не
  молчаливый незагружаемый <Group/>;
- секционная группа (NavigationPanel*/ActionsPanel*) + commandParameterType →
  ошибка (тип параметра доступен только для групп формы/CommandGroup — подтверждено
  выгрузкой ЕРП: команды с параметром только в Form*/CommandGroup).
Списки групп из реального erp_8.3.24. Зеркало ps1+py (идентично).

reference/blocks.md: каноничный список групп + правило параметра (без прощающего
ввода — usage-голос). Тесты: catalog-command-groups (рус-синонимы/секц/форма/кастом
через preRun CommandGroup, 1С-cert ✓) + 2 expectError (пустая group; секц+параметр).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:11:53 +03:00
Nick ShirokovandClaude Opus 4.8 63591ef132 test(meta-compile): закрыть 1С-cert пробелы (стабы кросс-ссылок/форм/макетов)
Сплошной verify-snapshots --skill meta-compile был 51/68 — 17 кейсов падали
на «Неизвестный объект метаданных»/картинки/предопределённые (cert-инфра не
создавала объекты, на которые ссылается вход). Все пред-существующие (не
связаны с dataLockControlMode). Доведено до 68/68, ноль регрессий.

verify-snapshots.mjs (инфра):
- getFieldStubs: обобщённый парсер MDObjectRef-путей (Тип.Имя[.ТЧ].Реквизит/
  Измерение/Ресурс.Поле, +рус.синонимы) → богатые стабы; enum-значения и
  предопределённые элементы из fillValue/choiceParameters; регистратор для
  register-стабов. Покрывает FilterCriterion/FunctionalOption/CommonAttribute/
  basedOn/owners и т.д.
- makeStubDSL +CommonPicture/DefinedType/SettingsStorage/CommonTemplate;
  extractTypeRefs +эти паттерны и Characteristic.X→ChartOfCharacteristicTypes.
- Step 5.5: верификатор досоздаёт формы/макеты (form-add/template-add) по
  ссылкам объекта (key-driven, любая нотация; гард поддерживаемых form-add типов).
- postWrite + ветка EventSubscription: стаб CommonModule с телом экспортного
  метода-обработчика.

Правки кейсов (реалистичность, не маскировка — компилятор эмитит верно):
- catalog-command: валидные группы командам (1С требует группу).
- catalog-inputbystring-datalock: owner (валидирует Владелец) + индексированное
  строковое поле inputByString (ссылочное 1С отвергает).
- catalog-characteristics: preRun-фикстура ДопРеквизиты по реальному ЕРП-паттерну
  (ПВХ ВидыСвойств + наборы + регистр с Значение=Characteristic+LinkByType) +
  собственная ТЧ объекта.

NB (в эту задачу не входит): пробелы валидации компилятора по командам — пустой
<Group> из безгруппной команды и неконтролируемое сочетание навигационной группы
с commandParameterType — закрыть отдельно (проверки/умолчания).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:51:40 +03:00
Nick ShirokovandClaude Opus 4.8 f817787f26 feat(form-add): поддержка DocumentJournal (v1.8)
DocumentJournal добавлен в список поддерживаемых типов form-add — журналы
теперь принимают форму (списочную, через общий DynamicList-путь). Нужно для
1С-сертификации журналов, ссылающихся на собственную defaultForm. Зеркало ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 18:50:32 +03:00
Nick ShirokovandClaude Opus 4.8 c574d1b7e8 docs(meta-compile): createOnInput для ПС/ПВР (консистентность charts.md)
В charts.md createOnInput был документирован у ChartOfCharacteristicTypes,
но не у ChartOfAccounts/ChartOfCalculationTypes (в одном файле — несогласованно,
читатель мог решить, что у них его нет). Добавлен с фактическим значением
DontUse (как эмитит компилятор).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:20:04 +03:00
Nick ShirokovandClaude Opus 4.8 4a6060d928 test(meta-compile): стаб документов последовательности для 1С-cert (sequence)
getStructuralDeps не стабил документы, на которые ссылается Sequence
(inp.documents + documentMap) → 1С-cert падал с «Неизвестный объект
метаданных Document.X» и «Ни один документ не участвует в последовательности».
Пред-существующий пробел (не связан с dataLock — падал идентично на до-dataLock
компиляторе). Добавлен case 'Sequence': стаб каждого документа с реквизитами
из documentMap (тип реквизита берётся из соответствующего измерения).
sequence 1С-cert теперь проходит.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:35:10 +03:00
Nick ShirokovandClaude Opus 4.8 b24b7146b2 test(cf-info,cf-validate): закоммитить забытые эталоны with-bot (#36)
Кейс with-bot добавлен в 91196ea6 (поддержка Bot), но эталоны снэпшотов не
были закоммичены — раннер авто-генерил их на каждом --update-snapshots как
untracked, и они постоянно всплывали в git status. Содержимое детерминировано
(UUID нормализованы, никакого random/timestamp), соседние кейсы cf-info
закоммичены. Коммит включает для with-bot байт-сверку выхода наравне с
остальными кейсами.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:32:42 +03:00
Nick ShirokovandClaude Opus 4.8 fd21f5da79 feat(meta-compile,meta-decompile): dataLockControlMode дефолт Managed для авторинга (роундтрип цел)
Единый дефолт Managed вместо per-type Automatic/Managed — удобно для создания
новых объектов (Automatic задаётся явно). Роундтрип остаётся байт-в-байт:
дефолт компилятора и порог omit-on-default декомпилятора сдвинуты синхронно —
объекты с Automatic теперь несут ключ в DSL явно, с Managed — опускают,
итоговый XML с обеих сторон не меняется. Зеркало ps1+py в обоих навыках.
Заодно вылечена латентная рассинхронизация AccountingRegister/CalculationRegister
(компилятор Automatic vs порог Managed).

Верификация: роундтрип-инвариант на реальном корпусе 7/7 match, 0 диффов
(Automatic+Managed справочники, ПС, Sequence, РБ); полный набор тестов 514/514;
пересъём снэпшотов — 91 файл, дрейф ровно DataLockControlMode Automatic->Managed,
ноль посторонних; py-паритет.

meta-compile v1.63 / meta-decompile v0.54

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:11:25 +03:00
Nick ShirokovandClaude Opus 4.8 5988c81244 docs(meta-compile): рекомпоновка reference по слою+форме DSL, usage-голос
Порезал документацию по слою абстракции и форме DSL вместо внутренней
таксономии 1С: тонкий SKILL.md с двухосевым индексом + 13 reference-файлов
(по типу/семейству + кросс-типовые attributes.md/blocks.md) взамен 4 types-*.md.
Вынес в поставку объектную форму реквизита/ТЧ и блоки объекта (fillValue,
choiceParameters, представления, команды, predefined и т.д.) — раньше жили
только в dev-спеке, модель их не видела. Usage-голос: каноничная форма,
дефолты, значения перечислений; без внутряка (синонимы/резолв/декомпилятор,
колонка XML). Починены противоречия дефолтов и убраны фантомные свойства
(по аудиту компилятора).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:10:52 +03:00
Nick ShirokovandClaude Opus 4.8 273296665d test(meta-decompile): минимальный регресс декомпилятора (3 кейса, ps1==py)
meta-decompile был единственным декомпилятором без коммит-кейсов (form/skd/mxl —
имеют). Раньше единственный гард — ad-hoc ps1==py сверка по внешнему корпусу cfsrc
(нет в репо). Кейсы (по образцу skd-decompile: preRun meta-compile → decompile → снэпшот
workDir) закрывают ps1==py (оба рантайма против одного снэпшота) + дрейф/краш:
- catalog-structural: ТЧ use (ForFolderAndItem), одноэлементный choiceParameters (unwrap),
  HE-синонимы аббревиатур в фикстуре (Договоры ЭДО / Ставка НДС).
- ccot-value-type-marker: маркер fillValue:{typeDescription:true} у ValueType ПВХ (B).
- error-bad-root: не-MetaDataObject root (Form.xml) → ring3 exit 3.

Самодостаточны (meta-compile/form-add preRun, без external-фикстур) → гоняются на маке.
NB: снэпшоты включают фикстуру meta-compile → дрейфят при смене его вывода (как form/skd-decompile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:42:39 +03:00
Nick ShirokovandClaude Opus 4.8 f3a9880d96 feat(meta-compile,meta-decompile): роундтрип TabularSection.Use + пустой FillValue v8:TypeDescription (v1.62/v0.53)
Два пре-существующих хвоста, вскрытых регистрочувствительным харнесом на 8.3.24.

A. Свойство Use табличной части (ForItem/ForFolder/ForFolderAndItem — иерархические
   Catalog/ПВХ). Компилятор хардкодил ForItem, декомпилятор не захватывал. Фикс:
   параметр tsUse в Emit-TabularSection (дефолт ForItem) + захват <Use> ТЧ + DSL-ключ
   `use` объектной формы ТЧ (omit при ForItem). Оба порта.

B. Пустой типизированный FillValue стандартного реквизита ValueType ПВХ:
   <xr:FillValue xsi:type="v8:TypeDescription"/> декомпилятор ловил как пустую строку →
   компилятор писал xs:string. Фикс — маркер fillValue:{typeDescription:true} по образцу
   emptyRef (SA-ридер захват + Emit-StandardAttribute эмиссия; только SA-уровень, на
   реквизитах не встречается). Оба порта.

Валидация: корпус 8.3.24 (1765) roundtrip 1765/1765 byte-exact TOTAL 0 (было 6 diff).
Декомпилятор ps1==py. Регресс 511/511 ps+py (правки аддитивные, снэпшоты не поехали).
spec §5 (use) + §4.2 (typeDescription).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:26:58 +03:00
Nick ShirokovandClaude Opus 4.8 e5746c33bd test(meta-edit): пересъём снэпшота под HE-казинг авто-синонима (Инн->ИНН)
Кросс-навыковый дрейф от meta-compile v1.61 (HE-эвристика аббревиатур): фикстура
remove-attribute строится preRun-прогоном meta-compile, авто-синоним реквизита ИНН
теперь сохраняет регистр. Только казинг, состав не менялся.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:09:37 +03:00
Nick ShirokovandClaude Opus 4.8 706cc919a2 feat(meta-compile,meta-decompile): HE-эвристика аббревиатур в авто-синониме + регистрочувствительный авто-вывод (v1.61/v0.52)
Корень: split_camel_case слепо лоуэркейзил хвост, теряя аббревиатуры (НДС/ЕГАИС/ОС/ЭП),
которые платформа сохраняет. Вскрыто py-раундтрипом на маке (АбонентыЭДО: синоним
"Абоненты ЭДО" регенерился как "эдо"). Дважды замаскировано: декомпилятор опускал синоним
регистронезависимо (ne_ci), харнес Compare-Object был без -CaseSensitive. Масштаб survey: 7706.

Фикс (правило подтверждено эмпирически по корпусу): сохранять максимальный прогон заглавных
>=2, если сразу за ним НЕ буква (пробел/цифра/спецсимвол/конец). ">=2" ловит ОС/ЭП; граница
"не буква" отсекает предлоги (РасчетыСКлиентами->склиентами) и бренды (ЮКасса).
- Компилятор Split-CamelCase -> HE (ps1+py), радиус = авто-синонимы + авто-описания предопределённых.
- Декомпилятор Split-CamelWords -> HE (зеркало) + регистрочувствительная страховка (-ne->-cne /
  ne_ci->ne_cs) в 9 точках: кастом-синонимы (ВетИС/МИР) эмитятся дословно.

Валидация 8.3.24 (1765): синонимы/описания закрыты полностью, ноль case-диффов. Декомпилятор
ps1==py 189/189. Регресс 511/511 ps+py. spec §2. Снэпшоты meta-compile — казинг аббревиатур.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:09:36 +03:00
Nick ShirokovandClaude Opus 4.8 4ca89343ae feat(meta-decompile): py-зеркало декомпилятора (v0.51, 35 типов, 2639/2639 byte-exact)
Порт meta-decompile.ps1 → meta-decompile.py 1:1 (lxml.etree для полного XPath 1.0,
собственный JSON-эмиттер зеркалирован, split_camel_words идентичен meta-compile.py).
Трапы: -ne CI vs -cne CS (Predef-счета/виды расчёта); unwrap одноэлементного @() при
return $arr в Parse-ChoiceParameter* (без ,$arr) → скаляр. Валидация: побайтовая сверка
ps1==py 2639/2639 (acc+erp, все 35 типов), паритет кодов выхода.

Переработка SKILL.md: инструкция под реальный кейс — заготовка для сборки НОВОГО объекта
по образцу; предупреждение о потере идентичности (UUID/модули/формы не сохраняются →
не для правки/бэкапа/переноса того же объекта).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 16:55:43 +03:00
Nick ShirokovandClaude Opus 4.8 30d5174351 feat(meta-compile,meta-decompile): поддержка CommonPicture + CommonTemplate (v1.60/v0.51)
31-32-й типы, 4506 объектов acc+erp. Только МЕТАДАННЫЕ + регистрация; содержимое
(Ext/Picture*, Ext/Template.* — бинарь/MXL) вне скоупа (импорт/mxl-compile отдельно).

- CommonPicture: AvailabilityForChoice/AvailabilityForAppearance (дефолт false).
- CommonTemplate: TemplateType (дефолт SpreadsheetDocument, корпус 725).

ПОЛНЫЙ КОРПУС 4506/4506 byte-exact, TOTAL 0, 0 крашей (метаданные). Регресс 70/70
ps1+py, ps1==py identical. spec §7.15g, кейсы common-picture/common-template.
**Модель meta-compile: 32 типа метаданных.**

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 14:05:59 +03:00
Nick ShirokovandClaude Opus 4.8 df42f6cc79 feat(meta-compile,meta-decompile): поддержка SessionParameter/CommonCommand/CommandGroup/CommonAttribute/FunctionalOptionsParameter/WSReference (v1.59/v0.50)
Группа B (простые служебные типы), 25-30-й типы, 1269 объектов acc+erp. Все НОВЫЕ.
ПОЛНЫЙ КОРПУС 1269/1269 byte-exact, TOTAL 0, 0 крашей. Регресс 68/68 ps1+py, ps1==py identical.

- SessionParameter — параметр сеанса (тип значения). FunctionalOptionsParameter — Use(MDObjectRef).
  WSReference — LocationURL +InternalInfo Manager. CommandGroup — Representation/Picture/Category.
- CommonCommand — общая команда (Group/Representation/Picture/CommandParameterType/ParameterUseMode/…)
  + заготовка Ext/CommandModule.bsl. Переиспользован Emit-CommandPicture.
- **CommonAttribute** (сложный) — богатый реквизит + Content(состав объектов {metadata,use,conditionalSeparation})
  + 9 свойств разделения данных + Indexing/FullTextSearch/DataHistory. **Ловушки:** (1) дефолт типа String(0)
  (не $def.type — это тип метаобъекта); (2) FillValue тип-зависим (String→typed-empty, Number→0), но системный
  реквизит-разделитель ОбластьДанных имеет nil на Number → маркер `fillValue:{nil:true}` (аналог emptyRef).
  Прощающий ввод состава — Normalize-MDObjectRef.

spec §7.15f, 6 кейсов. Кросс-дрейфа нет (новые типы). NB: py-декомпилятор отложен.
**Модель meta-compile: 30 типов метаданных.**

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 13:46:23 +03:00
Nick ShirokovandClaude Opus 4.8 2507437eb2 feat(meta-compile,meta-decompile): поддержка типа CommonForm (Общие формы) (v1.58/v0.49)
24-й тип. Решение по общим формам (совместно с пользователем): meta-compile делает то,
что вне компетенции form-compile — МЕТАДАННЫЕ + структуру файлов + регистрацию (как
form-add для форм объектов); СОДЕРЖИМОЕ формы (Ext/Form.xml) наполняет form-compile,
оно НЕ роундтрипится.

- Метаданные CommonForm: FormType(Managed)/IncludeHelpInContents/UsePurposes(набор
  ApplicationUsePurpose, дефолт [Platform+MobilePlatform]Application)/UseStandardCommands
  (дефолт false, корпус 1314/760)/презентации. Без InternalInfo/ChildObjects.
- **Заготовка структуры под компиляцию**: Ext/Form.xml (пустая управляемая форма —
  AutoCommandBar+ChildItems, зеркало form-add) + Ext/Form/Module.bsl + регистрация
  <CommonForm> в Configuration.xml. form-compile/form-edit далее наполняют форму.
- Декомпилятор: гейт +CommonForm; захват formType/usePurposes(omit при дефолте)/
  extendedPresentation; useStandardCommands дефолт false (как Enum).

МЕТАДАННЫЙ роундтрип: ПОЛНЫЙ КОРПУС acc+erp 836/836 byte-exact, TOTAL 0. Регресс 62/62
ps1+py, ps1==py identical (вкл. Form.xml/Module.bsl-заготовку). spec §7.15e, кейс common-form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 13:12:22 +03:00
Nick ShirokovandClaude Opus 4.8 f80446c035 feat(meta-compile,meta-decompile): роундтрип CommonModule + EventSubscription + ScheduledJob (v1.57/v0.48)
Верификация 3 типов, заявленных в validTypes, но НИКОГДА не проверенных роундтрипом
(как и ожидалось — легаси-баги). Декомпилятор снят гейт +3; захват свойств.
ПОЛНЫЙ КОРПУС acc+erp 8017/8017 byte-exact, TOTAL 0. Регресс 61/61 ps1+py, ps1==py identical.

- **CommonModule**: comment был захардкожен пустым → динамический (флаги контекста уже верны).
- **EventSubscription**: (1) comment динамический; (2) **Source переписан на Emit-TypeContent** —
  легаси эмиттер писал `d5p1:CatalogObject.X` для ВСЕХ, но объектные типы в корпусе `cfg:` (harness
  не нормализует d5p1↔cfg для не-Ref) → рассинхрон. (3) **Голые метатипы Source**: Object/RecordSet +
  ConstantValueManager → `<v8:TypeSet>cfg:X`, прочие Manager/List → `<v8:Type>cfg:X` (эмпирика корпуса:
  DocumentManager=Type, DocumentObject/ConstantValueManager=TypeSet). +Sequence/Recalculation в cfgObjectKinds.
- **ScheduledJob**: (1) comment динамический; (2) **Description дефолт синоним→ПУСТО** (корпус 662 пустых/
  209 заданы — синоним-дефолт рвал роундтрип); (3) Description Esc-Xml→Esc-XmlText (кавычки в тексте не экранируем).

Декомпилятор: CommonModule(флаги+returnValuesReuse), EventSubscription(source v8:Type|v8:TypeSet/event/
handler), ScheduledJob(methodName/description/key/use/predefined/restart*). NB: unf_8.5 остаётся с
косметическим xmlns:pal в корне (глобально, вне acc+erp-корпуса). spec-обновление и py-декомпилятор — позже.
Кейсы event-subscription-sources; event-subscription/scheduled-job переснята (d5p1→cfg, Description).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 12:49:49 +03:00
Nick ShirokovandClaude Opus 4.8 f4fb260f68 feat(meta-compile,meta-decompile): поддержка Sequence + FilterCriterion + DocumentNumerator + SettingsStorage (v1.56/v0.47)
20-23-й типы (хвост «мелких» служебных), 64 объекта acc×3+erp. Все НОВЫЕ (компилятор
не поддерживал). ПОЛНЫЙ КОРПУС 64/64 match, TOTAL 0 — byte-exact order-preserved.
Регресс 56/56 ps1+py, ps1==py identical.

- **Sequence** (последовательность документов): InternalInfo(Record/Manager/RecordSet)
  + MoveBoundaryOnPosting(дефолт DontMove)/Documents/RegisterRecords/DataLockControlMode
  (дефолт Automatic). Измерения с **DocumentMap/RegisterRecordsMap** (списки MDObjectRef —
  соответствие реквизитам документов/движениям) — гард общего dimensions-захвата,
  объектная форма измерения. Общий хелпер Emit-MDRefList.
- **FilterCriterion** (критерий отбора): InternalInfo(Manager/List) + Type(составной) +
  Content(объекты отбора) + формы + презентации. Несёт <Command> → эмиссия команд.
- **DocumentNumerator** (нумератор): БЕЗ InternalInfo/ChildObjects. NumberType/Length/
  AllowedLength/Periodicity/CheckUnique (дефолты String/11/Variable/Year/true).
- **SettingsStorage** (хранилище настроек): InternalInfo(Manager) + Default/Auxiliary
  Save/LoadForm (verbatim). Пустой ChildObjects (Form вне скоупа).

Прощающий ввод MDObjectRef (Documents/Content/documentMap) — Normalize-MDObjectRef.
spec §7.15a-d, кейсы sequence/filter-criterion/document-numerator/settings-storage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 11:12:53 +03:00
Nick ShirokovandClaude Opus 4.8 b4e6037fd4 feat(meta-compile,meta-decompile): поддержка типа DocumentJournal (Журналы документов) (v1.55/v0.46)
19-й тип, 82 объекта acc+erp. Рерайт Emit-DocumentJournalProperties/Emit-Column на
общие хелперы (был легаси-хардкод + ПРОПУЩЕН <IncludeHelpInContents>). Декомпилятор
снят гейт +DJ; захват defaultForm/auxiliaryForm/registeredDocuments + колонки
(columns: name/synonym/comment/indexing/references) + SA opt-out.

- **Class-3 фикс: команды журнала** — DJ-блок ChildObjects эмитил только колонки;
  журналы несут полноблочные <Command> (Взаимодействия: 15 команд, DefinedType-параметр)
  → добавлен парсинг+Emit-Command (декомпилятор захватывал общим Commands-ридером).
- IncludeHelpInContents (пропущен легаси) добавлен; comment/useStandardCommands/формы
  (verbatim — имя «Форма»)/презентации — динамические. StandardAttributes always-emit +
  opt-out (~7% опускают); Date Format ДЛФ=D — per-object override (не профиль, 71/203).
- **Class-1: пустой <Synonym/> колонки** ≠ авто → synonym:"" (как EnumValue).
- Прощающий ввод registeredDocuments/references — Normalize-MDObjectRef (русские корни).

ПОЛНЫЙ КОРПУС 82/82 match, TOTAL 0 — byte-exact order-preserved (колонки+команды).
Регресс 56/56 ps1+py, ps1==py identical. spec §7.15, кейс document-journal-full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 22:05:51 +03:00
Nick ShirokovandClaude Opus 4.8 edc8474ddd feat(meta-compile,meta-decompile): поддержка типа FunctionalOption (v1.54/v0.45)
18-й тип, 1381 объект acc+erp. Компилятор НЕ поддерживал FO вовсе (новый тип
с нуля): +validTypes/typePluralMap/dispatch/Emit-FunctionalOptionProperties +
рус.синоним ФункциональнаяОпция. FO без InternalInfo/ChildObjects/модулей —
Emit-InternalInfo уже раннее-возвращает без generatedTypes-записи, ChildObjects/
модули гейтятся списками типов → FO их не получает.

- Свойства: Location (хранилище значения), PrivilegedGetMode (дефолт true —
  корпус 2864/2864), Content (список зависимых объектов → <xr:Object>). Декомпилятор
  снят гейт +FO; захват location/privilegedGetMode(omit-true)/content.
- Прощающий ввод MDObjectRef-путей (Normalize-MDObjectRef): русские корни
  метаданных+подвидов (Документ→Document/ТабличнаяЧасть→TabularSection/Реквизит→
  Attribute/Измерение→Dimension/Ресурс→Resource/…) на чётных позициях-видах; имена
  не трогаются. Только компилятор (декомпилятор пишет полный англ. путь →
  роундтрип byte-exact). Location/Content — полные внешние ссылки, self-формы нет.

ПОЛНЫЙ КОРПУС 1381: 1380 byte-exact order-preserved + 1 MAX_PATH-артефакт харнеса
(имя 157 симв.; объект компилируется в короткий путь ✓). Регресс 55/55 ps1+py,
ps1==py identical. spec §7.7a, кейс functional-option.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 21:47:25 +03:00
Nick ShirokovandClaude Opus 4.8 47134425f3 test(meta-validate): пересъём valid-constant (DataLockControlMode Automatic→Managed, meta-compile v1.53)
Кейс строит константу через meta-compile (preRun); смена дефолта DataLockControlMode
на Managed. Единственное изменение — эта строка.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 21:18:43 +03:00
Nick ShirokovandClaude Opus 4.8 a13538cd16 feat(meta-compile,meta-decompile): поддержка типов Constant + DefinedType (v1.53/v0.44)
16-й/17-й типы, 2692 объекта acc+erp. ПОЛНЫЙ КОРПУС 2692/2692 match, TOTAL 0 —
byte-exact order-preserved. Регресс 54/54 ps1+py, ps1==py identical.

- Constant — «богатый одиночный реквизит»: рерайт Emit-ConstantProperties на общие
  leaf-хелперы (Emit-MinMaxValue/ChoiceParameterLinks/ChoiceParameters/LinkByType/MLText),
  был легаси-хардкод всех свойств. Декомпилятор: захват valueType + свойств значения
  (passwordMode/format/tooltip/mask/min-maxValue/fillChecking/choiceFoldersAndItems/
  choiceParameter*/quickChoice[enum]/choiceForm/linkByType) + object-уровень. QuickChoice
  у Constant — ENUM (Auto), не bool → гард общего bool-хендлера.
- DefinedType — тип-псевдоним: рерайт Emit-DefinedTypeProperties на единый Emit-ValueType
  (был дубль-эмиттер типа; составной через ' + '). Декомпилятор: valueType.
- Class-2: Constant DataLockControlMode Automatic→Managed (корпус 965/78).
- Общий фикс: cfg_object_kinds +ConstantValue — тип ConstantValueManager.X (менеджер
  значения константы, 321) терял cfg:-префикс (regex greedy бил ConstantValue+Manager).

Дефолт UseStandardCommands true (как Report/DataProcessor). spec §7.4/§7.7,
кейсы constant-full/defined-type-full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:55:20 +03:00
Nick ShirokovandClaude Opus 4.8 55f8ece404 test(cf-edit): добавлен пропущенный снэпшот кейса add-bot (#36)
Кейс cf-edit/add-bot.json закоммичен в 91196ea6 (поддержка типа Bot), но его
снэпшот-директория осталась незакоммиченной. Тест проходит 11/11. Закрываю пробел.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:27:14 +03:00
Nick ShirokovandClaude Opus 4.8 582de918fd test: пересъём кросс-навыковых DataProcessor-фикстур (UseStandardCommands false→true, meta-compile v1.52)
36 снэпшотов навыков, использующих meta-compile для сборки host-объекта DataProcessor
(form-*/cf-edit/role-compile). Единственное изменение в каждом — UseStandardCommands
false→true (смена дефолта, mode корпуса). Проверено: git diff содержит только эти строки.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:24:24 +03:00
Nick ShirokovandClaude Opus 4.8 db29324215 feat(meta-compile,meta-decompile): поддержка типов Report + DataProcessor (v1.52/v0.43)
14-й/15-й типы, 2546 объектов acc+erp. Рерайт Emit-ReportProperties/
Emit-DataProcessorProperties на общие хелперы (был легаси-хардкод Comment/
UseStandardCommands/формы/презентации). Декомпилятор: снят гейт +оба;
Report-специфика (defaultForm/mainDataCompositionSchema/*SettingsForm/
defaultVariantForm/variantsStorage/settingsStorage/extendedPresentation),
DataProcessor-специфика (defaultForm/auxiliaryForm/extendedPresentation).

Общие фиксы (не только Report/DataProcessor):
- Emit-VerbatimRef: ссылки форм/схем/хранилищ без Normalize-FormRef — имя формы
  может быть буквально «Форма» (Normalize перевёл бы имя-сегмент Форма→Form).
- Пустой <Type/> (реквизит без типа): маркер typeEmpty (декомпилятор type:"",
  компилятор <Type/> + FillValue nil вместо xs:string). Отличаем present-"" от absent.
- Платформенные типы v8:-префикса (ValueTable/ValueTree/ValueList/StandardPeriod/…),
  current-config cfg:-типы (ConstantsSet/ReportBuilder/*Object.X), выделенные ns
  (Chart/SettingsComposer/SpreadsheetDocument) — компилятор возвращал голое имя.
- Expand-DataPath: голый (отрицательный) индекс-маркер (-8 в ChoiceParameterLinks) verbatim.

Class-2: UseStandardCommands дефолт true (совместно с пользователем — авторски-
безопасно: доступность через стандартный командный интерфейс; при false и без
переопределения размещения команд объект доступен лишь по навигационной ссылке).
Декомпилятор явно фиксирует false. У DataProcessor мода корпуса и так true.

ПОЛНЫЙ КОРПУС 2546: match 2546/2546, TOTAL 0, byte-exact order-preserved (сверено
с реальными 1С-файлами). Регресс 52/52 ps1+py, ps1==py identical. spec §7.11/§7.12,
кейсы report-full/data-processor-full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:24:11 +03:00
Nick ShirokovandClaude Opus 4.8 117a06ff3e feat(meta-compile,meta-decompile): поддержка типа Enum (Перечисления) (v1.51/v0.42)
13-й тип. Рерайт Emit-EnumProperties на общие хелперы (был легаси-хардкод:
Comment/UseStandardCommands/ChoiceMode/формы/презентации/ChoiceHistoryOnInput).
Emit-EnumValue + comment; Parse-EnumValueShorthand больше не стрингифаит synonym
(строка|{ru,en}). Декомпилятор: снят гейт +Enum; захват EnumValue (values,
короткая строка|объект name/synonym/comment); StandardAttributes register-style
opt-out (блок Order/Ref present 85%, absent → standardAttributes:"").

Class-2 фиксы дефолтов (тип-зависимые, декомпилятор зеркалит компилятор):
- useStandardCommands: у Enum дефолт false (не true)
- quickChoice: у Enum дефолт true (не false)
- пустой <Synonym/> значения ≠ авто-синоним → synonym:"" (аналог object-level фикса)

ПОЛНЫЙ КОРПУС 2545 (acc+erp): match 2545/2545, TOTAL 0, 0 крашей — byte-exact,
order-preserved (сверено с реальными 1С-файлами modulo trailing newline+GUID).
Регресс 50/50 ps1+py, ps1==py identical. spec §7.3, кейс enum-full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:04:23 +03:00
Nick Shirokov de2e966311 Merge branch 'dev' into meta-roundtrip
# Conflicts:
#	.claude/skills/meta-compile/scripts/meta-compile.ps1
#	.claude/skills/meta-compile/scripts/meta-compile.py
2026-07-09 12:37:18 +03:00
Nick ShirokovandClaude Opus 4.8 a6ca515c96 fix(meta-compile): #38 — регистрация в Configuration.xml без пересериализации (сохранение xmlns)
Блок «Register in Configuration.xml» py-порта пересоздавал весь файл через
ElementTree.write, который объявляет в корне только namespace из имён элементов/
атрибутов и молча выкидывает объявления, живущие лишь в значениях атрибутов
(xsi:type="app:ApplicationUsePurpose" в UsePurposes). Необъявленный префикс → XDTO
читает значение как anyType → Конфигуратор отказывается грузить Configuration.xml.

Теперь ET используется только read-only (поиск ChildObjects + проверка дубля), а
запись — текстовой вставкой в исходное содержимое с сохранением BOM/EOL/всех xmlns
байт-в-байт (как уже делает subsystem-compile). Многострочная группировка по типу,
обработка self-closing <ChildObjects/>, идемпотентность сохранены.

PS1-порт (XmlDocument хранит xmlns как атрибутные узлы) багу не подвержен —
функциональных правок нет, версия синхронно поднята до v1.15.

tests(runner): снят xmlns-strip нормализатора для Configuration.xml — он маскировал
баг и прятал бы регрессии. Полный python-suite 472/472; на старом коде un-masked
runner ловит поломку (30/33). Радиус изменения нулевой (прочие навыки не роняют xmlns).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 12:26:00 +03:00
Nick ShirokovandClaude Opus 4.8 54225aeaf3 fix(tests): verify-snapshots — поддержка writeFile-шагов в preRun
runPreSteps безусловно вызывал step.script.split('/') и падал на
preRun-шаге типа writeFile (нет поля script) — «Cannot read properties
of undefined (reading 'split')». Из-за этого cfe-borrow/form-bindings
не верифицировался снэпшотами.

Добавлена обработка writeFile-шага (ранний continue с записью файла),
идентично основному раннеру tests/skills/runner.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 21:24:43 +03:00
Nick ShirokovandClaude Opus 4.8 91196ea63f fix(cf-validate): поддержка типа Bot в составе конфигурации (#36)
Тип метаданных Bot (Боты, платформа 8.3.18+) присутствует в
ChildObjects новых конфигураций, но не входил в зашитый список
типов — cf-validate падал с «Unknown type 'Bot'».

- cf-validate: Bot добавлен в список типов и маппинг каталогов (→ Bots)
- cf-edit: Bot в порядке типов + каталог + синоним DSL «бот»
- cf-info: Bot в порядке типов + рус. название «Боты»
- docs/1c-configuration-spec: Bot в перечне ChildObjects (поз. 11)
- tests: фикстуры + кейсы на Bot для cf-validate/cf-edit/cf-info

Позиция Bot — сразу после CommonModule (по выгрузке Конфигуратора).
Разнесение алгоритмов по версии формата/режиму совместимости
остаётся отдельной задачей бэклога.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 18:16:25 +03:00
Nick ShirokovandClaude Opus 4.8 7db3a514c8 test: пересъём кросс-навыковых Document/InformationRegister-фикстур (накопленный дрейф v1.43-v1.45)
Полный регресс вскрыл 21 устаревший снэпшот в 14 навыках (cf-edit/cfe-*/form-add/interface-validate/
meta-edit/meta-info/meta-validate/role-*/subsystem-*), строящих Document/InformationRegister-фикстуры
через meta-compile preRun. Дрейф накопился, т.к. переснимали только прямые кейсы meta-compile.

Затронуты ТОЛЬКО Documents/*.xml и InformationRegisters/*.xml (проверено git diff --name-only).
Каждая ±строка — один из ожидаемых паттернов (проверено остаточным grep'ом, посторонних нет):
- Document (v1.43): all-default StandardAttributes опущен (SA-conditional); CreateOnInput DontUse→Use;
  RegisterRecordsWritingOnPost WriteModified→WriteSelected; FillValue nil→typed-empty.
- InformationRegister (v1.45): DataLockControlMode Automatic→Managed; MainFilterOnPeriod true→false;
  FillValue nil→xs:decimal 0.

Полный регресс после пересъёма: 487/487 ps1 + 487/487 py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:14:40 +03:00
Nick ShirokovandClaude Opus 4.8 8af6b9d88e feat(meta-compile,meta-decompile): закрыт empty-DTR хвост + LineNumber FillValue (v1.49/v0.41)
Последний сквозной хвост всех типов — «пустая ссылка» как значение заполнения
(<FillValue xsi:type="xr:DesignTimeRef"/> без содержимого). Декомпилятор ловил его как
xs:string → тип терялся. Не выводится из типа (DefinedType.X непрозрачен: nil vs empty-DTR
зависит от вида). Фикс: маркер fillValue:{emptyRef:true} — декомпилятор проставляет,
компилятор воспроизводит (4 точки: Emit-FillValue + Emit-StandardAttribute × захват/эмиссия).

Попутно закрыт (B)-хвост Document: FillValue НомерСтроки ТЧ (xs:decimal 0, аномалия 1/1645) —
lineNumber-кастомизация расширена ключом fillValue.

Чистый выигрыш без регресса: Catalog 52→58/58, Document 142→151/151, BusinessProcess 20→25/25,
Task 1→3/3 — все byte-exact TOTAL 0. Регресс 49/49 ps1+py, ps1==py identical (0/20 modulo GUID).
Прежнее «принято как инертный шум» снято — закрыто честно. spec §4.2/§7.1.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 13:24:49 +03:00
Nick ShirokovandClaude Opus 4.8 101ca6e371 feat(meta-compile,meta-decompile): поддержка BusinessProcess + Task (v1.48/v0.40)
Одиннадцатый и двенадцатый типы — ссылочные (БП/Задачи, 25+3 объекта). Оба закрыты modulo
принятый empty-DTR хвост: BusinessProcess 20/25 (остаток empty-DTR), Task 1/3 (остаток empty-DTR
на 2 объектах). Регресс 49/49 ps1+py, ps1==py identical (0/20 modulo GUID). 1С-cert ✓ оба.

BusinessProcess:
- Рерайт Emit-BusinessProcessProperties на общие хелперы в каноническом порядке (был легаси-хардкод:
  Comment/Characteristics/BasedOn/формы/DataLockFields пустые, порядок неверный, пропущены
  NumberPeriodicity/CreateTaskInPrivilegedMode). Class-2: DataLockControlMode Automatic→Managed.
- Декомпилятор: снят гейт; BP-блок (нумерация/task/createTaskInPrivilegedMode); basedOn/dataLockFields/
  characteristics/inputByString уже в общем слое; opt-out standardAttributes:"".
- Фикс ref-типа BusinessProcessRoutePointRef (не был в regex d5p1-префикса → терялся cfg:-префикс).

Task:
- Рерайт Emit-TaskProperties на общие хелперы (порядок + Addressing/MainAddressingAttribute/CurrentPerformer/
  TaskNumberAutoPrefix/DescriptionLength/DefaultPresentation). Class-2: DataLockControlMode→Managed.
- Новый вид дочернего AddressingAttribute: легаси-эмиттер выдавал 8 тегов вместо ~26 → делегирован на богатый
  Emit-Attribute (контекст task-addressing) + AddressingDimension. Декомпилятор: захват AddressingAttribute → addressingAttributes.

spec §7.6c/§7.6d, кейсы business-process/task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 12:13:08 +03:00
Nick ShirokovandClaude Opus 4.8 239499a3f8 feat(meta-compile,meta-decompile): поддержка AccountingRegister + CalculationRegister (v1.47/v0.39)
Девятый и десятый типы — завершают семейство регистров (4 бух + 2 расч, самые сложные типы модели).
Оба byte-exact: AccountingRegister 4/4 match TOTAL 0, CalculationRegister 2/2 match TOTAL 0.
Регресс 49/49 ps1+py, ps1==py identical. 1С-cert ✓ оба (db-load-xml + db-update).

Общее:
- Новые контексты Emit-Attribute register-account/register-calc (ресурсы/измерения через богатый эмиттер).
- Декомпилятор: снят гейт +оба типа; захват спецсвойств; Attr-ToDsl +balance/accountingFlag/
  extDimensionAccountingFlag/baseDimension/scheduleLink; opt-out standardAttributes:"".

AccountingRegister:
- Рерайт Emit-AccountingRegisterProperties: верный порядок + пропущенный EnableTotalsSplitting; comment/
  useStandardCommands/includeHelp/формы/презентации; спецсвойства ChartOfAccounts/Correspondence/PeriodAdjustmentLength.
- Измерение: Balance+AccountingFlag+DenyIncompleteValues; ресурс: Balance+AccountingFlag+ExtDimensionAccountingFlag
  (без Indexing). AccountingFlag/ExtDimensionAccountingFlag — ссылки на признаки учёта ПС.
- SA-кастомизация linkByType (ExtDimensionN→Account): Emit-StandardAttribute +xr:LinkByType, SA-override +захват.

CalculationRegister:
- Рерайт Emit-CalculationRegisterProperties: comment/useStandardCommands/формы/презентации; исправлен порядок
  ChartOfCalculationTypes (был перед Periodicity). SA уже совпадал (переменный по ActionPeriod/BasePeriod).
- Измерение: DenyIncompleteValues+BaseDimension+ScheduleLink; реквизит: ScheduleLink; ресурс: только FullTextSearch.
  Реквизиты РР идут контекстом register-calc (несут ScheduleLink).

spec §7.6a/§7.6b, кейсы accounting-register/calculation-register.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:42:46 +03:00
Nick ShirokovandClaude Opus 4.8 dd22e2e17f test(form-compile-from-object): пересъём accumreg-фикстуры после meta-compile v1.46
Кросс-дрейф: accumreg-list-simple строит РН-фикстуру preRun-прогоном meta-compile.
Ожидаемо — только DataLockControlMode Automatic→Managed (1 строка, форма не тронута).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:51:05 +03:00
Nick ShirokovandClaude Opus 4.8 07a2fd4b4d feat(meta-compile,meta-decompile): поддержка типа AccumulationRegister (Регистры накопления) (v1.46/v0.38)
Восьмой тип, второе семейство регистров (314 объектов acc+erp). Переиспользована инфраструктура
регистров от InformationRegister. ПОЛНЫЙ КОРПУС 314: match 314/314, TOTAL 0, ноль диффов, 0 крашей
(чище InfoReg — register-accum пропускает FillValue, empty-DTR шума нет). Регресс 49/49 ps1+py,
ps1==py identical. 1С-cert ✓ (db-load-xml + db-update).

- Рерайт Emit-AccumulationRegisterProperties на общие хелперы (был легаси-хардкод): comment/
  useStandardCommands/includeHelpInContents/Emit-FormRef(DefaultListForm/AuxiliaryListForm)/презентации(ML).
- Class-2 дефолт: DataLockControlMode Automatic→Managed (корпус 59%).
- Новый контекст Emit-Attribute `register-accum` (ресурсы/измерения через богатый эмиттер): Resource —
  base+FullTextSearch (без Indexing/FillValue/DataHistory); Dimension — +DenyIncompleteValues+Indexing+
  FullTextSearch+UseInTotals (без Master/MainFilter). Флаги shorthand denyIncomplete/nouseintotals.
- StandardAttributes: always-emit + opt-out standardAttributes:"" (present 287/omitted 27 = 9%, не выводимо).
- Декомпилятор: снят гейт +AccumulationRegister; RegisterType/EnableTotalsSplitting capture;
  UseInTotals в Attr-ToDsl (дефолт true→захват при false); AccumReg в opt-out.

spec §7.6, кейс accumulation-register.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:51:05 +03:00
Nick ShirokovandClaude Opus 4.8 cdba298e86 test(meta-compile,verify-snapshots): ПВХ chart-of-characteristic-types — валидный справочник доп.значений (cert-фикс)
Кейс был семантически невалиден и не грузился в 1С (падал db-load-xml на чистом HEAD):
предопределённый «Цвет» имел тип CatalogRef.ЗначенияСвойств, которого НЕТ в типе значения ПВХ,
а стаб-справочник был плоский (не подчинён ПВХ, не указан в CharacteristicExtValues).

Фикс (по образцу реального ВидыСубконтоХозрасчетные→Catalog.Субконто):
- getStructuralDeps: ветка ChartOfCharacteristicTypes — справочник CharacteristicExtValues стабится
  ПОДЧИНЁННЫМ ПВХ (owners=[ChartOfCharacteristicTypes.X]), а не плоским.
- Кейс: +CatalogRef.ЗначенияСвойств в valueTypes (тип предопределённого входит в тип значения ПВХ),
  +characteristicExtValues: "Catalog.ЗначенияСвойств".

Cert ✓ (db-load-xml + db-update). Регресс 49/49 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:04:20 +03:00
Nick ShirokovandClaude Opus 4.8 9a6a93b945 test(form-compile-from-object): пересъём inforeg-фикстур после meta-compile v1.45
Кросс-навыковый дрейф: 3 inforeg-* кейса строят IR-фикстуру preRun-прогоном meta-compile.
Ожидаемый дрейф (только register-XML, формы не тронуты): MainFilterOnPeriod true→false
(расцеплён от periodicity), DataLockControlMode Automatic→Managed, FillValue nil→typed-empty
(ресурсы/измерения через Emit-Attribute).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:51:46 +03:00
Nick ShirokovandClaude Opus 4.8 3d0c8f233e feat(meta-compile,meta-decompile): поддержка типа InformationRegister (Регистры сведений) (v1.45/v0.37)
Седьмой тип, доминанта семейства регистров (2600 объектов). Пилот InformationRegister;
Accumulation/Accounting/Calculation — отдельными заходами позже. Итог 150-выборки: 148 match,
4 = known empty-DTR noise (класс A, как Document), 1 = MAX_PATH-артефакт харнеса. 1С-cert ✓.

- Рерайт Emit-InformationRegisterProperties на общие хелперы (был легаси-хардкод: UseStandardCommands=true,
  Comment/презентации/формы пусто, DataHistory хардкод): comment/useStandardCommands/editType/Emit-FormRef/
  презентации(RecordPresentation ML)/DataHistory-триплет.
- Class-2 дефолты: DataLockControlMode Automatic→Managed (88%); MainFilterOnPeriod расцеплён от periodicity
  (авто-вывод неверен для ~231 объекта) — теперь явное свойство.
- Ресурсы/измерения через богатый Emit-Attribute (context register-info, elemTag Resource/Dimension) вместо
  легаси Emit-Resource/Emit-Dimension (игнорили comment/tooltip/fullTextSearch/fillValue/choiceParameters).
  Dimension-специфика Master/MainFilter/DenyIncompleteValues (+захват Attr-ToDsl + проброс Parse-AttributeShorthand).
  Флаг shorthand master → +FillFromFillingValue=true (конвенция; расцепление обязательно — 203 master+false key-формой).
- Команды регистра: register-branch ChildObjects не эмитил Command → +парсинг+Emit-Command.
- Class-1 декомпилятор: пустой object-синоним <Synonym/> → synonym:"" (аналог EP-фикса; починил и Catalog 52→53).
- StandardAttributes: состав всегда 4 (Active/LineNumber/Period/Recorder), present 2465/omitted 135 (5%, не выводимо) —
  always-emit + opt-out standardAttributes:"" (декомпилятор эмитит при отсутствии блока).

Регресс 49/49 ps1+py, ps1==py identical (modulo random GUID). spec §7.5, кейс information-register.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 18:50:20 +03:00
Nick ShirokovandClaude Opus 4.8 cd6d305eea feat(meta-compile,meta-decompile): object-свойство fullTextSearchOnInputByString (v1.44/v0.36)
Компилятор хардкодил <FullTextSearchOnInputByString>DontUse в 8 местах (все типы),
декомпилятор не захватывал → значение Use терялось при раундтрипе. Корпус: DontUse
2757 / Use 4 (все Catalog) — дефолт эмиссии DontUse верен, чистый omit-on-default
редкий override. Компилятор → Get-EnumProp (+validEnumValues Use/DontUse), декомпилятор
Add-EnumProp дефолт DontUse. Таргет 4/4 match TOTAL 0, регресс 49/49 ps1+py identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:52:12 +03:00
Nick ShirokovandClaude Opus 4.8 89537e9b5d fix(meta-decompile): decimal FillValue терял тип на составном реквизите (v0.35)
Декомпилятор клал xs:decimal FillValue как строку ($fvText) → на составном типе
компилятор берёт xsi-тип из JSON-значения (строка "0" → xs:string, число 0 →
xs:decimal) → тип терялся, регенерился xs:string вместо xs:decimal. У плоского
Number эмиссия и так type-aware, поэтому вылезало только на составных.

Фикс: захват decimal как число ([long]/[double]). Роундтрип Document match
130→142, TOTAL 62→24. Catalog без изменений (52/12), 0 крашей.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:26:30 +03:00
Nick ShirokovandClaude Opus 4.8 87c7ca0e23 fix(meta-decompile): пробельный xs:string FillValue терялся (PreserveWhitespace, v0.34)
Загрузка XML с PreserveWhitespace=false рубила пробельное содержимое → FillValue
из одних пробелов (`<xr:FillValue xsi:type="xs:string">   </>`) декомпилировался
как пустой → компилятор эмитил typed-empty, роундтрип диффил (known NOT COVERED
с v1.27). Включение PreserveWhitespace=true захватывает пробелы, компилятор их
эмитит через Esc-XmlText.

Чистый выигрыш без регрессий (валидировано): Catalog match 44→52, TOTAL 28→12;
Document match 118→130, TOTAL 94→62; 0 крашей, 0 паразитных whitespace-ADDED.

Остаток FillValue — нишевые typed-fillValue (LineNumber decimal-0, composite-Number
type-loss, DTR).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:15:52 +03:00
Nick ShirokovandClaude Opus 4.8 d0e256a42f fix(meta-decompile): одноэлементный FixedArray choiceParameters терялся как скаляр (v0.33)
Get-ChoiceParamValue возвращал 1-элементный ArrayList → PowerShell разворачивал
коллекцию при `return` в скаляр → FixedArray с одним элементом декомпилировался как
скалярное значение → компилятор эмитил `app:value` вместо `v8:FixedArray`+`v8:Value`.
Фикс: унарная запятая `return ,$arr`. Многоэлементные массивы не задевало (не
разворачиваются). Компилятор на явном `["x"]` был корректен — баг чисто декомпиляторный.

Роундтрип Document: Attribute>Value 54→0, TOTAL 166→94, match 109→118/151.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:28:14 +03:00
Nick ShirokovandClaude Opus 4.8 ac3a373348 test(cf-info,form-compile-from-object,meta-info): пересъём Document-фикстур после meta-compile v1.43
Кросс-навыковый дрейф: фикстуры-документы строятся meta-compile — SA-блок стал
условным (опускается без ключа standardAttributes) + дефолты CreateOnInput→Use,
DataLockControlMode→Managed, RegisterRecordsWritingOnPost→WriteSelected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:09:19 +03:00
Nick ShirokovandClaude Opus 4.8 10933e7386 feat(meta-compile,meta-decompile): поддержка типа Document (Документы) (v1.43/v0.32)
Шестой тип. Базовый роундтрип 151-выборки acc+erp: TOTAL 11450→166 (−98.5%),
match 0→109/151, 0 крашей. Регресс 49/49 ps1+py, ps1==py identical. 1С-cert ✓.

Emit-DocumentProperties переписан на общие хелперы (был легаси-хардкод): выбило
Characteristics/формы/BasedOn/InputByString/презентации — декомпилятор их уже
захватывал, баг был чисто компиляторный. SA-conditional + профиль {Date:ShowError}.

Декомпилятор: Document-блок — нумерация, проведение, RegisterRecords (движения),
Numerator, DataHistory-триплет, checkUnique-дефолт.

Class-2 дефолт-фиксы (рассинхрон компилятор↔декомпилятор↔корпус): createOnInput
DontUse→Use, registerRecordsWritingOnPost WriteModified→WriteSelected,
dataLockControlMode Automatic→Managed.

Новые/исправленные ключи (общее для реквизитов, всплыло на Document): markNegatives,
choiceForm/choiceFoldersAndItems, format/editFormat станд.реквизита (SA-override),
TS fillChecking. TS-суппресс `lineNumber: ""` — opt-out наличия TS-блока стандартных
реквизитов (~6% ТЧ его опускают, правило не выводимо; чинит и хвост Catalog).

verify-snapshots: getStructuralDeps стабит регистры registerRecords по MDObjectRef.

Остаток (~166, 2 узких хвоста): choiceParameters FixedArray data-specific edge +
FillValue станд.реквизита decimal/DTR/пробелы. spec §7.2, кейс document-full.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:09:05 +03:00
Nick ShirokovandClaude Opus 4.8 4e6c8311d8 test(form-compile-from-object): пересъём пустого Content.xml ЭП (2-ns→4-ns)
Кросс-навыковый дрейф от meta-compile v1.42: фикстура-объект строится preRun-
прогоном meta-compile, пустой <ExchangePlanContent/> теперь с 4 namespace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:55:18 +03:00
Nick ShirokovandClaude Opus 4.8 11e7abfc3f feat(meta-compile,meta-decompile): состав плана обмена — Content.xml (v1.42/v0.31)
Соседний Ext/Content.xml (состав плана обмена: объекты-участники + признак
авторегистрации) был вне скоупа раундтрипа → при декомпиляции→компиляции состав
терялся (компилятор писал пустой <ExchangePlanContent/>, декомпилятор не захватывал).

DSL content/Состав: массив MDObjectRef — строка "Type.Name" (AutoRecord=Deny,
дефолт) / "Type.Name: autoRecord" (Allow; токен-признак autoRecord/АвтоРегистрация,
регистронезав.; прощающе : Allow/: Разрешить) ЛИБО объект {metadata, autoRecord:
bool|Allow/Deny/Разрешить/Запретить} (синонимы Метаданные/объект, АвтоРегистрация).
Декомпилятор пишет короткую строковую форму.

Class-2 фикс заголовка: пустой <ExchangePlanContent/> писался с 2 namespace,
реальный 1С — с 4 (+xmlns:xs/xsi); чинил и пустой шаблон.

Роундтрип 42 ЭП (acc+erp): match 39/42, TOTAL 146 без новых диффов (остаток —
известный хвост TS-LineNumber/FillValue-пробелы). Регресс 48/48 ps1+py, ps1↔py
identical. 1С-cert ✓ (пустой 4-ns корень + непустой состав); verify-snapshots
расширен: getStructuralDeps стабит объекты состава ЭП по MDObjectRef + стаб Constant.
spec §7.2a, кейс exchange-plan-content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:55:08 +03:00
Nick ShirokovandClaude Opus 4.8 126ce0bbd8 feat(meta-compile,meta-decompile): поддержка типа ChartOfCalculationTypes (План видов расчёта) (v1.41/v0.30)
Пятый тип раундтрипа (после Catalog/EP/ПВХ/ПС). Корпус — 5 дампов 8.3.24.

Декомпилятор: снят гейт +ПВР; дефолты codeLength 5, descriptionLength 100, codeAllowedLength Variable,
dataLockControlMode Automatic; спец-свойства dependenceOnCalculationTypes/baseCalculationTypes(список)/
actionPeriodUse + DataHistory-триплет; профиль StandardAttributes ПВР; захват предопределённых видов расчёта.

Компилятор: Emit-ChartOfCalculationTypesProperties переписан на общие хелперы в каноническом порядке
(был устаревший хардкод). Три платформенно-константные стандартные ТЧ (Leading/Displacing/Base
CalculationTypes — обёртка Synonym пустой-lang «…виды расчета», вложенный CalculationType→FillChecking=
ShowError). StandardAttributes-профиль условный (Наименование→ShowError; ActionPeriodIsBasic в фикс-списке
между DeletionMark и Description). Characteristics (есть у ПВР — между StandardAttributes и std-ТЧ).
Контекст реквизита = account (общий с ПС: как catalog, но без <Use>). BaseCalculationTypes — прощающий
ввод ПланВидовРасчета.X → ChartOfCalculationTypes.X.

Предопределённые виды расчёта — плоские (Name/Code/Description/ActionPeriodIsBase): строка "(Код) Имя
[Наим]" ЛИБО объект с actionPeriodIsBase. Emit-PredefCalcType/Build-PredefinedCalcTypeXml + декомпилятор
PredefCalcType-ToDsl.

Роундтрип 5/5 match, TOTAL 621→0. Регресс 47/47 ps1+py, ps1↔py identical. 1С-cert ✓ (base self-ref +
предопределённые). spec §7.2d, кейс chart-of-calculation-types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 22:42:36 +03:00
Nick ShirokovandClaude Opus 4.8 3e7bfc12c4 feat(meta-compile,meta-decompile): дефолт maxExtDimensionCount ПС завязан на наличие ПВХ (v1.40/v0.29)
В конфигураторе «Количество субконто» недоступно, пока не указан план видов характеристик видов
субконто. Зеркалим: дефолт maxExtDimensionCount = 3 при заданном extDimensionTypes, иначе 0.
Раньше компилятор всегда ставил 3 → план счетов «с нуля» без ПВХ был невалиден для 1С
(«у плана счетов с количеством субконто ≠ 0 должен быть установлен план видов характеристик»).

Декомпилятор зеркалит дефолт (с ПВХ 3, без 0) → omit-on-default сохранён. Роундтрип 3/3 match
(корпус несёт ПВХ → 3 опускается, без изменений). Регресс 47/47 ps1+py, form-compile 12/12.
spec §7.2c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:13:11 +03:00
Nick ShirokovandClaude Opus 4.8 faec6e6f19 test(verify-snapshots): убрать маскирующий hostEdit — верификатор не правит проверяемый объект
Верификатор для плана счетов с субконто без extDimensionTypes сам выдумывал стаб-ПВХ И патчил
план через meta-edit (modify-property ExtDimensionTypes=…). Это маскировка: в 1С грузилась
изменённая версия, а не та, что выдал meta-compile — байт-снэпшот (пустой <ExtDimensionTypes/>)
расходился с загружаемым артефактом. К тому же для главного объекта правка не работала вовсе
(hostEdit в Step 3.5 бил по объекту до его компиляции в Step 4 → «object not found»).

Убран весь механизм hostEdits (единственный источник — ветка ChartOfAccounts в getStructuralDeps).
Теперь верификатор создаёт только стабы ЗАВИСИМОСТЕЙ (объектов, на которые вход РЕАЛЬНО ссылается,
через extractTypeRefs); postEdit правит стабы-зависимости (легитимно), но не проверяемый объект.

Следствие: план с ext-dim обязан сам нести extDimensionTypes. Кейсы form-compile-from-object
(chartofaccounts-item/list) переведены на самодостаточный паттерн — реальный ПВХ ВидыСубконто
в preRun (типизированные предопределённые) + явный extDimensionTypes. Фикстура ПС теперь несёт
<ExtDimensionTypes>ChartOfCharacteristicTypes.ВидыСубконто (консистентно с загрузкой); Form.xml
не изменился. Байт 12/12, 1С-cert обоих ✓. Прочие ссылки на ПС (accounting-register) не затронуты —
makeStubDSL даёт maxExtDimensionCount:0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 21:06:55 +03:00
Nick ShirokovandClaude Opus 4.8 2c50030789 test(meta-compile): регресс субконто/признаков ПС + 1С-серт через preRun ПВХ
Кейс chart-of-accounts расширен до полного: ext-dim (maxExtDimensionCount + extDimensionTypes),
признаки учёта и субконто-признаки, предопределённые счета с субконто (короткая запись + токен
Turnover, обе формы Turnover=false/true). Раньше кейс был урезан (maxExtDimensionCount:0) из-за
1С-серта.

Ключ к серту (идея из обсуждения): нужный ПВХ видов субконто создаётся в `preRun` с
типизированными предопределёнными видами (String(100), в пределах valueType плана), а основной
ввод явно задаёт `extDimensionTypes` → авто-стаб/hostEdit харнеса не срабатывают (правки
verify-snapshots не понадобились). Так решены оба прежних блокера: пустой авто-стаб ПВХ и
ордеринг hostEdit.

Байт-регресс 47/47 ps1+py (ps1↔py identical). 1С-cert ✓ (грузится в 8.3.24).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:40:19 +03:00
Nick ShirokovandClaude Opus 4.8 90a05a58e2 feat(meta-compile,meta-decompile): «Только обороты» как токен-признак субконто (v1.39/v0.28)
«Только обороты» (<Turnover>) — предопределённый (встроенный) признак учёта субконто. Теперь он
выражается токеном `Turnover` (синонимы ТолькоОбороты/«Только обороты») в том же списке признаков,
что и добавленные: "Номенклатура | Turnover, Суммовой". Строковая форма стала полностью
самодостаточной — объектная {type, turnover?, flags?} остаётся принимаемым эквивалентом.

Компилятор вынимает токен turnover из списка flags (регистронезависимо) → <Turnover>true.
Декомпилятор всегда пишет строкой, ставя `Turnover` первым при Turnover=true (объектную форму
больше не порождает). Роундтрип 3/3 match; обе формы + смешанный массив проверены, ps1↔py identical.
Регресс 47/47 ps1+py. spec §7.2c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:30:58 +03:00
Nick ShirokovandClaude Opus 4.8 a08fcc1bcc feat(meta-compile,meta-decompile): короткая запись субконто ПС + фикс регистра описания счёта (v1.38/v0.27)
Субконто предопределённого счёта — компактная строка "Вид | Признак1, Признак2": type — голое
имя значения ПВХ (компилятор разворачивает через extDimensionTypes плана), признаки после '|' —
только TRUE (разворот по def-порядку extDimensionAccountingFlags). Объектная форма {type, turnover?,
flags?} остаётся для turnover=true («Только обороты», синоним `толькоОбороты`). Декомпилятор пишет
строку при turnover=false, объект — при true. Хелпер Resolve-TypePrefixSyn (общий с extDimensionTypes).

Фикс (нашёлся на реальных счетах): PS `-ne` регистронезависим → description предопределённого счёта
с хвостовой аббревиатурой (ОС/НМА) ошибочно опускался (auto==actual регистронезависимо), компилятор
регенерил lowercase через Split-CamelCase. Декомпилятор теперь сравнивает регистрочувствительно (-cne).
Harness маскировал (его семантический дифф регистронезависим) — реальная 1С регистр сохраняет.

Роундтрип 3/3 match. Проверены обе формы субконто (строка/объект, смешанный массив, turnover=true).
Регресс 47/47 ps1+py, form-compile-from-object 12/12. spec §7.2c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:25:21 +03:00
Nick ShirokovandClaude Opus 4.8 7eee8086ea test(form-compile-from-object): пересъём фикстур ПС после эволюции meta-compile
Фикстуры chartofaccounts-item/list строятся preRun-прогоном meta-compile; переписанный
Emit-ChartOfAccountsProperties (v1.37) сменил порядок Properties, добавил обёртку Synonym
StandardTabularSections и сделал StandardAttributes условным (у фикстур нет ключа → блок опущен,
как у Catalog/EP/ПВХ). Дрейф только в ChartsOfAccounts/Хозрасчетный.xml; Form.xml не затронут.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:00:42 +03:00
Nick ShirokovandClaude Opus 4.8 b730687623 feat(meta-compile,meta-decompile): поддержка типа ChartOfAccounts (План счетов) (v1.37/v0.26)
Четвёртый тип раундтрипа (после Catalog/ExchangePlan/ПВХ). Корпус — 3 дампа 8.3.24.

Декомпилятор: снят гейт +ChartOfAccounts; тип-зависимые дефолты (checkUnique=true,
dataLockControlMode=Automatic, codeSeries=WholeChartOfAccounts, defaultPresentation=AsCode);
спец-свойства extDimensionTypes/maxExtDimensionCount/codeMask/autoOrderByCode/orderLength +
DataHistory-триплет; профиль StandardAttributes ПС; захват признаков учёта и предопределённых счетов.

Компилятор: Emit-ChartOfAccountsProperties переписан на общие хелперы в каноническом порядке
(был устаревший хардкод неверного порядка/дефолтов). Всегда эмитится платформенно-константный
блок StandardTabularSections/ExtDimensionTypes (обёртка Synonym пустой-lang «Виды субконто»,
вложенный ExtDimensionType→FillChecking=ShowError). StandardAttributes-профиль ПС условный
(Наименование/Код→ShowError, Родитель→FFV).

Общие фиксы:
- Новый контекст реквизита `account` (= catalog, но БЕЗ <Use> — реквизиты ПС не иерархичны как
  справочник); ПВХ переведён с ошибочного отдельного case на общий с catalog.
- Признаки учёта (AccountingFlag/ExtDimensionAccountingFlag) — переиспользован Emit-Attribute с
  параметром тега + контекст `account-flag` (= account, но без Indexing/FullTextSearch, тип по
  умолчанию Boolean). Старые куцые Emit-AccountingFlag/Emit-ExtDimensionAccountingFlag удалены.
- ent:AccountType — FillValue реквизита Тип: Active/Passive/ActivePassive распознаются
  в Normalize-ChoiceValue. CodeSeries allowlist +WholeChartOfAccounts.

Предопределённые счета — отдельная грамматика (Emit-PredefAccount/Build-PredefinedAccountXml +
PredefAccount-ToDsl): AccountType/OffBalance/Order(вербатим)/AccountingFlags(только TRUE, разворот
по def-порядку признаков плана)/ExtDimensionTypes(субконто {type,turnover?,flags?})/ChildItems.

Роундтрип 3/3 match, TOTAL 25596→0. Регресс 47/47 ps1+py, ps1↔py identical. 1С-cert ✓.
spec §7.2c, кейс chart-of-accounts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 20:00:32 +03:00
Nick ShirokovandClaude Opus 4.8 44bfcf2256 feat(meta-compile,meta-decompile): короткая запись типа предопределённых ПВХ (v1.36/v0.25)
Предопределённые виды характеристик несут тип значения на элемент. Раньше это
требовало объектной формы; теперь тип выражается короткой строкой после ':' —
как в полях СКД/реквизитах:
  "(Код) Имя [Наименование]: Тип"    (тип составной через '+')

Правило: нет ':' → без блока Type (Catalog-стиль); непустой тип → короткая
строка; пустой <Type/> / папки / с детьми → объектная форма (ключ type).
Разбор зеркалит Parse-CalcShorthand: сначала вынуть [Наим] (может содержать ':'),
затем отделить тип по ':'. Компилятор принимает обе формы, декомпилятор пишет
короткую для плоских элементов.

На ВидыСубконтоХозрасчетные все 63 предопределённых из объектов стали
компактными строками. Роундтрип/вывод XML не меняется (52 TOTAL). Регресс 47/47
ps1+py, ps1↔py identical. spec §7.2b, кейс chart-of-characteristic-types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 18:01:50 +03:00
Nick ShirokovandClaude Opus 4.8 01ecd8a52d test(form-compile-from-object): пересъём фикстур ПВХ/EP после эволюции meta-compile
Кейсы ccoct-item-simple и exchangeplan-item-simple строят объект прогоном
meta-compile (preRun) и генерят из него форму. Вывод meta-compile для
ChartOfCharacteristicTypes (v1.35) и ExchangePlan (v1.34) изменился — переписаны
эмиттеры Properties (канонический порядок, условный StandardAttributes, новые
блоки). Пересняты только фикстуры-объекты (ВидыНоменклатуры.xml, ОбменДанными.xml);
формы не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:46:46 +03:00
Nick ShirokovandClaude Opus 4.8 f0840864dc feat(meta-compile,meta-decompile): поддержка типа ChartOfCharacteristicTypes (ПВХ) (v1.35/v0.24)
Третий тип после Catalog/ExchangePlan. Декомпилятор расширен на ПВХ. Компилятор
Emit-ChartOfCharacteristicTypesProperties переписан на общие хелперы в каноническом
порядке; новое — блок Type (тип значения характеристики, составной) и
CharacteristicExtValues; иерархия папки+элементы. StandardAttributes-профиль ПВХ
(Наименование=ShowError, Родитель=FFV=true), блок условный.

Крупная новая фича — предопределённые виды несут ТИП НА ЭЛЕМЕНТ: ключ `type` в
объектной форме predefined (строка/массив; '' → пустой <Type/>). Root-элемент
PlanOfCharacteristicKindPredefinedItems.

Общие фиксы (не только ПВХ):
  • Get-TypeShorthand ищет квалификаторы String/Number/Date по всему typeNode —
    составной тип значения группирует квалификаторы в конце, не сразу за типом.
  • Expand-DataPath гард [:/]: спец-путь 0:GUID/0:GUID (зависимости ПВХ) не разворачивается.
  • Контекст реквизита ПВХ = catalog (у него полные Use/FillFromFillingValue/DataHistory).
  • TabularSection Use, GeneratedType Characteristic-префикс, CodeSeries +WholeCharacteristicKind.

Роундтрип 24 ПВХ: match 0→16, TOTAL 4929→52 (−99%). Остаток (8) — принятые хвосты
(FillValue-пробелы, TS-LineNumber-опущение). Catalog/EP не регрессировали
(108/120, 39/42). Регресс 47/47 ps1+py, ps1↔py identical (6/6 ПВХ). spec §7.2b,
кейс chart-of-characteristic-types (+valueType/predefined-with-type/standardAttributes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:46:26 +03:00
Nick ShirokovandClaude Opus 4.8 d22bebb16f feat(meta-compile,meta-decompile): поддержка типа ExchangePlan (План обмена) (v1.34/v0.23)
Первый тип после Catalog-пилота. Декомпилятор расширен Catalog-only → Catalog+
ExchangePlan (снят гейт, type из XML, тип-зависимые дефолты descriptionLength/
createOnInput/dataLockControlMode, EP-свойства distributedInfoBase/
includeConfigurationExtensions/dataHistory-триплет). Компилятор
Emit-ExchangePlanProperties переписан на общие хелперы (InputByString-derive,
Characteristics, BasedOn, DataLockFields, презентации, формы) в каноническом
порядке — был устаревший (хардкод, пропущенный Characteristics, кривой порядок).

StandardAttributes EP: профиль Description/Code=ShowError + EP условный (блок при
кастомизации; редкий all-default EP его опускает — условная модель это ловит).

Общие фиксы (не только EP):
  • DSL-override стандартных реквизитов применялся лишь для условных типов —
    снят гейт (if $sa), теперь и для не-условных.
  • Доп./опциональные стандартные реквизиты вне фикс-списка (ExchangeDate у части
    EP, легаси) — эмиссия по факту ключа; декомпилятор эмитит по присутствию.
  • Пустой <Synonym/> реквизита ≠ авто-синоним из имени → декомпилятор пишет
    synonym:"" (латентный баг: у Catalog 0/4018, всплыл на EP).

Роундтрип 42 EP: match 0→39, TOTAL 754→146 (−81%). Остаток (3) — принятые хвосты
(TS-LineNumber-опущение, FillValue-пробелы). Catalog не регрессировал (108/120).
Регресс 47/47 ps1+py, ps1↔py identical (8/8 EP). spec §7.2a, кейс exchange-plan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 14:50:28 +03:00
Nick ShirokovandClaude Opus 4.8 8ea310f72e feat(meta-compile): тип реквизита Time (только время) (v1.33)
Реквизит типа Время (DateFractions=Time) компилятор писал <v8:Type>Time</>
(fallback) вместо xs:dateTime + DateQualifiers/DateFractions=Time. Всплыло на
Catalog.Календари (ВремяНачала/ВремяОкончания).

Ветка Date/DateTime обобщена на Date|DateTime|Time (структура одна, различается
лишь DateFractions); + русский синоним Время→Time. Декомпилятор уже отдавал
Time (InnerText DateFractions) → не менялся (v0.22).

Календари acc+erp → match. Регресс 47/47 ps1+py, ps1↔py identical.
spec §3.2/§3.3, кейс catalog-mixed-types (+Time/DateTime).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:23:40 +03:00
Nick ShirokovandClaude Opus 4.8 027e6a4a89 feat(meta-compile,meta-decompile): кастомизация LineNumber табличной части (v1.32/v0.22)
У каждой ТЧ ровно один стандартный реквизит — НомерСтроки (LineNumber), и его
свойства (синоним/подсказка/полнотекстовый поиск/…) тоже переопределяемы.
Раньше компилятор эмитил only-default блок, декомпилятор кастомизацию не ловил.

DSL `lineNumber` на объектной форме ТЧ (omit-on-default по каждому свойству):
synonym/comment/fullTextSearch/tooltip/format/editFormat/choiceHistoryOnInput.
Emit-StandardAttribute расширен (Format/EditFormat через Emit-MLText,
ChoiceHistoryOnInput из ov — были захардкожены); Emit-TabularStandardAttributes
принимает spec и строит ov. Декомпилятор захватывает из
xr:StandardAttribute[@name='LineNumber'] → ключ lineNumber.

Покрыта КАСТОМИЗАЦИЯ (в корпусе 2/1728, только synonym — но задокументирован
полный набор из 7 свойств). 44/1728 ТЧ блок вовсе ОПУСКАЮТ — правило опущения не
выводится (FillChecking/Use не различают опускающие/имеющие), компилятор эмитит
блок всегда → not-covered хвост (решение совместное).

Аддитивно: all-default блок (1684/1728) эмитится идентично, регрессий нет
(TOTAL 78 без изменений). Регресс 47/47 ps1+py, ps1↔py identical. spec §5.1,
кейс catalog-ts-linenumber.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 19:03:26 +03:00
Nick ShirokovandClaude Opus 4.8 177c9bb0fa feat(meta-compile,meta-decompile): extra-свойства реквизита minValue/maxValue/extendedEdit + String(N,fixed) (v1.31/v0.21)
Компилятор хардкодил <ExtendedEdit>false</>, <MinValue nil>, <MaxValue nil> и
AllowedLength=Variable; декомпилятор эти свойства не захватывал. Реквизиты с
диапазоном (год 2000-3999, код цены 1-3), фикс-длина строки, расширенное
редактирование теряли данные при роундтрипе.

Зеркало form-compile:
  • minValue/maxValue — граница диапазона, типизировано: JSON-число →
    xsi:type="xs:decimal", строка → xs:string (тип сохранён декомпилятором из
    XML). Хелпер Emit-MinMaxValue (применён к Emit-Attribute/Dimension/Resource).
  • extendedEdit — bool (многострочное поле).
  • String(N,fixed) → <v8:AllowedLength>Fixed</v8:AllowedLength> (фикс. длина);
    String(N)/String(N,variable) → Variable (дефолт). Парсер типа ps1+py.

Ловушка: Parse-AttributeShorthand строит НОВЫЙ hashtable только с известными
ключами — сначала забыл добавить minValue/maxValue/extendedEdit → $parsed.minValue
был $null, компилятор молча писал nil, хотя декомпилятор значение отдавал.

Роундтрип 120: match 100→108, TOTAL 120→78 (−42), Attribute>ExtendedEdit/
MaxValue/MinValue/AllowedLength=0, новых diff нет. Регресс 46/46 ps1+py, ps1↔py
identical. spec §3.2/§4.2, кейс catalog-attr-props (+min/max/String(3,fixed)/extEdit).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:03:18 +03:00
Nick ShirokovandClaude Opus 4.8 562f4617bc feat(meta-compile): голые метатипы-категории реквизита → TypeSet (v1.30)
Остаток Attribute>TypeSet после v1.21. Голый метатип без имени объекта
(CatalogRef/DocumentRef/EnumRef/9 *Ref + AnyRef/AnyIBRef) означает «любой
объект категории» и в XML — <v8:TypeSet>cfg:X</v8:TypeSet>, а компилятор писал
<v8:Type>X</v8:Type> (fallback: регексы TypeSet/ref ждали ".имя", голое имя
проваливалось). Напр. реквизит типа «любой документ» (DocumentRef) или
«любая ссылка» (AnyRef) разворачивался в конкретный Type.

Фикс: ветка голых метатипов перед concrete-ref match (ps1+py). Декомпилятор
уже отдавал голое имя (Strip-NsPrefix у TypeSet) → не менялся (v0.20).
В составном типе — каждый через "+": "DocumentRef + CatalogRef".

Корпус (частота голых TypeSet): DocumentRef 25, AnyIBRef 17, AnyRef 13,
ExchangePlanRef 10, CatalogRef 9, … Роундтрип 120: match 95→96, TOTAL
158→138 (−20), Attribute>TypeSet=0, новых diff нет. Регресс 46/46 ps1+py,
ps1↔py identical. spec §3.2, кейс catalog-attr-typeset-linkbytype
(+AnyRef/AnyIBRef/композит DocumentRef+CatalogRef).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:57:43 +03:00
Nick ShirokovandClaude Opus 4.8 1212cbd9ed feat(meta-compile,meta-decompile): Command>Picture — структурный блок (v1.29/v0.20)
Ключ `picture` команды компилятор писал плоской строкой, склеивая содержимое
структурного блока: <Picture><xr:Ref>CommonPicture.X</xr:Ref>
<xr:LoadTransparent>false</xr:LoadTransparent></Picture> → «CommonPicture.Xfalse»
(декомпилятор брал .InnerText всего блока). Багофикс формы значения.

Зеркало form-compile/form-decompile:
  • picture — строка-ref (StdPicture.X / CommonPicture.X; встроенная abs: → <xr:Abs>)
    + sibling `loadTransparent` (дефолт true, конвенция кнопки/команды — фиксируем
    только false) ЛИБО объект {src, loadTransparent?, transparentPixel?}.
  • Emit-CommandPicture (компилятор) + структурный захват в декомпиляторе.
Структура {Ref, LoadTransparent} стабильна на корпусе (40/40, Std/CommonPicture).

Попутно (вскрыто roundtrip-сверкой ps1↔py): в py object-form реквизита список
flags не лоуэркейзился (в отличие от строкового пути), из-за чего декомпиляторный
`indexAdditional` не матчил `indexadditional` — PS -contains регистронезависим, а
py `in` нет → py писал DontIndex вместо IndexWithAdditionalOrder. Фикс зеркалит
строковый путь.

Роундтрип 120 объектов: match 94→95, TOTAL 183→158 (−25), Command>Picture=0,
новых diff нет. Регресс 46/46 ps1+py, ps1↔py identical. spec §7.1.3, кейс
catalog-command (3 формы picture: строка/loadTransparent:false/объект+transparentPixel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:48:20 +03:00
Nick ShirokovandClaude Opus 4.8 d72a946eb5 test(cfe-borrow,cfe-diff,cfe-patch-method,cfe-validate): пересъём устаревших фикстур Товары
Снэпшоты заимствованного Catalog.Товары (строятся preRun-прогоном meta-compile)
протухли от двух прошлых изменений meta-compile, не переснятых для cfe-*:
  • df5c1ee1 — all-default <StandardAttributes> платформа опускает → блок снят;
  • ef20f6ec (v1.18) — дефолт CreateOnInput DontUse→Use.

Дифф всех 7 файлов = ровно эти две правки (снос StandardAttributes-блока,
0 добавленных xr:-строк; CreateOnInput DontUse→Use), постороннего нет.
Восстанавливает зелёную кросс-навыковую базу.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:15:28 +03:00
Nick ShirokovandClaude Opus 4.8 65c4fba471 feat(meta-compile,meta-decompile): object-списки InputByString/DataLockFields/BasedOn (v1.28/v0.19)
Три object-блока каталога компилятор хардкодил (InputByString={Descr,Code},
DataLockFields/BasedOn пустые), декомпилятор не захватывал.

Class-2 фикс InputByString: хардкод → вывод из Code/DescriptionLength
([Descr при D>0]+[Code при C>0]) — покрывает 88.5% каталогов, убил каскад
Field ADDED=80 (у 1057/1640 InputByString = только Наименование).

Class-3 DSL:
  • inputByString — массив имён полей (авто-резолв через Expand-DataPath,
    как dataPath), [] = пусто; декомпилятор эмитит только при отличии от дефолта.
  • dataLockFields — поля блокировки, omit-on-empty.
  • basedOn — «ввод на основании», MDObjectRef verbatim, omit-on-empty.
Хелперы Emit-FieldBlock/Emit-BasedOn (+py-порт); декомпилятор Short-Field
(частичная форма StandardAttribute.X/Attribute.X для dogfood резолвера).

Class-2 ловушка PS: пустой @() в позиц.параметр схлопывается в $null, а
@($null) = массив из 1 null → пустой <xr:Field>. Фикс — Where-Object фильтр.

Роундтрип 120 объектов: match 20→94, TOTAL 305→183 (−122), DataLockFields
35→0, InputByString/BasedOn=0, новых diff не внесено. Override-кейсы (реверс,
+реквизит, пустой, codeonly, dlf, basedOn) роундтрипятся чисто. Регресс 46/46
ps1+py, ps1↔py identical. spec §7.1.5, кейс catalog-inputbystring-datalock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:08:42 +03:00
Nick ShirokovandClaude Opus 4.8 e95fb6b619 feat(meta-compile,meta-decompile): кастомизация стандартных реквизитов — FillValue/ChoiceParameterLinks/… (v1.27/v0.18)
Внутри StandardAttributes стандартные реквизиты кастомизируются теми же
свойствами, что и обычные — профиль их не захватывал. Расширен override:
fillValue (DTR-путь/строка/bool через Normalize-ChoiceValue, дефолт nil),
choiceParameterLinks/choiceParameters (переиспользованы эмиттеры реквизита с
xr:-тегом; dataPath — self-резолв Ссылка→StandardAttribute.Ref), comment,
mask, choiceForm. Декомпилятор: парсинг вынесен в Parse-ChoiceParameter*
(namespace-параметр md:/xr:), захват в override.

Закрыто на выборке: ChoiceParameterLinks/Parameters/Comment/Mask=0;
FillValue — DTR-путь(863)/empty-string(150)/bool(21)/string(4). Регресс 45/45
ps1+py, ps1↔py identical. spec §7.1.1, кейс catalog-stdattr-custom.

NOT COVERED (вырожденные, редкие): fillValue стандартного реквизита как
xs:string из пробелов (119, теряется при PreserveWhitespace=false) и пустой
xr:DesignTimeRef (49).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 13:12:52 +03:00
Nick ShirokovandClaude Opus 4.8 8b01d7a6bd fix(meta-compile,meta-decompile): Characteristics edge-кейсы filterValue/-1/0 (v1.26/v0.17)
Досверка на всех 212 объектах корпуса вскрыла 3 недочёта первой версии
(category-objects по «Characteristics» их не ловил — листы отдельные owner'ы):

  • TypesFilterValue: в корпусе доминирует xs:string с ГОЛЫМ именем (315),
    а не DesignTimeRef с полным путём (62); +2 xs:boolean. Захардкоженный DTR
    ломал 315. Теперь через Normalize-ChoiceValue: голое→xs:string, полный
    путь→DTR, bool→xs:boolean, null→xsi:nil.
  • «Пустая» характеристика: поля = -1 → эмитим -1 verbatim (не разворачиваем).
  • DataPathField/MultipleValues* не всегда -1 (иногда 0) → опциональные ключи
    dataPathField/multipleValues*Field, дефолт -1.

remaining ВСЕХ листовых категорий Characteristics = 0 на 212 объектах (match
4→22). Кейс покрывает обе формы filterValue. Регресс 44/44 ps1+py, ps1↔py
identical. spec §7.1.4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 12:26:57 +03:00
Nick ShirokovandClaude Opus 4.8 e2f019d872 feat(meta-compile,meta-decompile): Characteristics — привязка ПВХ (v1.25/v0.16)
Блок «Дополнительные реквизиты и сведения»/контактная инфо. DSL `characteristics`:
массив {types:{from,key,filterField,filterValue}, values:{from,object,type,value}}
— имена зеркалят XML без xr:, -1-поля неявны. Синонимы XML-имён
(characteristicTypes/keyField/…) приняты.

Прощающий ввод (по мотивам dataPath): поля — голое→StandardAttribute.<EN>
(ссылочные Ref/Parent/Owner, RU→EN) / Attribute.<имя>, частичное Dimension.X/
Resource.X/StandardAttribute.X→+from (регистры ДопСведения), полный путь как
есть. from — рус.корни + короткая 3-сегм.→вставка TabularSection. filterValue —
голый предопределённый→+каталог из types.from.

Декомпилятор пишет короткую форму (dogfood: каждый из 212 объектов роундтрип-
тестирует резолвер). Асимметрия для безопасности: голая форма только для
Ref/Parent/Owner, прочие StandardAttribute.X — частичной формой.

remaining Characteristics=0 на ВСЕХ 212 объектах корпуса, регресс 44/44 ps1+py,
ps1↔py identical. spec §7.1.4, кейс catalog-characteristics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:27:56 +03:00
Nick ShirokovandClaude Opus 4.8 e9e3323542 feat(meta-compile): type-подсказка для голых ref-значений choiceParameters (v1.24)
Раньше голое значение параметра выбора (["ПустаяСсылка","ТТН"]) тихо
становилось xs:string вместо DTR — footgun для авторинга с нуля. Причина:
у значения нет типа (тип задаёт поле-фильтр, не реквизит).

Опциональный ключ `type` на элементе choiceParameters (напр. EnumRef.X /
СправочникСсылка.X) разворачивает голые значения через ref-машинку:
value ["EmptyRef","ТТН"] + type EnumRef.X → Enum.X.EmptyRef,
Enum.X.EnumValue.ТТН. Полные пути и скаляры — без изменений; принимает
Ref-форму и голый метатип (Enum.X), рус. корни.

Только компилятор (декомпилятор пишет полные пути) → роундтрип не затронут
(remaining=0). Кейс ТипТТН переведён на type+голые (снэпшот идентичен).
Регресс 43/43 ps1+py, ps1↔py identical. spec §4.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:28:50 +03:00
Nick ShirokovandClaude Opus 4.8 7cf910396b feat(meta-compile): короткая запись dataPath в linkByType/choiceParameterLinks (v1.23)
dataPath ссылается на реквизит самого объекта — прощающий ввод (компилятор
разворачивает по $objType/$objName):
  • стандартный (Ссылка/Ref, Наименование/Description, Владелец/Owner, …) →
    <Тип>.<Имя>.StandardAttribute.<EN> (RU→EN);
  • обычный (Свойство) → <Тип>.<Имя>.Attribute.Свойство;
  • частичное StandardAttribute.X/Attribute.X → +префикс; полный путь → verbatim.

Только компилятор (декомпилятор пишет полный путь) → роундтрип не затронут
(remaining=0 сохраняется). Кейсы choice-params/typeset-linkbytype переведены
на короткую запись (снэпшоты идентичны). Регресс 43/43 ps1+py, ps1↔py identical.
spec §4.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:46:14 +03:00
Nick ShirokovandClaude Opus 4.8 c706b92b03 test(meta-compile): покрыть короткую запись choiceParameters в кейсе
Партнёр переведён на shorthand "Отбор.ЭтоГруппа=false" (вместо объектной
формы) — лочит приём короткой записи параметра выбора. Вывод идентичен,
снэпшот без изменений. Links-shorthand уже покрыт (Валюта).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:38:05 +03:00
Nick ShirokovandClaude Opus 4.8 65de4e5c97 feat(meta-compile,meta-decompile): ChoiceParameterLinks + ChoiceParameters реквизита (v1.22/v0.15)
Ограничение выбора реквизита (связи/параметры) — порт из form-compile
(структура реквизита ⟷ элемента формы совпадает для Links). DSL-ключи:
  • choiceParameterLinks — [{name, dataPath, valueChange?}] ИЛИ строки
    "name=dataPath[:DontChange]"; valueChange дефолт Clear.
  • choiceParameters — [{name, value?}] ИЛИ строки "name=value"; значение
    bool/число/строка/DTR ИЛИ массив (→ FixedArray); без value → nil.

ВАЖНО: в метаданных значение ChoiceParameters ПРЯМОЕ на app:value
(xsi:type=тип), БЕЗ обёртки FormChoiceListDesTimeValue/Presentation (в отличие
от формы) — подтверждено survey (0 wrapper на 591 значений). Массив → app:value
xsi:type=v8:FixedArray с детьми v8:Value. Декомпилятор захватывает оба блока
(namespace app; valueChange=Clear → компактная строка).

remaining ChoiceParameterLinks/ChoiceParameters/value=0 на выборке (rt-cp),
регресс 43/43 ps1+py, ps1↔py идентичны. spec §4.2, кейс catalog-attr-choice-params.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:36:08 +03:00
Nick ShirokovandClaude Opus 4.8 9fec03bfb6 feat(meta-compile,meta-decompile): реквизит-Характеристика — TypeSet + LinkByType (v1.21/v0.14)
Паттерн «Вид субконто»/доп.реквизит: реквизит-значение типа Характеристики ПВХ.
Две связанные категории:
  • TypeSet: тип-множество эмитился только для DefinedType; обобщено на
    Characteristic (`Characteristic.X` → <v8:TypeSet>cfg:Characteristic.X). Оба —
    тип, подразумевающий набор типов.
  • LinkByType (связь по типу — тип значения берётся из реквизита-Вида):
    компилятор писал <LinkByType/> всегда; теперь Emit-LinkByType (порт TypeLink
    из form-compile: DataPath+LinkItem). DSL `linkByType`: {dataPath, linkItem?}
    ИЛИ строка-путь. Декомпилятор захватывает (linkItem=0 → компактно строкой).

Декомпилятор для TypeSet правок не требует (Get-TypeShorthand уже даёт
"Characteristic.X"). remaining TypeSet/LinkByType=0 на выборке (rt-ts), регресс
42/42 ps1+py, ps1↔py идентичны. spec §3.2/§4.2, кейс catalog-attr-typeset-linkbytype.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:57:46 +03:00
Nick ShirokovandClaude Opus 4.8 c7067dd319 test: пересъём снэпшотов, дрейфующих от эволюции meta-compile (StandardAttributes)
Снэпшоты 20 навыков строятся preRun-прогоном meta-compile и отстали от его
текущего вывода. Дрейф проверен построчно — исключительно известные изменения
meta-compile, поломок навыков не замаскировано:
  • <StandardAttributes>-блок больше не эмитится для некастомизированных
    объектов (условная эмиссия, df5c1ee1);
  • object-level CreateOnInput DontUse→Use;
  • boolean FillValue false→nil.
Ни одной удалённой/добавленной строки вне этих паттернов. Затронуты form-*,
role-*, cf-*, subsystem-*, interface-*, meta-info/remove, help-add. Все наборы
зелёные.

Примечание: meta-compile ещё в активной разработке (meta-roundtrip) — впредь
пересъём затронутых наборов включаем в петлю каждого изменения meta-compile.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:15:38 +03:00
Nick ShirokovandClaude Opus 4.8 5a9b111dc0 test(meta-edit): исправить формат кейсов на документированный JSON DSL
9 кейсов задавали правки через `{operations:[{op,...}]}` — такого формата
meta-edit не понимает (ключ не в add/remove/modify), поэтому операции молча
пропускались (Warn «Unknown operation»), и снэпшоты фиксировали лишь исходный
скомпилированный объект. Тесты фактически ничего не проверяли.

Переписаны на формат из json-dsl.md: add/remove/modify → attributes/
enumValues/tabularSections/properties (shorthand-строки). Снэпшоты
переснятые — теперь реально содержат эффекты (ИНН добавлен, Устаревший
удалён, Телефон→НомерТелефона, CodeLength=11, ТЧ с реквизитами, Цена в ТЧ).
Регресс 11/11 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:37:46 +03:00
Nick ShirokovandClaude Opus 4.8 d007da5eb8 feat(meta-edit,meta-validate): типозависимый отказ на зарезервированные имена реквизитов
Имя собственного реквизита, совпадающее со стандартным (англ. ИЛИ рус.,
регистронезависимо), платформа не примет — теперь жёсткий отказ вместо
предупреждения (как в meta-compile).

meta-edit: reservedByContext (catalog/document) в Build-AttributeFragment;
прочие контексты — прежнее предупреждение, реквизиты ТЧ не проверяются.
meta-validate Check 7b: было плоско и только по англ. именам с Warn — стало
типозависимо через standardAttributesByType, EN+RU, Report-Error.

Проверка типозависима: «Номер» — легальный реквизит справочника (стандартный
только у документа). Негативные кейсы error-reserved-attr (meta-edit +
meta-validate с фикстурой). Регресс 11/11 и 13/13 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:30:34 +03:00
Nick ShirokovandClaude Opus 4.8 c2703f043f test(meta-edit,meta-validate): пересъём устаревших снэпшотов
Снэпшоты meta-edit/meta-validate строятся preRun-прогоном meta-compile и
отстали: содержали безусловный <StandardAttributes> (до условной эмиссии
df5c1ee1) — теперь блок опускается для некастомизированных объектов. Заодно
подхватился object-level CreateOnInput DontUse→Use. Регресс 10/10 и 12/12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:23:16 +03:00
Nick ShirokovandClaude Opus 4.8 b4d48ca656 feat(meta-compile): отклонять имена реквизитов, совпадающие со стандартными (v1.20)
Раньше совпадение имени собственного реквизита со стандартным (Код,
Наименование, Владелец, Родитель, Ссылка, Предопределённый, …) давало лишь
предупреждение — теперь жёсткий отказ (платформа такое имя не примет).

Проверка типозависима (reservedByContext): у справочника один набор
стандартных имён, у документа — другой. Напр. «Номер» — легальный реквизит
справочника, отклоняется только у документа. Прочие контексты сохраняют
мягкое предупреждение по плоскому списку; реквизиты ТЧ не проверяются.

Кейс catalog-object-props: «Код»→«КодНастройки» (было ложное имя). Новый
негативный кейс catalog-attr-reserved-name. spec §4. Регресс 41/41 ps1+py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:07:55 +03:00
Nick ShirokovandClaude Opus 4.8 1565ae23ab feat(meta-compile): прощающий ввод ссылок на формы по умолчанию (v1.19)
Default*Form/Auxiliary*Form нормализуются как fillValue-ссылки: русский
корень (Справочник→Catalog), сегмент Форма→Form, и короткая запись без него
(Справочник.X.ФормаЭлемента ≡ Справочник.X.Форма.ФормаЭлемента ≡ канон).
Канон англ. — идемпотентно (снэпшоты не меняются). Декомпилятор эмитит
канон, роундтрип не затронут.

Кейс catalog-object-props переведён на русскую форму (лочит нормализацию),
spec §7.1. Регресс 40/40 ps1+py, ps1↔py идентичны.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:00:59 +03:00
Nick ShirokovandClaude Opus 4.8 ef20f6ece8 feat(meta-compile,meta-decompile): батч object-свойств каталога (v1.18/v0.13)
Object-уровневые свойства каталога, которые компилятор хардкодил, стали
DSL-ключами omit-on-default (зеркало батча реквизитов). Class-2 фикс:
CreateOnInput дефолт DontUse→Use (модальное значение платформы, 1163).

Новые скалярные ключи: useStandardCommands, editType, includeHelpInContents,
choiceHistoryOnInput, predefinedDataUpdate, searchStringModeOnInputByString,
createOnInput. Object comment теперь пробрасывается. 10 ключей форм по
умолчанию (default*Form/auxiliary*Form) — ссылки verbatim. Декомпилятор
эмитит все эти ключи (+ уже поддержанные defaultPresentation/quickChoice/
choiceMode/fullTextSearch). Хелперы Get-BoolProp (presence-aware) и
Emit-FormRef; +4 enum в validEnumValues.

Все root-уровневые object-property диффы = 0 на выборке (rt-op, baseline
~5.6k), регресс 40/40 ps1+py (12 снэпшотов CreateOnInput DontUse→Use).
spec §7.1, кейс catalog-object-props.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:57:30 +03:00
Nick ShirokovandClaude Opus 4.8 f11fe6808d docs(meta-dsl-spec): убрать строку версии (остальные *-dsl-spec её не ведут)
Версию вёл только meta-dsl-spec.md; form/mxl/role/skd-dsl-spec — без неё.
«Версия формата: 2.17» в epf/erf/role-спеках — версия XML-формата платформы,
другое понятие. Приведено к общей конвенции DSL-спек.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:53:55 +03:00
Nick ShirokovandClaude Opus 4.8 b93fd0807f feat(meta-compile,meta-decompile): значение заполнения реквизита fillValue (v1.27/v0.12)
Пара FillFromFillingValue+FillValue — единый блок «заполнения» (у top-level
реквизитов всегда, у ТЧ нет). Класс-2 фикс: boolean дефолт пустого FillValue
false→nil (модальная форма платформы, 3734 мисматча). Дефолт по типу:
String→typed-empty, Number→0, всё прочее→nil.

DSL-ключ fillValue (интерпретация по типу реквизита): bool/число/строка/дата-
литерал + null→nil-override + DTR-путь (полный рус/англ, GUID.GUID, короткая
запись по типу: EmptyRef / имя-значения перечисления / предопределённое).
Ref-резолвер портирован из form-compile (+ПустаяСсылка/Истина/Ложь, гард
составного типа). Декомпилятор: захват FillValue с omit-при-дефолте.

remaining=0 на выборке категории Attribute>FillValue (baseline-impact 7222),
регресс 39/39 ps1+py. spec v2.7 §4.2, кейс catalog-attr-fillvalue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 17:46:31 +03:00
Nick ShirokovandClaude Opus 4.8 d8146e1f1b feat(meta-compile,meta-decompile): команды объекта (Command + CommandModule.bsl) (v1.26/v0.11)
Новая возможность (не только раундтрип): meta-compile теперь умеет добавлять команды объекту
(раньше не умел вовсе). Корпус: 243 команды в 133 справочниках.

DSL `commands` (map имя→объект ИЛИ array): synonym/tooltip (ML, авто-синоним), comment, group,
commandParameterType (тип), parameterUseMode (Single), modifiesData (false), representation (Auto),
picture/shortcut, onMainServerUnavalableBehavior (Auto). Все omit-on-default.

- meta-compile (дуал-порт): Emit-Command → <Command>-блок в ChildObjects (после ТЧ) + генерация
  Commands/<Имя>/Ext/CommandModule.bsl с заготовкой обработчика ОбработкаКоманды.
- meta-decompile: захват команд из ChildObjects (тела модулей — вне скоупа, как ObjectModule).
- spec §7.1.3; тест-кейс catalog-command.

Валидация: PS==PY (XML+модуль); Command-категория 0; −6244 (53436→47192); регресс 38/38; 1С-cert
зелёный (справочник с командой+параметр-типом+модулем грузится в платформу).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:58:03 +03:00
Nick ShirokovandClaude Opus 4.8 1701bdac96 feat(meta-compile,meta-decompile): презентации объекта (Object/List/Extended*/Explanation, ML) (v1.25/v0.10)
Раундтрип-находка: презентации мультиязычны и непусты у многих справочников (корпус: ObjectPresentation
1316/803-multi, ListPresentation 208, Explanation 101, Extended* 69/75), компилятор писал пусто.

DSL-ключи (ML, omit-on-default): objectPresentation, extendedObjectPresentation, listPresentation,
extendedListPresentation, explanation. Эмиссия через Emit-MLText во всех 8 местах (типы, где эти свойства
есть — безопасно: без ключа → self-close = прежнее поведение). Декомпилятор захватывает через Get-MLValue.

spec §7.1; тест-кейс catalog-attr-ml расширен.
Валидация: PS==PY; ObjectPresentation/ListPresentation impact 0; −17135 (70571→53436, вкл. добитый
пробельный ToolTip-остаток нормализацией); регресс 37/37; 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:37:39 +03:00
Nick ShirokovandClaude Opus 4.8 40cdd57619 feat(meta-compile,meta-decompile): ML-синоним + ToolTip стандартных реквизитов (v1.24/v0.9)
Раундтрип-находка: у стандартных реквизитов синоним мультиязычен (корпус: 340 multi + 206 ru),
ToolTip непустой у 722 — компилятор писал синоним ru-only и <xr:ToolTip/> пусто, декомпилятор брал
Get-MLru (ru). Расширение существующего механизма standardAttributes.

- meta-compile (дуал-порт): Emit-StandardAttribute — синоним и ToolTip через Emit-MLText (строка|{ru,en});
  DSL standardAttributes.X.tooltip + synonym без стрингификации. Заодно Emit-MLItems → Esc-XmlText
  (кавычки в тексте элемента raw, не только у predefined).
- meta-decompile: синоним/tooltip станд. реквизита через Get-MLValue; Get-MLValue → $null при пустом
  ru-содержимом (пустой ML-item ≡ отсутствие значения, не tooltip:"").
- spec §7.1.1 (synonym/tooltip — ML); тест-кейс catalog-standard-attributes расширен.

Валидация: PS==PY; ToolFip станд.реквизита 5903→404 (остаток — косметика пустой-item≡self-close);
−9049 (79620→70571); регресс 37/37; 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:57:36 +03:00
Nick ShirokovandClaude Opus 4.8 03de2dc86d feat(meta-compile,meta-decompile): объектная форма табличной части (синоним/подсказка/комментарий ТЧ) (v1.23/v0.8)
Раундтрип-находка: синоним ТЧ кастомный/мультиязычный у 84% (корпус 1728 ТЧ: multi 1032, custom-ru 417,
auto лишь 279), ToolTip у 24% (413). Компилятор хардкодил синоним=Split-CamelCase, Comment/ToolTip пусто.

DSL: значение ТЧ — массив колонок (синоним авто) ЛИБО объект {synonym, tooltip, comment, attributes/columns}
(по образцу реквизита: shorthand vs object). synonym/tooltip — ML.

- meta-compile (дуал-порт): нормализация ТЧ → {columns, synonym, tooltip, comment}; Emit-TabularSection
  параметризован (synonym через Emit-MLText, Comment/ToolTip из DSL).
- meta-decompile: ТЧ → объектная форма при кастомном синониме/подсказке/комментарии, иначе массив.
- spec §5.

Валидация: PS==PY; TS-категории ~0 (item 4566→6, ToolTip 2188→44); −10067 (89687→79620); регресс 37/37;
1С-cert зелёный. Остаток TabularSection>Synonym (~50) — станд. реквизит LineNumber ТЧ (отдельная категория).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:33:18 +03:00
Nick ShirokovandClaude Opus 4.8 72d6d47454 feat(meta-compile,meta-decompile): мультиязычный синоним объекта + терминология spec (v1.22/v0.7)
Раундтрип-находка: синоним САМОГО объекта (уровня справочника) мультиязычен у 950 справочников
корпуса (все erp: ru+en), но компилятор стрингифаил его ($synonym → ru-only), декомпилятор брал
Get-MLru (ru-only) → потеря en.

- meta-compile (дуал-порт): $synonym/synonym пробрасывается без стрингификации (строка ИЛИ {ru,en})
  → Emit-MLText (уже мультиязычный). Гард на плоском Description-fallback (ScheduledJob) для не-строки.
- meta-decompile: синоним объекта через Get-MLValue (строка ru | {ru,en}).
- spec: терминология «каталог» → «справочник» (тип Catalog = Справочник; файловая директория не тронута).

Валидация: PS==PY; byte-match erp-справочника; −3772 (93459→89687); регресс 37/37; 1С-cert зелёный.
Остаток item/ToolTip того уровня — синоним/подсказка ТЧ + xr:ToolTip станд. реквизита (следующие заходы).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:58:00 +03:00
Nick ShirokovandClaude Opus 4.8 5f53c3f02e feat(meta-compile,meta-decompile): батч object-свойств реквизита (консистентно с form) (v1.21/v0.6)
Раундтрип-кластер extra-свойств реквизита. Компилятор хардкодил дефолты, реальные значения
варьируются (корпус 32642 реквизита): FullTextSearch DontUse 10531, Comment 7993, Mask 1489,
Format/EditFormat 684/686, Use ForFolderAndItem 1299, CreateOnInput/QuickChoice/FillFromFillingValue/
DataHistory. По философии DSL — в объектную форму (shorthand несёт только частое: req/index/multiline).

Object-ключи (omit-on-default), имена согласованы с form-compile где применимо: comment, fullTextSearch,
mask, format/editFormat (ML), use, createOnInput, quickChoice, dataHistory, fillFromFillingValue,
passwordMode, choiceHistoryOnInput. Прощающий ввод: fillCheck→fillChecking (bool true→ShowError),
quickChoice bool (true→Use/false→DontUse). Emit-Attribute параметризован; декомпилятор переключается
на object-форму при любом непокрытом свойстве.

spec v2.6 §4.2 (полная таблица ключей); тест-кейс catalog-attr-props.
Валидация: PS==PY паритет; категории батча 0 (FullTextSearch/Comment/Mask/Format/Use/...);
полный прогон −68458 (161917→93459); регресс 37/37 (ps+py); 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:46:14 +03:00
Nick ShirokovandClaude Opus 4.8 53ed8c4489 fix(meta-compile): типы ValueStorage/UUID → v8:ValueStorage/v8:UUID + прощающие синонимы (v1.20)
Раундтрип-находка (класс-2): meta-compile эмитил ValueStorage как xs:base64Binary, а канон 1С —
v8:ValueStorage (2954 реквизита корпуса; xs:base64Binary как тип реквизита НЕ встречается — только
в Template.xml). UUID эмитился raw. Оба типа теперь эмитятся канонически.

Прощающий ввод (по идее: модель может ошибиться формой): base64Binary / ХранилищеЗначений /
ХранилищеЗначения → ValueStorage; УникальныйИдентификатор → UUID.

spec v2.5 (§3.1 типы + §3.3 синонимы); тест-кейс catalog-mixed-types расширен ValueStorage/UUID.
Валидация: v8:Type-категория 0 на таргет-выборке; регресс 36/36 (ps+py); 1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:08:38 +03:00
Nick ShirokovandClaude Opus 4.8 253a4fbce0 feat(meta-compile,meta-decompile): предопределённые элементы Catalog (Ext/Predefined.xml) (v1.19/v0.5)
Раундтрип-находка (класс-3, непокрытый блок — крупнейшая категория ~56k). meta-compile вообще
не генерил Ext/Predefined.xml. DSL спроектирован по данным корпуса (9309 элементов) + совместно.

DSL `predefined` (массив; строка ИЛИ объект):
- строка "(Код) Имя [Наименование]": Имя обяз.; Наименование — нет [..] → авто(Split-CamelCase),
  [] → пусто (системные/placeholder, 29% корпуса), [текст] → задано;
- объект (группа/иерархия): name/code/description/isFolder/childItems + рус.синонимы
  имя/код/наименование/группа/подчиненные (прощающий ввод).
- Код: по codeType каталога (Number→xs:decimal, String→без типа), пусто→<Code/>.

meta-compile (дуал-порт): генерация Ext/Predefined.xml (root CatalogPredefinedItems + рекурсия Item).
Заодно фикс: Esc-XmlText для ТЕКСТА элемента (& < > без экранирования кавычек — в тексте 1С держит raw).
meta-decompile: захват predefined → компактный DSL (строка/объект). Фикс PS-ловушки распаковки
одноэлементного @() из if → папки с ОДНИМ ребёнком теряли его.
spec meta-dsl-spec.md v2.4 §7.1.2; тест-кейс catalog-predefined.

Валидация: PS==PY паритет; predefined-категории 0 (PredefinedData/Item/ChildItems/Description/Code);
полный прогон −56620 (218537→161917); регресс 36/36 (ps+py); 1С-cert зелёный (грузится в платформу).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:48:16 +03:00
Nick ShirokovandClaude Opus 4.8 58bc95264e feat(meta-compile,meta-decompile): мультиязычный ML для синонима и подсказки реквизита (v1.18/v0.4)
Раундтрип-находка (класс-1 синоним + класс-3 подсказка). Корпус: у реквизитов 21677 мультиязычных
синонимов (ru+en) и 16063 непустых подсказки (10689 мультиязычных). meta-compile писал ВСЁ ML только ru
(Emit-MLText брал строку) и хардкодил <ToolTip/> пустым → массовая потеря en + подсказок.

- meta-compile (дуал-порт): Emit-MLText/emit_mltext принимают строку (→ru) ИЛИ объект {lang:content}
  (→<v8:item> на язык, в порядке ключей) через новый Emit-MLItems/emit_ml_items. Parse object-форма
  реквизита пробрасывает synonym/tooltip без стрингификации; Emit-Attribute эмитит <ToolTip> из tooltip.
- meta-decompile: Get-MLValue → строка (ru-only) | {ru,en} (мультиязычно, порядок из XML); object-форма
  реквизита при кастомном синониме ИЛИ наличии подсказки.
- spec meta-dsl-spec.md v2.3: §4.2 (tooltip) + §4.4 ML-значения (строка/{ru,en}), консистентно с form-compile.
- тест-кейс catalog-attr-ml (мультиязычный синоним + подсказка, обе формы).

Валидация: PS==PY паритет; полный прогон −228748 строк (447285→218537, >половины); регресс 35/35 (ps+py);
1С-cert зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:18:01 +03:00
Nick ShirokovandClaude Opus 4.8 ed42d5e9cb refactor(meta-compile): единый data-driven эмиттер StandardAttributes вместо форка (v1.17)
Свернул два форка (Emit-StandardAttributes безусловный + Emit-StandardAttributesProfiled) в
ОДНУ функцию, поведение которой правят справочники, а не код:
- stdAttrConditionalTypes (пока {Catalog}) — типы, где блок только при DSL-ключе standardAttributes;
- stdAttrProfile[тип] — профиль материализованного блока.
Прочие 13 типов (не в справочниках) ведут себя ровно как раньше (безусловный all-default).

Убирает легаси-мост и будущий rename: вызов Catalog вернулся к каноничному
Emit-StandardAttributes(i,'Catalog'); нет второй функции/суффикса _profiled. Миграция типа =
+строчка в два справочника + переснять снэпшоты, кода не трогаем.

Поведение не изменилось: byte-match блока на синтетике, регресс 34/34 (ps+py), снэпшоты не тронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:21:39 +03:00
Nick ShirokovandClaude Opus 4.8 df5c1ee17d feat(meta-compile,meta-decompile): условный профильный StandardAttributes для Catalog (v1.16/v0.3)
Раундтрип-находка (класс-2 + новый DSL-блок). Правило платформы, выведенное из корпуса
(1596/1640 каталогов) и подтверждённое синтетикой: блок <StandardAttributes> материализуется
ТОЛЬКО при кастомизации ≥1 стандартного реквизита; 0 all-default блоков, 44 голых без блока.
При материализации платформа заполняет характеристический профиль (Owner{FC=ShowError,FFV=true},
Parent{FFV=true}, Description{FC=ShowError}), не зависящий от иерархии/владельца.

Было: meta-compile писал блок ВСЕГДА единым all-default шаблоном (неверные пер-атрибутные
дефолты) → ADDED-блок у 44 голых + потеря профиля/кастомизаций у 1596.

Стало:
- meta-compile (дуал-порт): блок эмитится только при наличии DSL-ключа `standardAttributes`
  (map реквизит→{synonym,fillChecking,fillFromFillingValue,fullTextSearch,dataHistory}); база =
  профиль типа (stdAttrProfile), поверх — override. Emit-StandardAttribute параметризован (ov).
  Эмиттер общий (Emit-StandardAttributesProfiled $type) — подключён только Catalog; прочие 13
  типов пока на старом безусловном пути (мигрируем при их пилоте).
- meta-decompile: захват `standardAttributes` как отклонений от профиля (блока нет → ключ опущен).
- spec meta-dsl-spec.md v2.2: раздел 7.1.1 (правило, профиль, формат).
- тест-кейс catalog-standard-attributes (профиль + override synonym + Code.fillChecking).

Валидация: byte-match блока на синтетике; полный прогон −27k строк (474339→447285), обвал
cascade ADDED 11538→1242; регресс 34/34 (ps+py); 1С-cert снэпшотов зелёный.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:12:56 +03:00
Nick ShirokovandClaude Opus 4.8 85e13e95f2 feat(meta-decompile): захват кастомного синонима реквизита (object-форма) + точное зеркало Split-CamelCase (v0.2)
Раундтрип-фикс (класс-1, декомпилятор): реквизит с синонимом ≠ авто теперь эмитится
в object-форме {name, type, synonym, flags} вместо shorthand (который синоним терял).
Синоним==авто → компактный shorthand как прежде.

Split-CamelWords переписан байт-в-байт под Split-CamelCase компилятора (прежняя версия
имела лишние правила: цифры, подряд-заглавные → риск ложных «синоним==авто»).

Срез ~12k строк на полном прогоне (1640 каталогов). Пометка: predefined-эмиссия
остаётся черновой; StandardAttributes-кастомизации — следующая категория (в обсуждении).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:37:32 +03:00
Nick ShirokovandClaude Opus 4.8 7000593561 fix(meta-compile): allowlist HierarchyType/CodeSeries — фантомное значение + пропуск (v1.15)
Раундтрип-находка (meta-roundtrip, класс-2): meta-compile падал на 90 каталогах
acc+erp из-за неполных/неверных enum-allowlist'ов:
- HierarchyType содержал ФАНТОМНОЕ "HierarchyItemsOnly" (в 1С такого значения нет;
  реальное — "HierarchyOfItems", 61 каталог) → компилятор сгенерил бы битый XML.
- CodeSeries без "WithinOwnerSubordination" (36 каталогов) → краш на валидном значении.

Значения сверены с корпусом (acc+erp 8.3.24). Фикс в обоих портах. После: dec-fail
90→0 на таргет-прогоне, регресс 33/33 (ps+py).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:17:24 +03:00
Nick ShirokovandClaude Opus 4.8 8b8620ae44 feat(meta-decompile): скрытый навык-декомпилятор объектов 1С (пилот Catalog)
Старт раундтрип-захода по meta-compile (как form/skd-roundtrip): scaffold скрытого
навыка meta-decompile (XML объекта → JSON-черновик формата meta-compile, инверс
компилятора, omit-on-default). Пилот — Catalog + общий слой (Properties, Attributes/
TabularSections shorthand с инверсией типов и флагов). Захват предопределённых из
соседнего Ext/Predefined.xml — ЧЕРНОВОЙ (DSL ещё не проработан).

disable-model-invocation (скрытый). Прочие типы → exit 3 (ring3). PY-зеркало — в конце.
Харнес раундтрипа и WORKFLOW — в debug/meta-roundtrip/ (gitignored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:04:08 +03:00
1485 changed files with 96655 additions and 26640 deletions
+23 -4
View File
@@ -1,4 +1,4 @@
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
@@ -44,6 +44,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -80,10 +90,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$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 (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -154,6 +167,11 @@ $script:xmlDoc = New-Object System.Xml.XmlDocument
$script:xmlDoc.PreserveWhitespace = $true
$script:xmlDoc.Load($resolvedPath)
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
$script:formatVersion = $script:xmlDoc.DocumentElement.GetAttribute("version")
if (-not $script:formatVersion) { $script:formatVersion = "2.17" }
$script:addCount = 0
$script:removeCount = 0
$script:modifyCount = 0
@@ -196,7 +214,7 @@ Info "Configuration: $($script:objName)"
$script:typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -212,7 +230,7 @@ $script:typeOrder = @(
$script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
@@ -700,6 +718,7 @@ $script:ruTypeMap = @{
"регистррасчёта" = "CalculationRegister"
"бизнеспроцесс" = "BusinessProcess"
"задача" = "Task"
"бот" = "Bot"
"планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage"
}
@@ -850,7 +869,7 @@ function Do-SetHomePage($valArg) {
$hpXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" 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">
<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" 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="$($script:formatVersion)">
<WorkingAreaTemplate>$tmpl</WorkingAreaTemplate>
$leftXml
$rightXml
+67 -9
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-edit v1.7 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.11 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -182,7 +199,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -198,7 +215,7 @@ TYPE_ORDER = [
TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
@@ -307,13 +324,49 @@ def parse_batch_value(val):
return items
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None → файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -357,6 +410,10 @@ def main():
tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot()
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
format_version = xml_root.get('version') or '2.17'
add_count = 0
remove_count = 0
modify_count = 0
@@ -771,6 +828,7 @@ def main():
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
}
@@ -905,7 +963,7 @@ def main():
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
'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">\r\n'
f'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="{format_version}">\r\n'
f'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
f'{left_xml}\r\n'
f'{right_xml}\r\n'
+3 -2
View File
@@ -1,4 +1,4 @@
# cf-info v1.3 — Compact summary of 1C configuration root
# cf-info v1.4 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
@@ -89,7 +89,7 @@ function Get-PropML([string]$propName) {
$typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -105,6 +105,7 @@ $typeRuNames = @{
"Language"="Языки"; "Subsystem"="Подсистемы"; "StyleItem"="Элементы стиля"; "Style"="Стили"
"CommonPicture"="Общие картинки"; "SessionParameter"="Параметры сеанса"; "Role"="Роли"
"CommonTemplate"="Общие макеты"; "FilterCriterion"="Критерии отбора"; "CommonModule"="Общие модули"
"Bot"="Боты"
"CommonAttribute"="Общие реквизиты"; "ExchangePlan"="Планы обмена"; "XDTOPackage"="XDTO-пакеты"
"WebService"="Веб-сервисы"; "HTTPService"="HTTP-сервисы"; "WSReference"="WS-ссылки"
"EventSubscription"="Подписки на события"; "ScheduledJob"="Регламентные задания"
+3 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-info v1.3 — Compact summary of 1C configuration root
# cf-info v1.4 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -95,7 +95,7 @@ def get_prop_ml(prop_name):
type_order = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -111,6 +111,7 @@ type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"Bot": "Боты",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
+9 -4
View File
@@ -1,4 +1,4 @@
# cf-init v1.2 — Create empty 1C configuration scaffold
# cf-init v1.4 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -7,7 +7,12 @@ param(
[string]$OutputDir = "src",
[string]$Version,
[string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24"
[string]$CompatibilityMode = "Version8_3_24",
# Версия формата выгрузки (MDClasses). Её задаёт ПЛАТФОРМА, которой выгружают, и от режима
# совместимости она не зависит: 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19,
# 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
[ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
)
$ErrorActionPreference = "Stop"
@@ -73,7 +78,7 @@ $versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version
# --- 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">
<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>
@@ -175,7 +180,7 @@ $cfgXml = @"
# --- 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">
<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">
<Properties>
<Name>Русский</Name>
+8 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-init v1.2 — Create empty 1C configuration scaffold
# cf-init v1.4 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid
@@ -24,6 +24,11 @@ def main():
parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24')
# Версия формата выгрузки (MDClasses) — её задаёт ПЛАТФОРМА, а не режим совместимости:
# 8.3.20-8.3.24 пишут 2.17, 8.3.25 — 2.18, 8.3.26 — 2.19, 8.3.27 — 2.20.
# Дефолт консервативный: 2.17 читается всеми платформами.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = parser.parse_args()
name = args.Name
@@ -96,7 +101,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="{args.FormatVersion}">
\t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo>
@@ -168,7 +173,7 @@ def main():
# --- Languages/Русский.xml ---
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="{args.FormatVersion}">
\t<Language uuid="{uuid_lang}">
\t\t<Properties>
\t\t\t<Name>Русский</Name>
@@ -1,4 +1,4 @@
# cf-validate v1.3 — Validate 1C configuration root structure
# cf-validate v1.5 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -104,11 +104,11 @@ $validClassIds = @(
"fb282519-d103-4dd3-bc12-cb271d631dfc" # home page / client app interface
)
# 44 types in canonical order
# 45 types in canonical order
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
@@ -125,6 +125,7 @@ $childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"Bot"="Bots"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
@@ -204,8 +205,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Must have Configuration child
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-validate v1.3 — Validate 1C configuration XML structure
# cf-validate v1.5 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
]
# 44 types in canonical order
# 45 types in canonical order
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -231,8 +232,9 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
@@ -1,4 +1,4 @@
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][string]$ExtensionPath,
@@ -285,14 +285,36 @@ $script:generatedTypes = @{
"DefinedType" = @(
@{ prefix = "DefinedType"; category = "DefinedType" }
)
"Sequence" = @(
@{ prefix = "SequenceRecord"; category = "Record" }
@{ prefix = "SequenceManager"; category = "Manager" }
@{ prefix = "SequenceRecordSet"; category = "RecordSet" }
)
"FilterCriterion" = @(
@{ prefix = "FilterCriterionManager"; category = "Manager" }
@{ prefix = "FilterCriterionList"; category = "List" }
)
"SettingsStorage" = @(
@{ prefix = "SettingsStorageManager"; category = "Manager" }
)
"IntegrationService" = @(
@{ prefix = "IntegrationServiceManager"; category = "Manager" }
)
"WSReference" = @(
@{ prefix = "WSReferenceManager"; category = "Manager" }
)
}
# Types that need ChildObjects element
# Types that need ChildObjects element — fallback when the source object cannot be probed.
# The platform emits <ChildObjects> for every container type even when empty, and rejects
# the file without it ("ожидаемое ChildObjects"); primary signal is the source object itself.
$typesWithChildObjects = @(
"Catalog","Document","ExchangePlan","ChartOfAccounts",
"ChartOfCharacteristicTypes","ChartOfCalculationTypes",
"BusinessProcess","Task","Enum",
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister"
"InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister",
"DataProcessor","Report","DocumentJournal","FilterCriterion","SettingsStorage",
"Sequence","HTTPService","WebService","IntegrationService","Subsystem"
)
# CommonModule properties to copy from source
@@ -348,7 +370,10 @@ function Detect-FormatVersion([string]$dir) {
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -454,6 +479,9 @@ function Read-SourceObject {
}
}
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
$srcProps["__HasChildObjects"] = ($srcEl.SelectSingleNode("md:ChildObjects", $srcNs) -ne $null)
return @{
Uuid = $srcUuid
Properties = $srcProps
@@ -1669,7 +1697,7 @@ function Build-BorrowedObjectXml {
$sb.AppendLine("`t`t</Properties>") | Out-Null
# ChildObjects (for types that need it)
if ($typesWithChildObjects -contains $typeName) {
if ($sourceProps["__HasChildObjects"] -or ($typesWithChildObjects -contains $typeName)) {
$sb.AppendLine("`t`t<ChildObjects/>") | Out-Null
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-borrow v1.8 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.11 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -254,13 +254,36 @@ GENERATED_TYPES = {
"DefinedType": [
{"prefix": "DefinedType", "category": "DefinedType"},
],
"Sequence": [
{"prefix": "SequenceRecord", "category": "Record"},
{"prefix": "SequenceManager", "category": "Manager"},
{"prefix": "SequenceRecordSet", "category": "RecordSet"},
],
"FilterCriterion": [
{"prefix": "FilterCriterionManager", "category": "Manager"},
{"prefix": "FilterCriterionList", "category": "List"},
],
"SettingsStorage": [
{"prefix": "SettingsStorageManager", "category": "Manager"},
],
"IntegrationService": [
{"prefix": "IntegrationServiceManager", "category": "Manager"},
],
"WSReference": [
{"prefix": "WSReferenceManager", "category": "Manager"},
],
}
# Types that need ChildObjects element — fallback when the source object cannot be probed.
# The platform emits <ChildObjects> for every container type even when empty, and rejects
# the file without it ("expected ChildObjects"); primary signal is the source object itself.
TYPES_WITH_CHILD_OBJECTS = [
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"BusinessProcess", "Task", "Enum",
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
"DataProcessor", "Report", "DocumentJournal", "FilterCriterion", "SettingsStorage",
"Sequence", "HTTPService", "WebService", "IntegrationService", "Subsystem",
]
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
@@ -349,13 +372,49 @@ def expand_self_closing(container, parent_indent):
container.text = "\r\n" + parent_indent
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None → файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -500,6 +559,9 @@ def main():
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
# Whether the platform emits <ChildObjects> for this type — the source object is the ground truth
src_props["__HasChildObjects"] = src_el.find(f"{{{MD_NS}}}ChildObjects") is not None
return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
def read_source_form_uuid(type_name, obj_name, form_name):
@@ -576,7 +638,7 @@ def main():
lines.append("\t\t</Properties>")
if type_name in TYPES_WITH_CHILD_OBJECTS:
if source_props.get("__HasChildObjects") or type_name in TYPES_WITH_CHILD_OBJECTS:
lines.append("\t\t<ChildObjects/>")
lines.append(f"\t</{type_name}>")
+95 -28
View File
@@ -1,7 +1,7 @@
---
name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools:
- Bash
- Read
@@ -10,22 +10,31 @@ allowed-tools:
# /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий.
Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры.
Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя
## Параметры
| Параметр | Описание | По умолчанию |
|----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ModulePath` | Путь к модулю (обязат.) | — |
| `MethodName` | Имя перехватываемого метода (обязат.) | — |
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` |
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
| `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
| `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
| `MethodName` | Имя перехватываемого метода | обязат. для генерации |
| `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
| `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
| `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
## Формат ModulePath
@@ -40,39 +49,97 @@ allowed-tools:
Аналогично для Report, DataProcessor, InformationRegister и других типов.
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
## Типы перехвата
| InterceptorType | Декоратор | Назначение |
|-----------------|-----------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода |
| `After` | `&После` | Код после вызова оригинального метода |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
| InterceptorType | Декоратор | Назначение | Применим к |
|-----------------|-----------|------------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
| `After` | `&После` | Код после вызова оригинального метода | процедуры |
| `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
- **Добавляешь код** → оберни его `#Вставка``#КонецВставки`.
- **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление``#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
Пример:
```bsl
&ИзменениеИКонтроль("ПриЗаписи")
Процедура Расш_ПриЗаписи(Отказ)
СуммаДокумента = РассчитатьСумму();
#Вставка
// доработка: округляем
СуммаДокумента = Окр(СуммаДокумента, 2);
#КонецВставки
#Удаление
Записать();
#КонецУдаления
#Вставка
ЗаписатьСПроверкой(Отказ);
#КонецВставки
КонецПроцедуры
```
Правила:
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
## Актуализация
После изменения оригинала в конфигурации-источнике перехватчик `&ИзменениеИКонтроль` может рассинхронизироваться — unmarked-контекст разойдётся с новым оригиналом. Платформа при загрузке об этом молчит, поэтому актуализируй сам:
- **Проверить** — `-Check`: отчёт по всем контролируемым методам расширения, ничего не пишет (`exit 1`, если есть дрейф или конфликт).
- **Актуализировать** — `-Actualize`: переносит правки (`#Вставка`/`#Удаление`) на новый оригинал по всему расширению. Сузить область: `-ModulePath` — один модуль, `+ -MethodName` — один метод. Для одного метода то же делает повторный вызов с `-InterceptorType ModificationAndControl`.
Статусы в выводе:
- `[АКТУАЛЕН]` — оригинал не менялся, правок нет;
- `[АКТУАЛИЗИРОВАН]` — тело обновлено по новому оригиналу, правки сохранены (в выводе — сводка);
- `[АКТУАЛИЗИРОВАН-ЧАСТИЧНО]` — часть правок не удалось разместить (якорь изменился в новом оригинале). Они помечены `// [РЕСИНК-КОНФЛИКТ]` в модуле и не потеряны; путь к merge-воркспейсу — в выводе (начни с `index.md`, дальше по каждому конфликту его `conflict.md`, размести блоки вручную в `.bsl` расширения);
- `[ПЕРЕНЕСЕНО В ОСНОВНУЮ]` — правка уже есть в новом оригинале (вставленный код внесён вендором) или удаляемый блок уже вырезан. Правка неактуальна — убирается из тела, дублировать/конфликтовать не нужно. Если так со всеми правками метода — перехватчик можно удалить. `-Check` этим не роняет `exit`.
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Перехват &Перед на сервере
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Код перед записью
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват &После на клиенте
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
# Перехват После на форме
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
# ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction
# Замена функции (ПродолжитьВызов)
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType Instead
# ИзменениеИКонтроль — копия тела для правки маркерами
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.РеализацияТоваров.ObjectModule" -MethodName "ОбработкаПроведения" -InterceptorType ModificationAndControl
# ModulePath как путь к файлу модуля-источника (без -ConfigPath)
... -ExtensionPath src\cfe\ИмяРасширения -ModulePath "src\cf\CommonModules\ОбщийМодуль\Ext\Module.bsl" -MethodName "ПолучитьДанные" -InterceptorType Instead
# Проверить все контролируемые методы расширения на дрейф
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Check
# Актуализировать дрейфнувшие контролируемые методы пачкой
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -Actualize
```
## Генерируемый код (Before)
## Верификация
```bsl
&НаСервере
&Перед("ПриЗаписи")
Процедура Расш1_ПриЗаписи()
// TODO: код перед вызовом оригинального метода
КонецПроцедуры
```
/cfe-validate <ExtensionPath>
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
# cfe-validate v1.4 — Validate 1C configuration extension structure (CFE)
# cfe-validate v1.5 — Validate 1C configuration extension structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -197,8 +197,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Must have Configuration child
@@ -930,6 +931,17 @@ if ($script:borrowedFormsWithTree.Count -eq 0) {
Report-OK "13. TypeLink: clean"
}
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0
foreach ($bslFile in (Get-ChildItem -Path $extRootDir -Recurse -Filter *.bsl -File -ErrorAction SilentlyContinue)) {
$txt = [System.IO.File]::ReadAllText($bslFile.FullName, [System.Text.Encoding]::UTF8)
$ctrlCount += ([regex]::Matches($txt, '(?m)^\s*&ИзменениеИКонтроль\(')).Count
}
if ($ctrlCount -gt 0) {
Out-Line "[INFO] Контролируемых методов (&ИзменениеИКонтроль): $ctrlCount — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>"
}
# --- Final output ---
& $finalize
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-validate v1.4 — Validate 1C configuration extension XML structure (CFE)
# cfe-validate v1.5 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re
@@ -216,8 +216,9 @@ def main():
version = root.get('version', '')
if not version:
r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
r.warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
# Must have Configuration child
cfg_node = None
@@ -885,6 +886,21 @@ def main():
elif check13_ok:
r.ok('13. TypeLink: clean')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0
for dp, _dn, files in os.walk(config_dir):
for fn in files:
if fn.endswith('.bsl'):
try:
with open(os.path.join(dp, fn), 'r', encoding='utf-8-sig') as f:
for ln in f:
if re.match(r'^\s*&ИзменениеИКонтроль\(', ln):
ctrl_count += 1
except OSError:
pass
if ctrl_count > 0:
r.out('[INFO] Контролируемых методов (&ИзменениеИКонтроль): %d — их актуальность здесь не проверяется. Сверьте: /cfe-patch-method -Check -ExtensionPath <ext> -ConfigPath <cf>' % ctrl_count)
# --- Final output ---
r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0)
+2
View File
@@ -45,6 +45,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <п
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
| `-AddToList` | нет | Добавить в список баз 1С |
| `-ListName <имя>` | нет | Имя базы в списке |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+247 -20
View File
@@ -1,4 +1,4 @@
# db-create v1.6 — Create 1C information base
# db-create v1.10 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -30,6 +30,12 @@
.PARAMETER ListName
Имя базы в списке
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
@@ -61,12 +67,163 @@ param(
[switch]$AddToList,
[Parameter(Mandatory=$false)]
[string]$ListName
[string]$ListName,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$UseTemplate = ConvertTo-CleanPath $UseTemplate '-UseTemplate'
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -111,35 +268,90 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-FileIbCreated {
# File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$IbPath)
$f = Join-Path $IbPath "1Cv8.1CD"
return (Test-Path $f) -and ((Get-Item $f -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/UseTemplate' = '-UseTemplate'; '/AddToList' = '-AddToList'; '--db-path' = '-InfoBasePath'; '--load' = '-UseTemplate'; '--restore' = '-UseTemplate' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -173,16 +385,21 @@ try {
}
}
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$ibMissing = ($exitCode -eq 0) -and -not (Test-FileIbCreated $InfoBasePath)
if ($ibMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green
} elseif ($ibMissing) {
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -190,6 +407,8 @@ try {
# --- Build arguments ---
$arguments = @("CREATEINFOBASE")
# Quotes go INSIDE the token (File="path"): 1C's own parser wants them there, quoting
# the whole token instead breaks a path with spaces. Hence -PreQuoted on the launch.
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`""
} else {
@@ -214,19 +433,26 @@ try {
$outFile = Join-Path $tempDir "create_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $((Format-ArgsForDisplay $arguments $engine) -join ' ')"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
$ibMissing = ($exitCode -eq 0) -and -not ($InfoBaseServer -and $InfoBaseRef) -and -not (Test-FileIbCreated $InfoBasePath)
if ($ibMissing) { $exitCode = 1 }
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
}
} elseif ($ibMissing) {
Write-Host "Error: exit code 0 but 1Cv8.1CD is missing or empty at $InfoBasePath — information base was not created" -ForegroundColor Red
} else {
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red
}
@@ -239,6 +465,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+301 -25
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-create v1.6 — Create 1C information base
# db-create v1.10 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -78,6 +235,13 @@ def resolve_v8path(v8path):
return v8path
def file_ib_created(ib_path):
"""File-infobase postcondition: the platform must have produced a non-empty 1Cv8.1CD.
Exit code 0 without it (broken/headless env) is a false success reject it."""
f = os.path.join(ib_path, "1Cv8.1CD")
return os.path.isfile(f) and os.path.getsize(f) > 0
IBCMD_NOUSER_HINT = (
"[ibcmd] No -UserName/-Password given; the infobase may require authentication. "
"On Windows ibcmd reads credentials from the console (stdin is ignored), so this "
@@ -86,6 +250,18 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +272,67 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def main():
@@ -113,11 +349,33 @@ def main():
parser.add_argument("-UseTemplate", default="")
parser.add_argument("-AddToList", action="store_true")
parser.add_argument("-ListName", default="")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
args.UseTemplate = clean_path(args.UseTemplate, "-UseTemplate")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/UseTemplate": "-UseTemplate",
"/AddToList": "-AddToList",
"--db-path": "-InfoBasePath",
"--load": "-UseTemplate",
"--restore": "-UseTemplate",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -143,17 +401,25 @@ def main():
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)}")
arguments.extend(quote_if_needed(a) for a in extra_args)
print(f"Running: ibcmd {' '.join(format_args_for_display(arguments, engine))}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
exit_code = result.returncode
ib_missing = exit_code == 0 and not file_ib_created(args.InfoBasePath)
if ib_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base created successfully: {args.InfoBasePath}")
elif ib_missing:
print(
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
"— information base was not created",
file=sys.stderr,
)
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)
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
print_platform_output(result)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
@@ -163,44 +429,53 @@ def main():
# --- Build arguments ---
arguments = ["CREATEINFOBASE"]
# Quotes go INSIDE the token (File="path"): that is where 1C's parser expects them.
# Quoting the whole token instead breaks a path with spaces — on both OSes.
if args.InfoBaseServer and 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}')
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:
arguments.extend(["/UseTemplate", args.UseTemplate])
arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
# --- Add to list ---
if args.AddToList:
if args.ListName:
arguments.extend(["/AddToList", args.ListName])
arguments.extend(["/AddToList", f'"{args.ListName}"'])
else:
arguments.append("/AddToList")
# --- Output ---
out_file = os.path.join(temp_dir, "create_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {' '.join(format_args_for_display(arguments, engine))}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition (file infobase only): exit 0 without a non-empty 1Cv8.1CD is a false success.
is_server = bool(args.InfoBaseServer and args.InfoBaseRef)
ib_missing = exit_code == 0 and not is_server and not file_ib_created(args.InfoBasePath)
if ib_missing:
exit_code = 1
if exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef:
if is_server:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else:
print(f"Information base created successfully: {args.InfoBasePath}")
elif ib_missing:
print(
f"Error: exit code 0 but 1Cv8.1CD is missing or empty at {args.InfoBasePath} "
"— information base was not created",
file=sys.stderr,
)
else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
@@ -214,6 +489,7 @@ def main():
print("--- End ---")
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
+2
View File
@@ -51,6 +51,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <п
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+263 -20
View File
@@ -1,4 +1,4 @@
# db-dump-cf v1.6 — Dump 1C configuration to CF file
# db-dump-cf v1.12 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER AllExtensions
Выгрузить все расширения
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
@@ -70,12 +76,183 @@ param(
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -120,35 +297,89 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -183,16 +414,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -222,15 +458,21 @@ try {
$outFile = Join-Path $tempDir "dump_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — configuration was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
@@ -243,6 +485,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+308 -24
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-cf v1.6 — Dump 1C configuration to CF file
# db-dump-cf v1.12 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -115,11 +369,34 @@ def main():
parser.add_argument("-OutputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -150,17 +427,20 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
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)
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
@@ -171,40 +451,43 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpCfg", args.OutputFile])
arguments.extend(["/DumpCfg", f'"{args.OutputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", args.Extension])
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_cf_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped", file=sys.stderr)
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
@@ -219,6 +502,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <п
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+264 -20
View File
@@ -1,4 +1,4 @@
# db-dump-dt v1.5 — Dump 1C information base to DT file
# db-dump-dt v1.11 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -29,6 +29,12 @@
.PARAMETER OutputFile
Путь к выходному DT-файлу
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
#>
@@ -54,12 +60,183 @@ param(
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$OutputFile
[string]$OutputFile,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -104,35 +281,89 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -163,16 +394,22 @@ try {
$arguments += "$OutputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -195,15 +432,21 @@ try {
$outFile = Join-Path $tempDir "dump_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Information base dumped successfully to: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — information base was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping information base (code: $exitCode)" -ForegroundColor Red
}
@@ -216,6 +459,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+307 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-dt v1.5 — Dump 1C information base to DT file
# db-dump-dt v1.11 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -113,11 +367,34 @@ def main():
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -143,17 +420,20 @@ def main():
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)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
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)
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_dt_{random.randint(0, 999999)}")
@@ -164,34 +444,37 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpIB", args.OutputFile])
arguments.extend(["/DumpIB", f'"{args.OutputFile}"'])
# --- Output ---
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Information base dumped successfully to: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped", file=sys.stderr)
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
@@ -206,6 +489,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -56,6 +56,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <
| `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
@@ -1,4 +1,4 @@
# db-dump-xml v1.8 — Dump 1C configuration to XML files
# db-dump-xml v1.14 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -48,6 +48,12 @@
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
@@ -93,12 +99,183 @@ param(
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -143,35 +320,89 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-DirNonEmpty {
# Postcondition: the platform must have written files into the output directory.
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -224,16 +455,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Configuration exported successfully to: $ConfigDir" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not exported" -ForegroundColor Red
} else {
Write-Host "Error exporting configuration (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -291,16 +527,22 @@ try {
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $ConfigDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Dump completed successfully" -ForegroundColor Green
Write-Host "Configuration dumped to: $ConfigDir"
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $ConfigDir — configuration was not dumped" -ForegroundColor Red
} else {
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red
}
@@ -313,6 +555,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+309 -25
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-xml v1.8 — Dump 1C configuration to XML files
# db-dump-xml v1.14 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def dir_nonempty(path):
"""Postcondition: the platform must have written files into the output directory.
Exit code 0 with an empty dir (broken/headless env) is a false success reject it."""
return os.path.isdir(path) and any(os.scandir(path))
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -128,12 +382,35 @@ def main():
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -181,17 +458,20 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Configuration exported successfully to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported", file=sys.stderr)
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)
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
@@ -202,16 +482,16 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpConfigToFiles", args.ConfigDir]
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
arguments += ["-Format", args.Format]
if args.Mode == "Full":
@@ -228,7 +508,7 @@ def main():
with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(object_list))
arguments += ["-listFile", list_file]
arguments += ["-listFile", f'"{list_file}"']
print(f"Objects to dump: {len(object_list)}")
for obj in object_list:
print(f" {obj}")
@@ -238,28 +518,31 @@ def main():
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", args.Extension]
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
out_missing = exit_code == 0 and not dir_nonempty(args.ConfigDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
@@ -274,6 +557,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+3
View File
@@ -29,6 +29,7 @@ allowed-tools:
```json
{
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
"v8args": ["/UseHwLicenses+"],
"databases": [
{
"id": "dev",
@@ -61,6 +62,8 @@ allowed-tools:
| Поле | Тип | Описание |
|------|-----|----------|
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
| `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
+2
View File
@@ -52,6 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
| `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение |
| `-AllExtensions` | нет | Загрузить все расширения из архива |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+266 -22
View File
@@ -1,4 +1,4 @@
# db-load-cf v1.6 — Load 1C configuration from CF file
# db-load-cf v1.13 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER AllExtensions
Загрузить все расширения из архива
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
@@ -70,12 +76,200 @@ param(
[string]$Extension,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -120,35 +314,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -183,16 +424,17 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -222,17 +464,18 @@ try {
$outFile = Join-Path $tempDir "load_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.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
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -243,6 +486,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+313 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-cf v1.6 — Load 1C configuration from CF file
# db-load-cf v1.13 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -115,11 +387,34 @@ def main():
parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -150,16 +445,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
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)
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
@@ -171,42 +463,39 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/LoadCfg", args.InputFile])
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", args.Extension])
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "load_cf_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -219,6 +508,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -68,6 +68,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <п
| `-InputFile <путь>` | да | Путь к DT-файлу |
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+267 -22
View File
@@ -1,4 +1,4 @@
# db-load-dt v1.5 — Load 1C information base from DT file
# db-load-dt v1.12 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER UnlockCode
Код разблокировки базы (/UC) если заблокировано начало сеансов
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
#>
@@ -67,12 +73,200 @@ param(
[int]$JobsCount = 0,
[Parameter(Mandatory=$false)]
[string]$UnlockCode
[string]$UnlockCode,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -117,35 +311,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -177,16 +418,18 @@ try {
$arguments += "$InputFile"
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
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
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -211,17 +454,18 @@ try {
$outFile = Join-Path $tempDir "load_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.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
Write-Host "Error restoring information base (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -232,6 +476,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+313 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-dt v1.5 — Load 1C information base from DT file
# db-load-dt v1.12 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -115,11 +387,34 @@ def main():
parser.add_argument("-InputFile", required=True)
parser.add_argument("-JobsCount", type=int, default=0)
parser.add_argument("-UnlockCode", default="")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile")
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -147,16 +442,13 @@ def main():
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)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
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)
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
@@ -168,40 +460,37 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
if args.UnlockCode:
arguments.append(f"/UC{args.UnlockCode}")
arguments.append(f'/UC"{args.UnlockCode}"')
arguments.extend(["/RestoreIB", args.InputFile])
arguments.extend(["/RestoreIB", f'"{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.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
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)
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -214,6 +503,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
@@ -1,4 +1,4 @@
# db-load-git v1.11 — Load Git changes into 1C database
# db-load-git v1.18 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -48,6 +48,12 @@
.PARAMETER DryRun
Только показать что будет загружено (без загрузки)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Source All
@@ -102,12 +108,164 @@ param(
[switch]$DryRun,
[Parameter(Mandatory=$false)]
[switch]$UpdateDB
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Helper: map sub-file path (BSL, HTML, etc.) to object XML ---
function Get-ObjectXmlFromSubFile {
param([string]$RelativePath)
@@ -167,32 +325,75 @@ if (-not $DryRun) {
# --- Detect engine + validate connection (skip if DryRun) ---
$engine = "1cv8"
if (-not $DryRun) {
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
@@ -206,6 +407,10 @@ function Invoke-IbcmdProcess {
}
}
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
@@ -372,32 +577,34 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading changes (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
Write-Host "Error loading changes (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
Write-PlatformOutput $output
exit $exitCode
}
Write-Host "Changes loaded successfully ($($configFiles.Count) files)" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
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 ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyArgs += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
Write-PlatformOutput $applyOut
}
exit $exitCode
}
@@ -442,21 +649,22 @@ try {
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host ""
Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.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
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -467,6 +675,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+318 -32
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-git v1.11 — Load Git changes into 1C database
# db-load-git v1.18 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,10 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def get_object_xml_from_subfile(relative_path):
@@ -121,6 +360,39 @@ def run_git(config_dir, git_args):
return []
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -152,7 +424,18 @@ def main():
)
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)")
parser.add_argument("-UpdateDB", action="store_true", help="Also update database configuration after load")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
# --- Resolve V8Path (skip if DryRun) ---
v8path = None
@@ -171,6 +454,18 @@ def main():
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate config dir ---
if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
@@ -307,18 +602,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
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)
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}", 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"]
@@ -327,17 +617,15 @@ def main():
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
apply_args.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
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)
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print_platform_output(ar)
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) ---
@@ -349,24 +637,24 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
arguments += ["-listFile", list_file]
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", f'"{list_file}"']
arguments += ["-Format", args.Format]
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", args.Extension]
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
@@ -376,19 +664,16 @@ def main():
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print("")
print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {' '.join(arguments)}")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
@@ -396,7 +681,7 @@ def main():
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -409,6 +694,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -59,6 +59,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
@@ -1,4 +1,4 @@
# db-load-xml v1.12 — Load 1C configuration from XML files
# db-load-xml v1.19 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -48,6 +48,12 @@
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
@@ -102,12 +108,201 @@ param(
[switch]$UpdateDB,
[Parameter(Mandatory=$false)]
[switch]$StrictLog
[switch]$StrictLog,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$ConfigDir = ConvertTo-CleanPath $ConfigDir '-ConfigDir'
$ListFile = ConvertTo-CleanPath $ListFile '-ListFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -152,35 +347,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -244,33 +486,35 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -ne 0) {
Write-Host "Error loading configuration from files (code: $exitCode)" -ForegroundColor Red
if ($output) { Write-Host ($output | Out-String) }
Write-Host "Error loading configuration from files (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
Write-PlatformOutput $output
exit $exitCode
}
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
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 ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $applyArgs
$applyArgs += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $applyArgs $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $applyArgs
$applyOut = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($applyOut) { Write-Host ($applyOut | Out-String) }
Write-PlatformOutput $applyOut
}
exit $exitCode
}
@@ -349,11 +593,12 @@ try {
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Read log ---
$logContent = $null
@@ -392,7 +637,7 @@ try {
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($logContent) {
@@ -400,6 +645,7 @@ try {
Write-Host $logContent
Write-Host "--- End ---"
}
Write-PlatformOutput $__v8.Output
if ($silentFailures.Count -gt 0) {
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs"
+319 -32
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-xml v1.12 — Load 1C configuration from XML files
# db-load-xml v1.19 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -135,13 +407,37 @@ def main():
action="store_true",
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.ConfigDir = clean_path(args.ConfigDir, "-ConfigDir")
args.ListFile = clean_path(args.ListFile, "-ListFile")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -199,18 +495,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
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)
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}", 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"]
@@ -219,17 +510,15 @@ def main():
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(apply_args)}")
apply_args.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(apply_args, engine)), args.Password, args.UserName)}")
ar = run_ibcmd([v8path] + apply_args, bool(args.UserName))
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)
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print_platform_output(ar)
sys.exit(exit_code)
# --- Temp dir ---
@@ -241,16 +530,16 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", args.ConfigDir]
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
if args.Mode == "Full":
print("Executing full configuration load...")
@@ -286,7 +575,7 @@ def main():
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", generated_list_file]
arguments += ["-listFile", f'"{generated_list_file}"']
arguments.append("-partial")
arguments.append("-updateConfigDumpInfo")
@@ -294,7 +583,7 @@ def main():
# --- Extensions ---
if args.Extension:
arguments += ["-Extension", args.Extension]
arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions:
arguments.append("-AllExtensions")
@@ -304,16 +593,13 @@ def main():
# --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Read log ---
@@ -352,13 +638,14 @@ def main():
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code})", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
print_platform_output(result)
if silent_failures:
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print(
+1
View File
@@ -52,6 +52,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <пар
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
| `-CParam <строка>` | нет | Параметр запуска (/C) |
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+203 -5
View File
@@ -1,4 +1,4 @@
# db-run v1.2 — Launch 1C:Enterprise
# db-run v1.7 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER URL
Навигационная ссылка (e1cib/...)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
@@ -73,12 +79,170 @@ param(
[string]$CParam,
[Parameter(Mandatory=$false)]
[string]$URL
[string]$URL,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$Execute = ConvertTo-CleanPath $Execute '-Execute'
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -122,6 +286,19 @@ if (-not (Test-Path $V8Path)) {
exit 1
}
# --- Resolve additional arguments ---
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
$engine = "1cv8"
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '/Execute' = '-Execute'; '/C' = '-CParam'; '/URL' = '-URL' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
function Format-ArgToken {
# ShellExecute re-joins the argument string, so quote each extra token that needs it.
param([string]$Token)
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
return " $Token"
}
# --- Validate connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
@@ -165,7 +342,28 @@ if ($URL) {
$argString += " /DisableStartupDialogs"
# --- Execute (background, no wait) ---
Write-Host "Running: 1cv8.exe $argString"
Start-Process -FilePath $V8Path -ArgumentList $argString
# The display string is built from the same tokens with secret-prone values redacted.
$displayString = $argString
foreach ($tok in $extraArgs) { $argString += (Format-ArgToken $tok) }
foreach ($tok in (Format-ArgsForDisplay $extraArgs $engine)) { $displayString += (Format-ArgToken $tok) }
# --- Execute (background) ---
# Redact the password/user before printing the command line — never leak secrets.
$displayArg = Protect-Secrets $displayString @($Password, $UserName)
Write-Host "Running: 1cv8.exe $displayArg"
$proc = Start-Process -FilePath $V8Path -ArgumentList $argString -PassThru
# --- Bounded early-exit check ---
# The launch is a background GUI process, so we don't wait for completion. But a process
# that dies within the first ~1.5s never really started (bad base, no display, license) —
# report that honestly instead of a blind "launched".
$deadline = (Get-Date).AddMilliseconds(1500)
while ((Get-Date) -lt $deadline -and -not $proc.HasExited) {
Start-Sleep -Milliseconds 200
}
if ($proc.HasExited) {
Write-Host "Error: 1C:Enterprise exited immediately (code: $($proc.ExitCode))" -ForegroundColor Red
if ($proc.ExitCode -ne 0) { exit $proc.ExitCode } else { exit 1 }
}
Write-Host "PID: $($proc.Id)"
Write-Host "1C:Enterprise launched" -ForegroundColor Green
+229 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-run v1.2 — Launch 1C:Enterprise
# db-run v1.7 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -9,6 +9,7 @@ import os
import re
import subprocess
import sys
import time
def _find_project_v8path():
@@ -32,6 +33,181 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -74,6 +250,15 @@ def resolve_v8path(v8path):
return v8path
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -90,10 +275,34 @@ def main():
parser.add_argument("-Execute", default="")
parser.add_argument("-CParam", default="")
parser.add_argument("-URL", default="")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
args.Execute = clean_path(args.Execute, "-Execute")
v8path = resolve_v8path(args.V8Path)
# --- Resolve additional arguments ---
# 1C:Enterprise is always launched by 1cv8 — ibcmd has no interactive mode.
engine = "1cv8"
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"/Execute": "-Execute",
"/C": "-CParam",
"/URL": "-URL",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
@@ -130,10 +339,25 @@ def main():
arguments.extend(["/URL", args.URL])
arguments.append("/DisableStartupDialogs")
arguments.extend(extra_args)
# --- Execute (background, no wait) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
subprocess.Popen([v8path] + arguments)
# --- Execute (background) ---
# Redact the password/user before printing the command line — never leak secrets.
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
proc = subprocess.Popen([v8path] + arguments)
# --- Bounded early-exit check ---
# The launch is a background GUI process, so we don't wait for completion. But a process
# that dies within the first ~1.5s never really started (bad base, no display, license) —
# report that honestly instead of a blind "launched".
deadline = time.monotonic() + 1.5
while time.monotonic() < deadline and proc.poll() is None:
time.sleep(0.2)
rc = proc.poll()
if rc is not None:
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
sys.exit(rc if rc and rc > 0 else 1)
print(f"PID: {proc.pid}")
print("1C:Enterprise launched")
+2
View File
@@ -53,6 +53,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
+265 -22
View File
@@ -1,4 +1,4 @@
# db-update v1.6 — Update 1C database configuration
# db-update v1.13 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -42,6 +42,12 @@
.PARAMETER WarningsAsErrors
Предупреждения считать ошибками
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
@@ -83,12 +89,199 @@ param(
[switch]$Server,
[Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors
[switch]$WarningsAsErrors,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
function Get-ExitAnnotation {
# Annotate an abnormal process exit code so a crash isn't reported as a bare number.
# A batch DESIGNER that crashes (e.g. missing license) may leave the infobase locked or
# half-updated — surface that instead of a plain code. (Windows exception codes only;
# POSIX signals are handled in the .py port.)
param([int]$Code)
$win = @{
-1073741819 = "0xC0000005 (access violation)"
-1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
}
if ($win.ContainsKey($Code)) {
return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
return ""
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -133,35 +326,82 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
# --- Validate connection ---
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
@@ -191,16 +431,17 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
if ($exitCode -eq 0) {
Write-Host "Database configuration updated successfully" -ForegroundColor Green
} else {
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -241,17 +482,18 @@ try {
$outFile = Join-Path $tempDir "update_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.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
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
@@ -262,6 +504,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+311 -22
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-update v1.6 — Update 1C database configuration
# db-update v1.13 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,43 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def describe_exit(code):
"""Annotate an abnormal process exit code so a crash isn't reported as a bare number.
Batch 1C in a broken/headless environment (no GUI session, no license) can crash mid-run
instead of returning a clean error, possibly leaving the infobase locked or half-mutated."""
if code is None:
return ""
win = {
3221225477: "0xC0000005 (access violation)", -1073741819: "0xC0000005 (access violation)",
3221225781: "0xC0000135 (missing DLL)", -1073741515: "0xC0000135 (missing DLL)",
3221226505: "0xC0000409 (stack overrun)", -1073740791: "0xC0000409 (stack overrun)",
}
if code in win:
return f" — abnormal termination, exception {win[code]}; the infobase may be left in an inconsistent state; verify it before retrying"
if -64 <= code < 0:
try:
import signal
name = signal.Signals(-code).name
except (ValueError, AttributeError):
name = f"signal {-code}"
return (f" — process terminated by {name} (abnormal termination, not a normal exit); "
"the infobase may be left in an inconsistent state; verify it before retrying")
return ""
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -117,12 +389,34 @@ def main():
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
@@ -151,16 +445,13 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, bool(args.UserName))
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)
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
@@ -172,14 +463,14 @@ def main():
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"])
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", args.InfoBasePath])
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments.append("/UpdateDBCfg")
@@ -193,29 +484,26 @@ def main():
# --- Extensions ---
if args.Extension:
arguments.extend(["-Extension", args.Extension])
arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions:
arguments.append("-AllExtensions")
# --- Output ---
out_file = os.path.join(temp_dir, "update_log.txt")
arguments.extend(["/Out", out_file])
arguments.extend(["/Out", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
if exit_code == 0:
print("Database configuration updated successfully")
else:
print(f"Error updating database configuration (code: {exit_code})", file=sys.stderr)
print(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
@@ -228,6 +516,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
+2
View File
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
+278 -22
View File
@@ -1,4 +1,4 @@
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.12 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -33,6 +33,12 @@
.PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
@@ -64,12 +70,184 @@ param(
[string]$SourceFile,
[Parameter(Mandatory=$true)]
[string]$OutputFile
[string]$OutputFile,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$SourceFile = ConvertTo-CleanPath $SourceFile '-SourceFile'
$OutputFile = ConvertTo-CleanPath $OutputFile '-OutputFile'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -114,34 +292,88 @@ if (-not (Test-Path $V8Path)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-OutputNonEmpty {
# Postcondition: the platform must have produced a non-empty output file.
# Exit code 0 without it (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
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
@@ -154,8 +386,20 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$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
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
# explicit ones are forwarded: the stub reads .v8-project.json itself.
# Invoked via -Command, not -File: -File takes the tail literally, so an array
# parameter would arrive as a single comma-glued token.
$q = { param($s) "'" + ($s -replace "'", "''") + "'" }
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $autoBasePath)"
if ($AdditionalV8Arguments.Count -gt 0) {
$stubCmd += " -AdditionalV8Arguments " + (($AdditionalV8Arguments | ForEach-Object { & $q $_ }) -join ',')
}
if ($AdditionalIbcmdArguments.Count -gt 0) {
$stubCmd += " -AdditionalIbcmdArguments " + (($AdditionalIbcmdArguments | ForEach-Object { & $q $_ }) -join ',')
}
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
if ($stubProc.ExitCode -ne 0) {
Write-Host "Error: failed to create stub database" -ForegroundColor Red
exit 1
@@ -188,16 +432,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "External data processor/report built successfully: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
} else {
Write-Host "Error building external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -220,15 +469,21 @@ try {
$outFile = Join-Path $tempDir "build_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-OutputNonEmpty $OutputFile)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no non-empty file at $OutputFile — build produced no output" -ForegroundColor Red
} else {
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red
}
@@ -241,6 +496,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+318 -27
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-build v1.6 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.12 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def output_nonempty(path):
"""Postcondition: the platform must have produced a non-empty output file.
Exit code 0 without it (broken/headless env) is a false success reject it."""
return os.path.isfile(path) and os.path.getsize(path) > 0
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -114,11 +368,35 @@ def main():
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.SourceFile = clean_path(args.SourceFile, "-SourceFile")
args.OutputFile = clean_path(args.OutputFile, "-OutputFile")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
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)
@@ -130,10 +408,16 @@ def main():
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
print("No database specified. Creating temporary stub database...")
result = subprocess.run(
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path],
capture_output=False,
)
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
"-TempBasePath", auto_base_path]
# The stub runs its own platform processes (CREATEINFOBASE, LoadConfigFromFiles,
# UpdateDBCfg) — they need the same extra arguments as the final build. Only the
# explicit ones are forwarded: the stub reads .v8-project.json itself.
if v8_extra:
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
if ibcmd_extra:
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
result = subprocess.run(stub_cmd, capture_output=False)
if result.returncode != 0:
print("Error: failed to create stub database", file=sys.stderr)
sys.exit(1)
@@ -166,50 +450,56 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"External data processor/report built successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
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)
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", args.SourceFile, args.OutputFile]
arguments += ["/LoadExternalDataProcessorOrReportFromFiles", f'"{args.SourceFile}"', f'"{args.OutputFile}"']
# --- Output ---
out_file = os.path.join(temp_dir, "build_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 without a non-empty output file is a false success.
out_missing = exit_code == 0 and not output_nonempty(args.OutputFile)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
else:
print(f"Error building (code: {exit_code})", file=sys.stderr)
@@ -224,6 +514,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
@@ -1,4 +1,4 @@
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -7,12 +7,162 @@ param(
[Parameter(Mandatory)]
[string]$V8Path,
[string]$TempBasePath
[string]$TempBasePath,
[string[]]$AdditionalV8Arguments = @(),
[string[]]$AdditionalIbcmdArguments = @()
)
$ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$SourceDir = ConvertTo-CleanPath $SourceDir '-SourceDir'
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$TempBasePath = ConvertTo-CleanPath $TempBasePath '-TempBasePath'
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
# --- 1. Scan XML files for reference types ---
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
@@ -1253,34 +1403,89 @@ $propsXml </Properties>$childObjLine
}
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
$stubEngine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-TempBasePath'; '--db-path' = '-TempBasePath' }
$extraArgs = @(Resolve-ExtraArgs $stubEngine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
function Format-ArgToken {
# Start-Process takes these argument lists as one string, so quote each token that needs it.
param([string]$Token)
if ($Token -match '[\s"]') { return ' "' + ($Token -replace '"', '\"') + '"' }
return " $Token"
}
$extraArgString = -join ($extraArgs | ForEach-Object { Format-ArgToken $_ })
if ($stubEngine -eq "ibcmd") {
Write-Host "Creating infobase (ibcmd): $TempBasePath"
$ibData = Join-Path $env:TEMP "stub_data_$(Get-Random)"
@@ -1288,12 +1493,13 @@ if ($stubEngine -eq "ibcmd") {
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$__ib = Invoke-IbcmdProcess $V8Path $ibArgs
$ibArgs += $extraArgs
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
$ibOut = $__ib.Output
$ibRc = $__ib.ExitCode
Remove-Item -Path $ibData -Recurse -Force -ErrorAction SilentlyContinue
if ($ibRc -ne 0) {
if ($ibOut) { Write-Host ($ibOut | Out-String) }
Write-PlatformOutput $ibOut
Write-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
@@ -1305,9 +1511,10 @@ if ($stubEngine -eq "ibcmd") {
# --- 5. Create infobase ---
Write-Host "Creating infobase: $TempBasePath"
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs"
$proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" + $extraArgString
$proc = Invoke-PlatformProcess $V8Path @($createArgs) -PreQuoted
if ($proc.ExitCode -ne 0) {
Write-PlatformOutput $proc.Output
Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
exit 1
}
@@ -1318,10 +1525,11 @@ if ($hasRefTypes) {
# LoadConfigFromFiles
Write-Host "Loading configuration from files..."
$loadLog = Join-Path $env:TEMP "stub_load_log.txt"
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs"
$proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $extraArgString
$proc = Invoke-PlatformProcess $V8Path @($loadArgs) -PreQuoted
if ($proc.ExitCode -ne 0) {
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host }
Write-PlatformOutput $proc.Output
Write-Error "Failed to load config (code: $($proc.ExitCode))"
exit 1
}
@@ -1329,10 +1537,11 @@ if ($hasRefTypes) {
# UpdateDBCfg
Write-Host "Updating database configuration..."
$updateLog = Join-Path $env:TEMP "stub_update_log.txt"
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs"
$proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $extraArgString
$proc = Invoke-PlatformProcess $V8Path @($updateArgs) -PreQuoted
if ($proc.ExitCode -ne 0) {
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host }
Write-PlatformOutput $proc.Output
Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
exit 1
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# stub-db-create v1.3 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.7 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -20,6 +20,75 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -30,7 +99,167 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def new_uuid():
@@ -802,7 +1031,17 @@ def main():
parser.add_argument('-SourceDir', required=True)
parser.add_argument('-V8Path', required=True)
parser.add_argument('-TempBasePath', default='')
args = parser.parse_args()
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
help='Extra ibcmd arguments in --key=value form')
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
type_map = scan_ref_types(args.SourceDir)
register_columns = scan_register_columns(args.SourceDir)
@@ -1057,6 +1296,10 @@ def main():
# Stub via ibcmd (one call: create [--import --apply])
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {"/F": "-TempBasePath", "--db-path": "-TempBasePath"}
extra_args = resolve_extra_args(stub_engine, v8_extra, ibcmd_extra, arg_hints)
if stub_engine == "ibcmd":
import shutil
print(f'Creating infobase (ibcmd): {temp_base}')
@@ -1065,6 +1308,7 @@ def main():
if has_ref_types:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
ib_args.extend(extra_args)
result = run_ibcmd(ib_args, warn_no_user=False)
shutil.rmtree(ib_data, ignore_errors=True)
if result.returncode != 0:
@@ -1083,11 +1327,10 @@ def main():
# Create infobase
print(f'Creating infobase: {temp_base}')
result = subprocess.run(
[args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'],
capture_output=True, text=True,
)
result = run_v8(args.V8Path, ['CREATEINFOBASE', f'File="{temp_base}"', '/DisableStartupDialogs']
+ [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
print_platform_output(result)
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
@@ -1095,21 +1338,18 @@ def main():
cfg_dir = os.path.join(temp_base, 'cfg')
# LoadConfigFromFiles
print('Loading configuration from files...')
result = subprocess.run(
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'],
capture_output=True, text=True,
)
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
print_platform_output(result)
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
# UpdateDBCfg
print('Updating database configuration...')
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
result = subprocess.run(
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'],
capture_output=True, text=True,
)
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/UpdateDBCfg', '/Out', f'"{update_log}"',
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
if os.path.isfile(update_log):
try:
@@ -1117,6 +1357,7 @@ def main():
print(f.read())
except Exception:
pass
print_platform_output(result)
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
+2
View File
@@ -55,6 +55,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <па
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
+264 -20
View File
@@ -1,4 +1,4 @@
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.11 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -36,6 +36,12 @@
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
@@ -71,12 +77,177 @@ param(
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical"
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
[Parameter(Mandatory=$false)]
[string[]]$AdditionalIbcmdArguments = @()
)
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Additional platform arguments ---
$script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
return -not [char]::IsLetter($Token[$Key.Length])
}
function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
$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.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
return @()
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return @()
}
function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
exit 1
}
}
}
}
function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# parameter for the other engine is an error; the same keys coming from .v8-project.json
# simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# powershell.exe -File — how skills are invoked — cannot bind an array parameter:
# space-separated values spill into positional ones, a comma-joined list arrives as a
# single token. So accept the repo's list convention (comma-separated) and split here;
# a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd' -and $IbcmdExtra.Count -gt 0) {
Write-Host "Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 (use -AdditionalV8Arguments)" -ForegroundColor Red
exit 1
}
if ($Engine -eq 'ibcmd') {
$extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
} else {
$extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
}
if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
# Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
# would nest the array — the tokens would then be glued into one argument.
return $extra
}
function Format-ArgsForDisplay {
# Redact values of secret-prone keys in glued, =-joined and separate forms.
# Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# a leaked password does.
param([string[]]$ArgList, [string]$Engine)
$keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
$res = @()
$maskNext = $false
foreach ($tok in $ArgList) {
if ($maskNext) { $res += '***'; $maskNext = $false; continue }
$hit = $null
foreach ($k in $keys) {
if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
}
if (-not $hit) { $res += $tok; continue }
if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
}
return ,$res
}
function ConvertTo-CleanPath {
# Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
# surrounding quotes that survived shell parsing, a trailing separator. A quote left
# inside afterwards cannot be part of a real path — reject it by name instead of letting
# 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
if (-not $Value) { return $Value }
$v = $Value.Trim()
if ($v.Length -ge 2 -and $v[0] -eq $v[-1] -and ($v[0] -eq '"' -or $v[0] -eq "'")) {
$v = $v.Substring(1, $v.Length - 2).Trim()
}
if ($v.Length -gt 3 -and ($v[-1] -eq '\' -or $v[-1] -eq '/')) { $v = $v.Substring(0, $v.Length - 1) }
if ($v.Contains('"')) {
Write-Host "Error: $ParamName contains a quote character: $Value" -ForegroundColor Red
exit 1
}
return $v
}
$V8Path = ConvertTo-CleanPath $V8Path '-V8Path'
$InfoBasePath = ConvertTo-CleanPath $InfoBasePath '-InfoBasePath'
$InputFile = ConvertTo-CleanPath $InputFile '-InputFile'
$OutputDir = ConvertTo-CleanPath $OutputDir '-OutputDir'
function Assert-InfoBaseExists {
# These skills work on a ready infobase. Saying so up front beats the platform's
# "Неверные или отсутствующие параметры соединения" after a launch.
param([string]$Path)
if (-not $Path) { return }
if (-not (Test-Path (Join-Path $Path "1Cv8.1CD"))) {
Write-Host "Error: information base not found at $Path (no 1Cv8.1CD)" -ForegroundColor Red
exit 1
}
}
Assert-InfoBaseExists $InfoBasePath
# --- Resolve V8Path ---
function Find-ProjectV8Path {
$dir = (Get-Location).Path
@@ -128,34 +299,95 @@ if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
function Invoke-IbcmdProcess {
# Run ibcmd non-interactively: a closed stdin pipe (EOF) makes ibcmd's auth prompt
# fast-fail instead of hanging. Returns @{ Output; ExitCode }. cp866 decodes ibcmd's
# native OEM output. The 1cv8/DESIGNER branch keeps using Start-Process.
param([string]$Exe, [string[]]$IbArgs)
function ConvertFrom-PlatformBytes {
# ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
# OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes — guessing
# one of them outright mangles Cyrillic.
param([byte[]]$Bytes)
if (-not $Bytes -or $Bytes.Length -eq 0) { return '' }
try {
$strict = New-Object System.Text.UTF8Encoding($false, $true)
return $strict.GetString($Bytes)
} catch {
return [System.Text.Encoding]::GetEncoding(866).GetString($Bytes)
}
}
function Invoke-PlatformProcess {
# Run the platform non-interactively and capture its console output. A closed stdin pipe
# (EOF) makes an auth prompt fast-fail instead of hanging; capturing keeps the child's
# text out of our stream until we print it labelled (and out of the wrong encoding).
# Returns @{ Output; ExitCode }.
#
# Quoting differs by engine, so the caller says which it built:
# ibcmd — tokens are bare (--db-path=C:\a b), the whole token gets quoted here;
# 1cv8 — -PreQuoted: the caller already put quotes inside the token (File="C:\a b"),
# which is where 1C's own parser expects them; quoting again breaks the value.
param([string]$Exe, [string[]]$ProcArgs, [switch]$PreQuoted)
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Exe
$psi.Arguments = ($IbArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
$psi.Arguments = if ($PreQuoted) {
$ProcArgs -join ' '
} else {
($ProcArgs | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' '
}
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.RedirectStandardInput = $true
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
try {
$psi.StandardOutputEncoding = [System.Text.Encoding]::GetEncoding(866)
$psi.StandardErrorEncoding = [System.Text.Encoding]::GetEncoding(866)
} catch {}
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
$out = $p.StandardOutput.ReadToEnd()
$err = $p.StandardError.ReadToEnd()
# stderr is drained in parallel: reading the streams one after another deadlocks
# as soon as the other one fills its pipe buffer.
$errMs = New-Object System.IO.MemoryStream
$errTask = $p.StandardError.BaseStream.CopyToAsync($errMs)
$outMs = New-Object System.IO.MemoryStream
$p.StandardOutput.BaseStream.CopyTo($outMs)
$errTask.Wait()
$p.WaitForExit()
$out = ConvertFrom-PlatformBytes $outMs.ToArray()
$err = ConvertFrom-PlatformBytes $errMs.ToArray()
if ($err) { $out += $err }
return [pscustomobject]@{ Output = $out; ExitCode = $p.ExitCode }
}
function Write-PlatformOutput {
# Print what the platform wrote to the console as its own labelled block. Silence stays
# silent: in batch mode 1cv8 reports through /Out and prints nothing here.
param([string]$Text)
if (-not $Text) { return }
$t = $Text.TrimEnd()
if (-not $t) { return }
$limit = 65536
if ($t.Length -gt $limit) {
$t = "[... обрезано, показаны последние $limit символов ...]`r`n" + $t.Substring($t.Length - $limit)
}
Write-Host "--- Вывод платформы ---"
Write-Host $t
Write-Host "--- End ---"
}
function Test-DirNonEmpty {
# Postcondition: the platform must have written files into the output directory.
# Exit code 0 with an empty dir (broken/headless env) is a false success — reject it.
param([string]$Path)
return (Test-Path $Path -PathType Container) -and ([bool](Get-ChildItem -LiteralPath $Path -Force -ErrorAction SilentlyContinue | Select-Object -First 1))
}
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
$argHints = @{ '/F' = '-InfoBasePath'; '/S' = '-InfoBaseServer + -InfoBaseRef'; '/N' = '-UserName'; '/P' = '-Password'; '--db-path' = '-InfoBasePath'; '--user' = '-UserName'; '--password' = '-Password' }
$extraArgs = @(Resolve-ExtraArgs $engine $AdditionalV8Arguments $AdditionalIbcmdArguments $argHints)
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
@@ -189,16 +421,21 @@ try {
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
Write-Host "Running: ibcmd $($arguments -join ' ')"
$__ib = Invoke-IbcmdProcess $V8Path $arguments
$arguments += $extraArgs
Write-Host "Running: ibcmd $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__ib = Invoke-PlatformProcess $V8Path $arguments
$output = $__ib.Output
$exitCode = $__ib.ExitCode
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "External data processor/report dumped successfully to: $OutputDir" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
} else {
Write-Host "Error dumping external data processor/report (code: $exitCode)" -ForegroundColor Red
}
if ($output) { Write-Host ($output | Out-String) }
Write-PlatformOutput $output
exit $exitCode
}
@@ -222,15 +459,21 @@ try {
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $($arguments -join ' ')"
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
$outMissing = ($exitCode -eq 0) -and -not (Test-DirNonEmpty $OutputDir)
if ($outMissing) { $exitCode = 1 }
if ($exitCode -eq 0) {
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green
} elseif ($outMissing) {
Write-Host "Error: exit code 0 but no files under $OutputDir — dump produced no output" -ForegroundColor Red
} else {
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red
}
@@ -243,6 +486,7 @@ try {
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
+308 -23
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-dump v1.6 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.11 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -36,6 +36,163 @@ def _find_project_v8path():
d = parent
# --- Additional platform arguments ---
V8_OWNED_KEYS = [
"DESIGNER", "ENTERPRISE", "CREATEINFOBASE", "CONFIG",
"/F", "/S", "/N", "/P", "/Out", "/DisableStartupDialogs",
"/UseTemplate", "/AddToList", "/Execute", "/C", "/URL", "/UC",
"/DumpIB", "/RestoreIB", "/DumpCfg", "/LoadCfg",
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
"--user", "--password",
]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
def arg_key_match(token, key):
"""Token matches a key when it equals it, or starts with it and the next character
is not a letter catches glued /N"user" and --password=x, while keeping
/ClearCache distinct from /C."""
if len(token) < len(key):
return False
if token[: len(key)].lower() != key.lower():
return False
if len(token) == len(key):
return True
return not token[len(key)].isalpha()
def project_extra_args(name):
"""v8args / ibcmdargs from .v8-project.json — same upward walk as 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(name)
if v:
return [str(x) for x in v]
except Exception:
pass
return []
parent = os.path.dirname(d)
if parent == d:
return []
d = parent
def assert_extra_args(extra, engine, hints):
"""The platform accepts only one batch operation, and a duplicate connection or
output key fails with an opaque 1C error reject what the skill owns itself."""
param = "-AdditionalIbcmdArguments" if engine == "ibcmd" else "-AdditionalV8Arguments"
owned = IBCMD_OWNED_KEYS if engine == "ibcmd" else V8_OWNED_KEYS
for tok in extra:
if engine == "ibcmd" and not tok.startswith("-"):
print(
f"Error: '{tok}' is a positional token — pass values as --key=value "
f"({param} cannot extend the ibcmd command)",
file=sys.stderr,
)
sys.exit(1)
for k in owned:
if arg_key_match(tok, k):
hint = f" (use {hints[k]})" if hints and k in hints else ""
print(
f"Error: {k} is controlled by the skill and cannot be passed via {param}{hint}",
file=sys.stderr,
)
sys.exit(1)
def format_args_for_display(arglist, engine):
"""Redact values of secret-prone keys in glued, =-joined and separate forms.
Matching here is a plain prefix (no letter rule): over-masking costs nothing,
a leaked password does."""
keys = IBCMD_SECRET_KEYS if engine == "ibcmd" else V8_SECRET_KEYS
res = []
mask_next = False
for tok in arglist:
if mask_next:
res.append("***")
mask_next = False
continue
hit = None
for k in keys:
if tok[: len(k)].lower() == k.lower():
hit = k
break
if hit is None:
res.append(tok)
elif len(tok) == len(hit):
res.append(tok)
mask_next = True
elif tok[len(hit)] == "=":
res.append(hit + "=***")
else:
res.append(hit + "***")
return res
def extract_extra_args(argv, known_opts):
"""argparse refuses values that start with '-' (every ibcmd key does), so pull the two
escape-hatch lists out of argv by hand: after the flag, take everything up to the next
declared skill option. Returns (remaining_argv, v8_extra, ibcmd_extra)."""
rest, v8, ibcmd = [], [], []
i = 0
while i < len(argv):
low = argv[i].lower()
if low in ("-additionalv8arguments", "-additionalibcmdarguments"):
target = v8 if low == "-additionalv8arguments" else ibcmd
i += 1
while i < len(argv) and argv[i].lower() not in known_opts:
target.append(argv[i])
i += 1
continue
rest.append(argv[i])
i += 1
return rest, v8, ibcmd
def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
"""Pick the argument list for the selected engine and validate it. An explicitly
passed parameter for the other engine is an error; the same keys coming from
.v8-project.json simply do not apply a project may describe both engines.
Comma-separated elements are split apart: PowerShell's -File cannot bind an array,
so that form is the documented one and both ports must accept it. A value containing
a comma is not supported."""
v8_extra = [p for tok in v8_extra for p in str(tok).split(",") if p]
ibcmd_extra = [p for tok in ibcmd_extra for p in str(tok).split(",") if p]
if engine == "ibcmd" and v8_extra:
print(
"Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd "
"(use -AdditionalIbcmdArguments)",
file=sys.stderr,
)
sys.exit(1)
if engine != "ibcmd" and ibcmd_extra:
print(
"Error: -AdditionalIbcmdArguments applies to ibcmd only; the selected engine is 1cv8 "
"(use -AdditionalV8Arguments)",
file=sys.stderr,
)
sys.exit(1)
if engine == "ibcmd":
extra = project_extra_args("ibcmdargs") + list(ibcmd_extra)
else:
extra = project_extra_args("v8args") + list(v8_extra)
if extra:
assert_extra_args(extra, engine, hints)
return extra
def _version_dir(p):
"""Version dir for both Windows (.../1cv8/<ver>/bin/1cv8.exe) and *nix (.../1cv8/<ver>/1cv8)."""
parent = os.path.dirname(p)
@@ -86,6 +243,85 @@ IBCMD_NOUSER_HINT = (
)
def decode_platform_bytes(data):
"""ibcmd writes UTF-8 (checked on 8.3.24, 8.3.27, 8.5), a crashing 1cv8 may still emit
OEM text. Decode strictly as UTF-8 and fall back to cp866 on invalid bytes the locale
code page (what text=True uses) mangles both."""
if not data:
return ""
try:
return data.decode("utf-8")
except UnicodeDecodeError:
return data.decode("cp866", errors="replace")
def assert_infobase_exists(path):
"""These skills work on a ready infobase. Saying so up front beats the platform's
"Неверные или отсутствующие параметры соединения" after a launch."""
if not path:
return
if not os.path.isfile(os.path.join(path, "1Cv8.1CD")):
print(f"Error: information base not found at {path} (no 1Cv8.1CD)", file=sys.stderr)
sys.exit(1)
def clean_path(value, param=""):
"""Forgive what is unambiguous in a path the caller passed: surrounding whitespace,
surrounding quotes that survived shell parsing, a trailing separator. A quote left
inside afterwards cannot be part of a real path reject it by name instead of letting
1C answer with its opaque "Неверные или отсутствующие параметры соединения"."""
if not value:
return value
v = value.strip()
if len(v) >= 2 and v[0] == v[-1] and v[0] in "\"'":
v = v[1:-1].strip()
if len(v) > 3 and v[-1] in "\\/":
v = v[:-1]
if '"' in v:
print(f"Error: {param or 'path'} contains a quote character: {value}", file=sys.stderr)
sys.exit(1)
return v
def quote_if_needed(token):
"""Extra arguments come from the caller unquoted; the 1cv8 command line is joined
verbatim, so a token with a space needs quotes of its own."""
if token and (" " in token or "\t" in token) and '"' not in token:
return f'"{token}"'
return token
def run_v8(v8path, arguments):
"""Run 1cv8 in batch mode and capture its console output.
The arguments carry their own quotes inside the value (File="C:\\a b") that is where
1C's parser expects them, on Windows and on *nix alike. Windows list2cmdline would
escape those quotes, so there the command line is handed over ready-made.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def print_platform_output(result):
"""Print what the platform wrote to the console as its own labelled block. Silence stays
silent: in batch mode 1cv8 reports through /Out and prints nothing here."""
text = ((result.stdout or "") + (result.stderr or "")).rstrip()
if not text:
return
limit = 65536
if len(text) > limit:
text = f"[... обрезано, показаны последние {limit} символов ...]\n" + text[-limit:]
print("--- Вывод платформы ---")
print(text)
print("--- End ---")
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -96,7 +332,25 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
return subprocess.run(cmd, input="", capture_output=True, encoding="utf-8", errors="replace")
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
return r
def dir_nonempty(path):
"""Postcondition: the platform must have written files into the output directory.
Exit code 0 with an empty dir (broken/headless env) is a false success reject it."""
return os.path.isdir(path) and any(os.scandir(path))
def _redact(text, *secrets):
"""Redact literal secret values (password, user) from a display string —
precise, never touches lookalike paths."""
for s in secrets:
if s:
text = text.replace(s, "***")
return text
def main():
@@ -120,12 +374,36 @@ def main():
choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)",
)
args = parser.parse_args()
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
help="Extra ibcmd arguments in --key=value form")
known_opts = {s.lower() for a in parser._actions for s in a.option_strings}
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = parser.parse_args(argv)
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
args.InputFile = clean_path(args.InputFile, "-InputFile")
args.OutputDir = clean_path(args.OutputDir, "-OutputDir")
# --- Resolve V8Path ---
v8path = resolve_v8path(args.V8Path)
engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
# --- Resolve additional arguments for the selected engine ---
arg_hints = {
"/F": "-InfoBasePath",
"/S": "-InfoBaseServer + -InfoBaseRef",
"/N": "-UserName",
"/P": "-Password",
"--db-path": "-InfoBasePath",
"--user": "-UserName",
"--password": "-Password",
}
extra_args = resolve_extra_args(engine, v8_extra, ibcmd_extra, arg_hints)
# --- 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)
@@ -163,51 +441,57 @@ def main():
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
print(f"Running: ibcmd {' '.join(arguments)}")
arguments.extend(extra_args)
print(f"Running: ibcmd {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_ibcmd([v8path] + arguments, warn_no_user=False)
if result.returncode == 0:
exit_code = result.returncode
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"External data processor/report dumped successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
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)
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", args.InfoBasePath]
arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f"/N{args.UserName}")
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f"/P{args.Password}")
arguments.append(f'/P"{args.Password}"')
arguments += ["/DumpExternalDataProcessorOrReportToFiles", args.OutputDir, args.InputFile]
arguments += ["/DumpExternalDataProcessorOrReportToFiles", f'"{args.OutputDir}"', f'"{args.InputFile}"']
arguments += ["-Format", args.Format]
# --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", out_file]
arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}")
result = subprocess.run(
[v8path] + arguments,
capture_output=True,
text=True,
)
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
# --- Result ---
# Postcondition: exit 0 with an empty output directory is a false success.
out_missing = exit_code == 0 and not dir_nonempty(args.OutputDir)
if out_missing:
exit_code = 1
if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
@@ -222,6 +506,7 @@ def main():
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
@@ -1,4 +1,4 @@
# epf-validate v1.2 — Validate 1C external data processor / report structure
# epf-validate v1.3 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
param(
@@ -185,8 +185,9 @@ if ($root.NamespaceURI -ne $expectedNs) {
$version = $root.GetAttribute("version")
if (-not $version) {
Report-Warn "1. Missing version attribute on MetaDataObject"
} elseif ($version -ne "2.17" -and $version -ne "2.20" -and $version -ne "2.21") {
Report-Warn "1. Unusual version '$version' (expected 2.17, 2.20 or 2.21)"
} elseif ($version -notin @("2.17", "2.18", "2.19", "2.20", "2.21")) {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
Report-Warn "1. Unusual version '$version' (expected 2.17-2.20 or 2.21)"
}
# Detect type: ExternalDataProcessor or ExternalReport
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-validate v1.2 — Validate 1C external data processor / report structure
# epf-validate v1.3 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -165,8 +165,9 @@ def main():
version = root.get("version", "")
if not version:
report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20", "2.21"):
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 2.21)")
elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
report_warn(f"1. Unusual version '{version}' (expected 2.17-2.20 or 2.21)")
# Detect type
child_elements = []
+2
View File
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
+2
View File
@@ -57,6 +57,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dum
| `-InputFile <путь>` | да | Путь к ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
+29 -5
View File
@@ -1,4 +1,4 @@
# form-add v1.7 — Add managed form to 1C config object
# form-add v1.12 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -33,6 +33,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -69,10 +79,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$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 (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -143,7 +156,10 @@ function Detect-FormatVersion([string]$dir) {
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -195,7 +211,7 @@ $supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
)
$objectType = $null
@@ -471,7 +487,10 @@ if (-not $childObjects) {
exit 1
}
# Добавить <Form>$FormName</Form>
# Добавить <Form>$FormName</Form> — идемпотентно (не дублировать уже зарегистрированную)
$alreadyRegistered = [bool]$childObjects.SelectSingleNode("md:Form[text()='$FormName']", $nsMgr)
if (-not $alreadyRegistered) {
$formElem = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName
@@ -525,6 +544,7 @@ if ($insertBefore) {
}
}
}
}
# --- SetDefault ---
@@ -590,7 +610,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host ""
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
if ($alreadyRegistered) {
Write-Host "Already registered: <Form>$FormName</Form> in ChildObjects (skipped duplicate)"
} else {
Write-Host "Registered: <Form>$FormName</Form> in ChildObjects"
}
if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue"
}
+103 -44
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-add v1.7 — Add managed form to 1C config object
# form-add v1.12 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -32,6 +32,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -71,6 +83,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -78,6 +93,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -193,14 +210,50 @@ def detect_format_version(d):
return "2.17"
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -256,7 +309,7 @@ def main():
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task",
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
]
object_type = None
@@ -539,47 +592,50 @@ def main():
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1)
# Add <Form>$FormName</Form>
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
if not already_registered:
form_elem = etree.Element(f"{{{ns}}}Form")
form_elem.text = form_name
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
# Find first <Template> to insert before it
first_template = child_objects.find("md:Template", NSMAP)
# Find first <TabularSection> to insert before it (if no Template)
first_tabular = child_objects.find("md:TabularSection", NSMAP)
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# Determine insertion point: before Template, before TabularSection, or at end
insert_before = None
if first_template is not None:
insert_before = first_template
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None:
# Insert before the found element
idx = list(child_objects).index(insert_before)
child_objects.insert(idx, form_elem)
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
form_elem.tail = "\n\t\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
# Add to end of ChildObjects
children = list(child_objects)
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
# Empty ChildObjects (self-closing)
child_objects.text = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
else:
if len(children) > 0:
last_child = children[-1]
old_tail = last_child.tail
last_child.tail = "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = old_tail if old_tail else "\n\t\t"
else:
child_objects.text = (child_objects.text or "") + "\n\t\t\t"
child_objects.append(form_elem)
form_elem.tail = "\n\t\t"
# --- SetDefault ---
@@ -624,7 +680,10 @@ def main():
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml")
print(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print()
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if already_registered:
print(f"Already registered: <Form>{form_name}</Form> in ChildObjects (skipped duplicate)")
else:
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated:
print(f"{default_prop_name}: {default_value}")
print()
+3 -2
View File
@@ -187,6 +187,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-compile.ps1" -
| `showTitle: true` | Показывать заголовок группы |
| `united: false` | Левый край полей ввода выравнивается только в пределах этой группы (по умолчанию `true` — сквозное выравнивание по самому длинному заголовку, в т.ч. с соседними группами) |
| `collapsed: true` | Для `behavior: "collapsible"` / `"popup"` — группа создаётся свёрнутой |
| `controlRepresentation` | Отображение управления свёрткой (`behavior: "collapsible"`): `"TitleHyperlink"` (гиперссылка заголовка, по умолчанию) / `"Picture"` (картинка) |
| `representation` | `"none"`, `"normal"`, `"weak"`, `"strong"` |
| `children: [...]` | Вложенные элементы |
@@ -549,8 +550,8 @@ PictureField, привязанный к булеву/числу, рисует и
## Workflow
1. **Компиляция**: `/form-compile` генерирует `Form.xml` и автоматически регистрирует `<Form>` в `ChildObjects` родительского объекта (если OutputPath следует конвенции `.../TypePlural/ObjectName/Forms/FormName/Ext/Form.xml`).
2. **Метаданные формы** (`ФормаСписка.xml`) и `Module.bsl` создаёт `/form-add`. Если `/form-add` ещё не вызывался — вызови после `/form-compile`. Он не перезаписывает существующий Form.xml.
1. **Каркас**: `/form-add` создаёт метаданные формы (`ФормаСписка.xml`), `Module.bsl` и регистрирует форму у объекта.
2. **Компиляция**: `/form-compile` наполняет `Form.xml` элементами.
3. **Проверка**: `/form-validate`, `/form-info`.
## Верификация
@@ -1,4 +1,4 @@
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata
# form-compile v1.176 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$JsonPath,
@@ -1337,7 +1337,10 @@ function Detect-FormatVersion([string]$dir) {
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -1362,6 +1365,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -1398,10 +1411,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$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 (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.174 — Compile 1C managed form from JSON or object metadata
# form-compile v1.176 — Compile 1C managed form from JSON or object metadata
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -34,6 +34,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -73,6 +85,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -80,6 +95,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
+18 -2
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements
# form-edit v1.6 — Edit 1C managed form elements
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -27,6 +27,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -63,10 +73,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$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 (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -257,8 +270,11 @@ function X {
}
function Esc-Xml {
# Экранирование ТЕКСТА элемента: только & < > . Кавычки в тексте платформа НЕ экранирует —
# пишет литерально (проверено: 92142 сырых кавычки на корпус, ни одной &quot;). &quot; платформа
# принимает, но при выгрузке нормализует обратно в кавычку → лишний шум в роундтрипе.
param([string]$s)
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;').Replace('"','&quot;')
return $s.Replace('&','&amp;').Replace('<','&lt;').Replace('>','&gt;')
}
function Emit-MLText {
+52 -7
View File
@@ -1,4 +1,4 @@
# form-edit v1.3 — Edit 1C managed form elements (Python port)
# form-edit v1.6 — Edit 1C managed form elements (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -209,7 +226,9 @@ def local_name(node):
# ── helpers ──────────────────────────────────────────────────
def esc_xml(s):
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;')
"""Экранирование ТЕКСТА элемента: только & < > . Кавычки платформа в тексте не экранирует
(92142 сырых кавычки на корпус, ни одной &quot;); &quot; она принимает, но нормализует обратно."""
return s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
# ── 1. Load Form.xml ────────────────────────────────────────
@@ -1458,14 +1477,40 @@ if elem_events_list:
# ── 13. Save ────────────────────────────────────────────────
# Round-trip: определить стиль исходного файла (на диске он ещё не перезаписан).
try:
_fe_raw = open(resolved_form_path, "rb").read()
except OSError:
_fe_raw = None
if _fe_raw is not None:
_fe_bom = _fe_raw.startswith(b"\xef\xbb\xbf")
_fe_body = _fe_raw[3:] if _fe_bom else _fe_raw
_fe_crlf = b"\r\n" in _fe_body
_fe_enc_m = re.search(rb'encoding="([^"]+)"', _fe_body[:200])
_fe_enc = _fe_enc_m.group(1).decode("ascii") if _fe_enc_m else "utf-8"
_fe_final_nl = _fe_body.endswith(b"\n")
else:
_fe_bom, _fe_crlf, _fe_enc, _fe_final_nl = True, False, "utf-8", True
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
# Fix XML declaration quotes
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
# Восстановить регистр encoding как в оригинале.
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + _fe_enc.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах).
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале.
xml_bytes = xml_bytes.rstrip(b"\n")
if _fe_final_nl:
xml_bytes += b"\n"
# Write with BOM
# EOL — как в оригинале.
if _fe_crlf:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
# Write preserving BOM as in original.
with open(resolved_form_path, "wb") as f:
f.write(b'\xef\xbb\xbf')
if _fe_bom:
f.write(b'\xef\xbb\xbf')
f.write(xml_bytes)
# ── 14. Summary ─────────────────────────────────────────────
+15 -2
View File
@@ -1,4 +1,4 @@
# form-info v1.4 — Analyze 1C managed form structure
# form-info v1.5 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory=$true)]
@@ -372,6 +372,16 @@ if ($formsIdx -ge 0 -and ($formsIdx + 1) -lt $parts.Count) {
# 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 Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Get-SupportStatusForPath([string]$targetPath) {
try {
$rp = (Resolve-Path $targetPath).Path
@@ -390,8 +400,10 @@ function Get-SupportStatusForPath([string]$targetPath) {
}
# The target file itself may be the element meta-xml (e.g. Subsystems/X.xml).
$elemUuid = Get-RootUuid $rp
if (Test-ExternalObjectRoot $rp) { return $null }
$d = [System.IO.Path]::GetDirectoryName($rp)
for ($i = 0; $i -lt 12 -and $d; $i++) {
if (Test-ExternalObjectRoot "$d.xml") { return $null }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $binPath) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -448,7 +460,8 @@ if ($formTitle) { $header += " — `"$formTitle`"" }
if ($objectContext) { $header += " ($objectContext)" }
$header += " ==="
$lines += $header
$lines += "Поддержка: $(Get-SupportStatusForPath $FormPath)"
$support = Get-SupportStatusForPath $FormPath
if ($null -ne $support) { $lines += "Поддержка: $support" }
# --- Form properties (Title excluded — shown in header) ---
+19 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.4 — Analyze 1C managed form structure
# form-info v1.5 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -353,14 +353,29 @@ def get_support_status_for_path(target_path):
except Exception:
pass
return None
def is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
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)
if is_external_root(rp):
return None
bin_path = None
d = os.path.dirname(rp)
for _ in range(12):
if not d:
break
if is_external_root(d + ".xml"):
return None
if not elem_uuid:
elem_uuid = root_uuid(d + ".xml")
if not bin_path:
@@ -513,7 +528,9 @@ def main():
header += f" ({object_context})"
header += " ==="
lines.append(header)
lines.append(f"Поддержка: {get_support_status_for_path(form_path)}")
_support = get_support_status_for_path(form_path)
if _support is not None:
lines.append(f"Поддержка: {_support}")
# --- Form properties (Title excluded -- shown in header) ---
prop_names = [
@@ -1,4 +1,4 @@
# form-remove v1.3 — Remove form from 1C object
# form-remove v1.4 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# remove-form v1.3 — Remove form from 1C object
# remove-form v1.4 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -13,14 +13,50 @@ from lxml import etree
NSMAP = {"md": "http://v8.1c.ru/8.3/MDClasses"}
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -1,4 +1,4 @@
# form-validate v1.8 — Validate 1C managed form
# form-validate v1.9 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -127,10 +127,11 @@ if ($root.LocalName -ne "Form") {
Report-Error "Root element is '$($root.LocalName)', expected 'Form'"
} else {
$version = $root.GetAttribute("version")
if ($version -eq "2.17" -or $version -eq "2.20") {
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
if ($version -in @("2.17", "2.18", "2.19", "2.20")) {
Report-OK "Root element: Form version=$version"
} elseif ($version) {
Report-Warn "Form version='$version' (expected 2.17 or 2.20)"
Report-Warn "Form version='$version' (expected 2.17-2.20)"
} else {
Report-Warn "Form version attribute missing"
}
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.8 — Validate 1C managed form
# form-validate v1.9 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -161,10 +161,11 @@ def main():
report_error(f"Root element is '{localname(root)}', expected 'Form'")
else:
version = root.get("version", "")
if version in ("2.17", "2.20"):
# Лестница версий формата: 2.17 (8.3.20-8.3.24), 2.18 (8.3.25), 2.19 (8.3.26), 2.20 (8.3.27).
if version in ("2.17", "2.18", "2.19", "2.20"):
report_ok(f"Root element: Form version={version}")
elif version:
report_warn(f"Form version='{version}' (expected 2.17 or 2.20)")
report_warn(f"Form version='{version}' (expected 2.17-2.20)")
else:
report_warn("Form version attribute missing")
+14 -1
View File
@@ -1,4 +1,4 @@
# help-add v1.7 — Add built-in help to 1C object
# help-add v1.9 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -28,6 +28,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -64,10 +74,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$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 (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
+60 -7
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# add-help v1.7 — Add built-in help to 1C object
# add-help v1.9 — Add built-in help to 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -33,6 +33,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -72,6 +84,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -79,6 +94,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -188,14 +205,50 @@ def detect_format_version(d):
return "2.17"
def save_xml_with_bom(tree, path):
"""Save XML tree to file with UTF-8 BOM."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_with_bom(tree, path):
"""Save XML tree preserving the existing file's BOM/EOL/encoding-case/final-newline."""
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
@@ -1,4 +1,4 @@
# interface-edit v1.6 — Edit 1C CommandInterface.xml
# interface-edit v1.9 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
@@ -39,6 +39,16 @@ function Get-RootUuid([string]$xmlPath) {
} catch {}
return $null
}
function Test-ExternalObjectRoot([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $false }
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) { return @('ExternalDataProcessor','ExternalReport') -contains $el.LocalName }
} catch {}
return $false
}
function Find-V8Project([string]$startDir) {
$d = $startDir
for ($i = 0; $i -lt 20 -and $d; $i++) {
@@ -75,10 +85,13 @@ function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if (Test-ExternalObjectRoot $rp) { return }
$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 (Test-ExternalObjectRoot "$d.xml") { return }
if (-not $elemUuid) { $elemUuid = Get-RootUuid "$d.xml" }
if (-not $cfgDir) {
$cand = Join-Path (Join-Path $d "Ext") "ParentConfigurations.bin"
@@ -151,7 +164,10 @@ function Detect-FormatVersion([string]$dir) {
while ($d) {
$cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) {
$head = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8).Substring(0, [Math]::Min(2000, (Get-Item $cfgPath).Length))
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# interface-edit v1.6 — Edit 1C CommandInterface.xml
# interface-edit v1.9 — Edit 1C CommandInterface.xml
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -31,6 +31,18 @@ def _sg_root_uuid(xml_path):
return None
def _sg_is_external_root(xml_path):
if not os.path.isfile(xml_path):
return False
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str):
return child.tag.split("}")[-1] in ("ExternalDataProcessor", "ExternalReport")
except Exception:
return False
return False
def _sg_find_v8project(start_dir):
d = start_dir
for _ in range(20):
@@ -70,6 +82,9 @@ def _sg_get_edit_mode(cfg_dir):
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# Autonomous external object (EPF/ERF): never part of a config on support (issue #39).
if _sg_is_external_root(rp):
return
elem_uuid = _sg_root_uuid(rp)
cfg_dir = None
bin_path = None
@@ -77,6 +92,8 @@ def assert_edit_allowed(target_path, require):
for _ in range(12):
if not d:
break
if _sg_is_external_root(d + ".xml"):
return
if not elem_uuid:
elem_uuid = _sg_root_uuid(d + ".xml")
if not cfg_dir:
@@ -270,13 +287,49 @@ def parse_value_list(val):
return [val]
def save_xml_bom(tree, path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>')
if not xml_bytes.endswith(b"\n"):
def _detect_xml_style(path):
"""Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
финальный перенос. None файл новый (сохранить текущее поведение)."""
try:
raw = open(path, "rb").read()
except OSError:
return None
bom = raw.startswith(b"\xef\xbb\xbf")
body = raw[3:] if bom else raw
crlf = b"\r\n" in body
m = re.search(rb'encoding="([^"]+)"', body[:200])
enc = m.group(1).decode("ascii") if m else "utf-8"
final_nl = body.endswith(b"\n")
return {"bom": bom, "crlf": crlf, "enc": enc, "final_nl": final_nl}
def _finalize_xml_bytes(xml_bytes, style):
"""Привести сериализованные байты к стилю оригинала (или к дефолту, если style is None)."""
enc_decl = style["enc"] if style else "utf-8"
xml_bytes = xml_bytes.replace(
b"<?xml version='1.0' encoding='UTF-8'?>",
b'<?xml version="1.0" encoding="' + enc_decl.encode("ascii") + b'"?>')
# Канонизировать переносы к LF (убирает &#13; от \r в tail'ах)
xml_bytes = (xml_bytes.replace(b"&#13;\n", b"\n").replace(b"&#13;", b"")
.replace(b"\r\n", b"\n").replace(b"\r", b"\n"))
# Финальный перенос — как в оригинале (новый файл → есть)
want_final_nl = style["final_nl"] if style else True
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → LF, текущее поведение)
if style and style["crlf"]:
xml_bytes = xml_bytes.replace(b"\n", b"\r\n")
return xml_bytes
def save_xml_bom(tree, path):
style = _detect_xml_style(path)
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8")
xml_bytes = _finalize_xml_bytes(xml_bytes, style)
with open(path, "wb") as f:
f.write(b"\xef\xbb\xbf")
if style is None or style["bom"]:
f.write(b"\xef\xbb\xbf")
f.write(xml_bytes)
+80 -61
View File
@@ -9,18 +9,19 @@ allowed-tools:
- Glob
---
# /meta-compile — генерация объектов метаданных из JSON DSL
# /meta-compile — генерация объектов метаданных из JSON
Принимает JSON-определение объекта метаданных → генерирует XML + модули в структуре выгрузки конфигурации + регистрирует в Configuration.xml.
Принимает JSON-определение объекта → генерирует XML + модули в структуре выгрузки конфигурации и
регистрирует объект в `Configuration.xml`.
`ConfigDumpInfo.xml` намеренно не трогается: это служебный файл версий объектов, которым управляет
платформа (для инкрементальной выгрузки).
## Порядок работы
1. Составь JSON по синтаксису и примерам ниже → запиши во временный файл
2. Запусти скрипт meta-compile
3. Если нужно изменить созданный объект — `/meta-edit`
4. Если нужно проверить — `/meta-validate`
## Команда
1. Составь JSON по синтаксису ниже → запиши во временный файл.
2. Запусти скрипт.
3. Изменить созданный объект — `/meta-edit`; проверить — `/meta-validate`.
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -JsonPath "<json>" -OutputDir "<ConfigDir>"
@@ -28,92 +29,110 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
| Параметр | Описание |
|----------|----------|
| `JsonPath` | Путь к JSON-файлу (один объект `{...}` или массив `[{...}, ...]`) |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/` и т.д.) |
| `JsonPath` | Путь к JSON-файлу |
| `OutputDir` | Корень выгрузки конфигурации (где `Configuration.xml`, `Catalogs/`, `Documents/`, …) |
## JSON DSL
## Формат JSON
### Общая структура
**Один объект** `{ ... }` или **массив** объектов `[{ ... }, { ... }]` (batch — несколько объектов за прогон).
```json
{ "type": "Catalog", "name": "Номенклатура", ...свойства типа... }
{ "type": "Catalog", "name": "Номенклатура", "...свойства типа...": "..." }
```
`type` и `name` — обязательные. `synonym` генерируется из `name` автоматически (CamelCase → слова через пробел). Можно задать явно: `"synonym": "Мой синоним"`.
`type` и `name` — обязательные. Остальное — по типу (см. индекс ниже). `synonym` по умолчанию выводится из
`name` (CamelCase → слова через пробел); можно задать явно строкой или мультиязычно: `"synonym": { "ru": "…", "en": "…" }`.
### Shorthand реквизитов
## Реквизиты (shorthand)
Используется в `attributes`, `dimensions`, `resources`, `tabularSections`:
Массивы `attributes`, `dimensions`, `resources` и колонки в `tabularSections` задаются строками:
```
"ИмяРеквизита" → String(10) по умолчанию
"ИмяРеквизита: Тип" → с типом
"ИмяРеквизита: Тип | req, index" → с флагами
"Имя" → String(10)
"Имя: Тип" → с типом
"Имя: Тип | req, index" с флагами
```
Типы: `String(100)`, `Number(15,2)`, `Boolean`, `Date`, `DateTime`, `CatalogRef.Xxx`, `DocumentRef.Xxx`, `EnumRef.Xxx`, `DefinedType.Xxx` и др. ссылочные.
**Типы:** `String(100)`, `String(10, fixed)` (фикс. длина), `Number(15,2)`, `Boolean`, `Date`, `DateTime`,
`Time`, ссылочные `CatalogRef.Xxx` / `DocumentRef.Xxx` / `EnumRef.Xxx` / `DefinedType.Xxx` и т.п.
Составной тип — через `+`: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
Составной тип: `"Значение: String + Number(15,2) + CatalogRef.Контрагенты"`.
**Флаги** (после `|`, через запятую):
Флаги: `req`, `index`, `indexAdditional`, `nonneg`, `master`, `mainFilter`, `denyIncomplete`, `useInTotals`.
| Флаг | Значение | Где |
|------|----------|-----|
| `req` | обязательное заполнение | attributes, dimensions, resources |
| `index` | индексировать | attributes, dimensions |
| `indexAdditional` | индекс с доп. упорядочиванием | attributes |
| `multiline` | многострочное поле | attributes |
| `nonneg` | неотрицательное (Number) | attributes, resources |
| `master` | ведущее измерение | dimensions (регистры) |
| `mainFilter` | основной отбор | dimensions (регистры) |
| `denyIncomplete` | запрет незаполненных | dimensions |
| `useInTotals` | использовать в итогах | dimensions (регистр накопления) |
### Свойства по типам
Реквизиту нужны свойства сверх shorthand (значение заполнения, параметры выбора, формат, подсказка, …) —
задаётся **объектной формой**, см. `reference/attributes.md`.
Примеров и shorthand-синтаксиса выше достаточно для типовых задач. Если нужны свойства типа, не показанные в примерах, и их допустимые значения — см. reference-файл:
- `reference/types-basic.md` — Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
- `reference/types-registers.md` — InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
- `reference/types-process.md` — BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
- `reference/types-web.md` — HTTPService, WebService
Эта инструкция и reference-файлы — полная документация для генерации. Не ищи примеры XML в выгрузках конфигураций.
## Примеры паттернов DSL
### Минимальный объект
## Табличные части
```json
{ "type": "Catalog", "name": "Валюты" }
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
```
### С реквизитами
Ключ — имя ТЧ, значение — массив колонок (shorthand) ЛИБО объект со свойствами ТЧ (см. `reference/attributes.md`).
## Индекс: свойства по типам
Для каждого типа — свой reference-файл со свойствами, дефолтами и допустимыми значениями:
| Тип(ы) | Файл |
|--------|------|
| Catalog (справочник) | `reference/catalog.md` |
| Document, DocumentJournal, Sequence, DocumentNumerator | `reference/document.md` |
| InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister | `reference/registers.md` |
| ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes | `reference/charts.md` |
| ExchangePlan | `reference/exchangeplan.md` |
| BusinessProcess, Task | `reference/process.md` |
| Report, DataProcessor | `reference/report-dataprocessor.md` |
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
| HTTPService, WebService | `reference/web.md` |
| Enum, Constant, DefinedType | `reference/simple.md` |
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
Кросс-типовые детали:
- **`reference/attributes.md`** — объектная форма реквизита и колонки ТЧ (значение заполнения, параметры
выбора, формат, подсказка, границы, …) + свойства самой ТЧ.
- **`reference/blocks.md`** — блоки объекта: представления, команды (+ характеристики/стандартные реквизиты).
Эта инструкция и reference-файлы — полная документация. Не ищи примеры XML в выгрузках конфигураций.
## Примеры
Справочник с реквизитами:
```json
{
"type": "Catalog", "name": "Организации",
"descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"]
}
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)", "Директор: CatalogRef.ФизическиеЛица"] }
```
### С табличной частью
Документ с движениями и ТЧ:
```json
{
"type": "Document", "name": "ПриходнаяНакладная",
{ "type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] }
}
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)", "Цена: Number(15,2)"] } }
```
### Регистровый паттерн (измерения + ресурсы)
Регистр сведений:
```json
{
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
}
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
```
### Batch — несколько объектов в одном файле
Batch:
```json
[
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
[ { "type": "Enum", "name": "Статусы", "values": ["Новый", "Закрыт"] },
{ "type": "Catalog", "name": "Валюты" },
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
]
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" } ]
```
@@ -0,0 +1,140 @@
# Объектная форма реквизита и табличной части
Когда реквизиту (в `attributes` / `dimensions` / `resources` / колонках ТЧ) нужны свойства сверх
shorthand — вместо строки задаётся объект:
```json
{ "name": "Цена", "type": "Number(15,2)", "tooltip": "Цена за единицу", "fillValue": 0 }
```
`name` и `type` обязательны (тип можно задать и раздельно: `"type": "Number", "length": 15, "precision": 2`).
Остальные ключи — ниже, все со значением по умолчанию (не задавать, если устраивает дефолт).
## Свойства реквизита
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML (строка или `{ru,en}`) |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (то же, что флаг `req`) |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fillFromFillingValue` | `false` | bool |
| `fillValue` | по типу (см. ниже) | значение заполнения |
| `createOnInput` | `Auto` | `Auto` / `Use` / `DontUse` |
| `quickChoice` | `Auto` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `dataHistory` | `Use` | `Use` / `DontUse` |
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (реквизит иерархического справочника) |
| `passwordMode` | `false` | bool |
| `multiLine` | `false` | bool (то же, что флаг `multiline`) |
| `extendedEdit` | `false` | bool (расширенное редактирование — многострочный ввод) |
| `mask` | пусто | строка маски ввода |
| `format` / `editFormat` | пусто | форматная строка 1С (ML) |
| `markNegatives` | `false` | bool (выделять отрицательные, для Number) |
| `minValue` / `maxValue` | не задано | граница диапазона (см. ниже) |
| `choiceParameterLinks` | пусто | связи параметров выбора (см. ниже) |
| `choiceParameters` | пусто | параметры выбора (см. ниже) |
| `choiceForm` | пусто | ссылка на форму выбора `Тип.Объект.Form.ИмяФормы` |
| `choiceFoldersAndItems` | `Items` | `Items` / `Folders` / `FoldersAndItems` (что выбирать в иерарх. справочнике) |
Индексирование задаётся флагом `index` / `indexAdditional` в shorthand, либо в объекте — как и в строковой форме,
через `"type": "… | index"`.
### `fillValue` — значение заполнения
Пустое значение по типу компилятор подставляет сам — ключ **не задают**:
| Тип реквизита | Пустое значение |
|---------------|-----------------|
| String | пустая строка |
| Number | `0` |
| Boolean, Date, ссылочный, составной | не задано (nil) |
Ключ `fillValue` задают для **конкретного** значения — интерпретируется по типу реквизита:
- **Boolean**`true` / `false`.
- **Number** — число (`21`, `1.5`).
- **String** — строка.
- **Date** — ISO-строка `"2020-01-01T00:00:00"`.
- **Ссылочный** — путь: `"Catalog.Валюты.EmptyRef"` (пустая ссылка), `"Enum.Периодичность.EnumValue.Месяц"`
(значение перечисления), `"Catalog.СтраныМира.Россия"` (предопределённый элемент).
- **`null`** — явно «значение не задано» (nil), когда нужно перекрыть непустой дефолт типа.
- **`{ "emptyRef": true }`** — пустая ссылка для реквизита типа `DefinedType.X` (когда тип из пути не выводится).
> Пустая ссылка (`EmptyRef`) и `null` — разное: платформа хранит их отдельно.
### `minValue` / `maxValue` — границы диапазона
Число → числовая граница; строка → строковая (напр. год `"2000"`). Без ключа — граница не задана.
### `choiceParameterLinks` — связи параметров выбора
Связывают параметр выбора этого реквизита с другим реквизитом объекта. Массив строк или объектов:
```json
"choiceParameterLinks": ["Отбор.Организация=Организация", "Отбор.Договор=Договор:DontChange"]
"choiceParameterLinks": [{ "name": "Отбор.Организация", "dataPath": "Организация", "valueChange": "Clear" }]
```
- `dataPath` — реквизит **того же объекта**: имя обычного реквизита (`"Организация"`) или стандартного
(`"Владелец"`, `"Ссылка"`).
- `valueChange``Clear` (по умолчанию) / `DontChange`.
### `choiceParameters` — параметры выбора
Фиксируют параметр выбора значением. Массив строк или объектов:
```json
"choiceParameters": ["Отбор.ЭтоГруппа=false"]
"choiceParameters": [{ "name": "Отбор.Владелец", "value": "Catalog.Организации.EmptyRef" }]
```
- `value` — bool / число / строка / ссылочный путь (несёт тип) ИЛИ массив (список фиксированных значений).
- Для набора голых имён-значений добавьте `type` (тип поля-фильтра), чтобы они стали ссылками:
`{ "name": "Отбор.Тип", "type": "EnumRef.ТипыВЕТИС", "value": ["EmptyRef", "ТТН"] }`.
### Редкие ключи
`linkByType` — связь по типу (тип реквизита-Характеристики берётся из другого реквизита):
`{ "dataPath": "Свойство", "linkItem": 0 }` или строка-путь. Применяется для реквизитов-характеристик.
---
## Табличная часть — объектная форма
Значение в `tabularSections` — массив колонок ЛИБО объект со свойствами самой ТЧ:
```json
"tabularSections": {
"Товары": {
"synonym": { "ru": "Товары", "en": "Goods" },
"tooltip": "Строки заказа",
"fillChecking": "ShowError",
"attributes": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"]
}
}
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `fillChecking` | `DontCheck` | `DontCheck` / `ShowError` / `ShowWarning` (обязательность заполнения ТЧ) |
| `use` | `ForItem` | `ForItem` / `ForFolder` / `ForFolderAndItem` (только Catalog / ChartOfCharacteristicTypes) |
| `attributes` | `[]` | колонки (shorthand или объектная форма реквизита) |
| `lineNumber` | — | кастомизация стандартного реквизита НомерСтроки (см. ниже) |
| `lineNumberLength` | по режиму совместимости | `5``9` — разрядность номера строки: `5` → до 99 999 строк, `9` → до 999 999 999. Требует формата 2.20 (платформа 8.3.27) |
### `lineNumber` — стандартный реквизит НомерСтроки
У каждой ТЧ есть стандартный реквизит НомерСтроки. По умолчанию все его свойства типовые. Ключ `lineNumber`
на объектной форме ТЧ их переопределяет:
```json
"Строки": { "lineNumber": { "synonym": "Номер п/п", "fullTextSearch": "DontUse" }, "attributes": [...] }
```
Переопределяемые: `synonym`, `comment`, `fullTextSearch` (`Use`/`DontUse`), `tooltip`, `format`, `editFormat`,
`choiceHistoryOnInput` (`Auto`/`DontUse`).
@@ -0,0 +1,109 @@
# Блоки объекта
Кросс-типовые блоки уровня объекта (применимы к ссылочным типам — Catalog, Document, ChartOf*, ExchangePlan,
BusinessProcess, Task и др.).
## Представления
Тексты представления объекта в интерфейсе (ML — строка или `{ru,en}`, по умолчанию пусто):
| Ключ | Смысл |
|------|-------|
| `objectPresentation` | представление объекта |
| `extendedObjectPresentation` | расширенное представление объекта |
| `listPresentation` | представление списка |
| `extendedListPresentation` | расширенное представление списка |
| `explanation` | пояснение |
Набор доступных ключей зависит от типа (у списочных без формы объекта нет `objectPresentation` и т.п.).
```json
"listPresentation": "Организации", "objectPresentation": { "ru": "Организация", "en": "Company" }
```
## Команды
Команды объекта. Ключ — имя команды, значение — объект свойств (map `имя → объект` или массив `[{name, …}]`).
Для каждой команды создаётся заготовка модуля с обработчиком `ОбработкаКоманды`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `synonym` | из имени | ML |
| `tooltip` | пусто | ML |
| `comment` | пусто | строка |
| `group` | **обязательно** | группа размещения (см. ниже) |
| `commandParameterType` | пусто | тип параметра (напр. `CatalogRef.Номенклатура`) — **только для групп формы** |
| `parameterUseMode` | `Single` | `Single` / `Multiple` |
| `modifiesData` | `false` | bool |
| `representation` | `Auto` | вид отображения |
| `picture` | пусто | ссылка на картинку (`StdPicture.Print`, `CommonPicture.Загрузка`) |
| `shortcut` | пусто | сочетание клавиш |
```json
"commands": {
"ПечатьЭтикеток": { "synonym": "Печать этикеток", "group": "FormCommandBarImportant",
"commandParameterType": "CatalogRef.Номенклатура", "picture": "StdPicture.Print" }
}
```
**Группа (`group`) обязательна** — каждая команда размещается в группе командного интерфейса:
- **Командный интерфейс раздела** (панель навигации / панель действий; `commandParameterType` **недоступен**):
`NavigationPanelImportant` / `NavigationPanelOrdinary` / `NavigationPanelSeeAlso`,
`ActionsPanelCreate` / `ActionsPanelReports` / `ActionsPanelTools`.
- **Командный интерфейс формы** (`commandParameterType` допустим): `FormCommandBarImportant` /
`FormCommandBarCreateBasedOn`, `FormNavigationPanelImportant` / `FormNavigationPanelGoTo` / `FormNavigationPanelSeeAlso`.
- **Кастомная группа:** `CommandGroup.<Имя>` (параметр допустим).
Группа раздела вместе с `commandParameterType` → ошибка.
## `inputByString` / `dataLockFields` / `basedOn`
Списки полей/объектов уровня объекта. Поля — по имени реквизита объекта (обычного или стандартного).
- **`inputByString`** — поля быстрого ввода по строке. По умолчанию выводятся из Кода/Наименования — ключ не нужен;
задать при другом наборе/порядке, либо `[]` для отключения.
```json
"inputByString": ["Код", "Наименование", "Контрагент"]
```
- **`dataLockFields`** — поля управляемой блокировки данных (по умолчанию пусто).
```json
"dataLockFields": ["Организация", "Контрагент"]
```
- **`basedOn`** — «ввод на основании»: список ссылок на объекты метаданных (по умолчанию пусто).
```json
"basedOn": ["Catalog.Контрагенты", "Document.ЗаказПоставщику"]
```
## `standardAttributes` — кастомизация стандартных реквизитов
Стандартные реквизиты объекта (Наименование, Код, Владелец, …) переопределяются блоком
`standardAttributes` — объект `{ ИмяРеквизита: { переопределения } }`. Имена — как в 1С: `Description`, `Code`,
`Owner`, `Parent`, `DeletionMark`, `Ref` и т.д. (для Document — `Date`, `Number`, `Posted`).
Переопределяемые поля — как у обычного реквизита (`synonym`, `tooltip`, `fillChecking`, `fillValue`,
`choiceParameters`, `comment`, `mask`, `choiceForm`; полный набор — `attributes.md`).
```json
"standardAttributes": {
"Description": { "synonym": "Наименование контрагента" },
"Code": { "fillChecking": "ShowError" }
}
```
## `characteristics` — «Дополнительные реквизиты и сведения»
Привязка плана видов характеристик. Массив; каждый элемент связывает **источник типов** (где определены
характеристики) и **источник значений** (где хранятся значения).
```json
"characteristics": [{
"types": { "from": "Catalog.НаборыДопРеквизитов.ДополнительныеРеквизиты",
"key": "Свойство", "filterField": "Ссылка", "filterValue": "Справочник_Организации" },
"values": { "from": "Catalog.Организации.TabularSection.ДополнительныеРеквизиты",
"object": "Ссылка", "type": "Свойство", "value": "Значение" }
}]
```
- `from` — таблица-источник; `key`/`filterField`/`object`/`type`/`value` — поля источника (по имени реквизита).
- `filterValue` — значение фильтра типов: имя предопределённого набора (строка) или путь к элементу.
@@ -0,0 +1,71 @@
# Catalog (Справочник)
```json
{ "type": "Catalog", "name": "Организации", "descriptionLength": 100,
"attributes": ["ИНН: String(12)", "КПП: String(9)"] }
```
## Свойства
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `hierarchical` | `false` | bool |
| `hierarchyType` | `HierarchyFoldersAndItems` | `HierarchyFoldersAndItems` / `HierarchyOfItems` |
| `limitLevelCount` | `false` | bool (ограничивать кол-во уровней) |
| `levelCount` | `2` | число уровней (при `limitLevelCount`) |
| `foldersOnTop` | `true` | bool (группы сверху) |
| `owners` | `[]` | массив владельцев: `["Catalog.Контрагенты"]` |
| `subordinationUse` | `ToItems` | `ToItems` / `ToFolders` / `ToFoldersAndItems` (кому подчинён) |
| `codeLength` | `9` | длина кода (0 — без кода) |
| `codeType` | `String` | `String` / `Number` |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `codeSeries` | `WholeCatalog` | `WholeCatalog` / `WithinSubordination` / `WithinOwnerSubordination` |
| `autonumbering` | `true` | bool (автонумерация) |
| `checkUnique` | `false` | bool (контроль уникальности кода) |
| `descriptionLength` | `25` | длина наименования |
| `defaultPresentation` | `AsDescription` | `AsDescription` / `AsCode` |
| `quickChoice` | `true` | bool (быстрый выбор) |
| `choiceMode` | `BothWays` | `BothWays` / `QuickChoice` / `FromForm` |
| `editType` | `InDialog` | `InDialog` / `InList` / `BothWays` |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `choiceHistoryOnInput` | `Auto` | `Auto` / `DontUse` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `fullTextSearchOnInputByString` | `DontUse` | `Use` / `DontUse` |
| `searchStringModeOnInputByString` | `Begin` | `Begin` / `AnyPart` |
| `predefinedDataUpdate` | `Auto` | `Auto` / `DontAutoUpdate` / `AutoUpdate` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` | `[]` | реквизиты (shorthand / объектная форма) |
| `tabularSections` | `{}` | табличные части |
**Формы.** Ссылка на форму — `Тип.Объект.Form.ИмяФормы` (напр. `Catalog.Организации.Form.ФормаЭлемента`).
Слоты основных форм: `defaultObjectForm`, `defaultFolderForm`, `defaultListForm`, `defaultChoiceForm`,
`defaultFolderChoiceForm`; вспомогательных — те же имена с префиксом `auxiliary` (`auxiliaryObjectForm`, …).
## `predefined` — предопределённые элементы
Массив предопределённых элементов → `Ext/Predefined.xml`. Элемент — строка (плоский случай) или объект (иерархия).
**Строка:** `"(Код) Имя [Наименование]"``Имя` обязательно; `(Код)` и `[Наименование]` опциональны.
Без `[...]` наименование выводится из имени; `[]` — пустое; `[текст]` — заданное.
```json
"predefined": [
"Основной",
"(1) ДокументОПриемке [Документ о приемке]",
{ "name": "Группа1", "isFolder": true, "description": "Прочие",
"childItems": ["Факс", "(7) Скайп"] }
]
```
**Объект:** `name` (обязательно), `code`, `description` (наименование), `isFolder` (признак группы),
`childItems` (вложенные, рекурсивно). Тип кода — по свойству `codeType`.
## Дополнительно
- Свойства реквизитов и табличных частей — `attributes.md`.
- Представления (`objectPresentation`, `listPresentation`, …), команды объекта, характеристики
(«ДопРеквизиты и сведения»), кастомизация стандартных реквизитов, `inputByString` / `dataLockFields` /
`basedOn``blocks.md`.
@@ -0,0 +1,105 @@
# Планы: ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes
Все три — ссылочные типы (наследуют слой Catalog: коды, `standardAttributes`, `characteristics`, `inputByString`,
формы, представления — см. `catalog.md` / `attributes.md` / `blocks.md`) с предопределёнными элементами и своими
специальными свойствами.
## ChartOfCharacteristicTypes (План видов характеристик)
Хранит определения характеристик (видов). Иерархический (папки+элементы).
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | любой примитив | тип значения характеристики (составной — строка `"A + B"` или массив `valueTypes`) |
| `characteristicExtValues` | пусто | ссылка на справочник доп. значений |
| `hierarchical` | `false` | bool |
| `foldersOnTop` | `true` | bool |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `codeSeries` | `WholeCharacteristicKind` | серия кодов |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды (несут тип значения — см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
**Предопределённые виды** несут **тип значения на элемент** — короткой строкой после `:`
(`"(Код) Имя [Наименование]: Тип"`, составной через `+`) или объектной формой с ключом `type`:
```json
"predefined": [
"(000001) Цвет: CatalogRef.Цвета",
"(000002) Размер [Размер одежды]: String(50) + Number(3,0)",
{ "name": "Группа", "isFolder": true, "type": "" }
]
```
## ChartOfAccounts (План счетов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `extDimensionTypes` | пусто | ссылка на ПВХ видов субконто `ChartOfCharacteristicTypes.X` |
| `maxExtDimensionCount` | `0` (без ПВХ) / `3` (с ПВХ) | макс. число субконто |
| `codeMask` | пусто | маска кода счёта (напр. `"@@@.@@"`) |
| `codeLength` | `9` | длина кода |
| `descriptionLength` | `25` | длина наименования |
| `checkUnique` | `true` | bool |
| `codeSeries` | `WholeChartOfAccounts` | серия кодов |
| `defaultPresentation` | `AsCode` | `AsCode` / `AsDescription` |
| `autoOrderByCode` | `true` | bool |
| `orderLength` | `9` | длина строки упорядочивания |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `accountingFlags` | `[]` | признаки учёта (как реквизиты, тип по умолчанию Boolean; массив имён/реквизитов) |
| `extDimensionAccountingFlags` | `[]` | признаки учёта субконто (как реквизиты) |
| `predefined` | `[]` | предопределённые счета (см. ниже) |
**Предопределённый счёт** (объектная форма):
| Поле | Умолчание | Значения |
|------|-----------|----------|
| `name` | — | имя (обязательно) |
| `code` | пусто | код счёта |
| `description` | из имени | наименование |
| `accountType` | `ActivePassive` | `Active` / `Passive` / `ActivePassive` |
| `offBalance` | `false` | bool (забалансовый) |
| `order` | — | строка сортировки |
| `flags` | `[]` | включённые признаки учёта (только TRUE) |
| `subconto` | `[]` | виды субконто (см. ниже) |
| `childItems` | `[]` | подчинённые счета |
`subconto` — строка `"Вид | Признак1, Признак2"` (после `|` — включённые признаки учёта субконто; токен `Turnover`
«только обороты») или объект `{ type, turnover, flags }`. `Вид` — имя предопределённого вида из ПВХ `extDimensionTypes`.
```json
"predefined": [
{ "name": "ОсновныеСредства", "code": "01", "accountType": "Active", "order": " 01",
"flags": ["Количественный"], "subconto": ["Номенклатура | Суммовой, Валютный"],
"childItems": [ { "name": "ОСВОрганизации", "code": "01.01", "accountType": "Active", "order": " 01.01" } ] }
]
```
## ChartOfCalculationTypes (План видов расчёта)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `codeLength` | `5` | длина кода |
| `descriptionLength` | `100` | длина наименования |
| `codeAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `dependenceOnCalculationTypes` | `DontUse` | `DontUse` / `OnPeriod` / `OnActionPeriod` |
| `baseCalculationTypes` | `[]` | базовые виды расчёта (список ссылок `ChartOfCalculationTypes.X`) |
| `actionPeriodUse` | `false` | bool (использовать период действия) |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `predefined` | `[]` | предопределённые виды расчёта (см. ниже) |
**Предопределённый вид расчёта** — плоский: строка `"(Код) Имя [Наименование]"` или объект
`{ name, code, description, actionPeriodIsBase }` (`actionPeriodIsBase` — bool, по умолчанию `false`).
```json
"predefined": [ "(00001) Оклад [Оклад по дням]", { "name": "Премия", "code": "00002", "actionPeriodIsBase": true } ]
```
> **ChartOfAccounts** ссылается на ПВХ через `extDimensionTypes`. Регистр бухгалтерии/расчёта требует
> соответствующий план (см. `registers.md`).
@@ -0,0 +1,56 @@
# CommonModule, ScheduledJob, EventSubscription (объекты, привязанные к коду)
## CommonModule (Общий модуль)
Флаги контекста выполнения (все bool, по умолчанию `false`). Создаёт пустой `Ext/Module.bsl`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `context` | — | шорткат флагов (см. ниже) |
| `global` | `false` | bool |
| `server` | `false` | bool |
| `serverCall` | `false` | bool (вызов сервера) |
| `clientManagedApplication` | `false` | bool (клиент управляемого приложения) |
| `clientOrdinaryApplication` | `false` | bool (клиент обычного приложения) |
| `externalConnection` | `false` | bool |
| `privileged` | `false` | bool |
| `returnValuesReuse` | `DontUse` | `DontUse` / `DuringRequest` / `DuringSession` |
Шорткат `context`: `"server"` → Server+ServerCall; `"client"` → ClientManagedApplication;
`"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбменДаннымиСервер", "context": "server", "returnValuesReuse": "DuringRequest" }
```
## ScheduledJob (Регламентное задание)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `methodName` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
| `description` | пусто | наименование задания |
| `key` | пусто | ключ |
| `use` | `false` | bool (использование) |
| `predefined` | `false` | bool (предопределённое) |
| `restartCountOnFailure` | `3` | число повторов при сбое |
| `restartIntervalOnFailure` | `10` | интервал повтора, сек |
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить", "use": true }
```
## EventSubscription (Подписка на событие)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `source` | `[]` | объекты-источники: `["CatalogObject.Контрагенты", "DocumentObject.Реализация"]` |
| `event` | `BeforeWrite` | `BeforeWrite` / `OnWrite` / `BeforeDelete` / `OnReadAtServer` / `FillCheckProcessing` … |
| `handler` | пусто | метод-обработчик `"МодульСервер.Процедура"` (дополняется до `CommonModule.…`) |
```json
{ "type": "EventSubscription", "name": "ПередЗаписьюКонтрагента",
"source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite",
"handler": "ОбщегоНазначенияСервер.ПередЗаписьюКонтрагента" }
```
> Процедура-обработчик (`methodName` / `handler`) должна существовать в указанном общем модуле (экспортная).
@@ -0,0 +1,79 @@
# Document, DocumentJournal, Sequence, DocumentNumerator
## Document (Документ)
```json
{ "type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] } }
```
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `numerator` | пусто | ссылка на нумератор `DocumentNumerator.X` |
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / `Month` / `Quarter` / `Year` |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `posting` | `Allow` | `Allow` / `Deny` (проведение) |
| `realTimePosting` | `Deny` | `Allow` / `Deny` (оперативное проведение) |
| `registerRecordsDeletion` | `AutoDelete` | `AutoDelete` / `AutoDeleteOnUnpost` / `AutoDeleteOff` |
| `registerRecordsWritingOnPost` | `WriteSelected` | `WriteModified` / `WriteSelected` / `WriteAll` |
| `sequenceFilling` | `AutoFill` | заполнение последовательностей |
| `postInPrivilegedMode` | `true` | bool |
| `unpostInPrivilegedMode` | `true` | bool |
| `createOnInput` | `Use` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `fullTextSearch` | `Use` | `Use` / `DontUse` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `registerRecords` | `[]` | движения: список ссылок `["AccumulationRegister.ОстаткиТоваров", "InformationRegister.Цены"]` |
| `useStandardCommands` | `true` | bool |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
Формы: `defaultObjectForm`, `defaultListForm`, `defaultChoiceForm`, `auxiliary*` (см. `catalog.md`).
Реквизиты и ТЧ — `attributes.md`. Представления, команды, характеристики, `basedOn`, `standardAttributes`,
`inputByString`, `dataLockFields``blocks.md`.
## DocumentJournal (Журнал документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `registeredDocuments` | `[]` | документы журнала: `["Document.Встреча", "Document.Звонок"]` |
| `columns` | `[]` | графы журнала (см. ниже) |
Графа — строка `"Имя"` или объект `{ name, synonym, indexing, references }`, где `indexing``Index`/`DontIndex`,
`references` — пути к реквизитам документов, отображаемым в графе.
```json
{ "type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index",
"references": ["Document.Встреча.Attribute.Организация"] }] }
```
## Sequence (Последовательность документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `moveBoundaryOnPosting` | `DontMove` | сдвиг границы при проведении |
| `documents` | `[]` | документы последовательности (список ссылок) |
| `registerRecords` | `[]` | движения (список ссылок) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` | `[]` | измерения `{name, type, documentMap[], registerRecordsMap[]}` |
`documentMap` / `registerRecordsMap` — пути к реквизитам документов / движениям, соответствующим измерению.
## DocumentNumerator (Нумератор документов)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `numberAllowedLength` | `Variable` | `Variable` / `Fixed` |
| `numberPeriodicity` | `Year` | `Nonperiodical` / `Day` / … / `Year` |
| `checkUnique` | `true` | bool |
@@ -0,0 +1,41 @@
# ExchangePlan (План обмена)
Близок к справочнику (без иерархии/владельцев), плюс состав объектов обмена. Наследует слой Catalog:
`codeLength`, `codeAllowedLength`, `descriptionLength`, `defaultPresentation`, `editType`, `quickChoice`,
`choiceMode`, формы, `standardAttributes`, `characteristics`, `inputByString`, `basedOn`, представления —
см. `catalog.md` / `attributes.md` / `blocks.md`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `distributedInfoBase` | `false` | bool (распределённая ИБ — РИБ) |
| `includeConfigurationExtensions` | `false` | bool (включать расширения конфигурации) |
| `descriptionLength` | `150` | длина наименования |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dataHistory` | `DontUse` | `Use` / `DontUse` |
| `useStandardCommands` | `true` | bool |
| `content` | `[]` | состав обмена (см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
## `content` — состав плана обмена
Список объектов-участников обмена; у каждого — признак авторегистрации изменений (по умолчанию выключена).
Элемент — ссылка на объект метаданных (строка) или объект с признаком:
```json
"content": [
"Catalog.Организации", // авторегистрация выключена
"InformationRegister.Курсы: autoRecord", // авторегистрация включена (токен)
{ "metadata": "Document.РеализацияТоваров", "autoRecord": true }
]
```
- Строка `"Тип.Имя"` — авторегистрация выключена; суффикс `: autoRecord` — включена.
- Объект: `metadata` (ссылка), `autoRecord` (bool или `Allow`/`Deny`).
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "distributedInfoBase": false,
"content": ["Catalog.Номенклатура: autoRecord", "Catalog.Контрагенты: autoRecord"],
"attributes": ["АдресСервера: String(200)"] }
```
@@ -0,0 +1,90 @@
# Прочие типы
Редкие/служебные объекты. Каждый — минимальный набор свойств.
## FunctionalOption (Функциональная опция)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `location` | пусто | где хранится значение: `Constant.X` / `InformationRegister.X.Resource.Y` / `<Тип>.X.Attribute.Y` |
| `content` | `[]` | реквизиты/измерения/ресурсы, зависящие от опции (полные пути к объектам) |
| `privilegedGetMode` | `true` | bool |
| `comment` | пусто | строка |
```json
{ "type": "FunctionalOption", "name": "ВестиУчетПоСкладам", "location": "Constant.ВестиУчетПоСкладам",
"content": ["Document.РеализацияТоваров.TabularSection.Товары.Attribute.Склад"] }
```
## FilterCriterion (Критерий отбора)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | — | тип значения отбора (составной через `+`) |
| `content` | `[]` | реквизиты, по которым идёт отбор (пути к объектам) |
| `useStandardCommands` | `true` | bool |
| `defaultForm` / `auxiliaryForm` | пусто | формы |
| `comment` | пусто | строка |
```json
{ "type": "FilterCriterion", "name": "ДокументыПоКонтрагенту", "valueType": "CatalogRef.Контрагенты",
"content": ["Document.Реализация.Attribute.Контрагент"] }
```
## SettingsStorage (Хранилище настроек)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `defaultSaveForm` / `defaultLoadForm` | пусто | формы сохранения / загрузки |
| `auxiliarySaveForm` / `auxiliaryLoadForm` | пусто | вспомогательные формы |
| `comment` | пусто | строка |
## CommonForm (Общая форма)
Создаёт метаданные + заготовку формы. Содержимое формы наполняется `/form-compile` или `/form-edit`.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `formType` | `Managed` | тип формы |
| `usePurposes` | `[PlatformApplication, MobilePlatformApplication]` | назначение (массив) |
| `useStandardCommands` | `false` | bool |
| `includeHelpInContents` | `false` | bool |
| `comment` | пусто | строка |
```json
{ "type": "CommonForm", "name": "НастройкиОбмена", "usePurposes": ["PlatformApplication"] }
```
## CommonPicture / CommonTemplate (Общие картинки и макеты)
Только метаданные + регистрация; содержимое (`Ext/Picture*`, `Ext/Template.*`) импортируется отдельно
(для табличного макета — `/mxl-compile`).
- **CommonPicture**`availabilityForChoice` / `availabilityForAppearance` (bool, по умолчанию `false`).
- **CommonTemplate**`templateType` (`SpreadsheetDocument` по умолчанию / `TextDocument` / `HTMLDocument` /
`BinaryData` / `AddIn` / `DataCompositionSchema` / `DataCompositionAppearanceTemplate` / `GraphicalSchema`).
```json
{ "type": "CommonTemplate", "name": "ПечатьЗаказа", "templateType": "SpreadsheetDocument" }
```
## Служебные типы
- **SessionParameter** (параметр сеанса) — `valueType` (тип значения, составной через `+`).
- **FunctionalOptionsParameter** (параметр функциональной опции) — `use` (массив измерений/реквизитов).
- **WSReference** (WS-ссылка) — `locationURL` (URL WSDL).
- **CommandGroup** (группа команд) — `category` (по умолч. `NavigationPanel`) — где размещается группа:
`NavigationPanel` / `ActionsPanel` (командный интерфейс раздела) или `FormCommandBar` / `FormNavigationPanel`
(командный интерфейс формы); `representation` (`Auto`), `tooltip` (ML), `picture`. Команды объекта ссылаются на
группу через `group: "CommandGroup.<Имя>"` (см. `blocks.md`).
- **CommonCommand** (общая команда) — `group`, `representation`, `tooltip`, `picture`, `shortcut`,
`commandParameterType`, `parameterUseMode` (`Single`/`Multiple`), `modifiesData`, `includeHelpInContents`.
Создаёт `Ext/CommandModule.bsl`.
- **CommonAttribute** (общий реквизит) — `valueType` (по умолчанию `String(0)`) + свойства реквизита
(`attributes.md`) + `content` (объекты, куда входит реквизит) + свойства разделения данных
(`dataSeparation`, `separatedDataUse`, `usersSeparation`, … — по умолчанию `DontUse`/`Independently`).
```json
{ "type": "CommonAttribute", "name": "Организация", "valueType": "CatalogRef.Организации",
"autoUse": "Use", "content": ["Document.РеализацияТоваров", "Document.ПоступлениеТоваров"] }
```
@@ -0,0 +1,49 @@
# BusinessProcess, Task (Бизнес-процессы и Задачи)
Ссылочные типы. Наследуют слой Catalog (нумерация, формы, `standardAttributes`, `characteristics`, `basedOn`,
представления — см. `catalog.md` / `attributes.md` / `blocks.md`). Бизнес-процесс всегда связан с задачей.
## BusinessProcess (Бизнес-процесс)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `task` | пусто | ссылка на задачу `Task.X` (обязательна для рабочего БП) |
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `11` | длина номера |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
Создаётся с картой маршрута (`Ext/Flowchart.xml`) и модулем объекта.
```json
{ "type": "BusinessProcess", "name": "Согласование", "task": "Task.ЗадачаИсполнителя",
"attributes": ["Документ: DocumentRef.ЗаявкаНаРасход"] }
```
## Task (Задача)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `numberType` | `String` | `String` / `Number` |
| `numberLength` | `14` | длина номера |
| `checkUnique` | `true` | bool |
| `autonumbering` | `true` | bool |
| `descriptionLength` | `150` | длина наименования |
| `addressing` | пусто | ссылка на регистр сведений адресации `InformationRegister.X` |
| `mainAddressingAttribute` | пусто | основной реквизит адресации (имя реквизита адресации) |
| `currentPerformer` | пусто | реквизит текущего исполнителя |
| `createOnInput` | `DontUse` | `Auto` / `Use` / `DontUse` |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `addressingAttributes` | `[]` | реквизиты адресации (см. ниже) |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
**Реквизит адресации** — shorthand `"Имя: Тип"` или объект `{ name, type, addressingDimension }`
(`addressingDimension` — измерение регистра адресации).
```json
{ "type": "Task", "name": "ЗадачаИсполнителя",
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи", "Роль: CatalogRef.Роли"] }
```
@@ -0,0 +1,76 @@
# Регистры: Information, Accumulation, Accounting, Calculation
**Измерения и ресурсы** задаются как реквизиты (shorthand `"Имя: Тип | флаги"` или объектная форма, см.
`attributes.md`). Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (регистр накопления).
```json
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter"],
"resources": ["Сумма: Number(15,2)"]
```
## InformationRegister (Регистр сведений)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `writeMode` | `Independent` | `Independent` / `RecorderSubordinate` |
| `periodicity` | `Nonperiodical` | `Nonperiodical` / `Second` / `Day` / `Month` / `Quarter` / `Year` / `RecorderPosition` |
| `mainFilterOnPeriod` | `false` | bool (основной отбор по периоду) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"] }
```
## AccumulationRegister (Регистр накопления)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `registerType` | `Balance` | `Balance` (остатки) / `Turnovers` (обороты) |
| `enableTotalsSplitting` | `true` | bool (разделение итогов) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
"dimensions": ["Номенклатура: CatalogRef.Номенклатура", "Склад: CatalogRef.Склады"],
"resources": ["Количество: Number(15,3)"] }
```
## AccountingRegister (Регистр бухгалтерии)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `chartOfAccounts` | — | **обязательно**: ссылка на план счетов `ChartOfAccounts.X` |
| `correspondence` | `false` | bool (корреспонденция) |
| `periodAdjustmentLength` | `0` | длина периода корректировки |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "AccountingRegister", "name": "Хозрасчетный",
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
"dimensions": ["Организация: CatalogRef.Организации"], "resources": ["Сумма: Number(15,2)"] }
```
## CalculationRegister (Регистр расчёта)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `chartOfCalculationTypes` | — | **обязательно**: ссылка на ПВР `ChartOfCalculationTypes.X` |
| `periodicity` | `Month` | периодичность |
| `actionPeriod` | `false` | bool (период действия) |
| `basePeriod` | `false` | bool (базовый период) |
| `schedule` | пусто | ссылка на регистр сведений графиков |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
| `dimensions` / `resources` / `attributes` | `[]` | измерения / ресурсы / реквизиты |
```json
{ "type": "CalculationRegister", "name": "Начисления",
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления", "periodicity": "Month",
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"], "resources": ["Сумма: Number(15,2)"] }
```
> **AccountingRegister** требует план счетов, **CalculationRegister** — план видов расчёта (и оба — документ-регистратор).
@@ -0,0 +1,45 @@
# Report, DataProcessor (Отчёты и Обработки)
Почти идентичны по составу: реквизиты, табличные части, формы, макеты, команды. Модуль объекта — `Ext/ObjectModule.bsl`.
Реквизиты и ТЧ — `attributes.md`; команды — `blocks.md`.
Ссылки на формы/схемы/хранилища пишутся **как есть** (имя формы может быть буквально «Форма»).
## Report (Отчёт)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `useStandardCommands` | `true` | bool (доступность через стандартный командный интерфейс) |
| `mainDataCompositionSchema` | пусто | основной макет СКД (`Report.X.Template.ОсновнаяСхемаКомпоновкиДанных`) |
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
| `defaultSettingsForm` / `auxiliarySettingsForm` / `defaultVariantForm` | пусто | формы настроек / вариантов |
| `variantsStorage` / `settingsStorage` | пусто | хранилища вариантов / настроек (`SettingsStorage.X`) |
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
```json
{ "type": "Report", "name": "АнализПродаж", "useStandardCommands": false,
"mainDataCompositionSchema": "Report.АнализПродаж.Template.ОсновнаяСхемаКомпоновкиДанных",
"attributes": ["Период: StandardPeriod"] }
```
## DataProcessor (Обработка)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `comment` | пусто | строка |
| `useStandardCommands` | `true` | bool |
| `defaultForm` / `auxiliaryForm` | пусто | основная / вспомогательная форма |
| `extendedPresentation` / `explanation` | пусто | представление / пояснение (ML) |
| `includeHelpInContents` | `false` | bool |
| `attributes` / `tabularSections` | `[]` / `{}` | реквизиты / табличные части |
```json
{ "type": "DataProcessor", "name": "ЗагрузкаТаблиц", "useStandardCommands": false,
"attributes": [{ "name": "Таблица", "type": "ValueTree" }, { "name": "Произвольные", "type": "" }] }
```
> Реквизиты отчётов/обработок допускают платформенные типы-коллекции: `ValueTable`, `ValueTree`, `ValueList`,
> `StandardPeriod`, `SpreadsheetDocument` и др., а также `"type": ""` — реквизит без типа.
@@ -0,0 +1,40 @@
# Enum, Constant, DefinedType
## Enum (Перечисление)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `values` | `[]` | значения перечисления (массив имён или объектов) |
Значение — строка `"ИмяЗначения"` или объект `{ name, synonym }`.
```json
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
```
## Constant (Константа)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueType` | `String` | тип значения (shorthand типа) |
| `dataLockControlMode` | `Managed` | `Automatic` / `Managed` |
`valueType` принимает shorthand: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`,
составной через `+`.
```json
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
```
## DefinedType (Определяемый тип)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `valueTypes` | `[]` | состав типа (массив shorthand-типов) |
| `valueType` | — | то же одной строкой (`"A + B"`) или строкой одного типа |
```json
{ "type": "DefinedType", "name": "ДенежныеСредства",
"valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
```
@@ -1,116 +0,0 @@
# Базовые типы: Catalog, Document, Enum, Constant, DefinedType, Report, DataProcessor
## Catalog
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `hierarchical` | `false` | Hierarchical |
| `hierarchyType` | `HierarchyFoldersAndItems` | HierarchyType |
| `limitLevelCount` | `false` | LimitLevelCount |
| `levelCount` | `2` | LevelCount |
| `foldersOnTop` | `true` | FoldersOnTop |
| `codeLength` | `9` | CodeLength |
| `codeType` | `String` | CodeType |
| `codeAllowedLength` | `Variable` | CodeAllowedLength |
| `codeSeries` | `WholeCatalog` | CodeSeries |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `defaultPresentation` | `AsDescription` | DefaultPresentation |
| `subordinationUse` | `ToItems` | SubordinationUse |
| `quickChoice` | `false` | QuickChoice |
| `choiceMode` | `BothWays` | ChoiceMode |
| `owners` | `[]` | Owners |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "Catalog", "name": "Организации", "attributes": ["ИНН: String(12)", "КПП: String(9)"] }
```
## Document
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `numberType` | `String` | NumberType |
| `numberLength` | `11` | NumberLength |
| `numberAllowedLength` | `Variable` | NumberAllowedLength |
| `numberPeriodicity` | `Year` | NumberPeriodicity |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `posting` | `Allow` | Posting |
| `realTimePosting` | `Deny` | RealTimePosting |
| `registerRecordsDeletion` | `AutoDelete` | RegisterRecordsDeletion |
| `registerRecordsWritingOnPost` | `WriteModified` | RegisterRecordsWritingOnPost |
| `postInPrivilegedMode` | `true` | PostInPrivilegedMode |
| `unpostInPrivilegedMode` | `true` | UnpostInPrivilegedMode |
| `registerRecords` | `[]` | RegisterRecords |
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
RegisterRecords — массив строк: `"AccumulationRegister.Продажи"`, `"InformationRegister.Цены"`.
```json
{
"type": "Document", "name": "ПриходнаяНакладная",
"registerRecords": ["AccumulationRegister.ОстаткиТоваров"],
"attributes": ["Организация: CatalogRef.Организации", "Контрагент: CatalogRef.Контрагенты"],
"tabularSections": { "Товары": ["Номенклатура: CatalogRef.Номенклатура", "Количество: Number(15,3)"] }
}
```
## Enum
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `values` | `[]` | → EnumValue в ChildObjects |
```json
{ "type": "Enum", "name": "Статусы", "values": ["Новый", "ВРаботе", "Закрыт"] }
```
## Constant
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueType` | `String` | Type |
`valueType` принимает shorthand типа: `"String(100)"`, `"Number(15,2)"`, `"Boolean"`, `"CatalogRef.Валюты"`.
```json
{ "type": "Constant", "name": "ОсновнаяВалюта", "valueType": "CatalogRef.Валюты" }
```
## DefinedType
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `valueTypes` | `[]` | Type (составной тип) |
| `valueType` | — | Алиас для `valueTypes` (строка или массив) |
```json
{ "type": "DefinedType", "name": "ДенежныеСредства", "valueTypes": ["CatalogRef.БанковскиеСчета", "CatalogRef.Кассы"] }
{ "type": "DefinedType", "name": "ФлагАктивности", "valueType": "Boolean" }
```
## Report
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "Report", "name": "ОстаткиТоваров" }
```
## DataProcessor
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `attributes` | `[]` | → Attribute в ChildObjects |
| `tabularSections` | `{}` | → TabularSection в ChildObjects |
```json
{ "type": "DataProcessor", "name": "ЗагрузкаДанных", "attributes": ["ПутьКФайлу: String(500)"] }
```
@@ -1,136 +0,0 @@
# Процессы и сервисные: BusinessProcess, Task, ExchangePlan, CommonModule, ScheduledJob, EventSubscription, DocumentJournal
## BusinessProcess
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `task` | `""` | Task (ссылка `Task.XXX`) |
| `numberType` | `String` | NumberType |
| `numberLength` | `11` | NumberLength |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
Модули: `Ext/ObjectModule.bsl`, `Ext/Flowchart.xml`.
```json
{ "type": "BusinessProcess", "name": "Задание", "task": "Task.ЗадачаИсполнителя", "attributes": ["Описание: String(200)"] }
```
## Task
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `numberType` | `String` | NumberType |
| `numberLength` | `14` | NumberLength |
| `checkUnique` | `true` | CheckUnique |
| `autonumbering` | `true` | Autonumbering |
| `descriptionLength` | `150` | DescriptionLength |
| `addressing` | `""` | Addressing (ссылка на РС адресации) |
| `mainAddressingAttribute` | `""` | MainAddressingAttribute |
| `currentPerformer` | `""` | CurrentPerformer |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
| `addressingAttributes` | `[]` | → AddressingAttribute (shorthand или объект) |
AddressingAttribute — shorthand `"Имя: Тип"` или объект `{ "name", "type", "addressingDimension" }`.
```json
{
"type": "Task", "name": "ЗадачаИсполнителя",
"addressingAttributes": ["Исполнитель: CatalogRef.Пользователи"]
}
```
## ExchangePlan
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `100` | DescriptionLength |
| `distributedInfoBase` | `false` | DistributedInfoBase |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
Модули: `Ext/ObjectModule.bsl`, `Ext/Content.xml`.
```json
{ "type": "ExchangePlan", "name": "ОбменССайтом", "attributes": ["АдресСервера: String(200)"] }
```
## CommonModule
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `context` | — | Шорткат (см. ниже) |
| `global` | `false` | Global |
| `server` | `false` | Server |
| `serverCall` | `false` | ServerCall |
| `clientManagedApplication` | `false` | ClientManagedApplication |
| `externalConnection` | `false` | ExternalConnection |
| `privileged` | `false` | Privileged |
| `returnValuesReuse` | `DontUse` | ReturnValuesReuse |
Шорткаты `context`: `"server"` → Server+ServerCall, `"client"` → ClientManagedApplication, `"serverClient"` → Server+ClientManagedApplication.
```json
{ "type": "CommonModule", "name": "ОбщиеФункции", "context": "serverClient" }
```
## ScheduledJob
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `methodName` | `""` | MethodName |
| `description` | = synonym | Description |
| `use` | `false` | Use |
| `predefined` | `false` | Predefined |
| `restartCountOnFailure` | `3` | RestartCountOnFailure |
| `restartIntervalOnFailure` | `10` | RestartIntervalOnFailure |
Формат `methodName`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
```json
{ "type": "ScheduledJob", "name": "ОбменДанными", "methodName": "ОбменДаннымиСервер.Выполнить" }
```
## EventSubscription
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `source` | `[]` | Source (массив, формат `XxxObject.Name`) |
| `event` | `BeforeWrite` | Event |
| `handler` | `""` | Handler |
Формат `handler`: `"МодульСервер.Процедура"` — авто-дополняется до `CommonModule.МодульСервер.Процедура`.
Значения `event`: `BeforeWrite`, `OnWrite`, `BeforeDelete`, `OnReadAtServer`, `FillCheckProcessing`.
Формат `source`: `"CatalogObject.Xxx"`, `"DocumentObject.Xxx"`.
```json
{ "type": "EventSubscription", "name": "ПередЗаписью", "source": ["CatalogObject.Контрагенты"], "event": "BeforeWrite", "handler": "ОбщиеФункции.ПередЗаписью" }
```
## DocumentJournal
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `registeredDocuments` | `[]` | RegisteredDocuments (массив `"Document.Xxx"`) |
| `columns` | `[]` | → Column |
Колонки — строка `"Имя"` или объект `{ "name", "synonym", "indexing": "Index"/"DontIndex", "references": ["Document.Xxx.Attribute.Yyy"] }`.
```json
{
"type": "DocumentJournal", "name": "Взаимодействия",
"registeredDocuments": ["Document.Встреча", "Document.Звонок"],
"columns": [{ "name": "Организация", "indexing": "Index", "references": ["Document.Встреча.Attribute.Организация"] }]
}
```
## Зависимости
- **ScheduledJob/EventSubscription** — процедура-обработчик должна существовать в модуле (экспортная)
- **BusinessProcess**`Task` (задача должна существовать)
@@ -1,174 +0,0 @@
# Регистры и планы: InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, ChartOfAccounts, ChartOfCharacteristicTypes, ChartOfCalculationTypes
## Измерения и ресурсы (общее)
Синтаксис аналогичен реквизитам (shorthand `"Имя: Тип | флаги"`).
Флаги измерений: `master`, `mainFilter`, `denyIncomplete`, `useInTotals` (AccumulationRegister only, default `true`).
```json
"dimensions": ["Организация: CatalogRef.Организации | master, mainFilter, denyIncomplete"],
"resources": ["Сумма: Number(15,2)"]
```
---
## InformationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `writeMode` | `Independent` | WriteMode |
| `periodicity` | `Nonperiodical` | InformationRegisterPeriodicity |
| `mainFilterOnPeriod` | авто* | MainFilterOnPeriod |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
\* `mainFilterOnPeriod` = `true` если `periodicity` != `Nonperiodical`.
```json
{
"type": "InformationRegister", "name": "КурсыВалют", "periodicity": "Day",
"dimensions": ["Валюта: CatalogRef.Валюты | master, mainFilter, denyIncomplete"],
"resources": ["Курс: Number(15,4)", "Кратность: Number(10,0)"]
}
```
## AccumulationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `registerType` | `Balance` | RegisterType (`Balance` / `Turnovers`) |
| `enableTotalsSplitting` | `true` | EnableTotalsSplitting |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "AccumulationRegister", "name": "ОстаткиТоваров", "registerType": "Balance",
"dimensions": ["Номенклатура: CatalogRef.Номенклатура"],
"resources": ["Количество: Number(15,3)"]
}
```
## AccountingRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `chartOfAccounts` | `""` | ChartOfAccounts (**обязательная** ссылка на план счетов) |
| `correspondence` | `false` | Correspondence |
| `periodAdjustmentLength` | `0` | PeriodAdjustmentLength |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "AccountingRegister", "name": "Хозрасчетный",
"chartOfAccounts": "ChartOfAccounts.Хозрасчетный",
"dimensions": ["Организация: CatalogRef.Организации"],
"resources": ["Сумма: Number(15,2)"]
}
```
## CalculationRegister
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `chartOfCalculationTypes` | `""` | ChartOfCalculationTypes (**обязательная** ссылка на ПВР) |
| `periodicity` | `Month` | Periodicity |
| `actionPeriod` | `false` | ActionPeriod |
| `basePeriod` | `false` | BasePeriod |
| `schedule` | `""` | Schedule (ссылка на РС графиков) |
| `dimensions` | `[]` | → Dimension |
| `resources` | `[]` | → Resource |
| `attributes` | `[]` | → Attribute |
```json
{
"type": "CalculationRegister", "name": "Начисления",
"chartOfCalculationTypes": "ChartOfCalculationTypes.Начисления",
"periodicity": "Month",
"dimensions": ["Сотрудник: CatalogRef.Сотрудники"],
"resources": ["Сумма: Number(15,2)"]
}
```
---
## ChartOfCharacteristicTypes
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `characteristicExtValues` | `""` | CharacteristicExtValues |
| `valueTypes` | авто* | Type (составной тип значений характеристик) |
| `hierarchical` | `false` | Hierarchical |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
\* По умолчанию: Boolean, String(100), Number(15,2), DateTime.
```json
{
"type": "ChartOfCharacteristicTypes", "name": "ВидыСубконто",
"valueTypes": ["CatalogRef.Номенклатура", "CatalogRef.Контрагенты", "Boolean", "String", "Number(15,2)"]
}
```
## ChartOfAccounts
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `extDimensionTypes` | `""` | ExtDimensionTypes (ссылка на ПВХ) |
| `maxExtDimensionCount` | `3` | MaxExtDimensionCount |
| `codeMask` | `""` | CodeMask |
| `codeLength` | `8` | CodeLength |
| `descriptionLength` | `120` | DescriptionLength |
| `codeSeries` | `WholeChartOfAccounts` | CodeSeries |
| `autoOrderByCode` | `true` | AutoOrderByCode |
| `orderLength` | `5` | OrderLength |
| `hierarchical` | `false` | Hierarchical |
| `accountingFlags` | `[]` | → AccountingFlag (Boolean-тип, массив имён) |
| `extDimensionAccountingFlags` | `[]` | → ExtDimensionAccountingFlag (Boolean-тип, массив имён) |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
```json
{
"type": "ChartOfAccounts", "name": "Хозрасчетный",
"extDimensionTypes": "ChartOfCharacteristicTypes.ВидыСубконто", "maxExtDimensionCount": 3,
"codeLength": 8, "codeMask": "@@@.@@.@",
"accountingFlags": ["Валютный", "Количественный"],
"extDimensionAccountingFlags": ["Суммовой", "Валютный"]
}
```
## ChartOfCalculationTypes
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `codeLength` | `9` | CodeLength |
| `descriptionLength` | `25` | DescriptionLength |
| `autonumbering` | `true` | Autonumbering |
| `checkUnique` | `false` | CheckUnique |
| `dependenceOnCalculationTypes` | `DontUse` | DependenceOnCalculationTypes |
| `actionPeriodUse` | `false` | ActionPeriodUse |
| `attributes` | `[]` | → Attribute |
| `tabularSections` | `{}` | → TabularSection |
`dependenceOnCalculationTypes`: `DontUse`, `OnActionPeriod`.
```json
{ "type": "ChartOfCalculationTypes", "name": "Начисления", "dependenceOnCalculationTypes": "OnActionPeriod" }
```
## Зависимости
- **AccountingRegister** требует `ChartOfAccounts` (и документ-регистратор)
- **CalculationRegister** требует `ChartOfCalculationTypes` (и документ-регистратор)
- **ChartOfAccounts** ссылается на `ChartOfCharacteristicTypes` через `extDimensionTypes`
@@ -1,103 +0,0 @@
# Веб-сервисы: HTTPService, WebService
## HTTPService
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `rootURL` | `= name.toLower()` | RootURL |
| `reuseSessions` | `DontUse` | ReuseSessions |
| `sessionMaxAge` | `20` | SessionMaxAge |
| `urlTemplates` | `{}` | → URLTemplate |
Модули: `Ext/Module.bsl`.
### urlTemplates — вложенная структура
`urlTemplates` — объект `{ "TemplateName": templateDef, ... }`.
Каждый `templateDef`:
- Строка — URL-шаблон: `"/v1/users"` (без методов)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `template` | `"/templatename"` | URL-путь (с параметрами `{id}`) |
| `methods` | `{}` | Методы: `{ "MethodName": "HTTPMethod" }` |
Допустимые HTTPMethod: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
Обработчик метода генерируется автоматически: `{TemplateName}{MethodName}` — должен быть реализован в `Ext/Module.bsl`.
```json
{
"type": "HTTPService", "name": "API", "rootURL": "api",
"urlTemplates": {
"Users": {
"template": "/v1/users/{id}",
"methods": { "Get": "GET", "Create": "POST", "Update": "PUT", "Delete": "DELETE" }
},
"Health": "/health"
}
}
```
## WebService
| Поле JSON | Умолчание | XML элемент |
|-----------|----------|-------------|
| `namespace` | `""` | Namespace (URI пространства имён WSDL) |
| `xdtoPackages` | `""` | XDTOPackages |
| `reuseSessions` | `DontUse` | ReuseSessions |
| `sessionMaxAge` | `20` | SessionMaxAge |
| `operations` | `{}` | → Operation |
Модули: `Ext/Module.bsl`.
### operations — вложенная структура
`operations` — объект `{ "OperationName": operationDef, ... }`.
Каждый `operationDef`:
- Строка — тип возврата: `"xs:boolean"` (параметров нет, обработчик = имя операции)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `returnType` | `xs:string` | XDTO-тип возврата |
| `nillable` | `false` | Может ли вернуть null |
| `transactioned` | `false` | Выполнять в транзакции |
| `handler` | `= operationName` | Имя процедуры в модуле |
| `parameters` | `{}` | Параметры операции |
### parameters — параметры операции
`parameters` — объект `{ "ParamName": paramDef, ... }`.
Каждый `paramDef`:
- Строка — XDTO-тип: `"xs:string"` (direction = In, nillable = true)
- Объект:
| Поле | Умолчание | Описание |
|------|----------|----------|
| `type` | `xs:string` | XDTO-тип параметра |
| `nillable` | `true` | Может ли быть null |
| `direction` | `In` | Направление: `In`, `Out`, `InOut` |
Стандартные XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
```json
{
"type": "WebService", "name": "DataExchange",
"namespace": "http://www.1c.ru/DataExchange",
"operations": {
"TestConnection": {
"returnType": "xs:boolean",
"handler": "ПроверкаПодключения",
"parameters": {
"ErrorMessage": { "type": "xs:string", "direction": "Out" }
}
},
"GetVersion": "xs:string"
}
}
```
@@ -0,0 +1,70 @@
# HTTPService, WebService (Веб-сервисы)
Модуль обоих — `Ext/Module.bsl`, в нём реализуются обработчики.
## HTTPService (HTTP-сервис)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `rootURL` | `= name` (в нижнем регистре) | корневой URL |
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
| `sessionMaxAge` | `20` | время жизни сессии, сек |
| `urlTemplates` | `{}` | шаблоны URL (см. ниже) |
`urlTemplates` — объект `{ "ИмяШаблона": def }`, где `def`:
- строка — URL-путь без методов: `"/health"`;
- объект: `template` (путь с параметрами `{id}`, по умолчанию `/имяшаблона`), `synonym`, `comment`,
`methods``{ "ИмяМетода": def }`.
`methods` — значение либо строка (только HTTP-метод), либо объект: `httpMethod`, `handler`,
`synonym`, `comment`.
HTTP-методы: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `MERGE`.
Обработчик по умолчанию именуется `{ИмяШаблона}{ИмяМетода}`; в типовых конфигурациях он часто
произвольный — тогда задавайте `handler` явно.
```json
{ "type": "HTTPService", "name": "API", "rootURL": "api",
"urlTemplates": {
"Users": { "template": "/v1/users/{id}", "methods": { "Get": "GET", "Create": "POST", "Delete": "DELETE" } },
"Health": "/health"
} }
```
## WebService (Веб-сервис, SOAP)
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `namespace` | пусто | URI пространства имён WSDL |
| `xdtoPackages` | пусто | список пакетов (см. ниже) |
| `descriptorFileName` | `= name` + `.1cws` | имя файла дескриптора |
| `reuseSessions` | `DontUse` | `DontUse` / `Use` / `AutoUse` |
| `sessionMaxAge` | `20` | время жизни сессии, сек |
| `operations` | `{}` | операции (см. ниже) |
`xdtoPackages`**массив** значений: `"XDTOPackage.Имя"` — пакет конфигурации, любое другое
значение — URI внешнего пространства имён (например `"http://v8.1c.ru/8.3/data/ext"`).
`operations` — объект `{ "ИмяОперации": def }`, где `def`:
- строка — XDTO-тип возврата без параметров: `"xs:string"`;
- объект: `returnType` (по умолчанию `xs:string`), `nillable` (bool), `transactioned` (bool),
`procedureName` (имя процедуры, по умолчанию = имя операции; синоним ключа — `handler`),
`dataLockControlMode` (по умолчанию `Managed`), `synonym`, `comment`, `parameters`.
`parameters` — объект `{ "ИмяПараметра": def }`, где `def`:
- строка — XDTO-тип (`direction` = `In`);
- объект: `type` (по умолчанию `xs:string`), `nillable` (bool, по умолчанию `true`),
`direction` (`In` / `Out` / `InOut`), `synonym`, `comment`.
XDTO-типы: `xs:string`, `xs:boolean`, `xs:int`, `xs:long`, `xs:decimal`, `xs:dateTime`, `xs:base64Binary`.
Тип из собственного пространства имён задаётся в нотации Кларка — `"{http://ваш.uri}ИмяТипа"`;
компилятор сам объявит локальный `xmlns` в теге, как это делает платформа.
```json
{ "type": "WebService", "name": "DataExchange", "namespace": "http://www.1c.ru/DataExchange",
"operations": {
"TestConnection": { "returnType": "xs:boolean", "handler": "ПроверкаПодключения",
"parameters": { "ErrorMessage": { "type": "xs:string", "direction": "Out" } } },
"GetVersion": "xs:string"
} }
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
---
name: meta-decompile
description: Декомпиляция объекта метаданных 1С в JSON-заготовку формата meta-compile. Используй когда нужно получить черновик DSL-описания нового объекта по образцу другого. Не сохраняет UUID/модули/формы.
argument-hint: <ObjectPath> [-OutputPath <out.json>]
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /meta-decompile — DSL-заготовка из XML объекта метаданных
Читает XML объекта метаданных (`Catalogs/Имя.xml` и т.п.) и эмитит компактный JSON в формате `/meta-compile`. Назначение — **взять существующий объект образцом и собрать по нему НОВЫЙ**: декомпилировать → поправить → скомпилировать под другим именем.
## ⚠️ Главное: это НЕ обратимая выгрузка
Компиляция черновика создаёт **новый объект с новой идентичностью**, а не копию исходного. В JSON **не** попадают: UUID (идентичность самого объекта и всех дочерних), тела модулей, формы, макеты, права. Захватываются только структура и свойства.
Отсюда правило: **никогда не компилируй черновик поверх объекта-источника и не выдавай его за «реимпорт»** — у пересобранного объекта другие UUID, поэтому все ссылки на исходный объект (из кода, других объектов, состава подсистем, предопределённых данных) сломаются, а код модулей и формы пропадут.
## Когда использовать
**Собрать новый объект по образцу существующего** — получить DSL-заготовку рабочего объекта, переименовать и адаптировать состав, скомпилировать в новый. Быстрее, чем писать DSL с нуля для богатого объекта.
## Когда **не** использовать
- **Точечная правка существующего объекта** (добавить реквизит, ТЧ, свойство) → `/meta-edit`. Цикл decompile→compile тут вреден: даёт объект с новой идентичностью и теряет модули/формы.
- **Сохранить / восстановить / перенести тот же объект** (бэкап, миграция между конфигурациями с сохранением ссылок) → штатная выгрузка 1С (`/db-dump-xml``/db-load-xml`, CF), а не decompile.
- **Просто понять структуру** объекта (реквизиты, ТЧ, типы) без пересборки → `/meta-info` (дешевле, не плодит файл).
## Параметры
| Параметр | Описание |
|----------|----------|
| `ObjectPath` | Путь к XML объекта (`Catalogs/Имя.xml`), обязательный |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-decompile.ps1" -ObjectPath "<Объект.xml>" -OutputPath "<out.json>"
```
Неподдерживаемый тип объекта или не-`MetaDataObject` root → ненулевой код выхода и сообщение в stderr.
## Workflow (сборка нового объекта по образцу)
1. `/meta-decompile <Образец.xml> -OutputPath draft.json` — получить заготовку.
2. В `draft.json` **сменить `name`** на имя нового объекта и адаптировать состав (реквизиты/ТЧ/свойства). Ссылки на *другие* объекты (владельцы, ввод на основании, типы) — по имени, сохраняются как есть.
3. `/meta-compile -JsonPath draft.json -OutputDir <ConfigDir>` — собрать (объект получит свежие UUID).
4. `/meta-validate` + `/meta-info` — проверить.
5. Модули, формы, макеты, права — добавить отдельно (в черновик они не попадают).
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3
View File
@@ -59,6 +59,9 @@ Batch через `;;` во всех операциях. Подробный си
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
### Свойства объекта — [properties-reference.md](properties-reference.md)
| Операция | Формат Value | Пример |
+48 -3
View File
@@ -11,7 +11,7 @@
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
Флаги: `req` (FillChecking=ShowError), `index` (Indexing=Index), `master` (Master=true, только dimensions), `mainFilter` (MainFilterOperand, только dimensions).
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только dimensions); `mainFilter` — основной отбор (только dimensions).
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
```powershell
@@ -84,6 +84,27 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
## add-predefined
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
идентификаторы сохраняются, новые получают свежий id.
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
```powershell
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
```
JSON — строки и/или объекты (для групп с вложенными):
```json
{ "add": { "predefined": [
"(001) Основной",
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
] } }
```
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
берётся из объекта автоматически.
## add-enumValue / add-form / add-template / add-command
Просто имена (batch через `;;`):
@@ -107,10 +128,34 @@ Batch через `;;` — можно указать разные ТЧ: `"Тов
Формат: `ИмяЭлемента: ключ=значение, ключ=значение`
Ключи: `name` (rename), `type`, `synonym`, `indexing`, `fillChecking`, `use` и др.
**Спец-операции** (строчные ключи): `name` (переименование), `type` (смена типа), `synonym`.
**Свойства** задавайте по имени свойства 1С (PascalCase, как в конфигураторе): `Indexing`, `FillChecking`,
`Use`, `FullTextSearch`, `DataHistory`, `PasswordMode`, `MultiLine`, `Mask`, `CreateOnInput`, `QuickChoice` и др.
Свойство можно задать, даже если у реквизита оно ещё не выставлено. Опечатка в имени свойства → ошибка
(правка не теряется молча).
```powershell
-Operation modify-attribute -Value "СтароеИмя: name=НовоеИмя, type=Строка(500)"
-Operation modify-attribute -Value "Комментарий: indexing=Index"
-Operation modify-attribute -Value "Комментарий: Indexing=Index, FullTextSearch=Use"
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
```
### Структурные свойства реквизита
Свойства со сложным значением задавайте через JSON DSL (`{ "modify": { "attributes": { "Имя": { ... } } } }`):
| Ключ | Значение | Пример (JSON) |
|------|----------|---------------|
| `Format` / `EditFormat` / `ToolTip` | строка (мультиязычная) | `"Format": "ДФ=dd.MM.yyyy"` |
| `ChoiceForm` | путь формы выбора | `"ChoiceForm": "Catalog.Товары.Form.ФормаВыбора"` |
| `MinValue` / `MaxValue` | число или строка | `"MinValue": 0, "MaxValue": 100` |
| `FillValue` | значение заполнения | `"FillValue": "EmptyRef"` · `true` · `10` · `{"nil": true}` |
| `LinkByType` | `{dataPath, linkItem?}` | `"LinkByType": {"dataPath": "Вид", "linkItem": 0}` |
| `ChoiceParameterLinks` | `[{name, dataPath, valueChange?}]` | `["Отбор.Организация=Организация"]` |
| `ChoiceParameters` | `[{name, type?, value?}]` | `[{"name": "Отбор.ЭтоГруппа", "value": false}]` |
- `FillValue`: `"EmptyRef"` — пустая ссылка по типу реквизита; `{"emptyRef": true}` / `{"nil": true}` — явные маркеры.
- `ChoiceParameters` value — булево/число/строка/ссылочный путь или массив; укажите `type` (напр.
`EnumRef.СтавкиНДС`), чтобы задавать значения короткими именами (`"Оптовая"` вместо полного пути).
- В путях данных (`LinkByType`/`ChoiceParameterLinks`) можно писать короткое имя реквизита вместо полного пути.

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