Compare commits

...
535 Commits
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 57c8cb6d23 feat(cfe-init): Caption и ShortCaption в корне расширения на формате 2.21
Свойства корня, добавленные форматом 2.21, между Version и DefaultLanguage.
В cf-init они уже были, в cfe-init — нет. Нашёл раундтрип расширения через базу
8.5 по сценарию из issue #57: загрузка расширения и обратная выгрузка давали
диff ровно на этих двух элементах.

После правки выгрузка расширения из базы 8.5 совпадает с исходниками полностью —
и по составу файлов, и побайтово.

663/663 ps1, 660/663 py; 1С-сертификация cfe-init и cfe-borrow на 8.5 — 2/2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 23:08:18 +03:00
Nick ShirokovandClaude Opus 5 3b1a2f64d5 feat(cf-init,form-add): TextToSpeech и UseInInterfaceCompatibilityMode в формате 2.21
Оба пробела нашёл цикл через базу 8.5 (собрать навыками → загрузить → выгрузить):

- cf-init не писал возможность мобильного приложения TextToSpeech, добавленную
  форматом 2.21 последней в списке UsedMobileApplicationFunctionalities;
- form-add не писал <UseInInterfaceCompatibilityMode>Any</…> в метаданных формы.
  meta-compile эмитил это свойство только для общей формы, а платформа 8.5 пишет
  его у форм любых объектов — сразу после UsePurposes, до ExtendedPresentation.

После правки выгрузка платформы совпала с исходниками везде, кроме
ConfigurationExtensionCompatibilityMode (платформа поднимает значение до своего
максимума — так и задумано), скелета Ext/Form.xml и тела MXL: оба расхождения
предсуществующие, к формату 2.21 не относятся.

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 22:25:50 +03:00
Nick ShirokovandClaude Opus 5 6315a706b5 test: кейсы формата 2.21 для навыков-эмиттеров
Фикстура empty-config-221 (cf-init -FormatVersion 2.21) и по кейсу на навык:
cf-init, meta-compile, meta-edit, form-add, form-compile, template-add,
mxl-compile, subsystem-compile, subsystem-edit, role-compile, xdto-compile,
cfe-init, cfe-borrow, epf-init, erf-init.

Проверки через expect.fileContains по сырым байтам, а не только снэпшотом:
позиция xmlns:pal (после lf, перед style) и его отсутствие там, где платформа
его не пишет (Rights.xml роли, Ext/ClientApplicationInterface.xml).

663/663 ps1, 660/663 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:56:26 +03:00
Nick ShirokovandClaude Opus 5 3ef9e76f03 feat(epf-init,erf-init,form-add,form-compile,template-add,mxl-compile,help-add): версия формата у автономных EPF/ERF
Детекторы версии искали только Configuration.xml, поэтому внутри автономной
внешней обработки или отчёта всегда получалось 2.17 — независимо от version в
корне самого объекта. Теперь версия наследуется от корня EPF/ERF: подъём по
дереву проверяет и <каталог>.xml с корнем ExternalDataProcessor/ExternalReport,
а form-add и template-add читают её прямо из файла объекта, с которым работают.

Только после этого у epf-init/erf-init появился параметр -FormatVersion
(2.17…2.21, дефолт 2.17 — прежнее поведение). Без наследования он дал бы
обработку 2.21, внутри которой формы и макеты остаются 2.17.

Проверено сквозным прогоном: epf-init -FormatVersion 2.21 → form-add →
template-add → mxl-compile, все файлы 2.21 с xmlns:pal, порты идентичны.

Снэпшоты role-info/role-validate/cf-edit обновлены — их фикстуры строит
role-compile, дрейф только от смены его формата на платформенный.

648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:41:42 +03:00
Nick ShirokovandClaude Opus 5 5bcf6d08cd fix(role-compile): формат Role.xml и Rights.xml как у платформы
Навык писал шапку многострочно (каждое объявление своей строкой) и отступы
четырьмя пробелами, тогда как платформа пишет шапку одной строкой и отступы
табами. На каждой роли это давало диff во всю длину файла — шум, из-за которого
реальные расхождения не видны.

Сверено с выгрузкой УНФ 8.5: скелет Role.xml совпал с платформенным полностью
(с точностью до uuid и текстов). 1С-сертификация: 10/10 кейсов загружаются.
Снэпшоты обновлены — дрейф только ожидаемый (склейка шапки и пробелы→табы).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:13:35 +03:00
Nick ShirokovandClaude Opus 5 5c3449d6c2 feat(mxl-compile): определение версии формата + xmlns:pal в 2.21
У корня <document> нет атрибута version, поэтому версия берётся из конфигурации,
в дерево которой пишется макет (подъём до Configuration.xml, как в остальных
навыках); вне конфигурации остаётся прежнее поведение — 2.17.

Платформа 8.5 объявляет палитру и в теле MXL: в выгрузке УНФ 8.5 xmlns:pal стоит
у всех 3471 файлов с корнем document.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 21:01:45 +03:00
Nick ShirokovandClaude Opus 5 53c1d59ec1 feat(cfe-borrow,cfe-init,form-add,form-compile,role-compile,subsystem-compile,subsystem-edit,template-add,xdto-compile): xmlns:pal в формате 2.21
Платформа 8.5 объявляет пространство палитры в шапках MetaDataObject и Form.
Радиус снят по выгрузке УНФ 8.5, а не угадан: pal есть у MetaDataObject, Form,
document (тело MXL), Style, AppearanceTemplate, GraphicalSchema; у Rights, Help,
CommandInterface, DataCompositionSchema и корней extrnprops его нет. Поэтому
role-compile правит шапку Role.xml и не трогает Rights.xml.

Вставка идёт на место — после lf, перед style: объявления платформа держит по
алфавиту, дописать в конец нельзя.

Попутно в cfe-borrow тег <Form> больше не копируется из исходной формы целиком:
из него берутся только объявления пространств, а version подставляется своя.
Раньше версия источника молча побеждала — вопреки комментарию рядом.

Тесты: снята нормализация xmlns в снэпшотах py-прогона (она прятала ровно этот
класс расхождений и к моменту снятия была мёртвой — ни один кейс на неё не
опирался); добавлены expect.fileContains/fileNotContains по сырым байтам и гейт
на неизвестные ключи expect.* — он вскрыл два кейса-пустышки (skd-compile,
skd-edit), они починены.

648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:57:55 +03:00
Nick ShirokovandClaude Opus 5 d60dd47f0b refactor(cfe-init,form-add,form-compile,subsystem-compile,subsystem-edit,template-add,xdto-compile): шапка XML одной переменной
Объявления пространств имён вынесены в переменную рядом с определением версии
формата; места эмиссии её только интерполируют. Раньше шапка была вклеена в
литерал в каждой точке записи, и любая правка шапки ложилась в каждый навык
по-своему.

Попутно в form-compile убрана мёртвая эмиссия Title и шапки перед пересозданием
буфера (её результат затирался).

Вывод байт-в-байт: 648/648 ps1, 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 20:33:42 +03:00
Nick ShirokovandClaude Opus 5 34b533a730 feat(meta-compile,meta-edit,cf-init,meta-validate): формат выгрузки 2.21 (8.5)
Дельта 2.20 → 2.21 снята синтетическим экспериментом: одна и та же минимальная
конфигурация загружена в две пустые базы, 8.3.27.1859 и 8.5.1.1302, и выгружена
из каждой (стенд debug/fmt221). Всё, что нашлось раундтрипом по УНФ,
воспроизвелось на синтетике — значит это ступень формата, а не особенность
конфигурации.

Версию формата задаёт ПЛАТФОРМА ВЫГРУЗКИ, а не режим совместимости:
CompatibilityMode в обеих выгрузках остался Version8_3_24, но 8.5 всё равно
написала 2.21. Поэтому нигде не появляется привязки к режиму совместимости —
только к версии из Configuration.xml, как и было устроено.

Реализовано:
- xmlns:pal (палитра) в шапках MetaDataObject и Form у meta-compile и cf-init.
  Вставка НА МЕСТО (после lf, перед style) — платформа держит объявления по
  алфавиту. В файлы с корнем extrnprops (Ext/ClientApplicationInterface.xml)
  платформа его не пишет, и мы не пишем.
- meta-compile: <Color>auto</Color> у значения перечисления, <AuxiliaryVariantForm/>
  у отчёта, <UseInInterfaceCompatibilityMode>Any</…> у общей формы. Позиции взяты
  из эталонной выгрузки, а не угаданы.
- meta-edit: <Color> при добавлении значения перечисления — навык сам эмитит
  EnumValue, и без этого добавленное значение отличалось бы от соседних.
- cf-init: 14 новых свойств корня при -FormatVersion 2.21.
- meta-validate: лестница версий (был единственным валидатором без 2.21) и три
  строки в реестре versionedProps — механизм для этого и заводился.

Проверка:
- наш вывод на 2.21 совпал с выгрузкой платформы ПОСТРОЧНО по Enums, Reports и
  CommonForms; порты идентичны modulo UUID;
- на 2.17-2.20 вывод не изменился ни в одном байте;
- юнит-тесты 648/648 ps1, 645/648 py.

Известное отклонение, НЕ относящееся к 2.21: скелет Ext/Form.xml общей формы
отличается от платформенного (dcssch в шапке, форма AutoCommandBar, Attributes) —
то же расхождение есть и на 2.20, разбирать отдельно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 19:39:31 +03:00
Nick ShirokovandClaude Opus 5 7892c9fbfa fix(meta-compile): позиция условных стандартных реквизитов бухрегистра
У бухрегистра условные стандартные реквизиты стоят по обе стороны фикс-списка:
PeriodAdjustment — перед Account, пары субконто — после Period. Эмиттер же клал
все ключи, которых нет в списке типа, скопом вперёд, поэтому порядок расходился
с выгрузкой.

Разделено на три части: ключи вне списка по умолчанию идут ПЕРЕД ним (легаси
вроде ExchangeDate у части планов обмена), подходящие под хвостовой шаблон типа —
ПОСЛЕ, а именованные условные (PeriodAdjustment) занимают своё место в самом
списке и эмитятся при наличии в DSL.

Пары субконто заданы ШАБЛОНОМ, а не списком имён: их количество определяется
свойством MaxExtDimensionCount плана счетов. В корпусе везде 3, но это
однородность выборки, а не правило, и список из ExtDimension1..3 закрыл бы
только наблюдаемый случай. Хвост сортируется по номеру, внутри номера
ExtDimensionN идёт перед ExtDimensionTypeN.

Проверка:
- синтетика с ПЯТЬЮ парами субконто и перемешанными ключами на входе даёт
  канонический порядок с пропусками номеров; порты байт-в-байт (modulo UUID);
- раундтрип «все виды» (112 объектов, 42 вида): порядок ≠ 0, TOTAL diff 0;
- юнит-тесты 648/648 ps1, 645/648 py; 1С-сертификация accounting-register ✓.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 18:00:31 +03:00
Nick ShirokovandClaude Opus 5 0fc2d738a2 fix(meta-compile): порядок стандартных реквизитов ещё четырёх типов + Use у ПВХ
Раундтрип покрывал пять видов объектов из сорока с лишним, поэтому расхождения
порядка в остальных были не видны. Списки расширены до «по три объекта из
каждого вида» (112 объектов, 42 вида) — содержательных расхождений нет нигде,
но всплыли перестановки.

1. Порядок стандартных реквизитов, снят с корпуса acc+erp:
   - ChartOfCharacteristicTypes: PredefinedDataName, ValueType, Description,
     Code, IsFolder, Parent, Predefined, DeletionMark, Ref;
   - BusinessProcess: Started, HeadTask, Completed, Ref, DeletionMark, Date, Number;
   - Task: Executed, Description, RoutePoint, BusinessProcess, Ref, DeletionMark,
     Date, Number;
   - AccountingRegister: Account, Active, LineNumber, Recorder, Period.
   IsFolder добавлен в фикс-список ПВХ: он есть во всех 47 объектных блоках
   корпуса (5 выгрузок) и НЕ связан с иерархичностью — плоских ПВХ вдвое больше
   иерархических (36 против 15), и у них он тоже есть.

2. Новый контекст реквизита `cct` для ПВХ. Структурно реквизит ПВХ совпадает со
   справочником, но <Use> у него идёт ПОСЛЕ <Indexing>, а у справочника — перед
   (корпус: Catalog `Use,Indexing,FullTextSearch`, ПВХ
   `Indexing,Use,FullTextSearch,DataHistory`). Раньше оба шли одним контекстом.

Проверка:
- ПВХ (24 объекта): порядок ≠ 23 → 0; все виды (112): 7 → 1;
- шесть основных списков (4897 объектов) без регрессии: порядок ≠ 0,
  TOTAL diff lines 0, match не просел;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация на 8.3.24: accounting-register, business-process, task,
  chart-of-characteristic-types — все приняты;
- дрейф эталонов 6 файлов: перестановки плюс единственное новое содержимое —
  блок IsFolder (проверено вырезанием: без него мультимножества совпадают).

Остаётся известным: у бухрегистра условные стандартные реквизиты стоят по обе
стороны фикс-списка (PeriodAdjustment перед, ExtDimension1..3 и
ExtDimensionType1..3 после), а эмиттер кладёт все «лишние» ключи вперёд. Это
правка механизма, а не таблицы — отдельной задачей.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 17:37:25 +03:00
Nick ShirokovandClaude Opus 5 0a2dba33cf fix(meta-compile): в Predefined.xml ссылочный тип остаётся локальным
Регрессия предыдущего коммита: cfg: применялся безусловно, а шапка
Ext/Predefined.xml объявляет только predef/v8/xr/xs/xsi — cfg там нет.
Получался неразрешимый префикс, и платформа отказывалась грузить конфигурацию
(1С-сертификация meta-compile/chart-of-characteristic-types: FAIL на
LoadConfigFromFiles; на коде до правки кейс проходил).

Правило платформы, снятое с корпуса, ровно то же, что уже заложено в meta-edit:
префикс из корня, если он там объявлен, иначе локальное объявление. В самом
Predefined.xml платформа пишет
`<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты</v8:Type>`.

Типизированные предопределённые есть только у ПВХ, а ПВХ не входит ни в один
из шести списков раундтрипа — поэтому корпус эту поломку поймать не мог, её
поймала только 1С-сертификация. Добавлен список list-cct-all.txt (24 объекта
acc+erp), чтобы пробел закрылся: content match 24/24, TOTAL diff 0.

Проверка: 1С-сертификация кейса проходит, runner 648/648 ps1 и 645/648 py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:50:45 +03:00
Nick ShirokovandClaude Opus 5 683e370938 fix(meta-compile,meta-edit): ссылочный тип корневым cfg:, как пишет платформа
Эмитился локальный xmlns:d5p1 на тот же URI, что уже объявлен в шапке файла.
Формально эквивалентно (значим URI, а не префикс), платформа принимала — но
первый же цикл «загрузить в базу → выгрузить» переписывал КАЖДЫЙ ссылочный тип
в cfg:, то есть давал diff-шум при неизменной семантике. Форма пришла из СКД,
где cfg: действительно не работает; в метаданных такого ограничения нет.

Радиус оказался втрое меньше прежней оценки: form-compile в него НЕ входил —
он уже писал cfg:, а все его d5p1 относятся к чужим пространствам
(txtedt/chart/geo/graphscheme/data-analysis), которых в корне нет и где
локальное объявление законно.

meta-edit берёт префикс из объявлений КОРНЯ правимого файла, а если URI там не
объявлен — остаётся на самодостаточной локальной форме. Это не перестраховка:
ссылочный тип живёт в ТЕКСТЕ узла, поэтому XML-слой про префикс не знает и сам
объявление не добавит — проверено, .NET на документе без xmlns:cfg молча пишет
неразрешимый префикс. Та же природа объясняет удалённый костыль в meta-edit.py:
lxml выбрасывал xmlns:d5p1 как «неиспользуемый», и его возвращали регуляркой
после сериализации.

Синхронно снята нормализация ref-префикса в раундтрип-харнесе (debug/, вне git):
она сводила cfg: и d5p1: к одному REF: и делала харнес слепым ровно к тому,
ради чего затевался переход.

Проверка:
- живой цикл через базу (db-create → db-load-xml → db-dump-xml): выгрузка
  платформы совпала с нашими исходниками ПОСТРОЧНО, расхождений ноль;
- корпусный раундтрип 4897 объектов уже БЕЗ маски префикса: match не просел
  (150/1640/151/2600/314/42), порядок ≠ 0, TOTAL diff lines 0;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация meta-edit 17/17 на платформе 8.3.24;
- дрейф эталонов 48 файлов, в диффе нет ни одной строки кроме d5p1: → cfg:;
  skd-* не затронуты ни одним файлом (там cfg: не работает).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:28:47 +03:00
Nick ShirokovandClaude Opus 5 a3aa8fe87c fix(meta-compile): порядок элементов по канону выгрузки
Детект порядка, добавленный в раундтрип-харнес, показал 1242 расхождения на
4897 объектах (25%). Раньше они были невидимы: дифф строился через
Compare-Object, то есть по мультимножеству строк, и позиция не проверялась.
Ложных срабатываний там нет по построению — проверка включается только когда
мультимножества уже совпали.

Три категории, все — порядок эмиссии; DSL и декомпилятор не затронуты.

1. Виды детей регистров (1140 объектов). Компилятор печатал
   Resource, Dimension, Attribute. Канон, снятый с корпуса acc+erp (разброса
   внутри типа нет): Resource, Attribute, Dimension у информационного,
   накопления и расчёта; Dimension, Resource, Attribute — у бухгалтерского.
   Команды у платформы идут последними, как и было.

2. Квалификаторы в составном типе (61 объект). Платформа пишет сначала ВСЕ
   <v8:Type>/<v8:TypeSet>, потом блоки квалификаторов, а Emit-TypeContent
   рекурсивно печатал каждую часть целиком. На одиночном типе оба порядка
   совпадают — потому и не всплывало. Порядок самих блоков тоже канонический
   и НЕ зеркалит порядок типов: Number, String, Date (при типах
   boolean,string,dateTime,decimal квалификаторы идут Number,String,Date;
   контрпримеров в корпусе нет).

3. Стандартные реквизиты плана обмена (41 объект, весь список): блок
   начинается с ThisNode, а не с Ref.

Проверка:
- корпусный раундтрип 4897 объектов: порядок ≠ 1242 → 0, match не просел
  (150/1640/151/2600/314/42), TOTAL diff lines 0;
- юнит-тесты 648/648 ps1, 645/648 py;
- 1С-сертификация на платформе 8.3.24: регистры информационный, накопления,
  бухгалтерский, расчёта и план обмена — все приняты;
- дрейф снэпшотов (10 файлов, 9 кейсов) сверен как чистая перестановка строк.
  NB: сверять нужно с нормализацией UUID-\d+ — раннер нумерует плейсхолдеры по
  порядку появления, и перестановка детей меняет, кому достанется UUID-015.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:22:18 +03:00
Nick ShirokovandClaude Opus 5 4833c3b707 fix(mxl-compile): PS-порт не создавал каталог назначения
Наполнение макета по свежему пути (Templates/<Имя>/Ext/Template.xml) на
PowerShell падало с «Could not find a part of the path». py-порт каталог
создаёт, form-compile и skd-compile — тоже; не создавал только этот порт.

Тестами не ловилось: во всех кейсах mxl-compile писал в уже существующий
каталог. Добавлен кейс с записью по несуществующему пути — проверено, что на
коде до правки он падает.

Найдено при разборе вопроса, нужна ли заглушка тела макета в template-add:
оказалось, заглушка держала не замысел, а этот дефект.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:11:11 +03:00
Nick ShirokovandClaude Opus 5 a7406d0982 test(8 навыков): байтовые проверки канона там, где их не было (#57)
Из ~46 навыков-эмиттеров XML восемь не имели ни одной проверки `preserves`:
form-edit, form-remove, meta-remove, skd-edit, template-remove, xdto-edit,
support-edit, cfe-patch-method. Шесть из них правились в предыдущем коммите,
и канон у них держался на том, что правка была механической, а не на проверке.

Ожидания писались ПО КАНОНУ, а не по текущему поведению — и это сразу вскрыло
три дефекта:

1. form-remove очищал слот формы через InnerText="" / text="" — получалась
   пустая пара <DefaultObjectForm></DefaultObjectForm>. Платформа пустых пар
   не пишет (0 на 476 942 XML). Теперь IsEmpty / text=None.

2. Опустевший <ChildObjects> оставался парой, разнесённой по строкам, —
   в PS-порту form-remove и template-remove. Корпус: 1394 самозакрывающихся
   <ChildObjects/> на acc+erp, пустых пар 0 в обеих формах.

3. template-add писал пустой макет парой <SpreadsheetDocument></...>.
   Во всей выгрузке acc_8.3.27 (65 040 XML) многострочных пустых пар нет
   ни для одного тега.

Заодно усилена сама проверка noEmptyPairs: она ловила только СМЕЖНЫЕ теги,
поэтому дефект №2 проходил мимо неё. Добавлен вариант с переводом строки
внутри; дискриминатором служит сам перевод строки — значащий пробельный
текст-узел (<xr:FillValue xsi:type="xs:string">   </xr:FillValue>) его не
содержит и под проверку не попадает.

support-edit покрыт частично (BOM у ParentConfigurations.bin — проверено, что
платформа пишет его с BOM во всех трёх выгрузках), cfe-patch-method — BOM+EOL
у .bsl: хвостовой перевод строки у модулей неканоничен (1235 с ним, 766 без),
поэтому не утверждается.

Регресс: 647/647 ps1, 644/647 py (3 skipped). Эталоны переснятые: три места,
каждое — ровно ожидаемая пара строк.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 13:05:27 +03:00
Nick ShirokovandClaude Opus 5 a3f2a18dbf test(runner): кейсы round-trip на LF-исходнике (#57)
Все 23 проверки preserves.eol ожидали crlf — ни одного LF-исходника в наборе
не было, поэтому расхождение портов по переводам строк жило целиком в
непокрытой ветке и не могло быть замечено.

Добавлены LF-фикстуры и кейсы roundtrip-lf-preserve:
- cf-edit, meta-edit, subsystem-edit — правка существующего файла через
  XmlWriter (наследование стиля исходника);
- role-compile, meta-compile, xdto-compile — регистрация в существующем
  Configuration.xml. Эти кейсы проверяют обе половины правила разом: правка
  наследует LF, а создаваемый рядом файл получает канон CRLF.

Проверено в требуемом порядке: на коде ДО правки кейсы падают
(role-compile/meta-compile/xdto-compile — «expected lf, got 251 CRLF»,
meta-edit — «40 CRLF + 136 lone LF», subsystem-edit — «1 CRLF + 24 lone LF»),
после правки проходят на обоих рантаймах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:42:08 +03:00
Nick ShirokovandClaude Opus 5 3ef0d74158 fix(19 навыков): единая финализация XML — стиль исходника и корректный CDATA (#57)
Три остатка волны #57, все в одной точке — блоке записи XML.

1. Порты расходились по EOL. PS гонял документ через XmlWriter с дефолтным
   NewLineHandling.Replace и принудительно переводил весь файл в CRLF, а py
   сохранял стиль исходника: role-compile на LF-ном Configuration.xml давал
   10311 байт против 10062. Ещё в пяти навыках None стоял, но финальной
   нормализации не было, и на LF-исходнике выходил смешанный EOL (meta-edit:
   40 CRLF + 136 одиночных LF) — та же форма, что у исходного дефекта #57.
   Приведено к форме cf-edit.ps1: None + канонизация к LF + целевой перевод
   строки (стиль файла-назначения, для создаваемого файла — канон CRLF).
   Replace не годится как замена: он превращает переводы строк внутри значений
   атрибутов в &#13;.

2. Гард `if (-notmatch CDATA)` не обрабатывал CDATA, а отказывался от канона
   во всём файле — то есть деградировал до «не сделал ничего». Заменён
   альтернацией: участки CDATA и комментариев возвращаются как есть, замена
   идёт только вне них. На реальных данных поведение не меняется — в корпусе
   из 476 942 XML нет ни одного CDATA и ни одного комментария.

3. py: три реализации одного правила детекта EOL сведены к одной. Мажоритарное
   правило в role-compile/subsystem-compile давало ДРУГОЙ ответ на смешанном
   входе. Дефолты _finalize_xml_bytes для нового файла приведены к канону
   (UTF-8, CRLF, без хвостового перевода); meta-edit срезает хвост в обеих
   ветках, а не только при создании файла.

Попутно, найдено байтовой сверкой портов:
- py вставлял <Role>/<Form>/<Subsystem> с пятью табами вместо трёх —
  подстановка по голому </ChildObjects> удваивала отступ строки; снэпшоты
  этого не видели, так как схлопывают пробелы между тегами;
- py-порты xdto-* писали XML-декларацию одинарными кавычками (так отдаёт
  lxml), платформа и PS пишут двойные.

Регресс: runner 647/647 ps1, 644/647 py (3 skipped), дрейфа снэпшотов нет.
Корпусный раундтрип метаданных: 4897 объектов, match 100%, совпадает с
эталонами захода #57.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 21:41:55 +03:00
Nick ShirokovandClaude Opus 5 750ba3bb13 refactor(эмиттеры): одна формулировка на повторяющийся комментарий
Код нормализации пустого тега повторён в 24 местах, а комментарий к нему за время
работы над #57 расплодился в шесть вариантов формулировки. Это то же расхождение
копий, что и в коде, только в тексте: читающий не понимает, значима разница или нет.

Сведено к одной паре формулировок — про сам тег с гардом и про запись через
MemoryStream. Комментарий теперь начинается с ПРАВИЛА, а не с причины: раньше
верхняя строка рассказывала, что here-string наследует LF от .ps1, и читалось это
как «переводы строк берём из скрипта навыка» — тогда как правило обратное:
создаём файл — пишем канон, правим существующий — наследуем его стиль.

Два пояснения в cfe-borrow унификация чуть не стёрла, они возвращены: источником
там служит OuterXml, а не XmlWriter, и нормализация стоит после вставки реквизитов,
чтобы накрыть и их. Это причина местного отличия, а не дубль.

Ссылки на ишью прорежены: #44/#46/#47 осталась по одной на файл — там, где правило
формулируется, — и убрана оттуда, где была эхом. #57 в скриптах не было вовсе.

Заодно выровнены две мелочи, найденные той же сверкой копий: interface-edit в
py-порте не обрезал хвостовой перевод там, где PS обрезает (поведение совпадало,
код — нет), а cf-edit нормализовал EOL инлайном вместо общей двухшаговой формы.

Версии подняты в ОБОИХ портах всех затронутых навыков, включая те, где текстуально
менялся только .ps1: номер версии в этом проекте читается как «одно и то же
поведение», и разводить его из-за комментария нельзя. Паритет проверен — 69 пар,
ноль расхождений.

Windows 641/641 обоими портами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:06:38 +03:00
Nick ShirokovandClaude Opus 5 4c64b97cec fix(skd-edit): гард на CDATA при нормализации пустого тега
skd-edit нормализовал ` />` в `/>` без проверки на CDATA и комментарии, в отличие
от остальных 23 мест, где такая нормализация есть. Вскрылось сверкой копий:
конструкция повторяется в 24 местах, и вариантов формы должно быть ровно два —
с гардом и никак иначе.

Гард нужен потому, что `>` не экранируется только внутри CDATA и комментариев —
там ` />` может оказаться содержимым, а не концом тега. В 1С-метаданных ни того,
ни другого не встречается (0 из 476 943 XML корпуса), так что практического
расхождения поведения нет — но полагаться на это в одном месте и защищаться
в остальных двадцати трёх непоследовательно.

Правка в обоих портах: у py-порта нормализация тоже была без гарда (она там
защитная — lxml и так пишет плотно).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 20:05:47 +03:00
Nick ShirokovandClaude Opus 5 67185657c1 refactor(эмиттеры): копии канон-writer'а приведены к одному виду
Навыки автономны, общей библиотеки нет — значит копии функции записи должны быть
буквально одинаковыми, иначе теряется смысл копипасты. PS-порт дисциплину держал
(7 копий байт-в-байт), а py разъехались: три варианта имени параметра, два стиля
кавычек, у cfe-borrow ещё и другое имя функции (save_xml_file).

Сведено к одной форме: имя write_xml_file, параметр content, одинаковое тело.
Различаются ровно две вещи, и обе осмысленные: вызов локального базового writer'а
(имена исторические — write_utf8_bom / write_text_with_bom / save_text_bom) и одна
строка примечания там, где у навыка есть исключение (модуль .bsl, HTML).

Текст комментария в PS выровнен с docstring'ом py: раньше он описывал ПРИЧИНУ
(here-string наследует LF), а не правило, и после правки py ports разъехались по
смыслу. Синтаксис остаётся разным по природе языков: в py docstring (в портах их
421 против 73 функций с #-комментарием — это местная норма), в PS — #.

Windows 641/641 обоими портами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 19:22:43 +03:00
Nick ShirokovandClaude Opus 5 d05c91be03 fix(epf-init,erf-init,form-add): CRLF в разделителях строк модуля
Скелетные генераторы писали Module.bsl с LF, а платформа пишет модули с CRLF:
в выгрузке БП 2643 CRLF и чисто-LF 0 из 3001 модуля (358 «смешанных» — LF внутри
строковых литералов кода), BOM 2001/2001. По тому же доводу сходимости, что и для
XML: платформа перепишет модуль в CRLF при первой выгрузке, значит наш LF — дифф,
созданный нами.

В репозитории уже была встречная конвенция: meta-compile (модуль команды) и
cfe-patch-method (base/local/remote.bsl) пишут .bsl явным CRLF в обоих портах.
Выбивались только эти три скелета.

Хвостовой перевод строки НЕ трогаем: у платформы он неканоничен — 1235 модулей
с ним, 766 без; это след автора кода, а не правило. Меняем только разделители.

Правка в точке записи, а не в шаблоне: так одна строка на скрипт и не трогаются
\u-экранированные кириллические литералы в py (Edit по ним переэкранирует).

Снэпшоты нормализуют EOL и увидеть это не могут — добавлен preserves на модуль
в epf-init/basic, erf-init/basic, form-add/basic.

Windows 641/641 обоими портами, мак 590/0/51, дрейфа снэпшотов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:53:50 +03:00
Nick ShirokovandClaude Opus 5 caf038a192 fix(cfe-borrow,template-add,help-add,interface-edit): вывод py-порта не зависит от ОС (#57)
Тот же writer без newline="", что чинился в form-add.py, остался ещё в четырёх
py-портах, и пишут они не модули, а 1С XML: заимствованные объекты и метаданные
формы (cfe-borrow), Template.xml и XML-макеты (template-add), Help.xml (help-add),
вновь создаваемый CommandInterface.xml (interface-edit).

Контент там собирается литералами с \n, поэтому текстовый режим Python давал CRLF
на Windows — «случайно правильно» — и LF на macOS, ломая канон #57 ровно там, где он
только что установлен. Проверено на маке ДО фикса: cfe-borrow 0 CRLF + 33 одиночных
LF, template-add 0 + 2, interface-edit 0 + 5. Порчи \r\r\n не было, но это везение:
подай такой writer CRLF-контент — каждая строка стала бы \r\r\n.

Почему не поймали раньше: аудит гонялся на Windows, где текстовый режим даёт канон;
снэпшоты нормализуют EOL; байтовый preserves ни разу не проверялся на маке.

Разделены два пути, которые я сперва смешал в cfe-borrow:

- save_xml_file — канон (CRLF, без хвоста) для файлов, которые СОЗДАЁМ;
- save_text_bom — пишет как есть, для файлов, которые ПРАВИМ: переводы строк уже
  пришли из самого файла и менять их нельзя (контракт #44/#46/#47). Туда же
  добавлено чтение с newline="" — иначе CRLF терялся ещё на входе.

interface-edit чинится и в PS-порте: ветка -CreateIfMissing давала смешанный EOL
(3 CRLF + 2 одиночных LF) уже на Windows. Аудит #57 её пропустил, потому что
фильтровал файлы по расширению .xml, а кейс создаёт файл с именем без расширения.

Канон, как выяснилось, зависит от типа артефакта: XML — CRLF, текстовый макет —
CRLF, а HTML-макет платформа хранит с LF (корпус: 399 LF из 400). Поэтому HTML
через канон-writer НЕ идёт.

preserves добавлен четырём навыкам (у них его не было вовсе) + отдельно на Help.xml.
Именно прогон на маке и даёт покрытие: на Windows эти кейсы зелены и без фикса.

Windows 641/641 обоими портами, мак 590/0/51, дрейфа снэпшотов нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 18:53:17 +03:00
Nick ShirokovandClaude Opus 5 668b145010 docs(form-remove,help-add,template-add,template-remove): единое имя навыка в шапках обоих портов
В .ps1 шапка называла НАВЫК, в .py — ФАЙЛ (help-add v1.12 против add-help v1.12).
Канон — имя навыка: так называется каталог, так навык вызывается, так он записан
в SKILL.md. Расхождение было только у этих четырёх: у них имя файла отличается от
имени навыка. Версии совпадали, поменялось только имя.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:29:54 +03:00
Nick ShirokovandClaude Opus 5 d7fd6d79d3 test(runner): байтовые проверки канона вместо масок (#57)
Раннер сам прятал дефекты, которые чинит #57. normalizeXmlContent при
runtime=python снимал ровно четыре измерения: пробел перед `/>`, whitespace между
тегами, пустую пару <Tag></Tag> и хвостовой пробельный мусор. Паритет PS<->PY по
этим измерениям проверялся ЧЕРЕЗ маску — расхождение физически не могло упасть.

Снято три из четырёх (пробел, пустая пара, хвост). Схлопывание whitespace между
тегами оставлено: порты кое-где расставляют отступы иначе, это форматирование, а
не байтовый канон.

Ужесточён checkPreserves: eol теперь считает ОДИНОЧНЫЕ LF, а не «есть ли хоть один
CR». Прежняя проверка пропускала смешанный выход — cfe-init давал 10 CR на 70
строк и проходил её, то есть головной дефект тикета был ей невидим. Добавлены
ключи selfClose:"tight" и noEmptyPairs; preserves проставлен в 13 кейсах
навыков-эмиттеров (по одному на навык, на его СОБСТВЕННЫЙ артефакт).

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

- form-edit собирает выход из OuterXml и писал `<a />`; в списке 17 навыков его не
  было, потому что искал по вызовам Save — здесь другой путь. Тот же случай, что
  с Form.xml в cfe-borrow;
- meta-edit py дописывал хвостовой перевод в создаваемый Ext/Predefined.xml и
  читал существующий без newline='' (терял CRLF);
- skd-info py писал отчёт -OutFile без хвостового перевода, PS через WriteAllLines
  — с ним. Это текстовый отчёт, канон Конфигуратора к нему не относится, поэтому
  выровнял py по существующему эталону;
- фикстуры кейсов содержали <Vendor></Vendor> — снимок нашего же старого вывода.
  Конфигуратор пустых пар не пишет, .NET их сохраняет, lxml схлопывает. Поправлены
  10 фикстур: пары → самозакрывающиеся, EOL и BOM не тронуты.

Результат: PS 641/641, python 638/641 (+3 runtimeOnly-скипа) — со снятыми масками.
Дрейф снэпшотов: 10 файлов, только пробельные теги и пустые пары.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 16:23:13 +03:00
Nick ShirokovandClaude Opus 5 11e4e4301f fix(cf-init,cfe-init,meta-compile): пустой элемент вместо пустой пары (#57)
Конфигуратор пишет <Vendor/>, а не <Vendor></Vendor>: в 8 выгрузках cfsrc
(476 943 XML) пустых пар ноль. Шаблоны подставляли значение ВНУТРЬ пары, поэтому
при пустом значении получалась пара.

Собираем элемент целиком: <Vendor>/<Version> (cf-init, cfe-init),
<DefaultRoles> (cfe-init при -NoRole), <Key> и <Namespace> (meta-compile —
план обмена и веб-сервис). Образец рядом же: <Comment> и <Description> в
meta-compile так делали и раньше.

Дефект был в ОБОИХ портах одинаково — здесь дело не в сериализаторе, а в шаблоне.

Пустая пара <Namespace> в cfe-borrow была не его: он копирует свойства
заимствуемого объекта, а порождал их meta-compile.

Дрейф снэпшотов: 340 <Vendor/>, 339 <Version/>, по одному <Key/>, <Namespace/>,
<DefaultRoles/>. Содержательных изменений ноль. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:55:31 +03:00
Nick ShirokovandClaude Opus 5 0b8231c467 fix(17 навыков): CRLF в разделителях строк XML (#57)
Головной дефект тикета: cfe-init выдавал Configuration.xml с 10 CR на 70 строк, а
роль и язык — вовсе без CR. Теперь 70/70, последний байт `>`.

Правило: меняем только РАЗДЕЛИТЕЛИ строк, содержимое текстовых узлов не трогаем.
Платформа его не трогает тоже — в запросах СКД из чистой выгрузки встречаются и
CRLF, и одиночный LF. Поэтому сплошная нормализация файла применяется только там,
где многострочных текстовых узлов нет по построению (скелеты), а в объектном XML
и формах разделители правятся в точке сборки.

Источников оказалось четыре, а не один:

1. Скелеты (cf-init, cfe-init, epf-init, erf-init, form-add, help-add,
   template-add) собираются here-string'ами, а .ps1/.py в репозитории хранятся с
   LF — отсюда LF и смешанный EOL.
2. py-порты склеивали документ через '\n'.join(lines); PS в тех же местах давал
   CRLF через AppendLine — порты расходились побайтово.
3. Билдеры Predefined в meta-compile собирались с явным LF и даже сворачивали
   CRLF→LF из общего эмиттера типов.
4. Чтение существующего файла в python БЕЗ newline='' молча схлопывает CRLF в LF
   (универсальные переводы строк), и запись потом кладёт LF. Так role-compile и
   subsystem-compile переписывали в LF весь Configuration.xml. meta-compile это
   уже делал правильно — там newline='' стоял с фикса #44/#46/#47.

Отдельно: XML-парсер по спецификации схлопывает CRLF при разборе, поэтому
lxml-порты (xdto-compile, xdto-edit) отдавали LF-документ там, где .NET возвращал
CRLF через NewLineHandling. Восстанавливаем EOL исходного файла.

Разделение канона и сохранения стиля:

- файл СОЗДАЁМ — канон (CRLF, без хвоста);
- существующий ПРАВИМ — наследуем его EOL, включая перевод строки вставки
  (контракт #44/#46/#47). Иначе LF-проект получал бы смешанные файлы — ровно то,
  на что заведён #57. Кейсы roundtrip-crlf-preserve остаются зелёными.

Хелпер записи скопирован в каждый навык (навыки автономны). У meta-compile он
называется Write-XmlFileKeepEol / write_xml_file_keep_eol: там нормализовать EOL
НЕЛЬЗЯ (многострочные запрос, синоним, значение заполнения), и одинаковое имя при
разном поведении было бы ловушкой.

Заодно: имя временного файла батча в meta-compile.ps1 получило GUID. Фиксированное
"meta-compile-batch-$idx.json" в общем %TEMP% сталкивало два параллельных запуска
(«file is being used by another process») — из-за этого полный набор приходилось
гонять с урезанной параллельностью. py-порт уже брал mkstemp.

Аудит: было ~250 файлов с дефектом EOL, стало 0 в обоих портах. Осталось четыре
законных случая — фикстуры roundtrip-crlf-preserve (сохранение стиля) и запрос
динсписка с многострочным текстом. Дрейф снэпшотов — 1 файл. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:37:26 +03:00
Nick ShirokovandClaude Opus 5 abe9431d8f fix(9 навыков): без хвостового перевода строки в конце XML (#57)
Конфигуратор не пишет перевод строки в конце файла — последний байт `>`. Это видно
на чистой выгрузке пустой ИБ (Windows и macOS) и на всех 8 выгрузках в cfsrc.
Наши эмиттеры добавляли лишний, причём оба порта и по разным причинам:

- PS: StringBuilder.AppendLine дописывает Environment.NewLine после последней строки;
- PY: '\n'.join(lines) + '\n' — хвост добавлен явно.

В meta-compile заведена единая точка записи Write-XmlFile: там документ собирают
и AppendLine, и явные `n в билдерах Predefined/Content/Flowchart, а выходов семь.
В остальных обрезка стоит в самом вызове записи.

Модули .bsl, JSON-выход form-compile, HTML-справка help-add и XSD из xdto-decompile
сюда НЕ входят: канон Конфигуратора описывает XML метаданных, а у этих артефактов
свои правила. Результаты XmlDocument.Save тоже не трогаются — у них хвоста нет.

Навыки редактирования продолжают сохранять стиль ВХОДНОГО файла: кейс
subsystem-edit/roundtrip-crlf-preserve, где фикстура намеренно с хвостом, остаётся
зелёным. Это контракт #44/#46/#47, и он не отменяется.

Дрейф снэпшотов: 547 файлов, у каждого только последняя строка, содержательных
изменений ноль. Тесты 641/641.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:53:05 +03:00
Nick ShirokovandClaude Opus 5 39cf8e49cf fix(17 навыков): самозакрывающийся тег без пробела и UTF-8 в декларации (#57)
.NET XmlWriter пишет `<a />`, а Конфигуратор — `<a/>`. Поскольку Save переписывает
файл целиком, meta-edit при добавлении одного реквизита переводил в пробельную форму
все пустые теги документа. Дефект только у PS-порта: lxml (45 из 49 py-портов) уже
пишет плотно, то есть порты расходились побайтово.

Канон измерен, а не предположен: чистая выгрузка пустой ИБ на Windows и macOS плюс
сплошной скан 8 выгрузок в cfsrc — 476 943 XML, 21 294 119 самозакрывающихся тегов,
пробельных 0. Форма не зависит от ОС, версии платформы (8.3.20-8.5) и наличия
атрибутов. Правило уже было реализовано в skd-edit — оттуда и взято.

Замена безопасна доказуемо: .NET экранирует `>` как `&gt;` и в тексте, и в атрибутах,
поэтому ` />` после Save — только конец тега. Лазейки (CDATA, комментарии) в
1С-метаданных не встречаются — 0 из 476 943 файлов; гард на них всё равно стоит.

Одиннадцать скриптов писали прямо в FileStream без пост-обработки — переведены на
MemoryStream, что заодно чинит `encoding="utf-8"` строчными (335 файлов в снэпшотах).
В cfe-borrow правится и сборка Form.xml: куски берутся из OuterXml, а он спацовывает
так же, как XmlWriter.

Дрейф снэпшотов: 12 838 строк тегов + 335 деклараций, содержательных изменений ноль.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 13:28:58 +03:00
Nick ShirokovandClaude Opus 5 ccd860ae89 docs(tests): эталон при preRun фиксирует вход теста
README называл info/validate-навыки типичным случаем для noSnapshot: эталон
зафиксировал бы выход preRun, а не проверяемого навыка. Факт верный, вывод — нет.

Когда preRun собирает фикстуру, эталон фиксирует ВХОД теста. Без него дрейф
навыка-генератора меняет фикстуру молча, и ожидание вроде «Составной (6)»
начинает проверяться на другом объекте — либо падает без внятной причины, либо
сходится случайно и перестаёт что-либо проверять. meta-compile такой дрейф даёт
регулярно. Поэтому ни один из семи info-навыков noSnapshot не использует —
у всех эталоны, и это осознанно, а не упущение.

Правило сформулировано явно: есть preRun с генерацией фикстуры → эталон;
нет preRun или фикстура тривиальна → noSnapshot.

В шапке verify-snapshots отмечено, что он работает и с кейсами навыков, которые
сами ничего не пишут, — там он проверяет, что платформа принимает собранную
preRun фикстуру. Плюс предупреждение, что на кейсах setup: external проверка
вырождается в загрузку 1С собственной выгрузки: ~3 минуты на кейс без пользы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:13:58 +03:00
Nick ShirokovandClaude Opus 5 5141cf546d fix(meta-info): иерархия не только у справочника
Строка «Иерархический: …» печаталась в ветке Catalog, хотя свойство
Hierarchical есть и у планов видов характеристик — в корпусе acc/erp/ut/unf
таких 13. У них иерархия не выводилась ни в overview, ни в full.

Хуже того, вывод противоречил сам себе: блок стандартных реквизитов в full
показывал Родитель (он определяется по тому же Hierarchical), а строки про
иерархию рядом не было — родитель появлялся ниоткуда.

Иерархия вынесена из ветки Catalog и печатается по наличию свойства;
подчинение осталось специфичным для справочника. Отсутствие иерархии
по-прежнему не печатается: сообщаем о наличии, молчим об отсутствии —
тихую ошибку в коде даёт только необнаруженное наличие.

Свойство Hierarchical проверено по корпусу: оно есть только у Catalog и
ChartOfCharacteristicTypes (у плана счетов иерархия по природе, свойства нет).
Прогон по корпусу: ровно 13 изменённых строк, побочных эффектов нет.

Находка из отклонённого PR #59 — там иерархия тоже была вынесена из ветки
Catalog. Реализация не заимствована: PR печатал «Иерархический: нет» для
неиерархических объектов, то есть строку-шум примерно у 4000 объектов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:54:43 +03:00
Nick ShirokovandClaude Opus 5 66807e02c7 test(meta-info): кроссплатформенные кейсы на свёртку ТипЗначения
Свёртку составного типа и её разворот через drill-down проверял только кейс
на выгрузке acc_8.3.24, а он привязан к локальному пути и на macOS
пропускается — то есть ключевая новая логика там не проверялась вовсе.

Добавлены два синтетических кейса на фикстуре от meta-compile: ПВХ с шестью
типами сворачивается в «Составной (6)» и печатает подсказку, а -Name
ТипЗначения разворачивает список. В составе фикстуры два v8:TypeSet
(cfg:CatalogRef, cfg:DocumentRef), поэтому кейсы заодно покрывают разбор
обобщённых ссылочных типов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:09:52 +03:00
Nick ShirokovandClaude Opus 5 50f8b497b6 feat(meta-info): блок стандартных реквизитов
Ишью #58: сводка не показывала владельца подчинённого справочника, и код,
написанный по ней, падал на Записать(). Владелец оказался частным случаем:
шапка «Код(9) | Наименование(100)» смешивала свойства объекта с полями и не
давала по полям ни типа, ни обязательности. По acc_8.3.24 из 690 справочников
23 имеют числовой код, 122 — фиксированной длины, 426 — вовсе без кода; всё
это было невидимо. У ПВХ, планов счетов и обмена, бизнес-процессов и задач
шапки не было вообще — включая ТипЗначения у ПВХ.

Поля вынесены в блок «Стандартные реквизиты» на языке блока «Реквизиты»
(имя — тип — [флаги]), в шапке остались свойства объекта. В overview
показываются те, что нельзя вывести из остального вывода: Владелец, Код,
Наименование, Дата, Номер, ТипЗначения. Ссылка, ПометкаУдаления, Родитель,
ЭтоГруппа следуют из типа объекта и строки «Иерархический» — они в full.

Обязательность берётся из StandardAttributes/FillChecking. Блок в XML
опционален (meta-dsl-spec §7.1.1), поэтому при его отсутствии действует
профиль платформенных дефолтов, синхронный с meta-compile — иначе объекты,
созданные meta-compile, теряли бы флаг, то есть ровно кейс из ишью.

Составной ТипЗначения ПВХ доходит до 151 типа и сворачивается в счётчик
«Составной (110)»; полный состав даёт drill-down -Name ТипЗначения, о чём
сообщает единственная строка в конце вывода. brief получил строку
«Подчинён: …» — факт структуры без обещаний про обязательность, которых
brief выполнить не может.

Проверено сравнением с эталонным прогоном по 4768 объектам acc/erp/ut/unf:
все расхождения классифицированы, необъяснённых нет, ненулевых кодов возврата
нет. Владелец появился у 242 объектов, числовой код — у 56.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 11:57:32 +03:00
Nick ShirokovandClaude Opus 5 c687860535 fix(meta-info): обобщённые ссылочные типы вместо сырого cfg:
Ветка TypeSet в Format-Type распознавала только DefinedType и Characteristic,
а остальные наборы отдавала в вывод как есть. Голое имя метатипа без «.Имя»
означает все ссылки этого класса (docs/meta-dsl-spec.md), и такие типы
печатались сырыми — 254 вхождения в 82 строках на корпусе acc/erp/ut/unf:

  ЗначениеПустойСсылки   cfg:ExchangePlanRef | cfg:BusinessProcessRef | …
  Свойство1              Булево | Строка(128) | ДатаВремя | cfg:AnyIBRef

Разбор вынесен в Format-SingleTypeSet: AnyRef → ЛюбаяСсылка, AnyIBRef →
ЛюбаяСсылкаИБ, голые XxxRef — через существующий refTypeMap. Конкретный тип
всегда пишется с точкой, поэтому «СправочникСсылка» без точки однозначно
читается как обобщённый.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 10:52:39 +03:00
Nick ShirokovandClaude Opus 5 cdf0944b2f fix(meta-info): имя команды вместо склеенного XML
Get-SimpleChildren брала InnerText дочернего узла ChildObjects. Для Form и
Template это верно — имя лежит прямо в тексте узла. Command же имеет вложенный
Properties, и InnerText склеивал всё его содержимое в одну строку:

  Команды: БанковскиеСчета_ИнтерфейсИнтеграцииСБанкомruБанковские счетаForm
  NavigationPanelImportantcfg:CatalogRef.ОрганизацииSinglefalseAutoAuto

Теперь имя берётся из Properties/Name, если оно есть, иначе — прежний InnerText.
Задеты обе точки вывода: overview у отчётов и обработок и full у всех типов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 22:59:30 +03:00
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
78b5b73fa7 fix(skills): db-load-git кириллические пути (#32) + img-grid валидация (#33)
#32: git diff/ls-files при дефолтном core.quotePath=true возвращают кириллицу
в octal-виде → объект с кириллическим именем не распознавался в partial-load.
Все git-вызовы теперь с `-c core.quotePath=false`. PY — в центральном run_git;
PS — унифицировал inline-вызовы в хелпер Invoke-GitLines (паритет с py). v1.11.

#33: img-grid падал ZeroDivisionError при -c 0 / некорректном -r. Добавлена
валидация (cols>0, rows>=0, auto-rows max(1,...)). Pillow задокументирован как
runtime-зависимость (README + python-porting-guide). overlay-grid.py получил
версионный заголовок (v1.1).

Тесты: runner error-кейсы db-dump-dt/db-load-dt/db-load-git (из PR #34).
img-grid runner-кейс не вводили (py-only + Pillow-зависимость) — #33 проверен вручную.

Closes #32, #33

Co-Authored-By: Korolev Pavel <korolev.vrn@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:33:10 +03:00
fb9783240e test(skills): smoke-кейсы form-remove/form-decompile/erf-validate + фикс form-remove (#30)
Добавлены runner-кейсы (из PR #31) для непокрытых навыков: form-remove,
form-decompile, erf-validate (последний делегирует epf-validate — auto-detect
ExternalReport). Сгенерированы канонические снапшоты → полноценные регрессы,
зелёные на обоих портах.

Генерация снапшотов вскрыла реальный баг form-remove: при удалении формы
очищался только generic <DefaultForm>, а form-add пишет свойство по назначению
(<DefaultObjectForm>/<DefaultListForm>/<DefaultChoiceForm>/<DefaultRecordForm>) —
оставалась висячая ссылка на удалённую форму (невалидный конфиг). Фикс (оба порта):
очищаем любой *Form-слот, указывающий на удаляемую форму. Бамп версий ps1+py (v1.3).

Co-Authored-By: Korolev Pavel <korolev.vrn@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:34:20 +03:00
4b6bdac484 fix(skills): ibcmd non-interactive — fast-fail вместо зависания на запросе учётки (#28)
ibcmd при отсутствии/неверных учётных данных уходил в интерактивный запрос и
зависал. Эмпирически (реальный ibcmd, macOS+Windows, базы с пользователями):
вис возникает только когда не задан -UserName; stdin=DEVNULL НЕ лечит (наоборот
виснет). Лечит закрытый stdin-пайп (EOF) → ibcmd падает за ~4-7с с внятным
«Идентификация пользователя не выполнена».

- python (12 файлов + stub): run_ibcmd(cmd, has_username) c input="" вместо
  прямого subprocess.run; без таймаута. Остаточный случай python+Windows+нет
  -UserName (ibcmd читает консоль) помечается hint'ом в stderr (English, model-facing).
- PowerShell (12 файлов): Invoke-IbcmdProcess через [System.Diagnostics.Process] +
  RedirectStandardInput + StandardInput.Close() → fast-fail; StandardOutputEncoding
  cp866 (кириллица ibcmd не мойибейлится). Ветка 1cv8/DESIGNER не тронута.
- Жёсткий таймаут не вводим: у вызывающей стороны свой бэкстоп, авто-kill рвал бы
  длинные легитимные операции.
- Синхронный бамп версий ps1+py всех затронутых навыков.

Fixes #28

Co-Authored-By: Korolev Pavel <korolev.vrn@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:23:59 +03:00
Nick ShirokovandClaude Opus 4.8 696af58cf7 fix(tests): clone-safe фикстура help-add/on-support (генерируемый объект)
guard-deny держался на статической фикстуре с пустыми каталогами Locked/Ext/ —
git не хранит пустые каталоги, поэтому на свежем клоне (macOS) каталог объекта
исчезал, и help-add падал «Каталог объекта не найден» раньше, чем срабатывал
support-guard (проверка os.path.isdir(ext_dir) идёт до assert_edit_allowed).

Пересобрал фикстуру как настоящую конфигурацию: cf-init + meta-compile Catalog
Locked (реальный Catalogs/Locked/Ext/ObjectModule.bsl → переживает клон) +
сгенерированный Ext/ParentConfigurations.bin с реальными uuid (config + объект
как f1=0, g=0 — семантика по-объектного замка сохранена). objectName → Catalogs/Locked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 15:05:54 +03:00
9d6ba55db1 fix(skd-edit): совместимость .py-порта с python 3.9 (PEP 701 f-string)
Два выражения автодат (ДатаНачала/ДатаОкончания) собирались внутри f-string
с бэкслешем (\uXXXX) в {…} — синтаксис валиден только с python 3.12+ (PEP 701),
на 3.9/3.11 → SyntaxError (skd-edit.py:1256/1270), весь модуль не импортируется.
Выносим выражения в переменные expr_start/expr_end перед f-string — поведение
байт-в-байт прежнее, конвенция \uXXXX сохранена. Единственный файл в корпусе
с этой несовместимостью (проверено AST-сканом всех 64 .py).

Синхронный бамп версий ps1+py (v1.28), коммент-маркер расхождения в .ps1.

Co-Authored-By: Korolev Pavel <korolev.vrn@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:55:09 +03:00
29999a2755 fix(skills): корректный запуск Python-портов на macOS/Linux
- subsystem-compile.py: авто-валидация через subprocess к subsystem-validate.py
  (вместо powershell.exe + .ps1), без проброса кода возврата — паритет с сиблингами
- resolve_v8path (12 db/epf .py): резолв каталога платформы по ОС (1cv8 без .exe
  на *nix), авто-поиск /opt/1cv8/*; ibcmd только явным путём, 1cv8c убран
- сообщения об ошибках платформо-нейтральные (ps1+py), коммент-заглушка PY-only в .ps1
- runner.mjs (v0.5): loadV8Context кросс-платформенный (1cv8/ibcmd без .exe)
- тесты: stdoutContains маркер валидатора (#26), негативный кейс db-update/error-bad-v8dir
- синхронный бамп версий ps1+py всех затронутых навыков

Fixes #26, #27

Co-Authored-By: Korolev Pavel <korolev.vrn@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 11:42:07 +03:00
Nick ShirokovandClaude Opus 4.8 3a7f1c1763 fix(web-test): заполнение data-ячеек безголового грида + устранение мигания чекбокса
Два фикса в fillTableRow на гридах:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 15:18:41 +03:00
Nick ShirokovandClaude Opus 4.8 3d36c20269 fix(epf-build,epf-dump,web-publish): надёжный резолв пути к платформе 1С
Распространение фикса резолва (см. предыдущий коммит по db-*) на
оставшиеся навыки с тем же дублированным блоком:
- epf-build, epf-dump (ps1+py): резолв 1cv8.exe — реестр .v8-project.json
  → числовая сортировка версий → glob Program Files [+ (x86)] с заметкой.
- web-publish (ps1+py): резолв bin-каталога (для wsap24.dll) — те же
  приоритеты; v8path из реестра уже есть нужный bin-каталог.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 15:40:17 +03:00
2648 changed files with 236909 additions and 108857 deletions
+1 -1
View File
@@ -1 +1 @@
__pycache__/ __pycache__/
+60 -60
View File
@@ -1,60 +1,60 @@
--- ---
name: cf-edit name: cf-edit
description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу description: Точечное редактирование конфигурации 1С. Используй когда нужно изменить свойства конфигурации, добавить или удалить объект из состава, настроить роли по умолчанию, поменять раскладку панелей, настроить начальную страницу
argument-hint: -ConfigPath <path> -Operation <op> -Value <value> argument-hint: -ConfigPath <path> -Operation <op> -Value <value>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Glob - Glob
--- ---
# /cf-edit — редактирование конфигурации 1С # /cf-edit — редактирование конфигурации 1С
Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию. Точечное редактирование Configuration.xml: свойства, состав ChildObjects, роли по умолчанию.
## Параметры и команда ## Параметры и команда
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | | `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Operation` | Операция (см. таблицу) | | `Operation` | Операция (см. таблицу) |
| `Value` | Значение для операции (batch через `;;`) | | `Value` | Значение для операции (batch через `;;`) |
| `DefinitionFile` | JSON-файл с массивом операций | | `DefinitionFile` | JSON-файл с массивом операций |
| `NoValidate` | Пропустить авто-валидацию | | `NoValidate` | Пропустить авто-валидацию |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1' powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -ConfigPath '<path>' -Operation modify-property -Value 'Version=1.0.0.1'
``` ```
## Операции ## Операции
| Операция | Формат Value | Описание | | Операция | Формат Value | Описание |
|----------|-------------|----------| |----------|-------------|----------|
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство | | `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически | | `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects | | `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию | | `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию | | `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию | | `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
| `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) | | `set-panels` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/ClientApplicationInterface.xml` (раскладка панелей) |
| `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) | | `set-home-page` | JSON-объект (см. [reference.md](reference.md)) | Перезаписать `Ext/HomePageWorkArea.xml` (начальная страница) |
Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md) Допустимые значения свойств, формат DefinitionFile (JSON), каноничный порядок: [reference.md](reference.md)
## Примеры ## Примеры
```powershell ```powershell
# Изменить версию и поставщика # Изменить версию и поставщика
... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С" ... -ConfigPath src -Operation modify-property -Value "Version=1.0.0.1 ;; Vendor=Фирма 1С"
# Добавить объекты # Добавить объекты
... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ" ... -ConfigPath src -Operation add-childObject -Value "Catalog.Товары ;; Document.Заказ"
# Удалить объект # Удалить объект
... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший" ... -ConfigPath src -Operation remove-childObject -Value "Catalog.Устаревший"
# Роли по умолчанию # Роли по умолчанию
... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава" ... -ConfigPath src -Operation add-defaultRole -Value "ПолныеПрава"
... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор" ... -ConfigPath src -Operation set-defaultRoles -Value "ПолныеПрава ;; Администратор"
``` ```
+150 -150
View File
@@ -1,150 +1,150 @@
# cf-edit — справочник операций # cf-edit — справочник операций
## modify-property ## modify-property
Свойства для редактирования: Свойства для редактирования:
### Скалярные ### Скалярные
`Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress` `Name`, `Version`, `Vendor`, `Comment`, `NamePrefix`, `UpdateCatalogAddress`
### LocalString (многоязычные) ### LocalString (многоязычные)
`Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress` `Synonym`, `BriefInformation`, `DetailedInformation`, `Copyright`, `VendorInformationAddress`, `ConfigurationInformationAddress`
### Enum ### Enum
| Свойство | Допустимые значения | | Свойство | Допустимые значения |
|----------|---------------------| |----------|---------------------|
| `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` | | `CompatibilityMode` | `Version8_3_20` ... `Version8_3_28`, `Version8_5_1`, `DontUse` |
| `ConfigurationExtensionCompatibilityMode` | то же | | `ConfigurationExtensionCompatibilityMode` | то же |
| `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` | | `DefaultRunMode` | `ManagedApplication`, `OrdinaryApplication`, `Auto` |
| `ScriptVariant` | `Russian`, `English` | | `ScriptVariant` | `Russian`, `English` |
| `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` | | `DataLockControlMode` | `Managed`, `Automatic`, `AutomaticAndManaged` |
| `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` | | `ObjectAutonumerationMode` | `NotAutoFree`, `AutoFree` |
| `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` | | `ModalityUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` | | `SynchronousPlatformExtensionAndAddInCallUseMode` | `DontUse`, `Use`, `UseWithWarnings` |
| `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` | | `InterfaceCompatibilityMode` | `Version8_2`, `Version8_2EnableTaxi`, `Taxi`, `TaxiEnableVersion8_2`, `TaxiEnableVersion8_5`, `Version8_5EnableTaxi`, `Version8_5` |
| `DatabaseTablespacesUseMode` | `DontUse`, `Use` | | `DatabaseTablespacesUseMode` | `DontUse`, `Use` |
| `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` | | `MainClientApplicationWindowMode` | `Normal`, `Fullscreen`, `Kiosk` |
### Ref ### Ref
`DefaultLanguage` — значение вида `Language.Русский` `DefaultLanguage` — значение вида `Language.Русский`
### Формат batch ### Формат batch
`"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"` `"Version=1.0.0.1 ;; Vendor=Фирма 1С ;; Synonym=Тестовая конфигурация"`
## add-childObject / remove-childObject ## add-childObject / remove-childObject
Формат: `Type.Name` — XML-тип и имя объекта через точку. Формат: `Type.Name` — XML-тип и имя объекта через точку.
**Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов. **Важно про `add-childObject`**: регистрирует в `<ChildObjects>` объект, **файл которого уже существует на диске**. Если файла нет — exit 1. Для создания нового объекта используй профильный навык — `/meta-compile` (Catalog, Document, Enum, Report, регистры и т.д.), `/role-compile` (Role), `/subsystem-compile` (Subsystem). Они создают файл И регистрируют его за один вызов.
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"` Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
## add-defaultRole / remove-defaultRole / set-defaultRoles ## add-defaultRole / remove-defaultRole / set-defaultRoles
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически). Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
`set-defaultRoles` полностью заменяет список ролей. `set-defaultRoles` полностью заменяет список ролей.
## set-panels ## set-panels
Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране. Перезаписывает `Ext/ClientApplicationInterface.xml` — раскладку панелей рабочего пространства Taxi. Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует на экране.
`value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона). `value` — объект с ключами `top`, `left`, `right`, `bottom`. Каждый ключ — массив записей. Ключ можно опустить (= пустая сторона).
**Запись** — одна из: **Запись** — одна из:
- Строка-алиас (одна панель в этом слоте) - Строка-алиас (одна панель в этом слоте)
- Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом) - Объект `{"group": [...]}` (стек: панели/подгруппы внутри располагаются друг под другом)
**Алиасы панелей:** **Алиасы панелей:**
| Алиас | Панель | | Алиас | Панель |
|-------|--------| |-------|--------|
| `sections` | Панель разделов | | `sections` | Панель разделов |
| `open` | Панель открытых | | `open` | Панель открытых |
| `favorites` | Панель избранного | | `favorites` | Панель избранного |
| `history` | Панель истории | | `history` | Панель истории |
| `functions` | Панель функций текущего раздела | | `functions` | Панель функций текущего раздела |
**Семантика:** **Семантика:**
- Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...) - Несколько записей в одной стороне → отдельные слоты «рядом» (несколько тегов `<top>`/...)
- `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком - `{"group":[...]}` → один тег с `<group>`-обёрткой, элементы внутри идут стеком
**Пример** (DefinitionFile): **Пример** (DefinitionFile):
```json ```json
[ [
{ {
"operation": "set-panels", "operation": "set-panels",
"value": { "value": {
"top": ["open"], "top": ["open"],
"left": ["sections"], "left": ["sections"],
"right": [{ "group": ["favorites", "history"] }], "right": [{ "group": ["favorites", "history"] }],
"bottom": ["functions"] "bottom": ["functions"]
} }
} }
] ]
``` ```
Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`. Через `-Value` (CLI): передай объект как JSON-строку — `... -Operation set-panels -Value '{"top":["open"]}'`.
## set-home-page ## set-home-page
Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует. Перезаписывает `Ext/HomePageWorkArea.xml` — раскладка форм на начальной странице (рабочая область). Файл создаётся с нуля; то, что не упомянуто в `value`, отсутствует.
`value` — объект: `value` — объект:
| Ключ | Канонич. (XML) | Описание | | Ключ | Канонич. (XML) | Описание |
|------|----------------|----------| |------|----------------|----------|
| `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` | | `template` | `WorkingAreaTemplate` | `OneColumn` / `TwoColumnsEqualWidth` (дефолт) / `TwoColumnsVariableWidth` |
| `left` | `LeftColumn` | массив записей форм | | `left` | `LeftColumn` | массив записей форм |
| `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) | | `right` | `RightColumn` | массив записей форм (запрещён при `OneColumn`) |
Принимаются и короткие и канонич. ключи (XML-имена) — оба работают. Принимаются и короткие и канонич. ключи (XML-имена) — оба работают.
**Запись формы** — одна из: **Запись формы** — одна из:
- Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true` - Строка `"<form>"` — только имя формы, дефолты `height=10`, `visibility=true`
- Объект `{form, height?, visibility?, roles?}` - Объект `{form, height?, visibility?, roles?}`
| Поле | Канонич. | Дефолт | Описание | | Поле | Канонич. | Дефолт | Описание |
|------|----------|--------|----------| |------|----------|--------|----------|
| `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) | | `form` | `Form` | — | `CommonForm.X` или `Type.Object.Form.Name` (или UUID) |
| `height` | `Height` | `10` | Высота | | `height` | `Height` | `10` | Высота |
| `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) | | `visibility` | `Visibility` | `true` | Общая видимость (`<xr:Common>`) |
| `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям | | `roles` | — | — | `{"Role.Имя": true|false, ...}` — переопределения по ролям |
**Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`. **Семантика visibility:** `visibility` = общее правило, `roles` — точечные исключения. Скрыть для всех кроме одной роли: `{"visibility": false, "roles": {"Role.Опер": true}}`.
**Пример:** **Пример:**
```json ```json
[ [
{ {
"operation": "set-home-page", "operation": "set-home-page",
"value": { "value": {
"template": "TwoColumnsVariableWidth", "template": "TwoColumnsVariableWidth",
"left": [ "left": [
"CommonForm.НачалоРаботы", "CommonForm.НачалоРаботы",
{ "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false }, { "form": "CommonForm.СписокЗадач", "height": 100, "visibility": false },
{ "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 }, { "form": "Catalog.Контрагенты.Form.ФормаСписка", "height": 50 },
{ {
"form": "CommonForm.РабочийСтолОператора", "form": "CommonForm.РабочийСтолОператора",
"visibility": false, "visibility": false,
"roles": { "Role.Оператор": true, "Role.ПолныеПрава": false } "roles": { "Role.Оператор": true, "Role.ПолныеПрава": false }
} }
], ],
"right": [ "right": [
{ "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 } { "form": "DataProcessor.Поиск.Form.ФормаПоиска", "height": 30 }
] ]
} }
} }
] ]
``` ```
## DefinitionFile (JSON) ## DefinitionFile (JSON)
```json ```json
[ [
{ "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" }, { "operation": "modify-property", "value": "Version=2.0.0.1 ;; Vendor=Test" },
{ "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" }, { "operation": "add-childObject", "value": "Catalog.Товары ;; Document.Заказ" },
{ "operation": "add-defaultRole", "value": "ПолныеПрава" } { "operation": "add-defaultRole", "value": "ПолныеПрава" }
] ]
``` ```
File diff suppressed because it is too large Load Diff
+232 -10
View File
@@ -1,16 +1,194 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-edit v1.4 — Edit 1C configuration root (Configuration.xml) # cf-edit v1.16 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import uuid as _uuid import uuid as _uuid
from html import escape as html_escape from html import escape as html_escape
from lxml import etree from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_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):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# 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
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
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:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
MD_NS = "http://v8.1c.ru/8.3/MDClasses" MD_NS = "http://v8.1c.ru/8.3/MDClasses"
XR_NS = "http://v8.1c.ru/8.3/xcf/readable" XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
@@ -21,7 +199,7 @@ XS_NS = "http://www.w3.org/2001/XMLSchema"
TYPE_ORDER = [ TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -37,7 +215,7 @@ TYPE_ORDER = [
TYPE_TO_DIR = { TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles", "Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates", "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", "XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions", "EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups", "FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
@@ -146,13 +324,50 @@ def parse_batch_value(val):
return items return items
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None → файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): 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) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
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"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
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: 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) f.write(xml_bytes)
@@ -190,10 +405,16 @@ def main():
resolved_path = os.path.abspath(config_path) resolved_path = os.path.abspath(config_path)
config_dir = os.path.dirname(resolved_path) config_dir = os.path.dirname(resolved_path)
assert_edit_allowed(resolved_path, "editable")
xml_parser = etree.XMLParser(remove_blank_text=False) xml_parser = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(resolved_path, xml_parser) tree = etree.parse(resolved_path, xml_parser)
xml_root = tree.getroot() xml_root = tree.getroot()
# Версия формата редактируемой конфигурации — создаваемые рядом файлы (Ext/HomePageWorkArea.xml)
# должны нести ту же версию, иначе в проекте окажутся файлы разных версий формата.
format_version = xml_root.get('version') or '2.17'
add_count = 0 add_count = 0
remove_count = 0 remove_count = 0
modify_count = 0 modify_count = 0
@@ -608,6 +829,7 @@ def main():
"регистррасчета": "CalculationRegister", "регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister", "регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess", "бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan", "задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage", "хранилищенастроек": "SettingsStorage",
} }
@@ -742,7 +964,7 @@ def main():
'<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" ' '<HomePageWorkArea xmlns="http://v8.1c.ru/8.3/xcf/extrnprops" '
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" ' 'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" '
'xmlns:xs="http://www.w3.org/2001/XMLSchema" ' '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'\t<WorkingAreaTemplate>{tmpl}</WorkingAreaTemplate>\r\n'
f'{left_xml}\r\n' f'{left_xml}\r\n'
f'{right_xml}\r\n' f'{right_xml}\r\n'
@@ -806,7 +1028,7 @@ def main():
if os.path.isfile(validate_script): if os.path.isfile(validate_script):
print() print()
print("--- Running cf-validate ---") print("--- Running cf-validate ---")
subprocess.run([sys.executable, validate_script, "-ConfigPath", "-Path", resolved_path]) subprocess.run([sys.executable, validate_script, "-ConfigPath", resolved_path])
# --- Summary --- # --- Summary ---
print() print()
+54 -54
View File
@@ -1,54 +1,54 @@
--- ---
name: cf-info name: cf-info
description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки description: Анализ структуры конфигурации 1С — свойства, состав, счётчики объектов. Используй для обзора конфигурации — какие объекты есть, сколько их, какие настройки
argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page] argument-hint: <ConfigPath> [-Mode overview|brief|full] [-Section home-page]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cf-info — Структура конфигурации 1С # /cf-info — Структура конфигурации 1С
Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры. Читает Configuration.xml из выгрузки конфигурации и выводит компактное описание структуры.
## Параметры и команда ## Параметры и команда
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки | | `ConfigPath` | Путь к Configuration.xml или каталогу выгрузки |
| `Mode` | Режим: `overview` (default), `brief`, `full` | | `Mode` | Режим: `overview` (default), `brief`, `full` |
| `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` | | `Section` | Drill-down по разделу (alias: `Name`). Сейчас: `home-page` |
| `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) | | `Limit` / `Offset` | Пагинация (по умолчанию 150 строк) |
| `OutFile` | Записать результат в файл (UTF-8 BOM) | | `OutFile` | Записать результат в файл (UTF-8 BOM) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-info.ps1" -ConfigPath "<путь>"
``` ```
## Три режима ## Три режима
| Режим | Что показывает | | Режим | Что показывает |
|---|---| |---|---|
| `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам | | `overview` *(default)* | Заголовок + ключевые свойства + таблица счётчиков объектов по типам |
| `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость | | `brief` | Одна строка: Имя — "Синоним" vВерсия \| N объектов \| совместимость |
| `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности | | `full` | Все свойства по категориям + полный список ChildObjects + DefaultRoles + мобильные функциональности |
## Примеры ## Примеры
```powershell ```powershell
# Обзор пустой конфигурации # Обзор пустой конфигурации
... -ConfigPath src ... -ConfigPath src
# Краткая сводка реальной конфигурации # Краткая сводка реальной конфигурации
... -ConfigPath src -Mode brief ... -ConfigPath src -Mode brief
# Полная информация # Полная информация
... -ConfigPath src -Mode full ... -ConfigPath src -Mode full
# С пагинацией # С пагинацией
... -ConfigPath src -Mode full -Limit 50 -Offset 100 ... -ConfigPath src -Mode full -Limit 50 -Offset 100
# Drill-down: только начальная страница (раскладка форм с ролями) # Drill-down: только начальная страница (раскладка форм с ролями)
... -ConfigPath src -Section home-page ... -ConfigPath src -Section home-page
``` ```
File diff suppressed because it is too large Load Diff
+74 -2
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-info v1.2 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import os import os
import re
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from lxml import etree from lxml import etree
@@ -94,7 +95,7 @@ def get_prop_ml(prop_name):
type_order = [ type_order = [
"Language", "Subsystem", "StyleItem", "Style", "Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate", "CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan", "FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference", "XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption", "EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup", "FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
@@ -110,6 +111,7 @@ type_ru_names = {
"Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили", "Language": "Языки", "Subsystem": "Подсистемы", "StyleItem": "Элементы стиля", "Style": "Стили",
"CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли", "CommonPicture": "Общие картинки", "SessionParameter": "Параметры сеанса", "Role": "Роли",
"CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули", "CommonTemplate": "Общие макеты", "FilterCriterion": "Критерии отбора", "CommonModule": "Общие модули",
"Bot": "Боты",
"CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты", "CommonAttribute": "Общие реквизиты", "ExchangePlan": "Планы обмена", "XDTOPackage": "XDTO-пакеты",
"WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки", "WebService": "Веб-сервисы", "HTTPService": "HTTP-сервисы", "WSReference": "WS-ссылки",
"EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания", "EventSubscription": "Подписки на события", "ScheduledJob": "Регламентные задания",
@@ -219,6 +221,71 @@ def get_home_page_layout():
home_page = get_home_page_layout() home_page = get_home_page_layout()
# --- Support state (Ext/ParentConfigurations.bin) ---
# Decodes the 1C support-state file. See docs/1c-support-state-spec.md.
# Returns None on absent/error; else dict: state='absent'|'removed'|'parsed',
# g (0=editing on, 1=off), k (vendor configs), vendors [{vendor,name,version}],
# counts [locked, editable, removed] by f1 — record tally (k>1 counts each
# vendor block separately); only computed when g==0.
def read_support_state(bin_path):
try:
if not os.path.isfile(bin_path):
return {"state": "absent"}
data = open(bin_path, "rb").read()
if len(data) <= 32:
return {"state": "removed"}
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return None
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return {"state": "removed"}
vendors = []
for m in re.finditer(r'"((?:[^"]|"")*)","((?:[^"]|"")*)","((?:[^"]|"")*)",\d+,', text):
vendors.append({
"version": m.group(1).replace('""', '"'),
"vendor": m.group(2).replace('""', '"'),
"name": m.group(3).replace('""', '"'),
})
counts = None
if g == 0:
counts = [0, 0, 0]
for m in re.finditer(r"([0-2]),0,[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", text):
counts[int(m.group(1))] += 1
return {"state": "parsed", "g": g, "k": k, "vendors": vendors, "counts": counts}
except Exception:
return None
def get_support_lines():
config_dir = os.path.dirname(config_path)
bin_path = os.path.join(config_dir, "Ext", "ParentConfigurations.bin")
st = read_support_state(bin_path)
res = []
if not st or st["state"] == "absent":
if cfg_ext_purpose:
res.append("Поддержка: расширение (CFE), правки свободны")
else:
res.append("Поддержка: не на поддержке (своя конфигурация)")
return res
if st["state"] == "removed":
res.append("Поддержка: снята с поддержки полностью")
return res
res.append("Поддержка: на поддержке")
if st["g"] == 0:
res.append(" Возможность изменения: включена")
res.append(f" Объектов: на замке {st['counts'][0]} / редактируется {st['counts'][1]} / снято {st['counts'][2]}")
else:
res.append(" Возможность изменения: выключена — вся конфигурация read-only (правки заблокированы)")
res.append(f" Конфигураций поставщика: {st['k']}")
if st["k"] > 1:
for v in st["vendors"]:
res.append(f" Поставщик: {v['vendor']}{v['name']} {v['version']}")
return res
def format_home_page_item(it, detailed): def format_home_page_item(it, detailed):
badges = [f"h={it['height']}"] badges = [f"h={it['height']}"]
if not it["common"]: if not it["common"]:
@@ -249,6 +316,7 @@ cfg_version = get_prop_text("Version")
cfg_vendor = get_prop_text("Vendor") cfg_vendor = get_prop_text("Vendor")
cfg_compat = get_prop_text("CompatibilityMode") cfg_compat = get_prop_text("CompatibilityMode")
cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode") cfg_ext_compat = get_prop_text("ConfigurationExtensionCompatibilityMode")
cfg_ext_purpose = get_prop_text("ConfigurationExtensionPurpose")
cfg_default_run = get_prop_text("DefaultRunMode") cfg_default_run = get_prop_text("DefaultRunMode")
cfg_script = get_prop_text("ScriptVariant") cfg_script = get_prop_text("ScriptVariant")
cfg_default_lang = get_prop_text("DefaultLanguage") cfg_default_lang = get_prop_text("DefaultLanguage")
@@ -281,6 +349,8 @@ if args.Mode == "overview" and not args.Section:
out(f"Поставщик: {cfg_vendor}") out(f"Поставщик: {cfg_vendor}")
if cfg_version: if cfg_version:
out(f"Версия: {cfg_version}") out(f"Версия: {cfg_version}")
for ln in get_support_lines():
out(ln)
out(f"Совместимость: {cfg_compat}") out(f"Совместимость: {cfg_compat}")
out(f"Режим запуска: {cfg_default_run}") out(f"Режим запуска: {cfg_default_run}")
out(f"Язык скриптов: {cfg_script}") out(f"Язык скриптов: {cfg_script}")
@@ -369,6 +439,8 @@ if args.Mode == "full" and not args.Section:
out(f"Поставщик: {cfg_vendor}") out(f"Поставщик: {cfg_vendor}")
if cfg_version: if cfg_version:
out(f"Версия: {cfg_version}") out(f"Версия: {cfg_version}")
for ln in get_support_lines():
out(ln)
cfg_update_addr = get_prop_text("UpdateCatalogAddress") cfg_update_addr = get_prop_text("UpdateCatalogAddress")
if cfg_update_addr: if cfg_update_addr:
out(f"Каталог обн.: {cfg_update_addr}") out(f"Каталог обн.: {cfg_update_addr}")
+49 -49
View File
@@ -1,49 +1,49 @@
--- ---
name: cf-init name: cf-init
description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля description: Создать пустую конфигурацию 1С (scaffold XML-исходников). Используй когда нужно начать новую конфигурацию с нуля
argument-hint: <Name> [-Synonym <name>] [-OutputDir src] argument-hint: <Name> [-Synonym <name>] [-OutputDir src]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cf-init — Создание пустой конфигурации 1С # /cf-init — Создание пустой конфигурации 1С
Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`. Создаёт scaffold исходников пустой конфигурации 1С: `Configuration.xml`, `Languages/Русский.xml`.
## Параметры и команда ## Параметры и команда
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `Name` | Имя конфигурации (обязат.) | | `Name` | Имя конфигурации (обязат.) |
| `Synonym` | Синоним (= Name если не указан) | | `Synonym` | Синоним (= Name если не указан) |
| `OutputDir` | Каталог для создания (default: `src`) | | `OutputDir` | Каталог для создания (default: `src`) |
| `Version` | Версия конфигурации | | `Version` | Версия конфигурации |
| `Vendor` | Поставщик | | `Vendor` | Поставщик |
| `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) | | `CompatibilityMode` | Режим совместимости (default: `Version8_3_24`) |
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-init.ps1" -Name "МояКонфигурация"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Базовая конфигурация # Базовая конфигурация
... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf ... -Name МояКонфигурация -Synonym "Моя конфигурация" -OutputDir test-tmp/cf
# С версией и поставщиком # С версией и поставщиком
... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2 ... -Name TestCfg -Synonym "Тестовая" -Version "1.0.0.1" -Vendor "Фирма 1С" -OutputDir test-tmp/cf2
# Другой режим совместимости # Другой режим совместимости
... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3 ... -Name TestCfg -CompatibilityMode Version8_3_27 -OutputDir test-tmp/cf3
``` ```
## Верификация ## Верификация
``` ```
/cf-init TestConfig -OutputDir test-tmp/cf /cf-init TestConfig -OutputDir test-tmp/cf
/cf-info test-tmp/cf — проверить созданное /cf-info test-tmp/cf — проверить созданное
/cf-validate test-tmp/cf — валидировать /cf-validate test-tmp/cf — валидировать
``` ```
+295 -249
View File
@@ -1,249 +1,295 @@
# cf-init v1.2 — Create empty 1C configuration scaffold # cf-init v1.8 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$OutputDir = "src", [string]$OutputDir = "src",
[string]$Version, [string]$Version,
[string]$Vendor, [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,
$ErrorActionPreference = "Stop" # 8.3.27 — 2.20. Дефолт консервативный: 2.17 читается всеми поддерживаемыми платформами.
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
# --- Resolve output dir --- )
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir $ErrorActionPreference = "Stop"
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Check existing --- # --- Resolve output dir ---
$cfgFile = Join-Path $OutputDir "Configuration.xml" if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
if (Test-Path $cfgFile) { $OutputDir = Join-Path (Get-Location).Path $OutputDir
Write-Error "Configuration.xml already exists: $cfgFile" }
exit 1
} # --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml"
# --- Generate UUIDs --- if (Test-Path $cfgFile) {
$uuidCfg = [guid]::NewGuid().ToString() Write-Error "Configuration.xml already exists: $cfgFile"
$uuidLang = [guid]::NewGuid().ToString() exit 1
# 7 ContainedObject ObjectIds }
$co1 = [guid]::NewGuid().ToString()
$co2 = [guid]::NewGuid().ToString() # --- Generate UUIDs ---
$co3 = [guid]::NewGuid().ToString() $uuidCfg = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString() $uuidLang = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString() # 7 ContainedObject ObjectIds
$co6 = [guid]::NewGuid().ToString() $co1 = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString() $co2 = [guid]::NewGuid().ToString()
$co3 = [guid]::NewGuid().ToString()
# --- Mobile functionalities --- $co4 = [guid]::NewGuid().ToString()
$mobileFuncs = @( $co5 = [guid]::NewGuid().ToString()
@("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"), $co6 = [guid]::NewGuid().ToString()
@("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"), $co7 = [guid]::NewGuid().ToString()
@("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
@("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"), # --- Mobile functionalities ---
@("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"), # Версия формата как число — по ней ниже включаются вставки 2.21.
@("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"), $is221 = (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221)
@("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"), $mobileFuncs = @(
@("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"), @("Biometrics","true"), @("Location","false"), @("BackgroundLocation","false"),
@("OSBackup","true"), @("ApplicationUsageStatistics","false"), @("BluetoothPrinters","false"), @("WiFiPrinters","false"), @("Contacts","false"),
@("BarcodeScanning","false"), @("BackgroundAudioRecording","false"), @("Calendars","false"), @("PushNotifications","false"), @("LocalNotifications","false"),
@("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"), @("InAppPurchases","false"), @("PersonalComputerFileExchange","false"), @("Ads","false"),
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"), @("NumberDialing","false"), @("CallProcessing","false"), @("CallLog","false"),
@("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false") @("AutoSendSMS","false"), @("ReceiveSMS","false"), @("SMSLog","false"),
) @("Camera","false"), @("Microphone","false"), @("MusicLibrary","false"),
@("PictureAndVideoLibraries","false"), @("AudioPlaybackAndVibration","false"),
$mobileXml = "" @("BackgroundAudioPlaybackAndVibration","false"), @("InstallPackages","false"),
foreach ($mf in $mobileFuncs) { @("OSBackup","true"), @("ApplicationUsageStatistics","false"),
$mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>" @("BarcodeScanning","false"), @("BackgroundAudioRecording","false"),
} @("AllFilesAccess","false"), @("Videoconferences","false"), @("NFC","false"),
@("DocumentScanning","false"), @("SpeechToText","false"), @("Geofences","false"),
# --- Synonym XML --- @("IncomingShareRequests","false"), @("AllIncomingShareRequestsTypesProcessing","false")
$synonymXml = "" )
if ($Synonym) { # TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t" # последней в списке. На младших форматах платформа её не пишет.
} if ($is221) { $mobileFuncs += ,@("TextToSpeech","false") }
# --- Optional properties --- $mobileXml = ""
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } foreach ($mf in $mobileFuncs) {
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" } $mobileXml += "`r`n`t`t`t`t<app:functionality>`r`n`t`t`t`t`t<app:functionality>$($mf[0])</app:functionality>`r`n`t`t`t`t`t<app:use>$($mf[1])</app:use>`r`n`t`t`t`t</app:functionality>"
}
# --- Configuration.xml ---
$cfgXml = @" # --- Synonym XML ---
<?xml version="1.0" encoding="UTF-8"?> $synonymXml = ""
<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"> if ($Synonym) {
<Configuration uuid="$uuidCfg"> $synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
<InternalInfo> }
<xr:ContainedObject>
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId> # --- Optional properties ---
<xr:ObjectId>$co1</xr:ObjectId> # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
</xr:ContainedObject> # пишет <Vendor/>, а не <Vendor></Vendor>.
<xr:ContainedObject> $vendorEl = if ($Vendor) { "<Vendor>$([System.Security.SecurityElement]::Escape($Vendor))</Vendor>" } else { "<Vendor/>" }
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId> $versionEl = if ($Version) { "<Version>$([System.Security.SecurityElement]::Escape($Version))</Version>" } else { "<Version/>" }
<xr:ObjectId>$co2</xr:ObjectId>
</xr:ContainedObject> # --- Свойства и пространство имён формата 2.21 (платформа 8.5) ---
<xr:ContainedObject> # Значения и ПОЗИЦИИ сняты с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники,
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId> # выгруженные с 8.3.27 и с 8.5.1, различаются ровно этим. Порядок важен — вставки идут
<xr:ObjectId>$co3</xr:ObjectId> # на своё место, а не в конец.
</xr:ContainedObject> $nl = "`r`n"
<xr:ContainedObject> $f221AuxForms = ""; $f221WindowVariant = ""; $f221OpenVariant = ""; $f221Captions = ""; $f221Migration = ""
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId> $palNs = ""
<xr:ObjectId>$co4</xr:ObjectId> if ($is221) {
</xr:ContainedObject> $palNs = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
<xr:ContainedObject> # Скобки вокруг -join обязательны: без них `$nl + (массив) -join $nl` разбирается как
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId> # `($nl + массив) -join $nl`, массив склеивается пробелами и все теги уезжают в одну строку.
<xr:ObjectId>$co5</xr:ObjectId> $f221AuxForms = $nl + ((@(
</xr:ContainedObject> "<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
<xr:ContainedObject> "<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId> "<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
<xr:ObjectId>$co6</xr:ObjectId> "<AuxiliaryCollaborationSystemUsersChoiceForm/>"
</xr:ContainedObject> ) | ForEach-Object { "`t`t`t$_" }) -join $nl)
<xr:ContainedObject> $f221WindowVariant = $nl + "`t`t`t<MainClientApplicationWindowInterfaceVariant>NavigationLeft</MainClientApplicationWindowInterfaceVariant>" +
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId> $nl + "`t`t`t<ClientApplicationTheme>Auto</ClientApplicationTheme>"
<xr:ObjectId>$co7</xr:ObjectId> $f221OpenVariant = $nl + "`t`t`t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs</ClientApplicationWindowsOpenVariant>"
</xr:ContainedObject> $f221Captions = $nl + "`t`t`t<Caption/>" + $nl + "`t`t`t<ShortCaption/>"
</InternalInfo> $f221Migration = $nl + "`t`t`t<Version85InterfaceMigrationMode>DontUse</Version85InterfaceMigrationMode>"
<Properties> }
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<Synonym>$synonymXml</Synonym> # --- Configuration.xml ---
<Comment/> $cfgXml = @"
<NamePrefix/> <?xml version="1.0" encoding="UTF-8"?>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode> <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"$palNs 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">
<DefaultRunMode>ManagedApplication</DefaultRunMode> <Configuration uuid="$uuidCfg">
<UsePurposes> <InternalInfo>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <xr:ContainedObject>
</UsePurposes> <xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<ScriptVariant>Russian</ScriptVariant> <xr:ObjectId>$co1</xr:ObjectId>
<DefaultRoles/> </xr:ContainedObject>
<Vendor>$vendorXml</Vendor> <xr:ContainedObject>
<Version>$versionXml</Version> <xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<UpdateCatalogAddress/> <xr:ObjectId>$co2</xr:ObjectId>
<IncludeHelpInContents>false</IncludeHelpInContents> </xr:ContainedObject>
<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> <xr:ContainedObject>
<UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication> <xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<AdditionalFullTextSearchDictionaries/> <xr:ObjectId>$co3</xr:ObjectId>
<CommonSettingsStorage/> </xr:ContainedObject>
<ReportsUserSettingsStorage/> <xr:ContainedObject>
<ReportsVariantsStorage/> <xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<FormDataSettingsStorage/> <xr:ObjectId>$co4</xr:ObjectId>
<DynamicListsUserSettingsStorage/> </xr:ContainedObject>
<URLExternalDataStorage/> <xr:ContainedObject>
<Content/> <xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<DefaultReportForm/> <xr:ObjectId>$co5</xr:ObjectId>
<DefaultReportVariantForm/> </xr:ContainedObject>
<DefaultReportSettingsForm/> <xr:ContainedObject>
<DefaultReportAppearanceTemplate/> <xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
<DefaultDynamicListSettingsForm/> <xr:ObjectId>$co6</xr:ObjectId>
<DefaultSearchForm/> </xr:ContainedObject>
<DefaultDataHistoryChangeHistoryForm/> <xr:ContainedObject>
<DefaultDataHistoryVersionDataForm/> <xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
<DefaultDataHistoryVersionDifferencesForm/> <xr:ObjectId>$co7</xr:ObjectId>
<DefaultCollaborationSystemUsersChoiceForm/> </xr:ContainedObject>
<RequiredMobileApplicationPermissions/> </InternalInfo>
<UsedMobileApplicationFunctionalities>$mobileXml <Properties>
</UsedMobileApplicationFunctionalities> <Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<StandaloneConfigurationRestrictionRoles/> <Synonym>$synonymXml</Synonym>
<MobileApplicationURLs/> <Comment/>
<AllowedIncomingShareRequestTypes/> <NamePrefix/>
<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode> <ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
<DefaultInterface/> <DefaultRunMode>ManagedApplication</DefaultRunMode>
<DefaultStyle/> <UsePurposes>
<DefaultLanguage>Language.Русский</DefaultLanguage> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<BriefInformation/> </UsePurposes>
<DetailedInformation/> <ScriptVariant>Russian</ScriptVariant>
<Copyright/> <DefaultRoles/>
<VendorInformationAddress/> $vendorEl
<ConfigurationInformationAddress/> $versionEl
<DataLockControlMode>Managed</DataLockControlMode> <UpdateCatalogAddress/>
<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode> <IncludeHelpInContents>false</IncludeHelpInContents>
<ModalityUseMode>DontUse</ModalityUseMode> <UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> <UseOrdinaryFormInManagedApplication>false</UseOrdinaryFormInManagedApplication>
<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode> <AdditionalFullTextSearchDictionaries/>
<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> <CommonSettingsStorage/>
<CompatibilityMode>$CompatibilityMode</CompatibilityMode> <ReportsUserSettingsStorage/>
<DefaultConstantsForm/> <ReportsVariantsStorage/>
</Properties> <FormDataSettingsStorage/>
<ChildObjects> <DynamicListsUserSettingsStorage/>
<Language>Русский</Language> <URLExternalDataStorage/>
</ChildObjects> <Content/>
</Configuration> <DefaultReportForm/>
</MetaDataObject> <DefaultReportVariantForm/>
"@ <DefaultReportSettingsForm/>
<DefaultReportAppearanceTemplate/>
# --- Languages/Русский.xml --- <DefaultDynamicListSettingsForm/>
$langXml = @" <DefaultSearchForm/>
<?xml version="1.0" encoding="UTF-8"?> <DefaultDataHistoryChangeHistoryForm/>
<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"> <DefaultDataHistoryVersionDataForm/>
<Language uuid="$uuidLang"> <DefaultDataHistoryVersionDifferencesForm/>
<Properties> <DefaultCollaborationSystemUsersChoiceForm/>$f221AuxForms
<Name>Русский</Name> <RequiredMobileApplicationPermissions/>
<Synonym> <UsedMobileApplicationFunctionalities>$mobileXml
<v8:item> </UsedMobileApplicationFunctionalities>
<v8:lang>ru</v8:lang> <StandaloneConfigurationRestrictionRoles/>
<v8:content>Русский</v8:content> <MobileApplicationURLs/>
</v8:item> <AllowedIncomingShareRequestTypes/>$f221WindowVariant
</Synonym> <MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>$f221OpenVariant
<Comment/> <DefaultInterface/>$f221Captions
<LanguageCode>ru</LanguageCode> <DefaultStyle/>
</Properties> <DefaultLanguage>Language.Русский</DefaultLanguage>
</Language> <BriefInformation/>
</MetaDataObject> <DetailedInformation/>
"@ <Copyright/>
<VendorInformationAddress/>
# --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) --- <ConfigurationInformationAddress/>
# Open panel on top, Sections panel on left; Functions/Favorites/History declared <DataLockControlMode>Managed</DataLockControlMode>
# via panelDef but not placed by default. Without this file the web client renders <ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
# section icons without labels (icon-only mode). <ModalityUseMode>DontUse</ModalityUseMode>
$openPanelInst = [guid]::NewGuid().ToString() <SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
$sectionsPanelInst = [guid]::NewGuid().ToString() <InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>$f221Migration
$caiXml = @" <DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
<?xml version="1.0" encoding="UTF-8"?> <CompatibilityMode>$CompatibilityMode</CompatibilityMode>
<ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter"> <DefaultConstantsForm/>
<top> </Properties>
<panel id="$openPanelInst"> <ChildObjects>
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid> <Language>Русский</Language>
</panel> </ChildObjects>
</top> </Configuration>
<left> </MetaDataObject>
<panel id="$sectionsPanelInst"> "@
<uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
</panel> # --- Languages/Русский.xml ---
</left> $langXml = @"
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/> <?xml version="1.0" encoding="UTF-8"?>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/> <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"$palNs 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">
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/> <Language uuid="$uuidLang">
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/> <Properties>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/> <Name>Русский</Name>
</ClientApplicationInterface> <Synonym>
"@ <v8:item>
<v8:lang>ru</v8:lang>
# --- Create directories --- <v8:content>Русский</v8:content>
if (-not (Test-Path $OutputDir)) { </v8:item>
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null </Synonym>
} <Comment/>
$langDir = Join-Path $OutputDir "Languages" <LanguageCode>ru</LanguageCode>
if (-not (Test-Path $langDir)) { </Properties>
New-Item -ItemType Directory -Path $langDir -Force | Out-Null </Language>
} </MetaDataObject>
$extDir = Join-Path $OutputDir "Ext" "@
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null # --- Ext/ClientApplicationInterface.xml (default ERP-style panel layout) ---
} # Open panel on top, Sections panel on left; Functions/Favorites/History declared
# via panelDef but not placed by default. Without this file the web client renders
# --- Write files with UTF-8 BOM --- # section icons without labels (icon-only mode).
$enc = New-Object System.Text.UTF8Encoding($true) $openPanelInst = [guid]::NewGuid().ToString()
$sectionsPanelInst = [guid]::NewGuid().ToString()
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) $caiXml = @"
$langFile = Join-Path $langDir "Русский.xml" <?xml version="1.0" encoding="UTF-8"?>
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) <ClientApplicationInterface xmlns="http://v8.1c.ru/8.2/managed-application/core" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="InterfaceLayouter">
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml" <top>
[System.IO.File]::WriteAllText($caiFile, $caiXml, $enc) <panel id="$openPanelInst">
<uuid>cbab57f2-a0f3-4f0a-89ea-4cb19570ab75</uuid>
# --- Output --- </panel>
Write-Host "[OK] Создана конфигурация: $Name" </top>
Write-Host " Каталог: $OutputDir" <left>
Write-Host " Configuration.xml: $cfgFile" <panel id="$sectionsPanelInst">
Write-Host " Languages: $langFile" <uuid>b553047f-c9aa-4157-978d-448ecad24248</uuid>
Write-Host " Ext/CAI: $caiFile" </panel>
</left>
<panelDef id="b553047f-c9aa-4157-978d-448ecad24248"/>
<panelDef id="13322b22-3960-4d68-93a6-fe2dd7f28ca3"/>
<panelDef id="c933ac92-92cd-459d-81cc-e0c8a83ced99"/>
<panelDef id="cbab57f2-a0f3-4f0a-89ea-4cb19570ab75"/>
<panelDef id="b2735bd3-d822-4430-ba59-c9e869693b24"/>
</ClientApplicationInterface>
"@
# --- Create directories ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
$extDir = Join-Path $OutputDir "Ext"
if (-not (Test-Path $extDir)) {
New-Item -ItemType Directory -Path $extDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $cfgFile $cfgXml $enc
$langFile = Join-Path $langDir "Русский.xml"
Write-XmlFile $langFile $langXml $enc
$caiFile = Join-Path $extDir "ClientApplicationInterface.xml"
Write-XmlFile $caiFile $caiXml $enc
# --- Output ---
Write-Host "[OK] Создана конфигурация: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
Write-Host " Ext/CAI: $caiFile"
+63 -16
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cf-init v1.2 — Create empty 1C configuration scaffold # cf-init v1.8 — Create empty 1C configuration scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration.""" """Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, uuid import sys, os, argparse, re, uuid
def esc_xml(s): def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
@@ -14,6 +14,16 @@ def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -24,6 +34,11 @@ def main():
parser.add_argument('-Version', dest='Version', default='') parser.add_argument('-Version', dest='Version', default='')
parser.add_argument('-Vendor', dest='Vendor', default='') parser.add_argument('-Vendor', dest='Vendor', default='')
parser.add_argument('-CompatibilityMode', dest='CompatibilityMode', default='Version8_3_24') 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() args = parser.parse_args()
name = args.Name name = args.Name
@@ -49,6 +64,10 @@ def main():
co = [new_uuid() for _ in range(7)] co = [new_uuid() for _ in range(7)]
# --- Mobile functionalities --- # --- Mobile functionalities ---
# Версия формата как число — по ней ниже включаются вставки 2.21.
_fm = re.match(r'^(\d+)\.(\d+)$', args.FormatVersion)
is_221 = bool(_fm) and int(_fm.group(1)) * 100 + int(_fm.group(2)) >= 221
mobile_funcs = [ mobile_funcs = [
("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"), ("Biometrics","true"), ("Location","false"), ("BackgroundLocation","false"),
("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"), ("BluetoothPrinters","false"), ("WiFiPrinters","false"), ("Contacts","false"),
@@ -65,6 +84,10 @@ def main():
("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"), ("DocumentScanning","false"), ("SpeechToText","false"), ("Geofences","false"),
("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"), ("IncomingShareRequests","false"), ("AllIncomingShareRequestsTypesProcessing","false"),
] ]
# TextToSpeech — возможность мобильного приложения, добавленная форматом 2.21 (8.5),
# последней в списке. На младших форматах платформа её не пишет.
if is_221:
mobile_funcs.append(("TextToSpeech", "false"))
mobile_xml = "" mobile_xml = ""
for func_name, func_use in mobile_funcs: for func_name, func_use in mobile_funcs:
@@ -75,8 +98,10 @@ def main():
if synonym: if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t" synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else "" # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
version_xml = esc_xml(version) if version else "" # пишет <Vendor/>, а не <Vendor></Vendor>.
vendor_el = f"<Vendor>{esc_xml(vendor)}</Vendor>" if vendor else "<Vendor/>"
version_el = f"<Version>{esc_xml(version)}</Version>" if version else "<Version/>"
class_ids = [ class_ids = [
"9cd510cd-abfc-11d4-9434-004095e12fc7", "9cd510cd-abfc-11d4-9434-004095e12fc7",
@@ -88,6 +113,28 @@ def main():
"fb282519-d103-4dd3-bc12-cb271d631dfc", "fb282519-d103-4dd3-bc12-cb271d631dfc",
] ]
# Свойства и пространство имён формата 2.21 (платформа 8.5). Значения и ПОЗИЦИИ сняты
# с выгрузки 8.5.1 (debug/fmt221/dump_v851): те же исходники, выгруженные с 8.3.27 и
# с 8.5.1, различаются ровно этим. Порядок важен — вставки идут на своё место.
pal_ns = ""
f221_aux_forms = f221_window_variant = f221_open_variant = f221_captions = f221_migration = ""
if is_221:
pal_ns = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette"'
f221_aux_forms = "\r\n" + "\r\n".join(
f"\t\t\t{t}" for t in (
"<AuxiliaryReportForm/>", "<AuxiliaryReportVariantForm/>", "<AuxiliaryReportSettingsForm/>",
"<AuxiliaryDynamicListSettingsForm/>", "<AuxiliaryDataHistoryChangeHistoryForm/>",
"<AuxiliaryDataHistoryVersionDataForm/>", "<AuxiliaryDataHistoryVersionDifferencesForm/>",
"<AuxiliaryCollaborationSystemUsersChoiceForm/>"))
f221_window_variant = ("\r\n\t\t\t<MainClientApplicationWindowInterfaceVariant>NavigationLeft"
"</MainClientApplicationWindowInterfaceVariant>"
"\r\n\t\t\t<ClientApplicationTheme>Auto</ClientApplicationTheme>")
f221_open_variant = ("\r\n\t\t\t<ClientApplicationWindowsOpenVariant>OpenDataInDialogs"
"</ClientApplicationWindowsOpenVariant>")
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
f221_migration = ("\r\n\t\t\t<Version85InterfaceMigrationMode>DontUse"
"</Version85InterfaceMigrationMode>")
contained_objects = "" contained_objects = ""
for i in range(7): for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject> contained_objects += f"""\t\t\t<xr:ContainedObject>
@@ -96,7 +143,7 @@ def main():
\t\t\t</xr:ContainedObject>\n""" \t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?> 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"{pal_ns} 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<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo> \t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo> {contained_objects}\t\t</InternalInfo>
@@ -112,8 +159,8 @@ def main():
\t\t\t</UsePurposes> \t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant> \t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles/> \t\t\t<DefaultRoles/>
\t\t\t<Vendor>{vendor_xml}</Vendor> \t\t\t{vendor_el}
\t\t\t<Version>{version_xml}</Version> \t\t\t{version_el}
\t\t\t<UpdateCatalogAddress/> \t\t\t<UpdateCatalogAddress/>
\t\t\t<IncludeHelpInContents>false</IncludeHelpInContents> \t\t\t<IncludeHelpInContents>false</IncludeHelpInContents>
\t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication> \t\t\t<UseManagedFormInOrdinaryApplication>false</UseManagedFormInOrdinaryApplication>
@@ -135,15 +182,15 @@ def main():
\t\t\t<DefaultDataHistoryChangeHistoryForm/> \t\t\t<DefaultDataHistoryChangeHistoryForm/>
\t\t\t<DefaultDataHistoryVersionDataForm/> \t\t\t<DefaultDataHistoryVersionDataForm/>
\t\t\t<DefaultDataHistoryVersionDifferencesForm/> \t\t\t<DefaultDataHistoryVersionDifferencesForm/>
\t\t\t<DefaultCollaborationSystemUsersChoiceForm/> \t\t\t<DefaultCollaborationSystemUsersChoiceForm/>{f221_aux_forms}
\t\t\t<RequiredMobileApplicationPermissions/> \t\t\t<RequiredMobileApplicationPermissions/>
\t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml} \t\t\t<UsedMobileApplicationFunctionalities>{mobile_xml}
\t\t\t</UsedMobileApplicationFunctionalities> \t\t\t</UsedMobileApplicationFunctionalities>
\t\t\t<StandaloneConfigurationRestrictionRoles/> \t\t\t<StandaloneConfigurationRestrictionRoles/>
\t\t\t<MobileApplicationURLs/> \t\t\t<MobileApplicationURLs/>
\t\t\t<AllowedIncomingShareRequestTypes/> \t\t\t<AllowedIncomingShareRequestTypes/>{f221_window_variant}
\t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode> \t\t\t<MainClientApplicationWindowMode>Normal</MainClientApplicationWindowMode>{f221_open_variant}
\t\t\t<DefaultInterface/> \t\t\t<DefaultInterface/>{f221_captions}
\t\t\t<DefaultStyle/> \t\t\t<DefaultStyle/>
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage> \t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/> \t\t\t<BriefInformation/>
@@ -155,7 +202,7 @@ def main():
\t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode> \t\t\t<ObjectAutonumerationMode>NotAutoFree</ObjectAutonumerationMode>
\t\t\t<ModalityUseMode>DontUse</ModalityUseMode> \t\t\t<ModalityUseMode>DontUse</ModalityUseMode>
\t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode> \t\t\t<SynchronousPlatformExtensionAndAddInCallUseMode>DontUse</SynchronousPlatformExtensionAndAddInCallUseMode>
\t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode> \t\t\t<InterfaceCompatibilityMode>TaxiEnableVersion8_2</InterfaceCompatibilityMode>{f221_migration}
\t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode> \t\t\t<DatabaseTablespacesUseMode>DontUse</DatabaseTablespacesUseMode>
\t\t\t<CompatibilityMode>{compat}</CompatibilityMode> \t\t\t<CompatibilityMode>{compat}</CompatibilityMode>
\t\t\t<DefaultConstantsForm/> \t\t\t<DefaultConstantsForm/>
@@ -168,7 +215,7 @@ def main():
# --- Languages/Русский.xml --- # --- Languages/Русский.xml ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?> 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"{pal_ns} 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<Language uuid="{uuid_lang}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>Русский</Name> \t\t\t<Name>Русский</Name>
@@ -217,11 +264,11 @@ def main():
os.makedirs(ext_dir, exist_ok=True) os.makedirs(ext_dir, exist_ok=True)
# --- Write files --- # --- Write files ---
write_utf8_bom(cfg_file, cfg_xml) write_xml_file(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml") lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml) write_xml_file(lang_file, lang_xml)
cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml") cai_file = os.path.join(ext_dir, "ClientApplicationInterface.xml")
write_utf8_bom(cai_file, cai_xml) write_xml_file(cai_file, cai_xml)
print(f"[OK] Создана конфигурация: {name}") print(f"[OK] Создана конфигурация: {name}")
print(f" Каталог: {output_dir}") print(f" Каталог: {output_dir}")
+29 -29
View File
@@ -1,29 +1,29 @@
--- ---
name: cf-validate name: cf-validate
description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности description: Валидация конфигурации 1С. Используй после создания или модификации конфигурации для проверки корректности
argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30] argument-hint: <ConfigPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cf-validate — валидация конфигурации 1С # /cf-validate — валидация конфигурации 1С
Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов. Проверяет Configuration.xml на структурные ошибки: XML well-formedness, InternalInfo, свойства, enum-значения, ChildObjects, DefaultLanguage, файлы языков, каталоги объектов.
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------| |------------|:-----:|---------|-------------------------------------------------|
| ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки | | ConfigPath | да | — | Путь к Configuration.xml или каталогу выгрузки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | | OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-validate.ps1" -ConfigPath "upload/cfempty/Configuration.xml"
``` ```
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages.""" """Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -33,11 +33,11 @@ VALID_CLASS_IDS = [
'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface 'fb282519-d103-4dd3-bc12-cb271d631dfc', # home page / client app interface
] ]
# 44 types in canonical order # 45 types in canonical order
CHILD_OBJECT_TYPES = [ CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style', 'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate', 'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan', 'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference', 'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption', 'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup', 'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
@@ -54,6 +54,7 @@ CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles', 'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles', 'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules', 'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages', 'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences', 'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs', 'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -231,8 +232,9 @@ def main():
version = root.get('version', '') version = root.get('version', '')
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'): elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 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 # Must have Configuration child
cfg_node = None cfg_node = None
+101 -101
View File
@@ -1,101 +1,101 @@
--- ---
name: cfe-borrow name: cfe-borrow
description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации description: Заимствование объектов из конфигурации 1С в расширение (CFE). Используй когда нужно перехватить метод, изменить форму или добавить реквизит к существующему объекту конфигурации
argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute argument-hint: -ExtensionPath <path> -ConfigPath <path> -Object "Catalog.Контрагенты.Form.ФормаЭлемента" -BorrowMainAttribute
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-borrow — Заимствование объектов из конфигурации # /cfe-borrow — Заимствование объектов из конфигурации
Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения. Заимствует объекты из основной конфигурации в расширение. Создаёт XML-файлы с `ObjectBelonging=Adopted` и `ExtendedConfigurationObject`, добавляет запись в ChildObjects расширения.
## Предусловие ## Предусловие
Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`. Расширение должно быть создано (`/cfe-init`) и содержать валидный `Configuration.xml`.
### Авто-определение ConfigPath ### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта 1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) 2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` 3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя 4. Если `configSrc` нет — спроси у пользователя
## Параметры ## Параметры
| Параметр | Описание | | Параметр | Описание |
|----------|----------| |----------|----------|
| `ExtensionPath` | Путь к каталогу расширения (обязат.) | | `ExtensionPath` | Путь к каталогу расширения (обязат.) |
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) | | `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
| `Object` | Что заимствовать (обязат.), batch через `;;` | | `Object` | Что заимствовать (обязат.), batch через `;;` |
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object | | `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
## Формат -Object ## Формат -Object
- `Catalog.Контрагенты` — справочник - `Catalog.Контрагенты` — справочник
- `CommonModule.РаботаСФайлами` — общий модуль - `CommonModule.РаботаСФайлами` — общий модуль
- `Document.РеализацияТоваров` — документ - `Document.РеализацияТоваров` — документ
- `Enum.ВидыОплат` — перечисление - `Enum.ВидыОплат` — перечисление
- `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы) - `Catalog.Контрагенты.Form.ФормаЭлемента` — форма объекта (заимствование формы)
- `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов - `Catalog.X ;; CommonModule.Y ;; Enum.Z` — несколько объектов
Поддерживаются все 44 типа объектов конфигурации. Поддерживаются все 44 типа объектов конфигурации.
### Заимствование форм ### Заимствование форм
Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически. Формат `Тип.Имя.Form.ИмяФормы` заимствует форму конкретного объекта. Если родительский объект ещё не заимствован — он будет заимствован автоматически.
Создаётся: Создаётся:
1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed` 1. **Метаданные формы**`Forms/ИмяФормы.xml` с `ObjectBelonging=Adopted`, `FormType=Managed`
2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние) 2. **Form.xml**`Forms/ИмяФормы/Ext/Form.xml` с копией исходной формы + `<BaseForm>` (начальное состояние)
3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl` 3. **Module.bsl** — пустой файл `Forms/ИмяФормы/Ext/Form/Module.bsl`
4. **Регистрация**`<Form>` в ChildObjects родительского объекта 4. **Регистрация**`<Form>` в ChildObjects родительского объекта
### Заимствование основного реквизита формы (-BorrowMainAttribute) ### Заимствование основного реквизита формы (-BorrowMainAttribute)
**Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`. **Когда нужно**: пользователь хочет добавить новый реквизит в существующий объект конфигурации и вывести его на заимствованную форму. Без `-BorrowMainAttribute` форма заимствуется "пустой" — только визуальные элементы, без привязки к данным объекта. С `-BorrowMainAttribute` форма сохраняет привязки к реквизитам объекта (DataPath), что позволяет затем добавить на неё новые элементы через `/form-edit`.
**Два режима**: **Два режима**:
- `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев - `Form` (по умолчанию) — заимствует только те реквизиты объекта, которые уже выведены на форму. Оптимальный выбор для большинства случаев
- `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет - `All` — заимствует все реквизиты и табличные части объекта. Используй если планируешь выводить на форму реквизиты, которых на ней ещё нет
**Типовой сценарий** (добавление реквизита + вывод на форму): **Типовой сценарий** (добавление реквизита + вывод на форму):
1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами 1. `/cfe-borrow` с `-BorrowMainAttribute` — заимствовать форму с реквизитами
2. `/meta-edit` — добавить новый реквизит в объект расширения 2. `/meta-edit` — добавить новый реквизит в объект расширения
3. `/form-edit` — вывести реквизит на заимствованную форму 3. `/form-edit` — вывести реквизит на заимствованную форму
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее. **Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Заимствовать один объект # Заимствовать один объект
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты" ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты"
# Заимствовать форму (автоматически заимствует родительский объект) # Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента" ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
# Несколько объектов за раз # Несколько объектов за раз
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат" ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Контрагенты ;; CommonModule.ОбщийМодуль ;; Enum.ВидыОплат"
# Заимствовать форму с основным реквизитом (реквизиты по DataPath формы) # Заимствовать форму с основным реквизитом (реквизиты по DataPath формы)
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute
# Заимствовать форму с ВСЕМИ реквизитами объекта # Заимствовать форму с ВСЕМИ реквизитами объекта
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Object "Catalog.Номенклатура.Form.ФормаЭлемента" -BorrowMainAttribute All
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <ExtensionPath> /cfe-validate <ExtensionPath>
``` ```
File diff suppressed because it is too large Load Diff
+336 -132
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-borrow v1.3 — Borrow objects from configuration into extension (CFE) # cfe-borrow v1.16 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -14,6 +14,36 @@ XR_NS = "http://v8.1c.ru/8.3/xcf/readable"
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance" XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core" V8_NS = "http://v8.1c.ru/8.1/data/core"
# Form data-binding tags (value = attribute path). A binding survives only if its root
# attribute is borrowed into the form's <Attributes>; otherwise it must be stripped or the
# platform rejects the form with "Неверный путь к данным" on load.
FORM_BINDING_DATA_TAGS = ["DataPath", "TitleDataPath", "FooterDataPath", "HeaderDataPath", "MultipleValueDataPath", "MultipleValuePresentDataPath"]
# Picture-path binding tags (value = picture index path, never a data attribute) — always stripped in the skeleton.
FORM_BINDING_PICTURE_TAGS = ["RowPictureDataPath", "MultipleValuePictureDataPath"]
def strip_form_bindings(xml, keep_objekt):
"""Strip data-binding tags whose root attribute isn't borrowed.
keep_objekt=True (BorrowMainAttribute): keep Объект.* data bindings, strip the rest.
keep_objekt=False (default skeleton): strip all bindings. Picture-path tags are always stripped."""
for tag in FORM_BINDING_DATA_TAGS:
if keep_objekt:
xml = re.sub(rf'\s*<{tag}>(?!Объект\.)[^<]*</{tag}>', '', xml)
else:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
for tag in FORM_BINDING_PICTURE_TAGS:
xml = re.sub(rf'\s*<{tag}>[^<]*</{tag}>', '', xml)
return xml
def decode_numeric_entities(s):
"""lxml emits numeric character refs (&#xNNNN;) for non-ASCII in some self-closed
elements where the PowerShell port writes literal characters. Normalize numeric refs
back to literal so PS↔PY output matches. Named entities (&amp; &lt; ...) are left intact."""
s = re.sub(r'&#x([0-9A-Fa-f]+);', lambda m: chr(int(m.group(1), 16)), s)
s = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), s)
return s
def localname(el): def localname(el):
return etree.QName(el.tag).localname return etree.QName(el.tag).localname
@@ -224,13 +254,36 @@ GENERATED_TYPES = {
"DefinedType": [ "DefinedType": [
{"prefix": "DefinedType", "category": "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 = [ TYPES_WITH_CHILD_OBJECTS = [
"Catalog", "Document", "ExchangePlan", "ChartOfAccounts", "Catalog", "Document", "ExchangePlan", "ChartOfAccounts",
"ChartOfCharacteristicTypes", "ChartOfCalculationTypes", "ChartOfCharacteristicTypes", "ChartOfCalculationTypes",
"BusinessProcess", "Task", "Enum", "BusinessProcess", "Task", "Enum",
"InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister", "InformationRegister", "AccumulationRegister", "AccountingRegister", "CalculationRegister",
"DataProcessor", "Report", "DocumentJournal", "FilterCriterion", "SettingsStorage",
"Sequence", "HTTPService", "WebService", "IntegrationService", "Subsystem",
] ]
COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"] COMMON_MODULE_PROPS = ["Global", "ClientManagedApplication", "Server", "ExternalConnection", "ClientOrdinaryApplication", "ServerCall"]
@@ -270,6 +323,23 @@ def detect_format_version(d):
return "2.17" return "2.17"
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def apply_pal_ns(format_version):
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
дописать в конец нельзя."""
global XMLNS_DECL
if format_rank(format_version) >= 221:
XMLNS_DECL = XMLNS_DECL.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
def get_child_indent(container): def get_child_indent(container):
if container.text and "\n" in container.text: if container.text and "\n" in container.text:
after_nl = container.text.rsplit("\n", 1)[-1] after_nl = container.text.rsplit("\n", 1)[-1]
@@ -319,21 +389,77 @@ def expand_self_closing(container, parent_indent):
container.text = "\r\n" + parent_indent container.text = "\r\n" + parent_indent
def save_xml_bom(tree, path): def _detect_xml_style(path):
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") """Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') финальный перенос. None → файл новый (сохранить текущее поведение)."""
if not xml_bytes.endswith(b"\n"): 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) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
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"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
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: 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) f.write(xml_bytes)
def save_text_bom(path, text): def save_text_bom(path, text):
with open(path, "w", encoding="utf-8-sig") as fh: """Записать текст как есть, ничего не нормализуя.
Для файлов, которые мы ПРАВИМ: переводы строк уже пришли из самого файла, и
менять их нельзя (контракт #44/#46/#47). newline="" обязателен — без него
текстовый режим Python дал бы CRLF на Windows и LF на macOS, то есть вывод
навыка зависел бы от ОС.
"""
with open(path, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(text) fh.write(text)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
Только для файлов, которые СОЗДАЁМ: правка существующего наследует его стиль.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
save_text_bom(path, text)
def new_guid(): def new_guid():
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -382,6 +508,7 @@ def main():
cfg_dir = os.path.dirname(cfg_resolved) cfg_dir = os.path.dirname(cfg_resolved)
format_version = detect_format_version(ext_dir) format_version = detect_format_version(ext_dir)
apply_pal_ns(format_version)
# --- 2. Load extension Configuration.xml --- # --- 2. Load extension Configuration.xml ---
xml_parser = etree.XMLParser(remove_blank_text=False) xml_parser = etree.XMLParser(remove_blank_text=False)
@@ -462,6 +589,16 @@ def main():
prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}") prop_node = props_node.find(f"{{{MD_NS}}}{prop_name}")
if prop_node is not None: if prop_node is not None:
src_props[prop_name] = (prop_node.text or "").strip() src_props[prop_name] = (prop_node.text or "").strip()
# DefinedType: carry the <Type> definition. A type alias is meaningless as a bare shell —
# the platform needs its underlying type (e.g. to know a column is a summable Number for totals).
if type_name == "DefinedType":
type_node = props_node.find(f"{{{MD_NS}}}Type")
if type_node is not None:
type_xml = etree.tostring(type_node, encoding="unicode")
src_props["__TypeXml"] = re.sub(r'\s+xmlns(?::\w+)?="[^"]*"', '', type_xml)
# 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} return {"Uuid": src_uuid, "Properties": src_props, "Element": src_el}
@@ -533,9 +670,13 @@ def main():
prop_val = source_props.get(prop_name, "false") prop_val = source_props.get(prop_name, "false")
lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>") lines.append(f"\t\t\t<{prop_name}>{prop_val}</{prop_name}>")
# DefinedType: emit the carried <Type> definition (needed for the alias to resolve, e.g. totals)
if type_name == "DefinedType" and "__TypeXml" in source_props:
lines.append(f"\t\t\t{source_props['__TypeXml']}")
lines.append("\t\t</Properties>") 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("\t\t<ChildObjects/>")
lines.append(f"\t</{type_name}>") lines.append(f"\t</{type_name}>")
@@ -644,7 +785,26 @@ def main():
first_level = {} first_level = {}
deep_paths = [] deep_paths = []
for m in re.finditer(r'<DataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</DataPath>', content): # Scan every data-binding tag (DataPath/TitleDataPath/FooterDataPath/HeaderDataPath/MultipleValue*)
# for Объект.* references — picture-path tags carry picture indices, not data attributes.
for tag in FORM_BINDING_DATA_TAGS:
for m in re.finditer(r'<' + tag + r'>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</' + tag + r'>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
if len(segments) >= 2:
seg1 = segments[1]
if seg1 in STANDARD_FIELDS:
continue
seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also scan <Field>Объект.X</Field> — object attributes referenced by filter/conditional-appearance
# fields (and dynamic lists), not via a *DataPath binding (e.g. УдалитьЮрФизЛицо). Designer borrows these too.
for m in re.finditer(r'<Field>[^<]*\bОбъект\.(\w+(?:\.\w+)*)</Field>', content):
path = m.group(1) path = m.group(1)
segments = path.split(".") segments = path.split(".")
seg0 = segments[0] seg0 = segments[0]
@@ -655,22 +815,14 @@ def main():
seg1 = segments[1] seg1 = segments[1]
if seg1 in STANDARD_FIELDS: if seg1 in STANDARD_FIELDS:
continue continue
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1}) seg2 = segments[2] if len(segments) >= 3 else None
deep_paths.append({"ObjectAttr": seg0, "SubAttr": seg1, "SubSubAttr": seg2})
# Also collect from TitleDataPath
for m in re.finditer(r'<TitleDataPath>[^<]*\b\u041e\u0431\u044a\u0435\u043a\u0442\.(\w+(?:\.\w+)*)</TitleDataPath>', content):
path = m.group(1)
segments = path.split(".")
seg0 = segments[0]
if seg0 in STANDARD_FIELDS:
continue
first_level[seg0] = True
# Deduplicate deep paths # Deduplicate deep paths
seen = set() seen = set()
unique_deep = [] unique_deep = []
for dp in deep_paths: for dp in deep_paths:
key = f"{dp['ObjectAttr']}.{dp['SubAttr']}" key = f"{dp['ObjectAttr']}.{dp['SubAttr']}.{dp.get('SubSubAttr')}"
if key not in seen: if key not in seen:
seen.add(key) seen.add(key)
unique_deep.append(dp) unique_deep.append(dp)
@@ -883,7 +1035,9 @@ def main():
warn(f"Cannot merge attributes: {obj_file} not found") warn(f"Cannot merge attributes: {obj_file} not found")
return return
with open(obj_file, "r", encoding="utf-8-sig") as fh: # newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
# и файл будет переписан в LF.
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
obj_content = fh.read() obj_content = fh.read()
# Collect existing attribute names for dedup (text-based) # Collect existing attribute names for dedup (text-based)
@@ -941,26 +1095,42 @@ def main():
# Step 3: Build the adopted content and insert into main object XML # Step 3: Build the adopted content and insert into main object XML
obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml") obj_file = os.path.join(ext_dir, dir_name, f"{obj_name}.xml")
# Generate full object XML with attributes and TS # Read existing object XML (needed for dedup + enrichment)
content_parts = [] # newline="" => без трансляции: иначе CRLF молча схлопнется в LF при чтении
for attr in src_attrs: # и файл будет переписан в LF.
attr_xml = build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t") with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
content_parts.append(attr_xml)
for ts in src_ts:
ts_xml = build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t")
content_parts.append(ts_xml)
adopted_content = "\n".join(content_parts).rstrip()
# Read existing object XML and inject
with open(obj_file, "r", encoding="utf-8-sig") as fh:
obj_content = fh.read() obj_content = fh.read()
# Inject extra properties after ExtendedConfigurationObject # Dedup: skip attributes/TS already present in object's ChildObjects (idempotent re-borrow)
existing_child_names = set()
m_co = re.search(r'(?s)<ChildObjects>(.*?)</ChildObjects>', obj_content)
if m_co:
for nm in re.findall(r'<Name>(\w+)</Name>', m_co.group(1)):
existing_child_names.add(nm)
insert_attrs = [a for a in src_attrs if a["Name"] not in existing_child_names]
insert_ts = [t for t in src_ts if t["Name"] not in existing_child_names]
# Generate full object XML with attributes and TS
content_parts = []
for attr in insert_attrs:
content_parts.append(build_adopted_attribute_xml(attr["Name"], attr["Uuid"], attr["TypeXml"], "\t\t\t"))
for ts in insert_ts:
content_parts.append(build_adopted_tabular_section_xml(ts["Name"], ts["Uuid"], ts["GeneratedTypes"], ts["Attributes"], "\t\t\t"))
adopted_content = "\n".join(content_parts).rstrip()
# Inject extra properties into the object's OWN Properties only — idempotent and anchored to the
# first ExtendedConfigurationObject (the object's). On re-borrow, adopted attributes each have their
# own ExtendedConfigurationObject; a global replace would push object props inside every <Attribute>.
if extra_props: if extra_props:
m_props = re.search(r'(?s)<Properties>(.*?)</Properties>', obj_content)
obj_props_block = m_props.group(1) if m_props else ""
props_xml = "" props_xml = ""
for p_name, p_val in extra_props.items(): for p_name, p_val in extra_props.items():
if f"<{p_name}>" in obj_props_block:
continue
props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>" props_xml += f"\r\n\t\t\t<{p_name}>{p_val}</{p_name}>"
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}") if props_xml:
obj_content = obj_content.replace("</ExtendedConfigurationObject>", f"</ExtendedConfigurationObject>{props_xml}", 1)
# Replace empty ChildObjects with adopted content # Replace empty ChildObjects with adopted content
if adopted_content: if adopted_content:
@@ -1005,86 +1175,100 @@ def main():
target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]]) target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[rt["TypeName"]])
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml") target_file = os.path.join(target_dir, f"{rt['ObjName']}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects(rt["TypeName"], rt["ObjName"]) add_to_child_objects(rt["TypeName"], rt["ObjName"])
borrowed_files.append(target_file) borrowed_files.append(target_file)
info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}") info(f" Auto-borrowed: {rt['TypeName']}.{rt['ObjName']}")
# Step 5: Handle deep paths (Form mode only) # Step 5: Handle deep paths (Form mode only)
if mode == "Form" and deep_paths: if mode == "Form" and deep_paths:
# Filter out deep paths where ObjectAttr is a TabularSection # Top-level ref deep paths: Объект.<Ref>.<Sub> — borrow the ref attribute's catalog with the sub-attribute
real_deep = [dp for dp in deep_paths if dp["ObjectAttr"] not in ts_names] deep_by_attr = {}
for dp in deep_paths:
if real_deep: if dp["ObjectAttr"] in ts_names:
info(f" Processing {len(real_deep)} deep path(s)...") continue
deep_by_attr.setdefault(dp["ObjectAttr"], [])
# Group by ObjectAttr -> target catalog if dp["SubAttr"] not in deep_by_attr[dp["ObjectAttr"]]:
deep_by_attr = {}
for dp in real_deep:
if dp["ObjectAttr"] not in deep_by_attr:
deep_by_attr[dp["ObjectAttr"]] = []
deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"]) deep_by_attr[dp["ObjectAttr"]].append(dp["SubAttr"])
if deep_by_attr:
info(f" Processing {len(deep_by_attr)} deep path attribute(s)...")
for attr_name, sub_attr_names in deep_by_attr.items(): for attr_name, sub_attr_names in deep_by_attr.items():
# Find the attribute's type to determine target catalog attr_info = next((a for a in src_attrs if a["Name"] == attr_name), None)
attr_info = None
for a in src_attrs:
if a["Name"] == attr_name:
attr_info = a
break
if not attr_info: if not attr_info:
continue continue
# Extract catalog name from type: cfg:CatalogRef.XXX
cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"]) cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', attr_info["TypeXml"])
if not cat_match: if not cat_match:
continue continue
borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
target_type_name = cat_match.group(1) # Tabular-section deep paths: Объект.<ТЧ>.<Колонка>.<Sub> — borrow the column's catalog with the sub-attribute
target_obj_name = cat_match.group(2) ts_deep_by_col = {}
for dp in deep_paths:
# Ensure target is borrowed if dp["ObjectAttr"] not in ts_names:
if not test_object_borrowed(target_type_name, target_obj_name): continue
t_src = read_source_object(target_type_name, target_obj_name) if not dp.get("SubSubAttr"):
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"]) continue
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name]) if dp["SubSubAttr"] in STANDARD_FIELDS:
os.makedirs(t_target_dir, exist_ok=True) continue
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml") k = (dp["ObjectAttr"], dp["SubAttr"])
save_text_bom(t_target_file, t_borrowed_xml) ts_deep_by_col.setdefault(k, [])
add_to_child_objects(target_type_name, target_obj_name) if dp["SubSubAttr"] not in ts_deep_by_col[k]:
borrowed_files.append(t_target_file) ts_deep_by_col[k].append(dp["SubSubAttr"])
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}") if ts_deep_by_col:
info(f" Processing {len(ts_deep_by_col)} tabular-section deep path(s)...")
# Resolve sub-attributes in target catalog for (ts_name, col_name), sub_attr_names in ts_deep_by_col.items():
sub_names = {sn: True for sn in sub_attr_names} ts_info = next((t for t in src_ts if t["Name"] == ts_name), None)
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names) if not ts_info:
continue
if sub_resolved["Attributes"]: col_info = next((c for c in ts_info["Attributes"] if c["Name"] == col_name), None)
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"]) if not col_info:
continue
# Collect and borrow ref types from deep attributes cat_match = re.search(r'cfg:(\w+)Ref\.(\w+)', col_info["TypeXml"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]] if not cat_match:
sub_ref_types = collect_reference_types(sub_type_xmls) continue
for srt in sub_ref_types: borrow_deep_target_attrs(cat_match.group(1), cat_match.group(2), sub_attr_names)
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
save_text_bom(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
info(" Main attribute borrowing complete") info(" Main attribute borrowing complete")
def borrow_deep_target_attrs(target_type_name, target_obj_name, sub_attr_names):
# Borrow a deep-path target catalog together with the referenced sub-attributes, for both
# Объект.<Ref>.<Sub> and Объект.<ТЧ>.<Колонка>.<Sub>. Mirrors Designer: the referenced catalog
# is adopted WITH the sub-attributes the form shows, else the platform rejects the deep DataPath.
if not test_object_borrowed(target_type_name, target_obj_name):
t_src = read_source_object(target_type_name, target_obj_name)
t_borrowed_xml = build_borrowed_object_xml(target_type_name, target_obj_name, t_src["Uuid"], t_src["Properties"])
t_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[target_type_name])
os.makedirs(t_target_dir, exist_ok=True)
t_target_file = os.path.join(t_target_dir, f"{target_obj_name}.xml")
write_xml_file(t_target_file, t_borrowed_xml)
add_to_child_objects(target_type_name, target_obj_name)
borrowed_files.append(t_target_file)
info(f" Auto-borrowed for deep path: {target_type_name}.{target_obj_name}")
sub_names = {sn: True for sn in sub_attr_names}
sub_resolved = resolve_source_attributes(target_type_name, target_obj_name, sub_names)
if sub_resolved["Attributes"]:
merge_attributes_into_object(target_type_name, target_obj_name, sub_resolved["Attributes"])
sub_type_xmls = [sa["TypeXml"] for sa in sub_resolved["Attributes"]]
sub_ref_types = collect_reference_types(sub_type_xmls)
for srt in sub_ref_types:
if srt["TypeName"] not in CHILD_TYPE_DIR_MAP:
continue
if test_object_borrowed(srt["TypeName"], srt["ObjName"]):
continue
s_src_file = os.path.join(cfg_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]], f"{srt['ObjName']}.xml")
if not os.path.isfile(s_src_file):
continue
s_src = read_source_object(srt["TypeName"], srt["ObjName"])
s_borrowed_xml = build_borrowed_object_xml(srt["TypeName"], srt["ObjName"], s_src["Uuid"], s_src["Properties"])
s_target_dir = os.path.join(ext_dir, CHILD_TYPE_DIR_MAP[srt["TypeName"]])
os.makedirs(s_target_dir, exist_ok=True)
s_target_file = os.path.join(s_target_dir, f"{srt['ObjName']}.xml")
write_xml_file(s_target_file, s_borrowed_xml)
add_to_child_objects(srt["TypeName"], srt["ObjName"])
borrowed_files.append(s_target_file)
info(f" Auto-borrowed (deep): {srt['TypeName']}.{srt['ObjName']}")
def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False): def borrow_form(type_name, obj_name, form_name, borrow_main_attr=False):
dir_name = CHILD_TYPE_DIR_MAP[type_name] dir_name = CHILD_TYPE_DIR_MAP[type_name]
@@ -1100,8 +1284,22 @@ def main():
with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh: with open(src_form_xml_path, "r", encoding="utf-8-sig") as fh:
src_form_content = fh.read() src_form_content = fh.read()
# 3. Generate form metadata XML # 3. Generate form metadata XML.
new_form_uuid = new_guid() # If the wrapper was already borrowed, reuse its uuid so re-borrow is idempotent
# (regenerating it would churn the form's identity on every rerun).
existing_wrapper = os.path.join(ext_dir, dir_name, obj_name, "Forms", f"{form_name}.xml")
new_form_uuid = ""
if os.path.isfile(existing_wrapper):
try:
existing_root = etree.parse(existing_wrapper).getroot()
for c in existing_root:
if isinstance(c.tag, str) and localname(c) == "Form":
new_form_uuid = c.get("uuid", "") or ""
break
except Exception:
new_form_uuid = ""
if not new_form_uuid:
new_form_uuid = new_guid()
form_meta_lines = [ form_meta_lines = [
'<?xml version="1.0" encoding="UTF-8"?>', '<?xml version="1.0" encoding="UTF-8"?>',
f'<MetaDataObject {XMLNS_DECL} version="{format_version}">', f'<MetaDataObject {XMLNS_DECL} version="{format_version}">',
@@ -1123,7 +1321,7 @@ def main():
os.makedirs(form_meta_dir, exist_ok=True) os.makedirs(form_meta_dir, exist_ok=True)
form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml") form_meta_file = os.path.join(form_meta_dir, f"{form_name}.xml")
save_text_bom(form_meta_file, "\n".join(form_meta_lines)) write_xml_file(form_meta_file, "\n".join(form_meta_lines))
info(f" Created: {form_meta_file}") info(f" Created: {form_meta_file}")
# 5. Generate Form.xml with BaseForm # 5. Generate Form.xml with BaseForm
@@ -1131,7 +1329,10 @@ def main():
src_form_tree = etree.parse(src_form_xml_path, src_form_parser) src_form_tree = etree.parse(src_form_xml_path, src_form_parser)
src_form_el = src_form_tree.getroot() src_form_el = src_form_tree.getroot()
form_version = src_form_el.get("version", format_version) # Borrowed form uses the extension's format version (not the source form's) — keeps the
# extension uniform; otherwise the platform rejects the import on a version mismatch
# (e.g. a 2.13 form inside a 2.17 extension). The platform upgrades the form to the root version.
form_version = format_version
src_auto_cmd = None src_auto_cmd = None
form_props = [] form_props = []
@@ -1149,25 +1350,21 @@ def main():
continue continue
if not reached_visual: if not reached_visual:
# Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.) # Form-level properties before AutoCommandBar (WindowOpeningMode, AutoFillCheck, etc.)
form_props.append(etree.tostring(fc, encoding="unicode")) form_props.append(decode_numeric_entities(etree.tostring(fc, encoding="unicode")))
ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"') ns_strip_pattern = re.compile(r'\s+xmlns(?::\w+)?="[^"]*"')
# AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false # AutoCommandBar: keep ChildItems (buttons with CommandName->0), Autofill->false
auto_cmd_xml = "" auto_cmd_xml = ""
if src_auto_cmd is not None: if src_auto_cmd is not None:
auto_cmd_xml = etree.tostring(src_auto_cmd, encoding="unicode") auto_cmd_xml = decode_numeric_entities(etree.tostring(src_auto_cmd, encoding="unicode"))
auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml) auto_cmd_xml = ns_strip_pattern.sub("", auto_cmd_xml)
auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml) auto_cmd_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', auto_cmd_xml)
auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>') auto_cmd_xml = auto_cmd_xml.replace('<Autofill>true</Autofill>', '<Autofill>false</Autofill>')
# Strip ExcludedCommand (references to standard commands invalid in extension) # Strip ExcludedCommand (references to standard commands invalid in extension)
auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml) auto_cmd_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', auto_cmd_xml)
# Strip DataPath in AutoCommandBar buttons # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: auto_cmd_xml = strip_form_bindings(auto_cmd_xml, borrow_main_attr)
# Keep only Объект.* DataPaths
auto_cmd_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', auto_cmd_xml)
else:
auto_cmd_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', auto_cmd_xml)
# ChildItems: copy full tree, clean up base-config references # ChildItems: copy full tree, clean up base-config references
child_items_xml = "" child_items_xml = ""
@@ -1178,20 +1375,12 @@ def main():
break break
if src_child_items is not None: if src_child_items is not None:
child_items_xml = etree.tostring(src_child_items, encoding="unicode") child_items_xml = decode_numeric_entities(etree.tostring(src_child_items, encoding="unicode"))
child_items_xml = ns_strip_pattern.sub("", child_items_xml) child_items_xml = ns_strip_pattern.sub("", child_items_xml)
# Replace all CommandName values with 0 # Replace all CommandName values with 0
child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml) child_items_xml = re.sub(r'<CommandName>[^<]*</CommandName>', '<CommandName>0</CommandName>', child_items_xml)
# Strip DataPath / TitleDataPath / RowPictureDataPath # Strip data-binding tags whose root attribute isn't borrowed
if borrow_main_attr: child_items_xml = strip_form_bindings(child_items_xml, borrow_main_attr)
# Keep only Объект.* DataPaths — strip form-attribute DataPaths (not borrowed)
child_items_xml = re.sub(r'\s*<DataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>(?!\u041e\u0431\u044a\u0435\u043a\u0442\.)[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
else:
child_items_xml = re.sub(r'\s*<DataPath>[^<]*</DataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<TitleDataPath>[^<]*</TitleDataPath>', '', child_items_xml)
child_items_xml = re.sub(r'\s*<RowPictureDataPath>[^<]*</RowPictureDataPath>', '', child_items_xml)
# Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension) # Strip ExcludedCommand in nested AutoCommandBars (references to standard commands invalid in extension)
child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml) child_items_xml = re.sub(r'\s*<ExcludedCommand>[^<]*</ExcludedCommand>', '', child_items_xml)
# Strip TypeLink blocks with human-readable DataPath (Items.XXX) # Strip TypeLink blocks with human-readable DataPath (Items.XXX)
@@ -1218,7 +1407,7 @@ def main():
target_dir = os.path.join(ext_dir, "CommonPictures") target_dir = os.path.join(ext_dir, "CommonPictures")
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{pic_name}.xml") target_file = os.path.join(target_dir, f"{pic_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects("CommonPicture", pic_name) add_to_child_objects("CommonPicture", pic_name)
auto_borrowed_pics.append(pic_name) auto_borrowed_pics.append(pic_name)
borrowed_files.append(target_file) borrowed_files.append(target_file)
@@ -1267,7 +1456,7 @@ def main():
target_dir = os.path.join(ext_dir, "StyleItems") target_dir = os.path.join(ext_dir, "StyleItems")
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{style_name}.xml") target_file = os.path.join(target_dir, f"{style_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects("StyleItem", style_name) add_to_child_objects("StyleItem", style_name)
borrowed_files.append(target_file) borrowed_files.append(target_file)
info(f" Auto-borrowed: StyleItem.{style_name}") info(f" Auto-borrowed: StyleItem.{style_name}")
@@ -1331,14 +1520,17 @@ def main():
target_dir = os.path.join(ext_dir, "Enums") target_dir = os.path.join(ext_dir, "Enums")
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{enum_name}.xml") target_file = os.path.join(target_dir, f"{enum_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
add_to_child_objects("Enum", enum_name) add_to_child_objects("Enum", enum_name)
borrowed_files.append(target_file) borrowed_files.append(target_file)
info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))") info(f" Auto-borrowed: Enum.{enum_name} (with {len(ev_xmls)} EnumValue(s))")
else: else:
warn(f" Enum.{enum_name} not found in source config") warn(f" Enum.{enum_name} not found in source config")
# Extract the <Form ...> opening tag from source text # Открывающий тег <Form ...> берём из исходной формы — ради её объявлений пространств
# имён, но version подставляем СВОЮ: форма обязана нести версию расширения, иначе
# платформа отвергает импорт (форма 2.13 внутри расширения 2.17). Раньше тег
# копировался целиком, и версия источника молча побеждала.
xml_decl = '<?xml version="1.0" encoding="UTF-8"?>' xml_decl = '<?xml version="1.0" encoding="UTF-8"?>'
form_tag = f'<Form version="{form_version}">' form_tag = f'<Form version="{form_version}">'
m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content) m_decl = re.search(r'^(<\?xml[^?]*\?>)', src_form_content)
@@ -1346,7 +1538,15 @@ def main():
xml_decl = m_decl.group(1) xml_decl = m_decl.group(1)
m_tag = re.search(r'(<Form[^>]*>)', src_form_content) m_tag = re.search(r'(<Form[^>]*>)', src_form_content)
if m_tag: if m_tag:
form_tag = m_tag.group(1) src_ns = re.sub(r'^<Form\s*', '', m_tag.group(1))
src_ns = re.sub(r'\s*/?>$', '', src_ns)
src_ns = re.sub(r'\s*version="[^"]*"', '', src_ns)
# 2.21 (8.5): пространство палитры. Место строгое — после lf, перед style.
if format_rank(form_version) >= 221 and 'xmlns:pal=' not in src_ns:
src_ns = src_ns.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
form_tag = f'<Form {src_ns} version="{form_version}">' if src_ns else f'<Form version="{form_version}">'
# Build output # Build output
parts = [] parts = []
@@ -1425,15 +1625,19 @@ def main():
form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext") form_xml_dir = os.path.join(form_meta_dir, form_name, "Ext")
os.makedirs(form_xml_dir, exist_ok=True) os.makedirs(form_xml_dir, exist_ok=True)
form_xml_file = os.path.join(form_xml_dir, "Form.xml") form_xml_file = os.path.join(form_xml_dir, "Form.xml")
save_text_bom(form_xml_file, "".join(parts)) write_xml_file(form_xml_file, "".join(parts))
info(f" Created: {form_xml_file}") info(f" Created: {form_xml_file}")
# 6. Create empty Module.bsl # 6. Create empty Module.bsl — but NEVER overwrite an existing one (re-borrow must
# not clobber user code added to the form module).
module_dir = os.path.join(form_xml_dir, "Form") module_dir = os.path.join(form_xml_dir, "Form")
os.makedirs(module_dir, exist_ok=True) os.makedirs(module_dir, exist_ok=True)
module_bsl_file = os.path.join(module_dir, "Module.bsl") module_bsl_file = os.path.join(module_dir, "Module.bsl")
save_text_bom(module_bsl_file, "") if os.path.isfile(module_bsl_file):
info(f" Created: {module_bsl_file}") info(" Preserved existing Module.bsl")
else:
save_text_bom(module_bsl_file, "")
info(f" Created: {module_bsl_file}")
# 7. Register form in parent object ChildObjects # 7. Register form in parent object ChildObjects
register_form_in_object(type_name, obj_name, form_name) register_form_in_object(type_name, obj_name, form_name)
@@ -1505,7 +1709,7 @@ def main():
target_dir = os.path.join(ext_dir, dir_name) target_dir = os.path.join(ext_dir, dir_name)
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml") target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}") info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name) add_to_child_objects(type_name, obj_name)
@@ -1532,7 +1736,7 @@ def main():
os.makedirs(target_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml") target_file = os.path.join(target_dir, f"{obj_name}.xml")
save_text_bom(target_file, borrowed_xml) write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}") info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name) add_to_child_objects(type_name, obj_name)
+57 -57
View File
@@ -1,57 +1,57 @@
--- ---
name: cfe-diff name: cfe-diff
description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию description: Анализ расширения конфигурации 1С (CFE) — состав, заимствованные объекты, перехватчики, проверка переноса. Используй когда нужно понять что содержит расширение или проверить перенесены ли вставки в конфигурацию
argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B] argument-hint: -ExtensionPath <path> -ConfigPath <path> [-Mode A|B]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-diff — Анализ расширения конфигурации # /cfe-diff — Анализ расширения конфигурации
Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B). Анализирует расширение в двух режимах: обзор изменений (Mode A) или проверка переноса (Mode B).
## Параметры ## Параметры
| Параметр | Описание | По умолчанию | | Параметр | Описание | По умолчанию |
|----------|----------|--------------| |----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — | | `ExtensionPath` | Путь к расширению (обязат.) | — |
| `ConfigPath` | Путь к конфигурации (обязат.) | — | | `ConfigPath` | Путь к конфигурации (обязат.) | — |
| `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` | | `Mode` | `A` (обзор) / `B` (проверка переноса) | `A` |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-diff.ps1" -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
``` ```
## Mode A — обзор расширения ## Mode A — обзор расширения
Для каждого объекта показывает: Для каждого объекта показывает:
- `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы - `[BORROWED]` — заимствованный: перехватчики (`&Перед`, `&После`, `&ИзменениеИКонтроль`, `&Вместо`), собственные реквизиты/ТЧ/формы
- `[OWN]` — собственный: количество реквизитов, ТЧ, форм - `[OWN]` — собственный: количество реквизитов, ТЧ, форм
Для каждой формы заимствованного объекта показывается: Для каждой формы заимствованного объекта показывается:
- `(borrowed)` / `(own)` — заимствованная или собственная форма - `(borrowed)` / `(own)` — заимствованная или собственная форма
- callType-события формы и элементов - callType-события формы и элементов
- callType на командах - callType на командах
## Mode B — проверка переноса ## Mode B — проверка переноса
Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации. Для каждого `&ИзменениеИКонтроль` извлекает блоки `#Вставка`/`#КонецВставки` из расширения и ищет их в соответствующем модуле конфигурации.
Статусы: Статусы:
- `[TRANSFERRED]` — код найден в конфигурации - `[TRANSFERRED]` — код найден в конфигурации
- `[NOT_TRANSFERRED]` — код не найден - `[NOT_TRANSFERRED]` — код не найден
- `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден - `[NEEDS_REVIEW]` — нет блоков `#Вставка` или модуль конфигурации не найден
## Примеры ## Примеры
```powershell ```powershell
# Обзор — что изменено в расширении # Обзор — что изменено в расширении
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode A
# Проверка переноса — все ли #Вставка перенесены # Проверка переноса — все ли #Вставка перенесены
... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B ... -ExtensionPath src -ConfigPath C:\cfsrc\erp -Mode B
``` ```
+471 -471
View File
@@ -1,471 +1,471 @@
# cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE) # cfe-diff v1.0 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$ExtensionPath, [string]$ExtensionPath,
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$ConfigPath, [string]$ConfigPath,
[ValidateSet("A","B")] [ValidateSet("A","B")]
[string]$Mode = "A" [string]$Mode = "A"
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Resolve paths --- # --- Resolve paths ---
if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) { if (-not [System.IO.Path]::IsPathRooted($ExtensionPath)) {
$ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath $ExtensionPath = Join-Path (Get-Location).Path $ExtensionPath
} }
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) { if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath $ConfigPath = Join-Path (Get-Location).Path $ConfigPath
} }
if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent } if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $ExtensionPath -Parent }
if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent } if (Test-Path $ConfigPath -PathType Leaf) { $ConfigPath = Split-Path $ConfigPath -Parent }
$extCfg = Join-Path $ExtensionPath "Configuration.xml" $extCfg = Join-Path $ExtensionPath "Configuration.xml"
$srcCfg = Join-Path $ConfigPath "Configuration.xml" $srcCfg = Join-Path $ConfigPath "Configuration.xml"
if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 } if (-not (Test-Path $extCfg)) { Write-Error "Extension Configuration.xml not found: $extCfg"; exit 1 }
if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 } if (-not (Test-Path $srcCfg)) { Write-Error "Config Configuration.xml not found: $srcCfg"; exit 1 }
# --- Type -> directory mapping --- # --- Type -> directory mapping ---
$childTypeDirMap = @{ $childTypeDirMap = @{
"Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums" "Catalog"="Catalogs"; "Document"="Documents"; "Enum"="Enums"
"CommonModule"="CommonModules"; "CommonPicture"="CommonPictures" "CommonModule"="CommonModules"; "CommonPicture"="CommonPictures"
"CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates" "CommonCommand"="CommonCommands"; "CommonTemplate"="CommonTemplates"
"ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors" "ExchangePlan"="ExchangePlans"; "Report"="Reports"; "DataProcessor"="DataProcessors"
"InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters" "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes" "ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"
"ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters" "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters" "ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks" "BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants" "Subsystem"="Subsystems"; "Role"="Roles"; "Constant"="Constants"
"FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes" "FunctionalOption"="FunctionalOptions"; "DefinedType"="DefinedTypes"
"FunctionalOptionsParameter"="FunctionalOptionsParameters" "FunctionalOptionsParameter"="FunctionalOptionsParameters"
"CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals" "CommonForm"="CommonForms"; "DocumentJournal"="DocumentJournals"
"SessionParameter"="SessionParameters"; "StyleItem"="StyleItems" "SessionParameter"="SessionParameters"; "StyleItem"="StyleItems"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs" "EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria" "SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators" "CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices" "Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes" "CommonAttribute"="CommonAttributes"
} }
# --- Parse extension Configuration.xml --- # --- Parse extension Configuration.xml ---
$extDoc = New-Object System.Xml.XmlDocument $extDoc = New-Object System.Xml.XmlDocument
$extDoc.PreserveWhitespace = $false $extDoc.PreserveWhitespace = $false
$extDoc.Load($extCfg) $extDoc.Load($extCfg)
$ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable) $ns = New-Object System.Xml.XmlNamespaceManager($extDoc.NameTable)
$ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $ns.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable") $ns.AddNamespace("xr", "http://v8.1c.ru/8.3/xcf/readable")
$extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns) $extProps = $extDoc.SelectSingleNode("//md:Configuration/md:Properties", $ns)
$extNameNode = $extProps.SelectSingleNode("md:Name", $ns) $extNameNode = $extProps.SelectSingleNode("md:Name", $ns)
$extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" } $extName = if ($extNameNode) { $extNameNode.InnerText } else { "?" }
$prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns) $prefixNode = $extProps.SelectSingleNode("md:NamePrefix", $ns)
$namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" } $namePrefix = if ($prefixNode -and $prefixNode.InnerText) { $prefixNode.InnerText } else { "" }
$purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns) $purposeNode = $extProps.SelectSingleNode("md:ConfigurationExtensionPurpose", $ns)
$purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" } $purpose = if ($purposeNode) { $purposeNode.InnerText } else { "?" }
Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ===" Write-Host "=== cfe-diff Mode ${Mode}: $extName (${purpose}) ==="
Write-Host " NamePrefix: $namePrefix" Write-Host " NamePrefix: $namePrefix"
Write-Host "" Write-Host ""
# --- Collect ChildObjects --- # --- Collect ChildObjects ---
$childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns) $childObjNode = $extDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $ns)
if (-not $childObjNode) { if (-not $childObjNode) {
Write-Host "[WARN] No ChildObjects in extension" Write-Host "[WARN] No ChildObjects in extension"
exit 0 exit 0
} }
$objects = @() $objects = @()
foreach ($child in $childObjNode.ChildNodes) { foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue } if ($child.NodeType -ne 'Element') { continue }
if ($child.LocalName -eq "Language") { continue } if ($child.LocalName -eq "Language") { continue }
$objects += @{ Type = $child.LocalName; Name = $child.InnerText } $objects += @{ Type = $child.LocalName; Name = $child.InnerText }
} }
if ($objects.Count -eq 0) { if ($objects.Count -eq 0) {
Write-Host "No objects (besides Language) in extension." Write-Host "No objects (besides Language) in extension."
exit 0 exit 0
} }
# --- Helper: check if object is borrowed --- # --- Helper: check if object is borrowed ---
function Get-ObjectInfo { function Get-ObjectInfo {
param([string]$objType, [string]$objName) param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return $null } if (-not $childTypeDirMap.ContainsKey($objType)) { return $null }
$dirName = $childTypeDirMap[$objType] $dirName = $childTypeDirMap[$objType]
$objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml" $objFile = Join-Path (Join-Path $ExtensionPath $dirName) "${objName}.xml"
if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } } if (-not (Test-Path $objFile)) { return @{ Borrowed = $false; File = $objFile; Exists = $false } }
$doc = New-Object System.Xml.XmlDocument $doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $false $doc.PreserveWhitespace = $false
$doc.Load($objFile) $doc.Load($objFile)
$objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable) $objNs = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$objEl = $null $objEl = $null
foreach ($c in $doc.DocumentElement.ChildNodes) { foreach ($c in $doc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq 'Element') { $objEl = $c; break } if ($c.NodeType -eq 'Element') { $objEl = $c; break }
} }
if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } } if (-not $objEl) { return @{ Borrowed = $false; File = $objFile; Exists = $true } }
$propsEl = $objEl.SelectSingleNode("md:Properties", $objNs) $propsEl = $objEl.SelectSingleNode("md:Properties", $objNs)
$obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null } $obNode = if ($propsEl) { $propsEl.SelectSingleNode("md:ObjectBelonging", $objNs) } else { $null }
$info = @{ $info = @{
Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted") Borrowed = ($obNode -and $obNode.InnerText -eq "Adopted")
File = $objFile File = $objFile
Exists = $true Exists = $true
Type = $objType Type = $objType
Name = $objName Name = $objName
DirName = $dirName DirName = $dirName
ObjElement = $objEl ObjElement = $objEl
ObjNs = $objNs ObjNs = $objNs
} }
return $info return $info
} }
# --- Helper: find .bsl files for object --- # --- Helper: find .bsl files for object ---
function Get-BslFiles { function Get-BslFiles {
param([string]$objType, [string]$objName) param([string]$objType, [string]$objName)
if (-not $childTypeDirMap.ContainsKey($objType)) { return @() } if (-not $childTypeDirMap.ContainsKey($objType)) { return @() }
$dirName = $childTypeDirMap[$objType] $dirName = $childTypeDirMap[$objType]
$objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName $objDir = Join-Path (Join-Path $ExtensionPath $dirName) $objName
if (-not (Test-Path $objDir -PathType Container)) { return @() } if (-not (Test-Path $objDir -PathType Container)) { return @() }
$bslFiles = @() $bslFiles = @()
$extDir = Join-Path $objDir "Ext" $extDir = Join-Path $objDir "Ext"
if (Test-Path $extDir) { if (Test-Path $extDir) {
$items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue $items = Get-ChildItem -Path $extDir -Filter "*.bsl" -ErrorAction SilentlyContinue
foreach ($item in $items) { $bslFiles += $item.FullName } foreach ($item in $items) { $bslFiles += $item.FullName }
} }
# Forms # Forms
$formsDir = Join-Path $objDir "Forms" $formsDir = Join-Path $objDir "Forms"
if (Test-Path $formsDir) { if (Test-Path $formsDir) {
$formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue $formModules = Get-ChildItem -Path $formsDir -Recurse -Filter "Module.bsl" -ErrorAction SilentlyContinue
foreach ($fm in $formModules) { $bslFiles += $fm.FullName } foreach ($fm in $formModules) { $bslFiles += $fm.FullName }
} }
return $bslFiles return $bslFiles
} }
# --- Helper: parse interceptors from .bsl --- # --- Helper: parse interceptors from .bsl ---
function Get-Interceptors { function Get-Interceptors {
param([string]$bslPath) param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() } if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$interceptors = @() $interceptors = @()
$i = 0 $i = 0
while ($i -lt $lines.Count) { while ($i -lt $lines.Count) {
$line = $lines[$i].Trim() $line = $lines[$i].Trim()
if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') { if ($line -match '^&(Перед|После|ИзменениеИКонтроль|Вместо)\("([^"]+)"\)') {
$type = $Matches[1] $type = $Matches[1]
$method = $Matches[2] $method = $Matches[2]
$interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath } $interceptors += @{ Type = $type; Method = $method; Line = $i + 1; File = $bslPath }
} }
$i++ $i++
} }
return $interceptors return $interceptors
} }
# --- Helper: extract #Вставка blocks from .bsl --- # --- Helper: extract #Вставка blocks from .bsl ---
function Get-InsertionBlocks { function Get-InsertionBlocks {
param([string]$bslPath) param([string]$bslPath)
if (-not (Test-Path $bslPath)) { return @() } if (-not (Test-Path $bslPath)) { return @() }
$lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8) $lines = [System.IO.File]::ReadAllLines($bslPath, [System.Text.Encoding]::UTF8)
$blocks = @() $blocks = @()
$inBlock = $false $inBlock = $false
$blockLines = @() $blockLines = @()
$startLine = 0 $startLine = 0
for ($i = 0; $i -lt $lines.Count; $i++) { for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i].Trim() $line = $lines[$i].Trim()
if ($line -eq "#Вставка") { if ($line -eq "#Вставка") {
$inBlock = $true $inBlock = $true
$blockLines = @() $blockLines = @()
$startLine = $i + 1 $startLine = $i + 1
} elseif ($line -eq "#КонецВставки" -and $inBlock) { } elseif ($line -eq "#КонецВставки" -and $inBlock) {
$inBlock = $false $inBlock = $false
$blocks += @{ $blocks += @{
StartLine = $startLine StartLine = $startLine
EndLine = $i + 1 EndLine = $i + 1
Code = ($blockLines -join "`n").Trim() Code = ($blockLines -join "`n").Trim()
File = $bslPath File = $bslPath
} }
} elseif ($inBlock) { } elseif ($inBlock) {
$blockLines += $lines[$i] $blockLines += $lines[$i]
} }
} }
return $blocks return $blocks
} }
# --- Helper: analyze form for callType events and commands --- # --- Helper: analyze form for callType events and commands ---
function Get-FormInterceptors { function Get-FormInterceptors {
param([string]$formXmlPath) param([string]$formXmlPath)
if (-not (Test-Path $formXmlPath)) { return $null } if (-not (Test-Path $formXmlPath)) { return $null }
$formDoc = New-Object System.Xml.XmlDocument $formDoc = New-Object System.Xml.XmlDocument
$formDoc.PreserveWhitespace = $false $formDoc.PreserveWhitespace = $false
try { $formDoc.Load($formXmlPath) } catch { return $null } try { $formDoc.Load($formXmlPath) } catch { return $null }
$fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable) $fNs = New-Object System.Xml.XmlNamespaceManager($formDoc.NameTable)
$fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform") $fNs.AddNamespace("f", "http://v8.1c.ru/8.3/xcf/logform")
$fRoot = $formDoc.DocumentElement $fRoot = $formDoc.DocumentElement
$baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs) $baseForm = $fRoot.SelectSingleNode("f:BaseForm", $fNs)
$isBorrowed = ($baseForm -ne $null) $isBorrowed = ($baseForm -ne $null)
$interceptors = @() $interceptors = @()
# Form-level events with callType # Form-level events with callType
$eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs) $eventsNode = $fRoot.SelectSingleNode("f:Events", $fNs)
if ($eventsNode) { if ($eventsNode) {
foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) { foreach ($evt in $eventsNode.SelectNodes("f:Event", $fNs)) {
$ct = $evt.GetAttribute("callType") $ct = $evt.GetAttribute("callType")
if ($ct) { if ($ct) {
$interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" $interceptors += "Event:$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
} }
} }
} }
# Element-level events with callType (scan all elements recursively) # Element-level events with callType (scan all elements recursively)
$childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs) $childItems = $fRoot.SelectSingleNode("f:ChildItems", $fNs)
if ($childItems) { if ($childItems) {
foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) { foreach ($evtNode in $childItems.SelectNodes(".//*[f:Events/f:Event[@callType]]", $fNs)) {
$elName = $evtNode.GetAttribute("name") $elName = $evtNode.GetAttribute("name")
foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) { foreach ($evt in $evtNode.SelectNodes("f:Events/f:Event[@callType]", $fNs)) {
$ct = $evt.GetAttribute("callType") $ct = $evt.GetAttribute("callType")
$interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)" $interceptors += "Element:${elName}.$($evt.GetAttribute('name')) [$ct] -> $($evt.InnerText)"
} }
} }
} }
# Commands with callType on Action # Commands with callType on Action
foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) { foreach ($cmd in $fRoot.SelectNodes("f:Commands/f:Command", $fNs)) {
$cmdName = $cmd.GetAttribute("name") $cmdName = $cmd.GetAttribute("name")
foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) { foreach ($action in $cmd.SelectNodes("f:Action[@callType]", $fNs)) {
$ct = $action.GetAttribute("callType") $ct = $action.GetAttribute("callType")
$interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)" $interceptors += "Command:$cmdName [$ct] -> $($action.InnerText)"
} }
} }
return @{ return @{
IsBorrowed = $isBorrowed IsBorrowed = $isBorrowed
Interceptors = $interceptors Interceptors = $interceptors
} }
} }
# ============================================================ # ============================================================
# MODE A: Extension overview # MODE A: Extension overview
# ============================================================ # ============================================================
if ($Mode -eq "A") { if ($Mode -eq "A") {
$borrowedList = @() $borrowedList = @()
$ownList = @() $ownList = @()
foreach ($obj in $objects) { foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name $info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info) { if (-not $info) {
Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type" Write-Host " [?] $($obj.Type).$($obj.Name) — unknown type"
continue continue
} }
if (-not $info.Exists) { if (-not $info.Exists) {
Write-Host " [?] $($obj.Type).$($obj.Name) — file not found" Write-Host " [?] $($obj.Type).$($obj.Name) — file not found"
continue continue
} }
if ($info.Borrowed) { if ($info.Borrowed) {
$borrowedList += $obj $borrowedList += $obj
Write-Host " [BORROWED] $($obj.Type).$($obj.Name)" Write-Host " [BORROWED] $($obj.Type).$($obj.Name)"
# Find .bsl files and interceptors # Find .bsl files and interceptors
$bslFiles = Get-BslFiles $obj.Type $obj.Name $bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) { foreach ($bsl in $bslFiles) {
$relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") $relPath = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
$interceptors = Get-Interceptors $bsl $interceptors = Get-Interceptors $bsl
if ($interceptors.Count -gt 0) { if ($interceptors.Count -gt 0) {
foreach ($ic in $interceptors) { foreach ($ic in $interceptors) {
Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath" Write-Host " &$($ic.Type)(`"$($ic.Method)`") — line $($ic.Line) in $relPath"
} }
} else { } else {
Write-Host " $relPath (no interceptors)" Write-Host " $relPath (no interceptors)"
} }
} }
# Check for own attributes/forms in ChildObjects # Check for own attributes/forms in ChildObjects
if ($info.ObjElement) { if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) { if ($childObj) {
$ownAttrs = 0 $ownAttrs = 0
$ownForms = 0 $ownForms = 0
$ownTS = 0 $ownTS = 0
$borrowedItems = 0 $borrowedItems = 0
$formNames = @() $formNames = @()
foreach ($c in $childObj.ChildNodes) { foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue } if ($c.NodeType -ne 'Element') { continue }
$cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs) $cProps = $c.SelectSingleNode("md:Properties", $info.ObjNs)
if ($cProps) { if ($cProps) {
$cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs) $cOb = $cProps.SelectSingleNode("md:ObjectBelonging", $info.ObjNs)
if ($cOb -and $cOb.InnerText -eq "Adopted") { if ($cOb -and $cOb.InnerText -eq "Adopted") {
$borrowedItems++ $borrowedItems++
continue continue
} }
} }
switch ($c.LocalName) { switch ($c.LocalName) {
"Attribute" { $ownAttrs++ } "Attribute" { $ownAttrs++ }
"TabularSection" { $ownTS++ } "TabularSection" { $ownTS++ }
"Form" { $formNames += $c.InnerText; $ownForms++ } "Form" { $formNames += $c.InnerText; $ownForms++ }
} }
} }
$parts = @() $parts = @()
if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" } if ($ownAttrs -gt 0) { $parts += "$ownAttrs own attrs" }
if ($ownTS -gt 0) { $parts += "$ownTS own TS" } if ($ownTS -gt 0) { $parts += "$ownTS own TS" }
if ($ownForms -gt 0) { $parts += "$ownForms own forms" } if ($ownForms -gt 0) { $parts += "$ownForms own forms" }
if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" } if ($borrowedItems -gt 0) { $parts += "$borrowedItems borrowed items" }
if ($parts.Count -gt 0) { if ($parts.Count -gt 0) {
Write-Host " ChildObjects: $($parts -join ', ')" Write-Host " ChildObjects: $($parts -join ', ')"
} }
# Analyze forms # Analyze forms
$borrowedFormCount = 0 $borrowedFormCount = 0
$ownFormCount = 0 $ownFormCount = 0
foreach ($fn in $formNames) { foreach ($fn in $formNames) {
$formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml" $formXmlPath = Join-Path (Join-Path (Join-Path (Join-Path (Join-Path $ExtensionPath $info.DirName) $info.Name) "Forms") $fn) "Ext/Form.xml"
$fi = Get-FormInterceptors $formXmlPath $fi = Get-FormInterceptors $formXmlPath
if (-not $fi) { if (-not $fi) {
Write-Host " Form.$fn (?)" Write-Host " Form.$fn (?)"
continue continue
} }
$formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ } $formTag = if ($fi.IsBorrowed) { "borrowed"; $borrowedFormCount++ } else { "own"; $ownFormCount++ }
if ($fi.Interceptors.Count -gt 0) { if ($fi.Interceptors.Count -gt 0) {
Write-Host " Form.$fn ($formTag):" Write-Host " Form.$fn ($formTag):"
foreach ($ic in $fi.Interceptors) { foreach ($ic in $fi.Interceptors) {
Write-Host " $ic" Write-Host " $ic"
} }
} else { } else {
Write-Host " Form.$fn ($formTag)" Write-Host " Form.$fn ($formTag)"
} }
} }
} }
} }
} else { } else {
$ownList += $obj $ownList += $obj
Write-Host " [OWN] $($obj.Type).$($obj.Name)" Write-Host " [OWN] $($obj.Type).$($obj.Name)"
# Brief info for own objects # Brief info for own objects
if ($info.ObjElement) { if ($info.ObjElement) {
$childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs) $childObj = $info.ObjElement.SelectSingleNode("md:ChildObjects", $info.ObjNs)
if ($childObj) { if ($childObj) {
$attrs = 0; $forms = 0; $ts = 0 $attrs = 0; $forms = 0; $ts = 0
foreach ($c in $childObj.ChildNodes) { foreach ($c in $childObj.ChildNodes) {
if ($c.NodeType -ne 'Element') { continue } if ($c.NodeType -ne 'Element') { continue }
switch ($c.LocalName) { switch ($c.LocalName) {
"Attribute" { $attrs++ } "Attribute" { $attrs++ }
"TabularSection" { $ts++ } "TabularSection" { $ts++ }
"Form" { $forms++ } "Form" { $forms++ }
} }
} }
$parts = @() $parts = @()
if ($attrs -gt 0) { $parts += "$attrs attrs" } if ($attrs -gt 0) { $parts += "$attrs attrs" }
if ($ts -gt 0) { $parts += "$ts TS" } if ($ts -gt 0) { $parts += "$ts TS" }
if ($forms -gt 0) { $parts += "$forms forms" } if ($forms -gt 0) { $parts += "$forms forms" }
if ($parts.Count -gt 0) { if ($parts.Count -gt 0) {
Write-Host " $($parts -join ', ')" Write-Host " $($parts -join ', ')"
} }
} }
} }
} }
} }
Write-Host "" Write-Host ""
Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ===" Write-Host "=== Summary: $($borrowedList.Count) borrowed, $($ownList.Count) own objects ==="
} }
# ============================================================ # ============================================================
# MODE B: Transfer check # MODE B: Transfer check
# ============================================================ # ============================================================
if ($Mode -eq "B") { if ($Mode -eq "B") {
$transferred = 0 $transferred = 0
$notTransferred = 0 $notTransferred = 0
$needsReview = 0 $needsReview = 0
foreach ($obj in $objects) { foreach ($obj in $objects) {
$info = Get-ObjectInfo $obj.Type $obj.Name $info = Get-ObjectInfo $obj.Type $obj.Name
if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue } if (-not $info -or -not $info.Exists -or -not $info.Borrowed) { continue }
# Find .bsl files with &ИзменениеИКонтроль # Find .bsl files with &ИзменениеИКонтроль
$bslFiles = Get-BslFiles $obj.Type $obj.Name $bslFiles = Get-BslFiles $obj.Type $obj.Name
foreach ($bsl in $bslFiles) { foreach ($bsl in $bslFiles) {
$interceptors = Get-Interceptors $bsl $interceptors = Get-Interceptors $bsl
$macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" }) $macInterceptors = @($interceptors | Where-Object { $_.Type -eq "ИзменениеИКонтроль" })
if ($macInterceptors.Count -eq 0) { continue } if ($macInterceptors.Count -eq 0) { continue }
foreach ($ic in $macInterceptors) { foreach ($ic in $macInterceptors) {
$methodName = $ic.Method $methodName = $ic.Method
$relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/") $relBsl = $bsl.Replace($ExtensionPath, "").TrimStart("\", "/")
# Find #Вставка blocks in this file # Find #Вставка blocks in this file
$insertBlocks = Get-InsertionBlocks $bsl $insertBlocks = Get-InsertionBlocks $bsl
if ($insertBlocks.Count -eq 0) { if ($insertBlocks.Count -eq 0) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks" Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — no #Вставка blocks"
$needsReview++ $needsReview++
continue continue
} }
# Find corresponding module in config # Find corresponding module in config
if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue } if (-not $childTypeDirMap.ContainsKey($obj.Type)) { continue }
$dirName = $childTypeDirMap[$obj.Type] $dirName = $childTypeDirMap[$obj.Type]
$configBsl = $bsl.Replace($ExtensionPath, $ConfigPath) $configBsl = $bsl.Replace($ExtensionPath, $ConfigPath)
if (-not (Test-Path $configBsl)) { if (-not (Test-Path $configBsl)) {
Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found" Write-Host " [NEEDS_REVIEW] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — config module not found"
$needsReview++ $needsReview++
continue continue
} }
$configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8) $configContent = [System.IO.File]::ReadAllText($configBsl, [System.Text.Encoding]::UTF8)
$allTransferred = $true $allTransferred = $true
foreach ($block in $insertBlocks) { foreach ($block in $insertBlocks) {
$code = $block.Code $code = $block.Code
if (-not $code) { continue } if (-not $code) { continue }
# Normalize whitespace for comparison # Normalize whitespace for comparison
$codeNorm = $code -replace '\s+', ' ' $codeNorm = $code -replace '\s+', ' '
$configNorm = $configContent -replace '\s+', ' ' $configNorm = $configContent -replace '\s+', ' '
if ($configNorm.Contains($codeNorm)) { if ($configNorm.Contains($codeNorm)) {
# Found in config # Found in config
} else { } else {
$allTransferred = $false $allTransferred = $false
} }
} }
if ($allTransferred) { if ($allTransferred) {
Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)" Write-Host " [TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — $($insertBlocks.Count) block(s)"
$transferred++ $transferred++
} else { } else {
Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config" Write-Host " [NOT_TRANSFERRED] $($obj.Type).$($obj.Name) — &ИзменениеИКонтроль(`"$methodName`") — some blocks not found in config"
$notTransferred++ $notTransferred++
} }
} }
} }
} }
Write-Host "" Write-Host ""
Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ===" Write-Host "=== Transfer check: $transferred transferred, $notTransferred not transferred, $needsReview needs review ==="
} }
+71 -71
View File
@@ -1,71 +1,71 @@
--- ---
name: cfe-init name: cfe-init
description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации description: Создать расширение конфигурации 1С (CFE) — scaffold XML-исходников. Используй когда нужно создать новое расширение для исправления, доработки или дополнения конфигурации
argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24] argument-hint: <Name> [-ConfigPath <path>] [-Purpose Patch|Customization|AddOn] [-CompatibilityMode Version8_3_24]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-init — Создание расширения конфигурации 1С # /cfe-init — Создание расширения конфигурации 1С
Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`. Создаёт scaffold расширения: `Configuration.xml`, `Languages/Русский.xml`, опционально `Roles/`.
## Подготовка ## Подготовка
Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации. Если есть выгрузка базовой конфигурации, передай `-ConfigPath` — скрипт автоматически определит `CompatibilityMode` и UUID языка из базовой конфигурации.
### Авто-определение ConfigPath ### Авто-определение ConfigPath
Если пользователь не указал `-ConfigPath` — попробуй определить автоматически: Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
1. Прочитай `.v8-project.json` из корня проекта 1. Прочитай `.v8-project.json` из корня проекта
2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`) 2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
3. Если у базы есть поле `configSrc` — используй как `-ConfigPath` 3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
4. Если `configSrc` нет — спроси у пользователя 4. Если `configSrc` нет — спроси у пользователя
Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию). Если `.v8-project.json` не найден и `-ConfigPath` не задан — расширение создастся с предупреждением (UUID языка = нули, CompatibilityMode по умолчанию).
## Параметры ## Параметры
| Параметр | Описание | По умолчанию | | Параметр | Описание | По умолчанию |
|----------|----------|--------------| |----------|----------|--------------|
| `Name` | Имя расширения (обязат.) | — | | `Name` | Имя расширения (обязат.) | — |
| `Synonym` | Синоним | = Name | | `Synonym` | Синоним | = Name |
| `NamePrefix` | Префикс собственных объектов | = Name + "_" | | `NamePrefix` | Префикс собственных объектов | = Name + "_" |
| `OutputDir` | Каталог для создания | `src` | | `OutputDir` | Каталог для создания | `src` |
| `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` | | `Purpose` | `Patch` (исправление) / `Customization` (доработка) / `AddOn` (дополнение) | `Customization` |
| `Version` | Версия расширения | — | | `Version` | Версия расширения | — |
| `Vendor` | Поставщик | — | | `Vendor` | Поставщик | — |
| `CompatibilityMode` | Режим совместимости | `Version8_3_24` | | `CompatibilityMode` | Режим совместимости | `Version8_3_24` |
| `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — | | `ConfigPath` | Путь к выгрузке базовой конфигурации (авто-определяет CompatibilityMode и Language UUID) | — |
| `NoRole` | Без основной роли | false | | `NoRole` | Без основной роли | false |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-init.ps1" -Name "МоёРасширение"
``` ```
## Примеры ## Примеры
```powershell ```powershell
# Расширение для ERP с авто-определением совместимости из базовой конфигурации # Расширение для ERP с авто-определением совместимости из базовой конфигурации
... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src ... -Name Расш1 -ConfigPath C:\WS\tasks\cfsrc\erp_8.3.24 -OutputDir src
# Расширение-исправление с явным режимом совместимости # Расширение-исправление с явным режимом совместимости
... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src ... -Name Расш1 -Purpose Patch -CompatibilityMode Version8_3_17 -OutputDir src
# Расширение-доработка с версией # Расширение-доработка с версией
... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src ... -Name МоёРасширение -Version "1.0.0.1" -Vendor "Компания" -OutputDir src
# Без роли, с явным префиксом # Без роли, с явным префиксом
... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src ... -Name ИсправлениеБага -NamePrefix "ИБ_" -Purpose Patch -NoRole -OutputDir src
``` ```
## Верификация ## Верификация
``` ```
/cfe-validate <OutputDir> /cfe-validate <OutputDir>
``` ```
+313 -270
View File
@@ -1,270 +1,313 @@
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.7 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$NamePrefix, [string]$NamePrefix,
[string]$OutputDir = "src", [string]$OutputDir = "src",
[ValidateSet("Patch","Customization","AddOn")] [ValidateSet("Patch","Customization","AddOn")]
[string]$Purpose = "Customization", [string]$Purpose = "Customization",
[string]$Version, [string]$Version,
[string]$Vendor, [string]$Vendor,
[string]$CompatibilityMode = "Version8_3_24", [string]$CompatibilityMode = "Version8_3_24",
[string]$ConfigPath, [string]$ConfigPath,
[switch]$NoRole [switch]$NoRole
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Default NamePrefix --- # --- Default NamePrefix ---
if (-not $NamePrefix) { if (-not $NamePrefix) {
$NamePrefix = "${Name}_" $NamePrefix = "${Name}_"
} }
# --- Resolve output dir --- # --- Resolve output dir ---
if (-not [System.IO.Path]::IsPathRooted($OutputDir)) { if (-not [System.IO.Path]::IsPathRooted($OutputDir)) {
$OutputDir = Join-Path (Get-Location).Path $OutputDir $OutputDir = Join-Path (Get-Location).Path $OutputDir
} }
# --- Check existing --- # --- Check existing ---
$cfgFile = Join-Path $OutputDir "Configuration.xml" $cfgFile = Join-Path $OutputDir "Configuration.xml"
if (Test-Path $cfgFile) { if (Test-Path $cfgFile) {
Write-Error "Configuration.xml already exists: $cfgFile" Write-Error "Configuration.xml already exists: $cfgFile"
exit 1 exit 1
} }
# --- Resolve ConfigPath --- # MDClasses format version — inherited from the base config so the extension stays uniform
$baseLangUuid = "00000000-0000-0000-0000-000000000000" # with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
if ($ConfigPath) { $formatVersion = "2.17"
if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
$ConfigPath = Join-Path (Get-Location).Path $ConfigPath # --- Resolve ConfigPath ---
} $baseLangUuid = "00000000-0000-0000-0000-000000000000"
if (Test-Path $ConfigPath -PathType Container) { if ($ConfigPath) {
$candidate = Join-Path $ConfigPath "Configuration.xml" if (-not [System.IO.Path]::IsPathRooted($ConfigPath)) {
if (Test-Path $candidate) { $ConfigPath = $candidate } $ConfigPath = Join-Path (Get-Location).Path $ConfigPath
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 } }
} if (Test-Path $ConfigPath -PathType Container) {
if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 } $candidate = Join-Path $ConfigPath "Configuration.xml"
$cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent if (Test-Path $candidate) { $ConfigPath = $candidate }
else { Write-Error "No Configuration.xml in config directory: $ConfigPath"; exit 1 }
# 3a. Read Language UUID from base config }
$baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml" if (-not (Test-Path $ConfigPath)) { Write-Error "Config file not found: $ConfigPath"; exit 1 }
if (Test-Path $baseLangFile) { $cfgDir = Split-Path (Resolve-Path $ConfigPath).Path -Parent
$baseLangDoc = New-Object System.Xml.XmlDocument
$baseLangDoc.PreserveWhitespace = $false # 3a. Read Language UUID from base config
$baseLangDoc.Load($baseLangFile) $baseLangFile = Join-Path (Join-Path $cfgDir "Languages") "Русский.xml"
$langEl = $null if (Test-Path $baseLangFile) {
foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) { $baseLangDoc = New-Object System.Xml.XmlDocument
if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break } $baseLangDoc.PreserveWhitespace = $false
} $baseLangDoc.Load($baseLangFile)
if ($langEl) { $langEl = $null
$baseLangUuid = $langEl.GetAttribute("uuid") foreach ($c in $baseLangDoc.DocumentElement.ChildNodes) {
Write-Host "[INFO] Base config Language UUID: $baseLangUuid" if ($c.NodeType -eq 'Element' -and $c.LocalName -eq 'Language') { $langEl = $c; break }
} else { }
Write-Host "[WARN] No <Language> element in $baseLangFile" if ($langEl) {
} $baseLangUuid = $langEl.GetAttribute("uuid")
} else { Write-Host "[INFO] Base config Language UUID: $baseLangUuid"
Write-Host "[WARN] Base config language not found: $baseLangFile" } else {
} Write-Host "[WARN] No <Language> element in $baseLangFile"
}
# 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config } else {
$baseCfgDoc = New-Object System.Xml.XmlDocument Write-Host "[WARN] Base config language not found: $baseLangFile"
$baseCfgDoc.PreserveWhitespace = $false }
$baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable) # 3b. Read CompatibilityMode and InterfaceCompatibilityMode from base config
$baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses") $baseCfgDoc = New-Object System.Xml.XmlDocument
$compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs) $baseCfgDoc.PreserveWhitespace = $false
if ($compatNode -and $compatNode.InnerText) { $baseCfgDoc.Load((Resolve-Path $ConfigPath).Path)
$CompatibilityMode = $compatNode.InnerText.Trim() $baseCfgNs = New-Object System.Xml.XmlNamespaceManager($baseCfgDoc.NameTable)
Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode" $baseCfgNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
} else { $fmtVer = $baseCfgDoc.DocumentElement.GetAttribute("version")
Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode" if ($fmtVer) {
} $formatVersion = $fmtVer
$ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs) Write-Host "[INFO] Base config format version: $formatVersion"
if ($ifcNode -and $ifcNode.InnerText) { }
$InterfaceCompatibilityMode = $ifcNode.InnerText.Trim() $compatNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:CompatibilityMode", $baseCfgNs)
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode" if ($compatNode -and $compatNode.InnerText) {
} else { $CompatibilityMode = $compatNode.InnerText.Trim()
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2" Write-Host "[INFO] Base config CompatibilityMode: $CompatibilityMode"
Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode" } else {
} Write-Host "[WARN] CompatibilityMode not found in base config, using default: $CompatibilityMode"
} else { }
$InterfaceCompatibilityMode = "TaxiEnableVersion8_2" $ifcNode = $baseCfgDoc.SelectSingleNode("//md:Configuration/md:Properties/md:InterfaceCompatibilityMode", $baseCfgNs)
Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading." if ($ifcNode -and $ifcNode.InnerText) {
} $InterfaceCompatibilityMode = $ifcNode.InnerText.Trim()
Write-Host "[INFO] Base config InterfaceCompatibilityMode: $InterfaceCompatibilityMode"
# --- Generate UUIDs --- } else {
$uuidCfg = [guid]::NewGuid().ToString() $InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
$uuidLang = [guid]::NewGuid().ToString() Write-Host "[WARN] InterfaceCompatibilityMode not found in base config, using default: $InterfaceCompatibilityMode"
$uuidRole = [guid]::NewGuid().ToString() }
} else {
# 7 ContainedObject ObjectIds $InterfaceCompatibilityMode = "TaxiEnableVersion8_2"
$co1 = [guid]::NewGuid().ToString() Write-Host "[WARN] Language ExtendedConfigurationObject set to zeros. Use -ConfigPath to auto-resolve from base config, or fix manually before loading."
$co2 = [guid]::NewGuid().ToString() }
$co3 = [guid]::NewGuid().ToString()
$co4 = [guid]::NewGuid().ToString() # --- Generate UUIDs ---
$co5 = [guid]::NewGuid().ToString() $uuidCfg = [guid]::NewGuid().ToString()
$co6 = [guid]::NewGuid().ToString() $uuidLang = [guid]::NewGuid().ToString()
$co7 = [guid]::NewGuid().ToString() $uuidRole = [guid]::NewGuid().ToString()
# --- Synonym XML --- # 7 ContainedObject ObjectIds
$synonymXml = "" $co1 = [guid]::NewGuid().ToString()
if ($Synonym) { $co2 = [guid]::NewGuid().ToString()
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t" $co3 = [guid]::NewGuid().ToString()
} $co4 = [guid]::NewGuid().ToString()
$co5 = [guid]::NewGuid().ToString()
# --- Optional properties --- $co6 = [guid]::NewGuid().ToString()
$vendorXml = if ($Vendor) { [System.Security.SecurityElement]::Escape($Vendor) } else { "" } $co7 = [guid]::NewGuid().ToString()
$versionXml = if ($Version) { [System.Security.SecurityElement]::Escape($Version) } else { "" }
# --- Synonym XML ---
# --- Role name --- $synonymXml = ""
$roleName = "${NamePrefix}ОсновнаяРоль" if ($Synonym) {
$synonymXml = "`r`n`t`t`t`t<v8:item>`r`n`t`t`t`t`t<v8:lang>ru</v8:lang>`r`n`t`t`t`t`t<v8:content>$([System.Security.SecurityElement]::Escape($Synonym))</v8:content>`r`n`t`t`t`t</v8:item>`r`n`t`t`t"
# --- DefaultRoles XML --- }
$defaultRolesXml = ""
if (-not $NoRole) { # --- Optional properties ---
$defaultRolesXml = "`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t" # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
} # пишет <Vendor/>, а не <Vendor></Vendor>.
$vendorEl = if ($Vendor) { "<Vendor>$([System.Security.SecurityElement]::Escape($Vendor))</Vendor>" } else { "<Vendor/>" }
# --- ChildObjects --- $versionEl = if ($Version) { "<Version>$([System.Security.SecurityElement]::Escape($Version))</Version>" } else { "<Version/>" }
$childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
if (-not $NoRole) { # --- Role name ---
$childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>" $roleName = "${NamePrefix}ОсновнаяРоль"
}
$childObjectsXml += "`r`n`t`t" # --- DefaultRoles XML ---
# Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
# --- Configuration.xml --- $defaultRolesEl = "<DefaultRoles/>"
$cfgXml = @" if (-not $NoRole) {
<?xml version="1.0" encoding="UTF-8"?> $defaultRolesEl = "<DefaultRoles>`r`n`t`t`t`t<xr:Item xsi:type=`"xr:MDObjectRef`">Role.$roleName</xr:Item>`r`n`t`t`t</DefaultRoles>"
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> }
<Configuration uuid="$uuidCfg">
<InternalInfo> # --- ChildObjects ---
<xr:ContainedObject> $childObjectsXml = "`r`n`t`t`t<Language>Русский</Language>"
<xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId> if (-not $NoRole) {
<xr:ObjectId>$co1</xr:ObjectId> $childObjectsXml += "`r`n`t`t`t<Role>$roleName</Role>"
</xr:ContainedObject> }
<xr:ContainedObject> $childObjectsXml += "`r`n`t`t"
<xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<xr:ObjectId>$co2</xr:ObjectId> # Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
</xr:ContainedObject> # Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
<xr:ContainedObject> $xmlnsDecl = '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"'
<xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<xr:ObjectId>$co3</xr:ObjectId> # Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
</xr:ContainedObject> # Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
<xr:ContainedObject> function Get-FormatRank([string]$ver) {
<xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId> if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
<xr:ObjectId>$co4</xr:ObjectId> return 0
</xr:ContainedObject> }
<xr:ContainedObject>
<xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId> # 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
<xr:ObjectId>$co5</xr:ObjectId> # Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
</xr:ContainedObject> # дописать в конец нельзя.
<xr:ContainedObject> # Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
<xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId> # DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
<xr:ObjectId>$co6</xr:ObjectId> $f221Captions = ""
</xr:ContainedObject> if ((Get-FormatRank $formatVersion) -ge 221) {
<xr:ContainedObject> $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
<xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId> $f221Captions = "`r`n`t`t`t<Caption/>`r`n`t`t`t<ShortCaption/>"
<xr:ObjectId>$co7</xr:ObjectId> }
</xr:ContainedObject>
</InternalInfo> # --- Configuration.xml ---
<Properties> $cfgXml = @"
<ObjectBelonging>Adopted</ObjectBelonging> <?xml version="1.0" encoding="UTF-8"?>
<Name>$([System.Security.SecurityElement]::Escape($Name))</Name> <MetaDataObject $xmlnsDecl version="$formatVersion">
<Synonym>$synonymXml</Synonym> <Configuration uuid="$uuidCfg">
<Comment/> <InternalInfo>
<ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose> <xr:ContainedObject>
<KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs> <xr:ClassId>9cd510cd-abfc-11d4-9434-004095e12fc7</xr:ClassId>
<NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix> <xr:ObjectId>$co1</xr:ObjectId>
<ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode> </xr:ContainedObject>
<DefaultRunMode>ManagedApplication</DefaultRunMode> <xr:ContainedObject>
<UsePurposes> <xr:ClassId>9fcd25a0-4822-11d4-9414-008048da11f9</xr:ClassId>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <xr:ObjectId>$co2</xr:ObjectId>
</UsePurposes> </xr:ContainedObject>
<ScriptVariant>Russian</ScriptVariant> <xr:ContainedObject>
<DefaultRoles>$defaultRolesXml</DefaultRoles> <xr:ClassId>e3687481-0a87-462c-a166-9f34594f9bba</xr:ClassId>
<Vendor>$vendorXml</Vendor> <xr:ObjectId>$co3</xr:ObjectId>
<Version>$versionXml</Version> </xr:ContainedObject>
<DefaultLanguage>Language.Русский</DefaultLanguage> <xr:ContainedObject>
<BriefInformation/> <xr:ClassId>9de14907-ec23-4a07-96f0-85521cb6b53b</xr:ClassId>
<DetailedInformation/> <xr:ObjectId>$co4</xr:ObjectId>
<Copyright/> </xr:ContainedObject>
<VendorInformationAddress/> <xr:ContainedObject>
<ConfigurationInformationAddress/> <xr:ClassId>51f2d5d8-ea4d-4064-8892-82951750031e</xr:ClassId>
<InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode> <xr:ObjectId>$co5</xr:ObjectId>
</Properties> </xr:ContainedObject>
<ChildObjects>$childObjectsXml</ChildObjects> <xr:ContainedObject>
</Configuration> <xr:ClassId>e68182ea-4237-4383-967f-90c1e3370bc7</xr:ClassId>
</MetaDataObject> <xr:ObjectId>$co6</xr:ObjectId>
"@ </xr:ContainedObject>
<xr:ContainedObject>
# --- Languages/Русский.xml (adopted format) --- <xr:ClassId>fb282519-d103-4dd3-bc12-cb271d631dfc</xr:ClassId>
$langXml = @" <xr:ObjectId>$co7</xr:ObjectId>
<?xml version="1.0" encoding="UTF-8"?> </xr:ContainedObject>
<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"> </InternalInfo>
<Language uuid="$uuidLang"> <Properties>
<InternalInfo/> <ObjectBelonging>Adopted</ObjectBelonging>
<Properties> <Name>$([System.Security.SecurityElement]::Escape($Name))</Name>
<ObjectBelonging>Adopted</ObjectBelonging> <Synonym>$synonymXml</Synonym>
<Name>Русский</Name> <Comment/>
<Comment/> <ConfigurationExtensionPurpose>$Purpose</ConfigurationExtensionPurpose>
<ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject> <KeepMappingToExtendedConfigurationObjectsByIDs>true</KeepMappingToExtendedConfigurationObjectsByIDs>
<LanguageCode>ru</LanguageCode> <NamePrefix>$([System.Security.SecurityElement]::Escape($NamePrefix))</NamePrefix>
</Properties> <ConfigurationExtensionCompatibilityMode>$CompatibilityMode</ConfigurationExtensionCompatibilityMode>
</Language> <DefaultRunMode>ManagedApplication</DefaultRunMode>
</MetaDataObject> <UsePurposes>
"@ <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
</UsePurposes>
# --- Role XML --- <ScriptVariant>Russian</ScriptVariant>
$roleXml = @" $defaultRolesEl
<?xml version="1.0" encoding="UTF-8"?> $vendorEl
<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"> $versionEl$f221Captions
<Role uuid="$uuidRole"> <DefaultLanguage>Language.Русский</DefaultLanguage>
<Properties> <BriefInformation/>
<Name>$([System.Security.SecurityElement]::Escape($roleName))</Name> <DetailedInformation/>
<Synonym/> <Copyright/>
<Comment/> <VendorInformationAddress/>
</Properties> <ConfigurationInformationAddress/>
</Role> <InterfaceCompatibilityMode>$InterfaceCompatibilityMode</InterfaceCompatibilityMode>
</MetaDataObject> </Properties>
"@ <ChildObjects>$childObjectsXml</ChildObjects>
</Configuration>
# --- Create directories --- </MetaDataObject>
if (-not (Test-Path $OutputDir)) { "@
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
} # --- Languages/Русский.xml (adopted format) ---
$langDir = Join-Path $OutputDir "Languages" $langXml = @"
if (-not (Test-Path $langDir)) { <?xml version="1.0" encoding="UTF-8"?>
New-Item -ItemType Directory -Path $langDir -Force | Out-Null <MetaDataObject $xmlnsDecl version="$formatVersion">
} <Language uuid="$uuidLang">
<InternalInfo/>
# --- Write files with UTF-8 BOM --- <Properties>
$enc = New-Object System.Text.UTF8Encoding($true) <ObjectBelonging>Adopted</ObjectBelonging>
<Name>Русский</Name>
[System.IO.File]::WriteAllText($cfgFile, $cfgXml, $enc) <Comment/>
$langFile = Join-Path $langDir "Русский.xml" <ExtendedConfigurationObject>$baseLangUuid</ExtendedConfigurationObject>
[System.IO.File]::WriteAllText($langFile, $langXml, $enc) <LanguageCode>ru</LanguageCode>
</Properties>
# --- Role --- </Language>
if (-not $NoRole) { </MetaDataObject>
$roleDir = Join-Path $OutputDir "Roles" "@
if (-not (Test-Path $roleDir)) {
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null # --- Role XML ---
} $roleXml = @"
$roleFile = Join-Path $roleDir "$roleName.xml" <?xml version="1.0" encoding="UTF-8"?>
[System.IO.File]::WriteAllText($roleFile, $roleXml, $enc) <MetaDataObject $xmlnsDecl version="$formatVersion">
} <Role uuid="$uuidRole">
<Properties>
# --- Output --- <Name>$([System.Security.SecurityElement]::Escape($roleName))</Name>
Write-Host "[OK] Создано расширение: $Name" <Synonym/>
Write-Host " Каталог: $OutputDir" <Comment/>
Write-Host " Назначение: $Purpose" </Properties>
Write-Host " Префикс: $NamePrefix" </Role>
Write-Host " Совместимость: $CompatibilityMode" </MetaDataObject>
Write-Host " Configuration.xml: $cfgFile" "@
Write-Host " Languages: $langFile"
if (-not $NoRole) { # --- Create directories ---
Write-Host " Role: $roleFile" if (-not (Test-Path $OutputDir)) {
} New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
$langDir = Join-Path $OutputDir "Languages"
if (-not (Test-Path $langDir)) {
New-Item -ItemType Directory -Path $langDir -Force | Out-Null
}
# --- Write files with UTF-8 BOM ---
$enc = New-Object System.Text.UTF8Encoding($true)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $cfgFile $cfgXml $enc
$langFile = Join-Path $langDir "Русский.xml"
Write-XmlFile $langFile $langXml $enc
# --- Role ---
if (-not $NoRole) {
$roleDir = Join-Path $OutputDir "Roles"
if (-not (Test-Path $roleDir)) {
New-Item -ItemType Directory -Path $roleDir -Force | Out-Null
}
$roleFile = Join-Path $roleDir "$roleName.xml"
Write-XmlFile $roleFile $roleXml $enc
}
# --- Output ---
Write-Host "[OK] Создано расширение: $Name"
Write-Host " Каталог: $OutputDir"
Write-Host " Назначение: $Purpose"
Write-Host " Префикс: $NamePrefix"
Write-Host " Совместимость: $CompatibilityMode"
Write-Host " Configuration.xml: $cfgFile"
Write-Host " Languages: $langFile"
if (-not $NoRole) {
Write-Host " Role: $roleFile"
}
+78 -15
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# cfe-init v1.1 — Create 1C configuration extension scaffold (CFE) # cfe-init v1.7 — Create 1C configuration extension scaffold (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension.""" """Generates minimal XML source files for a 1C configuration extension."""
import sys, os, argparse, uuid import sys, os, re, argparse, uuid
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
def esc_xml(s): def esc_xml(s):
@@ -15,6 +15,22 @@ def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -50,6 +66,10 @@ def main():
print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr) print(f"Configuration.xml already exists: {cfg_file}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# MDClasses format version — inherited from the base config so the extension stays uniform
# with it (a 2.13 base must yield a 2.13 extension, else platform import rejects the mismatch).
format_version = "2.17"
# --- Resolve ConfigPath --- # --- Resolve ConfigPath ---
base_lang_uuid = "00000000-0000-0000-0000-000000000000" base_lang_uuid = "00000000-0000-0000-0000-000000000000"
if args.ConfigPath: if args.ConfigPath:
@@ -88,6 +108,10 @@ def main():
try: try:
base_cfg_tree = ET.parse(os.path.abspath(config_path)) base_cfg_tree = ET.parse(os.path.abspath(config_path))
base_cfg_root = base_cfg_tree.getroot() base_cfg_root = base_cfg_tree.getroot()
fmt_ver = base_cfg_root.get("version")
if fmt_ver:
format_version = fmt_ver
print(f"[INFO] Base config format version: {format_version}")
ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'} ns = {'md': 'http://v8.1c.ru/8.3/MDClasses'}
compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns) compat_node = base_cfg_root.find('.//md:Configuration/md:Properties/md:CompatibilityMode', ns)
if compat_node is not None and compat_node.text: if compat_node is not None and compat_node.text:
@@ -120,16 +144,21 @@ def main():
if synonym: if synonym:
synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t" synonym_xml = f"\r\n\t\t\t\t<v8:item>\r\n\t\t\t\t\t<v8:lang>ru</v8:lang>\r\n\t\t\t\t\t<v8:content>{esc_xml(synonym)}</v8:content>\r\n\t\t\t\t</v8:item>\r\n\t\t\t"
vendor_xml = esc_xml(vendor) if vendor else "" # Элемент целиком, а не значение внутри пары: при пустом значении Конфигуратор
version_xml = esc_xml(version) if version else "" # пишет <Vendor/>, а не <Vendor></Vendor>.
vendor_el = f"<Vendor>{esc_xml(vendor)}</Vendor>" if vendor else "<Vendor/>"
version_el = f"<Version>{esc_xml(version)}</Version>" if version else "<Version/>"
# --- Role name --- # --- Role name ---
role_name = f"{name_prefix}ОсновнаяРоль" role_name = f"{name_prefix}ОсновнаяРоль"
# --- DefaultRoles XML --- # --- DefaultRoles XML ---
default_roles_xml = "" # Элемент целиком: без роли Конфигуратор пишет <DefaultRoles/>, а не пустую пару.
default_roles_el = "<DefaultRoles/>"
if not args.NoRole: if not args.NoRole:
default_roles_xml = f'\r\n\t\t\t\t<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>\r\n\t\t\t' default_roles_el = ('<DefaultRoles>\r\n\t\t\t\t'
f'<xr:Item xsi:type="xr:MDObjectRef">Role.{role_name}</xr:Item>'
'\r\n\t\t\t</DefaultRoles>')
# --- ChildObjects --- # --- ChildObjects ---
child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>" child_objects_xml = f"\r\n\t\t\t<Language>Русский</Language>"
@@ -148,6 +177,40 @@ def main():
] ]
contained_objects = "" contained_objects = ""
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
# Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
xmlns_decl = (
'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"'
)
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
# Caption/ShortCaption — свойства корня из того же формата 2.21, между Version и
# DefaultLanguage (позиция снята с выгрузки расширения из базы 8.5).
f221_captions = ""
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
f221_captions = "\r\n\t\t\t<Caption/>\r\n\t\t\t<ShortCaption/>"
for i in range(7): for i in range(7):
contained_objects += f"""\t\t\t<xr:ContainedObject> contained_objects += f"""\t\t\t<xr:ContainedObject>
\t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId> \t\t\t\t<xr:ClassId>{class_ids[i]}</xr:ClassId>
@@ -155,7 +218,7 @@ def main():
\t\t\t</xr:ContainedObject>\n""" \t\t\t</xr:ContainedObject>\n"""
cfg_xml = f'''<?xml version="1.0" encoding="UTF-8"?> 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_decl} version="{format_version}">
\t<Configuration uuid="{uuid_cfg}"> \t<Configuration uuid="{uuid_cfg}">
\t\t<InternalInfo> \t\t<InternalInfo>
{contained_objects}\t\t</InternalInfo> {contained_objects}\t\t</InternalInfo>
@@ -173,9 +236,9 @@ def main():
\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> \t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
\t\t\t</UsePurposes> \t\t\t</UsePurposes>
\t\t\t<ScriptVariant>Russian</ScriptVariant> \t\t\t<ScriptVariant>Russian</ScriptVariant>
\t\t\t<DefaultRoles>{default_roles_xml}</DefaultRoles> \t\t\t{default_roles_el}
\t\t\t<Vendor>{vendor_xml}</Vendor> \t\t\t{vendor_el}
\t\t\t<Version>{version_xml}</Version> \t\t\t{version_el}{f221_captions}
\t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage> \t\t\t<DefaultLanguage>Language.Русский</DefaultLanguage>
\t\t\t<BriefInformation/> \t\t\t<BriefInformation/>
\t\t\t<DetailedInformation/> \t\t\t<DetailedInformation/>
@@ -190,7 +253,7 @@ def main():
# --- Languages/Русский.xml (adopted format) --- # --- Languages/Русский.xml (adopted format) ---
lang_xml = f'''<?xml version="1.0" encoding="UTF-8"?> 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_decl} version="{format_version}">
\t<Language uuid="{uuid_lang}"> \t<Language uuid="{uuid_lang}">
\t\t<InternalInfo/> \t\t<InternalInfo/>
\t\t<Properties> \t\t<Properties>
@@ -205,7 +268,7 @@ def main():
# --- Role XML --- # --- Role XML ---
role_xml = f'''<?xml version="1.0" encoding="UTF-8"?> role_xml = f'''<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> <MetaDataObject {xmlns_decl} version="{format_version}">
\t<Role uuid="{uuid_role}"> \t<Role uuid="{uuid_role}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{esc_xml(role_name)}</Name> \t\t\t<Name>{esc_xml(role_name)}</Name>
@@ -221,9 +284,9 @@ def main():
os.makedirs(lang_dir, exist_ok=True) os.makedirs(lang_dir, exist_ok=True)
# --- Write files --- # --- Write files ---
write_utf8_bom(cfg_file, cfg_xml) write_xml_file(cfg_file, cfg_xml)
lang_file = os.path.join(lang_dir, "Русский.xml") lang_file = os.path.join(lang_dir, "Русский.xml")
write_utf8_bom(lang_file, lang_xml) write_xml_file(lang_file, lang_xml)
# --- Role --- # --- Role ---
role_file = None role_file = None
@@ -231,7 +294,7 @@ def main():
role_dir = os.path.join(output_dir, "Roles") role_dir = os.path.join(output_dir, "Roles")
os.makedirs(role_dir, exist_ok=True) os.makedirs(role_dir, exist_ok=True)
role_file = os.path.join(role_dir, f"{role_name}.xml") role_file = os.path.join(role_dir, f"{role_name}.xml")
write_utf8_bom(role_file, role_xml) write_xml_file(role_file, role_xml)
# --- Output --- # --- Output ---
print(f"[OK] Создано расширение: {name}") print(f"[OK] Создано расширение: {name}")
+145 -78
View File
@@ -1,78 +1,145 @@
--- ---
name: cfe-patch-method name: cfe-patch-method
description: Генерация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после или вместо оригинального description: Генерация и актуализация перехватчика метода в расширении 1С (CFE). Используй когда нужно перехватить метод заимствованного объекта — вставить код до, после, вместо оригинала, изменить его тело (ИзменениеИКонтроль) — или актуализировать перехватчик после изменения оригинала
argument-hint: -ExtensionPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before argument-hint: -ExtensionPath <path> -ConfigPath <path> -ModulePath "Catalog.X.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-patch-method — Генерация перехватчика метода # /cfe-patch-method — Генерация перехватчика метода
Генерирует `.bsl` файл с декоратором перехвата для заимствованного объекта расширения. Создаёт файл или дописывает в существующий. Читает оригинал метода из конфигурации-источника и генерирует `.bsl`-перехватчик заимствованного объекта: с правильной директивой контекста, полной сигнатурой, обрамляющими инструкциями препроцессора и областями. Для `ИзменениеИКонтроль` копирует всё тело оригинала. Создаёт файл модуля, дописывает в существующий или актуализирует уже заимствованный метод.
## Предусловие ## Предусловие
Объект должен быть заимствован в расширение (`/cfe-borrow`). Скрипт читает `NamePrefix` из `Configuration.xml` расширения для формирования имени процедуры. Объект должен быть заимствован в расширение (`/cfe-borrow`). Нужен доступ к исходникам базовой конфигурации (`-ConfigPath`) — оттуда читается оригинал метода. `NamePrefix` берётся из `Configuration.xml` расширения.
## Параметры ### Авто-определение ConfigPath
| Параметр | Описание | По умолчанию | Если пользователь не указал `-ConfigPath` — попробуй определить автоматически:
|----------|----------|--------------| 1. Прочитай `.v8-project.json` из корня проекта
| `ExtensionPath` | Путь к расширению (обязат.) | — | 2. Разреши целевую базу (по имени, ветке или `default` — алгоритм из `/db-list`)
| `ModulePath` | Путь к модулю (обязат.) | — | 3. Если у базы есть поле `configSrc` — используй как `-ConfigPath`
| `MethodName` | Имя перехватываемого метода (обязат.) | — | 4. Если `configSrc` нет — спроси у пользователя
| `InterceptorType` | `Before` / `After` / `ModificationAndControl` (обязат.) | — |
| `Context` | Директива контекста | `НаСервере` | ## Параметры
| `IsFunction` | Метод — функция (добавит `Возврат`) | false |
| Параметр | Описание | По умолчанию |
## Формат ModulePath |----------|----------|--------------|
| `ExtensionPath` | Путь к расширению (обязат.) | — |
| ModulePath | Файл | | `ConfigPath` | Путь к конфигурации-источнику | обязат., кроме случая, когда `ModulePath` — путь к файлу |
|------------|------| | `ModulePath` | Логическое имя (`Тип.Имя.Модуль`) **или** путь к файлу модуля `.bsl` | обязат. для генерации |
| `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` | | `MethodName` | Имя перехватываемого метода | обязат. для генерации |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` | | `InterceptorType` | `Before` / `After` / `Instead` / `ModificationAndControl` | обязат. для генерации |
| `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` | | `Check` | Проверить контролируемые методы на дрейф (только отчёт) | — |
| `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` | | `Actualize` | Актуализировать дрейфнувшие контролируемые методы | — |
| `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` | ## Формат ModulePath
Аналогично для Report, DataProcessor, InformationRegister и других типов. | ModulePath | Файл |
|------------|------|
## Типы перехвата | `Catalog.X.ObjectModule` | `Catalogs/X/Ext/ObjectModule.bsl` |
| `Catalog.X.ManagerModule` | `Catalogs/X/Ext/ManagerModule.bsl` |
| InterceptorType | Декоратор | Назначение | | `Catalog.X.Form.Y` | `Catalogs/X/Forms/Y/Ext/Form/Module.bsl` |
|-----------------|-----------|------------| | `CommonModule.X` | `CommonModules/X/Ext/Module.bsl` |
| `Before` | `&Перед` | Код до вызова оригинального метода | | `Document.X.ObjectModule` | `Documents/X/Ext/ObjectModule.bsl` |
| `After` | `&После` | Код после вызова оригинального метода | | `Document.X.Form.Y` | `Documents/X/Forms/Y/Ext/Form/Module.bsl` |
| `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела метода с маркерами `#Вставка`/`#Удаление` |
Аналогично для Report, DataProcessor, InformationRegister и других типов.
## Команда
Вместо логического имени в `ModulePath` можно передать **путь к файлу** модуля-источника `.bsl` — тогда `-ConfigPath` указывать не нужно (оригинал читается прямо из этого файла). Путь модуля расширения определяется от типовой папки в пути автоматически.
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-patch-method.ps1" -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before ## Типы перехвата
```
| InterceptorType | Декоратор | Назначение | Применим к |
## Примеры |-----------------|-----------|------------|------------|
| `Before` | `&Перед` | Код до вызова оригинального метода | процедуры |
```powershell | `After` | `&После` | Код после вызова оригинального метода | процедуры |
# Перехват &Перед на сервере | `Instead` | `&Вместо` | Замена метода; в теле — скаффолд `ПродолжитьВызов(...)` | процедуры и функции |
... -ExtensionPath src -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before | `ModificationAndControl` | `&ИзменениеИКонтроль` | Копия тела оригинала для правки маркерами `#Вставка`/`#Удаление` | процедуры и функции |
# Перехват &После на клиенте ## Маркеры `#Вставка` / `#Удаление` (для `ModificationAndControl`)
... -ExtensionPath src -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After -Context "НаКлиенте"
`&ИзменениеИКонтроль` вставляет в перехватчик **копию тела оригинала**. Дальше отредактируй тело под свою доработку, **помечая каждое изменение** — платформа так отличает твою правку от неизменного оригинала:
# ИзменениеИКонтроль для функции
... -ExtensionPath src -ModulePath "CommonModule.ОбщийМодуль" -MethodName "ПолучитьДанные" -InterceptorType ModificationAndControl -IsFunction - **Добавляешь код** → оберни его `#Вставка``#КонецВставки`.
``` - **Удаляешь код оригинала** → оберни удаляемые строки `#Удаление``#КонецУдаления`, но сами строки **оставь между маркерами** (платформа сверяет их с оригиналом).
- **Заменяешь** → `#Удаление` старое `#КонецУдаления` сразу за ним `#Вставка` новое `#КонецВставки`.
## Генерируемый код (Before)
Пример:
```bsl ```bsl
&НаСервере &ИзменениеИКонтроль("ПриЗаписи")
&Перед("ПриЗаписи") Процедура Расш_ПриЗаписи(Отказ)
Процедура Расш1_ПриЗаписи() СуммаДокумента = РассчитатьСумму();
// TODO: код перед вызовом оригинального метода #Вставка
КонецПроцедуры // доработка: округляем
``` СуммаДокумента = Окр(СуммаДокумента, 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\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
```
## Примеры
```powershell
# Код перед записью
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Catalog.Контрагенты.ObjectModule" -MethodName "ПриЗаписи" -InterceptorType Before
# Перехват После на форме
... -ExtensionPath src\cfe\ИмяРасширения -ConfigPath src\cf -ModulePath "Document.Заказ.Form.ФормаДокумента" -MethodName "ПослеЗаписиНаСервере" -InterceptorType After
# Замена функции (ПродолжитьВызов)
... -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
```
## Верификация
```
/cfe-validate <ExtensionPath>
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -1,29 +1,29 @@
--- ---
name: cfe-validate name: cfe-validate
description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности description: Валидация расширения конфигурации 1С (CFE). Используй после создания или модификации расширения для проверки корректности
argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30] argument-hint: <ExtensionPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /cfe-validate — валидация расширения конфигурации (CFE) # /cfe-validate — валидация расширения конфигурации (CFE)
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений. Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|---------------|:-----:|---------|-------------------------------------------------| |---------------|:-----:|---------|-------------------------------------------------|
| ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения | | ExtensionPath | да | — | Путь к каталогу или Configuration.xml расширения |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл | | OutFile | нет | — | Записать результат в файл |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-validate.ps1" -ExtensionPath "src/Configuration.xml"
``` ```
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects.""" """Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re import sys, os, argparse, re
@@ -216,8 +216,9 @@ def main():
version = root.get('version', '') version = root.get('version', '')
if not version: if not version:
r.warn('1. Missing version attribute on MetaDataObject') r.warn('1. Missing version attribute on MetaDataObject')
elif version not in ('2.17', '2.20', '2.21'): elif version not in ('2.17', '2.18', '2.19', '2.20', '2.21'):
r.warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 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 # Must have Configuration child
cfg_node = None cfg_node = None
@@ -885,6 +886,21 @@ def main():
elif check13_ok: elif check13_ok:
r.ok('13. TypeLink: clean') 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 --- # --- Final output ---
r.finalize(out_file) r.finalize(out_file)
sys.exit(1 if r.errors > 0 else 0) sys.exit(1 if r.errors > 0 else 0)
+72 -78
View File
@@ -1,78 +1,72 @@
--- ---
name: db-create name: db-create
description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу description: Создание информационной базы 1С. Используй когда нужно создать базу, новую ИБ, пустую базу
argument-hint: <path|name> argument-hint: <path|name>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-create — Создание информационной базы # /db-create — Создание информационной базы
Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`. Создаёт новую информационную базу 1С (файловую или серверную) и предлагает зарегистрировать в `.v8-project.json`.
## Usage ## Usage
``` ```
/db-create <path> — файловая база по указанному пути /db-create <path> — файловая база по указанному пути
/db-create <server>/<name> — серверная база /db-create <server>/<name> — серверная база
/db-create — интерактивно /db-create — интерактивно
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе). Прочитай `.v8-project.json` из корня проекта для `v8path` (путь к платформе).
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
После создания базы предложи зарегистрировать через `/db-list add`. После создания базы предложи зарегистрировать через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Путь к файловой базе | | `-InfoBasePath <путь>` | * | Путь к файловой базе |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) | | `-UseTemplate <файл>` | нет | Создать из шаблона (.cf или .dt) |
| `-AddToList` | нет | Добавить в список баз 1С | | `-AddToList` | нет | Добавить в список баз 1С |
| `-ListName <имя>` | нет | Имя базы в списке | | `-ListName <имя>` | нет | Имя базы в списке |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Коды возврата > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
| Код | Описание | ## После создания
|-----|----------|
| 0 | Успешно | Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`)
| 1 | Ошибка (см. лог) | 3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона
## После создания ## Примеры
1. Прочитай лог-файл и покажи результат ```powershell
2. Предложи зарегистрировать базу в `.v8-project.json` (через `/db-list add`) # Создать файловую базу
3. Если указан шаблон `/UseTemplate` — предупреди что конфигурация будет загружена из шаблона powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB"
## Примеры # Создать серверную базу
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
```powershell
# Создать файловую базу # Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать серверную базу # Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
# Создать из шаблона CF
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf"
# Создать и добавить в список баз
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-create.ps1" -InfoBasePath "C:\Bases\NewDB" -AddToList -ListName "Новая база"
```
+476 -163
View File
@@ -1,163 +1,476 @@
# db-create v1.0 — Create 1C information base # db-create v1.10 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Создание информационной базы 1С .SYNOPSIS
Создание информационной базы 1С
.DESCRIPTION
Создаёт новую информационную базу 1С (файловую или серверную). .DESCRIPTION
Поддерживает создание из шаблона и добавление в список баз. Создаёт новую информационную базу 1С (файловую или серверную).
Поддерживает создание из шаблона и добавление в список баз.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt) .PARAMETER UseTemplate
Путь к файлу шаблона (.cf или .dt)
.PARAMETER AddToList
Добавить в список баз 1С .PARAMETER AddToList
Добавить в список баз 1С
.PARAMETER ListName
Имя базы в списке .PARAMETER ListName
Имя базы в списке
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" .PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база" .EXAMPLE
#> .\db-create.ps1 -InfoBasePath "C:\Bases\NewDB"
[CmdletBinding()] .EXAMPLE
param( .\db-create.ps1 -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UseTemplate, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[switch]$AddToList, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$ListName [Parameter(Mandatory=$false)]
) [string]$UseTemplate,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [switch]$AddToList,
# --- Resolve V8Path --- [Parameter(Mandatory=$false)]
if (-not $V8Path) { [string]$ListName,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { # --- Additional platform arguments ---
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $script:V8OwnedKeys = @(
exit 1 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
} '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
# --- Validate connection --- '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
exit 1 )
} $script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
# --- Validate template --- '--import', '--export', '--apply', '--force', '--create-database',
if ($UseTemplate -and -not (Test-Path $UseTemplate)) { '--user', '--password'
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red )
exit 1 $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
} $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
# --- Temp dir --- function Test-ArgKeyMatch {
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)" # A token matches a key when it equals the key, or starts with it and the next
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null # character is not a letter — catches glued /N"user" and --password=x, while
# keeping /ClearCache distinct from /C.
try { param([string]$Token, [string]$Key)
# --- Build arguments --- if ($Token.Length -lt $Key.Length) { return $false }
$arguments = @("CREATEINFOBASE") if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($Token.Length -eq $Key.Length) { return $true }
if ($InfoBaseServer -and $InfoBaseRef) { return -not [char]::IsLetter($Token[$Key.Length])
$arguments += "Srvr=`"$InfoBaseServer`";Ref=`"$InfoBaseRef`"" }
} else {
$arguments += "File=`"$InfoBasePath`"" function Get-ProjectExtraArgs {
} # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
# --- Template --- $dir = (Get-Location).Path
if ($UseTemplate) { while ($dir) {
$arguments += "/UseTemplate", "`"$UseTemplate`"" $pf = Join-Path $dir ".v8-project.json"
} if (Test-Path $pf) {
try {
# --- Add to list --- $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($AddToList) { if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
if ($ListName) { } catch {}
$arguments += "/AddToList", "`"$ListName`"" return @()
} else { }
$arguments += "/AddToList" $parent = Split-Path $dir -Parent
} if (-not $parent -or $parent -eq $dir) { break }
} $dir = $parent
}
# --- Output --- return @()
$outFile = Join-Path $tempDir "create_log.txt" }
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs" function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# --- Execute --- # output key fails with an opaque 1C error — reject what the skill owns itself.
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$exitCode = $process.ExitCode $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
foreach ($tok in $ExtraArgs) {
# --- Result --- if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
if ($exitCode -eq 0) { Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
if ($InfoBaseServer -and $InfoBaseRef) { exit 1
Write-Host "Information base created successfully: $InfoBaseServer/$InfoBaseRef" -ForegroundColor Green }
} else { foreach ($k in $owned) {
Write-Host "Information base created successfully: $InfoBasePath" -ForegroundColor Green if (Test-ArgKeyMatch $tok $k) {
} $hint = ''
} else { if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "Error creating information base (code: $exitCode)" -ForegroundColor Red Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
} exit 1
}
if (Test-Path $outFile) { }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue }
if ($logContent) { }
Write-Host "--- Log ---"
Write-Host $logContent function Resolve-ExtraArgs {
Write-Host "--- End ---" # 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)
exit $exitCode # 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
} finally { # single token. So accept the repo's list convention (comma-separated) and split here;
if (Test-Path $tempDir) { # a native array call keeps working. A value containing a comma is not supported.
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue $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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate template ---
if ($UseTemplate -and -not (Test-Path $UseTemplate)) {
Write-Host "Error: template file not found: $UseTemplate" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_create_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "create", "--db-path=$InfoBasePath", "--create-database")
if ($UseTemplate) {
if ([System.IO.Path]::GetExtension($UseTemplate) -ieq ".dt") {
$arguments += "--restore=$UseTemplate"
} else {
$arguments += "--load=$UseTemplate", "--apply"
}
}
$arguments += "--data=$tempDir"
$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
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- 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 {
$arguments += "File=`"$InfoBasePath`""
}
# --- Template ---
if ($UseTemplate) {
$arguments += "/UseTemplate", "`"$UseTemplate`""
}
# --- Add to list ---
if ($AddToList) {
if ($ListName) {
$arguments += "/AddToList", "`"$ListName`""
} else {
$arguments += "/AddToList"
}
}
# --- Output ---
$outFile = Join-Path $tempDir "create_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+400 -25
View File
@@ -1,35 +1,340 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-create v1.0 — Create 1C information base # db-create v1.10 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return 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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -44,12 +349,39 @@ def main():
parser.add_argument("-UseTemplate", default="") parser.add_argument("-UseTemplate", default="")
parser.add_argument("-AddToList", action="store_true") parser.add_argument("-AddToList", action="store_true")
parser.add_argument("-ListName", default="") 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) 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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -58,6 +390,37 @@ def main():
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr) print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "create", f"--db-path={args.InfoBasePath}", "--create-database"]
if args.UseTemplate:
if os.path.splitext(args.UseTemplate)[1].lower() == ".dt":
arguments.append(f"--restore={args.UseTemplate}")
else:
arguments.extend([f"--load={args.UseTemplate}", "--apply"])
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
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)
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: {exit_code})", file=sys.stderr)
print_platform_output(result)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_create_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -66,6 +429,8 @@ def main():
# --- Build arguments --- # --- Build arguments ---
arguments = ["CREATEINFOBASE"] 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: if args.InfoBaseServer and args.InfoBaseRef:
arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"') arguments.append(f'Srvr="{args.InfoBaseServer}";Ref="{args.InfoBaseRef}"')
else: else:
@@ -73,35 +438,44 @@ def main():
# --- Template --- # --- Template ---
if args.UseTemplate: if args.UseTemplate:
arguments.extend(["/UseTemplate", args.UseTemplate]) arguments.extend(["/UseTemplate", f'"{args.UseTemplate}"'])
# --- Add to list --- # --- Add to list ---
if args.AddToList: if args.AddToList:
if args.ListName: if args.ListName:
arguments.extend(["/AddToList", args.ListName]) arguments.extend(["/AddToList", f'"{args.ListName}"'])
else: else:
arguments.append("/AddToList") arguments.append("/AddToList")
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "create_log.txt") 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.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") print(f"Running: 1cv8.exe {' '.join(format_args_for_display(arguments, engine))}")
result = subprocess.run( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- 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 exit_code == 0:
if args.InfoBaseServer and args.InfoBaseRef: if is_server:
print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}") print(f"Information base created successfully: {args.InfoBaseServer}/{args.InfoBaseRef}")
else: else:
print(f"Information base created successfully: {args.InfoBasePath}") 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: else:
print(f"Error creating information base (code: {exit_code})", file=sys.stderr) print(f"Error creating information base (code: {exit_code})", file=sys.stderr)
@@ -115,6 +489,7 @@ def main():
print("--- End ---") print("--- End ---")
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
+70 -79
View File
@@ -1,79 +1,70 @@
--- ---
name: db-dump-cf name: db-dump-cf
description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF description: Выгрузка конфигурации 1С в CF-файл. Используй когда нужно выгрузить конфигурацию в CF, сохранить конфигурацию, сделать бэкап CF
argument-hint: "[database] [output.cf]" argument-hint: "[database] [output.cf]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-dump-cf — Выгрузка конфигурации в CF-файл # /db-dump-cf — Выгрузка конфигурации в CF-файл
Выгружает конфигурацию информационной базы в бинарный CF-файл. Выгружает конфигурацию информационной базы в бинарный CF-файл.
## Usage ## Usage
``` ```
/db-dump-cf [database] [output.cf] /db-dump-cf [database] [output.cf]
/db-dump-cf dev config.cf /db-dump-cf dev config.cf
/db-dump-cf — база по умолчанию, файл config.cf /db-dump-cf — база по умолчанию, файл config.cf
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному CF-файлу | | `-OutputFile <путь>` | да | Путь к выходному CF-файлу |
| `-Extension <имя>` | нет | Выгрузить расширение | | `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения | | `-AllExtensions` | нет | Выгрузить все расширения |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Коды возврата > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
| Код | Описание | ## Примеры
|-----|----------|
| 0 | Успешно | ```powershell
| 1 | Ошибка (см. лог) | # Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
## После выполнения
# Серверная база
Прочитай лог-файл и покажи результат. Если есть ошибки — покажи содержимое лога. powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
## Примеры # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```powershell ```
# Выгрузка конфигурации (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\config.cf"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "config.cf"
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "ext.cfe" -Extension "МоёРасширение"
```
+496 -166
View File
@@ -1,166 +1,496 @@
# db-dump-cf v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Выгрузка конфигурации 1С в CF-файл .SYNOPSIS
Выгрузка конфигурации 1С в CF-файл
.DESCRIPTION
Выгружает конфигурацию информационной базы в бинарный CF-файл. .DESCRIPTION
Поддерживает выгрузку расширений. Выгружает конфигурацию информационной базы в бинарный CF-файл.
Поддерживает выгрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному CF-файлу .PARAMETER OutputFile
Путь к выходному CF-файлу
.PARAMETER Extension
Имя расширения для выгрузки .PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения .PARAMETER AllExtensions
Выгрузить все расширения
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "config.cf"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$true)]
[string]$OutputFile, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions [Parameter(Mandatory=$true)]
) [string]$OutputFile,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [string]$Extension,
# --- Resolve V8Path --- [Parameter(Mandatory=$false)]
if (-not $V8Path) { [switch]$AllExtensions,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red # --- Additional platform arguments ---
exit 1 $script:V8OwnedKeys = @(
} 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
# --- Ensure output directory exists --- '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
$outDir = Split-Path $OutputFile -Parent '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
if ($outDir -and -not (Test-Path $outDir)) { '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
New-Item -ItemType Directory -Path $outDir -Force | Out-Null '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
} )
$script:IbcmdOwnedKeys = @(
# --- Temp dir --- '--db-path', '--data', '--out', '--file', '--load', '--restore',
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)" '--import', '--export', '--apply', '--force', '--create-database',
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null '--user', '--password'
)
try { $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
# --- Build arguments --- $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
$arguments = @("DESIGNER")
function Test-ArgKeyMatch {
if ($InfoBaseServer -and $InfoBaseRef) { # A token matches a key when it equals the key, or starts with it and the next
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" # character is not a letter — catches glued /N"user" and --password=x, while
} else { # keeping /ClearCache distinct from /C.
$arguments += "/F", "`"$InfoBasePath`"" 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 ($UserName) { $arguments += "/N`"$UserName`"" } if ($Token.Length -eq $Key.Length) { return $true }
if ($Password) { $arguments += "/P`"$Password`"" } return -not [char]::IsLetter($Token[$Key.Length])
}
$arguments += "/DumpCfg", "`"$OutputFile`""
function Get-ProjectExtraArgs {
# --- Extensions --- # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
if ($Extension) { param([string]$Name)
$arguments += "-Extension", "`"$Extension`"" $dir = (Get-Location).Path
} elseif ($AllExtensions) { while ($dir) {
$arguments += "-AllExtensions" $pf = Join-Path $dir ".v8-project.json"
} if (Test-Path $pf) {
try {
# --- Output --- $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
$outFile = Join-Path $tempDir "dump_cf_log.txt" if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
$arguments += "/Out", "`"$outFile`"" } catch {}
$arguments += "/DisableStartupDialogs" return @()
}
# --- Execute --- $parent = Split-Path $dir -Parent
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" if (-not $parent -or $parent -eq $dir) { break }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $dir = $parent
$exitCode = $process.ExitCode }
return @()
# --- Result --- }
if ($exitCode -eq 0) {
Write-Host "Configuration dumped successfully to: $OutputFile" -ForegroundColor Green function Assert-ExtraArgs {
} else { # The platform accepts only one batch operation, and a duplicate connection or
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red # 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' }
if (Test-Path $outFile) { $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue foreach ($tok in $ExtraArgs) {
if ($logContent) { if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
Write-Host "--- Log ---" Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
Write-Host $logContent exit 1
Write-Host "--- End ---" }
} foreach ($k in $owned) {
} if (Test-ArgKeyMatch $tok $k) {
$hint = ''
exit $exitCode 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
} finally { exit 1
if (Test-Path $tempDir) { }
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue }
} }
} }
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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config save does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "save", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$OutputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpCfg", "`"$OutputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+414 -28
View File
@@ -1,35 +1,358 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-cf v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -46,12 +369,40 @@ def main():
parser.add_argument("-OutputFile", required=True) parser.add_argument("-OutputFile", required=True)
parser.add_argument("-Extension", default="") parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true") 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) 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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -60,6 +411,37 @@ def main():
if out_dir and not os.path.isdir(out_dir): if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True) os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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))
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: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -69,40 +451,43 @@ def main():
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]) arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else: else:
arguments.extend(["/F", args.InfoBasePath]) arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: 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 --- # --- Extensions ---
if args.Extension: if args.Extension:
arguments.extend(["-Extension", args.Extension]) arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions: elif args.AllExtensions:
arguments.append("-AllExtensions") arguments.append("-AllExtensions")
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "dump_cf_log.txt") 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.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- 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: if exit_code == 0:
print(f"Configuration dumped successfully to: {args.OutputFile}") 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: else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
@@ -117,6 +502,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
+74
View File
@@ -0,0 +1,74 @@
---
name: db-dump-dt
description: Выгрузка информационной базы 1С в DT-файл (вся база — конфигурация + данные). Используй когда нужно выгрузить информационную базу, выгрузить архив базы, сделать бэкап, выгрузить dt
argument-hint: "[database] [output.dt]"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-dump-dt — Выгрузка информационной базы в DT-файл
Выгружает информационную базу целиком (конфигурация **+ данные**) в DT-файл — полный снимок ИБ.
> В отличие от `/db-dump-cf` (только конфигурация), `.dt` содержит **всю базу**: данные,
> настройки, пользователей. Это бэкап/точка отката, а не выгрузка метаданных.
## Usage
```
/db-dump-dt [database] [output.dt]
/db-dump-dt dev backup.dt
/db-dump-dt — база по умолчанию, имя файла по базе и дате
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-OutputFile <путь>` | да | Путь к выходному DT-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Примеры
```powershell
# Выгрузка ИБ (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -OutputFile "C:\backup\base.dt"
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -OutputFile "base.dt"
```
## Связанные навыки
- `/db-load-dt` — загрузка ИБ из DT (обратная операция)
- `/db-dump-cf` — выгрузка только конфигурации (без данных)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,470 @@
# 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 не исполняется.
<#
.SYNOPSIS
Выгрузка информационной базы 1С в DT-файл
.DESCRIPTION
Выгружает информационную базу целиком (конфигурация + данные) в DT-файл.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER OutputFile
Путь к выходному DT-файлу
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$OutputFile,
[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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "dump", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$OutputFile"
$arguments += "--data=$tempDir"
$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
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpIB", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "dump_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -0,0 +1,501 @@
#!/usr/bin/env python3
# db-dump-dt v1.11 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
# --- 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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path):
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Dump 1C information base to DT file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-OutputFile", required=True)
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:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.isdir(out_dir):
os.makedirs(out_dir, exist_ok=True)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "dump", f"--db-path={args.InfoBasePath}"]
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.OutputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
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))
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: {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)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
arguments.extend(["/DumpIB", f'"{args.OutputFile}"'])
# --- Output ---
out_file = os.path.join(temp_dir, "dump_dt_log.txt")
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 {_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)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+92 -97
View File
@@ -1,97 +1,92 @@
--- ---
name: db-dump-xml name: db-dump-xml
description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles description: Выгрузка конфигурации 1С в XML-файлы. Используй когда нужно выгрузить конфигурацию в файлы, XML, исходники, DumpConfigToFiles
argument-hint: "[database] [outputDir]" argument-hint: "[database] [outputDir]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-dump-xml — Выгрузка конфигурации в XML # /db-dump-xml — Выгрузка конфигурации в XML
Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo. Выгружает конфигурацию информационной базы в XML-файлы (исходники). Поддерживает полную, инкрементальную, частичную выгрузку и обновление ConfigDumpInfo.
## Usage ## Usage
``` ```
/db-dump-xml [database] [outputDir] /db-dump-xml [database] [outputDir]
/db-dump-xml dev src/config /db-dump-xml dev src/config
/db-dump-xml dev src/config -Mode Full /db-dump-xml dev src/config -Mode Full
/db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" /db-dump-xml dev src/config -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог для выгрузки | | `-ConfigDir <путь>` | да | Каталог для выгрузки |
| `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` | | `-Mode <режим>` | нет | `Full` / `Changes` (по умолч.) / `Partial` / `UpdateInfo` |
| `-Objects <список>` | для Partial | Имена объектов через запятую | | `-Objects <список>` | для Partial | Имена объектов через запятую |
| `-Extension <имя>` | нет | Выгрузить расширение | | `-Extension <имя>` | нет | Выгрузить расширение |
| `-AllExtensions` | нет | Выгрузить все расширения | | `-AllExtensions` | нет | Выгрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
### Режимы выгрузки > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
| Режим | Описание | ### Режимы выгрузки
|-------|----------|
| `Full` | Полная выгрузка — все объекты конфигурации | | Режим | Описание |
| `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) | |-------|----------|
| `Partial` | Частичная выбранные объекты из параметра `-Objects` | | `Full` | Полная выгрузка — все объекты конфигурации |
| `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов | | `Changes` | Инкрементальная — только изменённые с последней выгрузки (использует ConfigDumpInfo.xml) |
| `Partial` | Частичная — выбранные объекты из параметра `-Objects` |
## Коды возврата | `UpdateInfo` | Обновить только ConfigDumpInfo.xml без выгрузки файлов |
| Код | Описание | > Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`.
|-----|----------|
| 0 | Успешно | ## Примеры
| 1 | Ошибка (см. лог) |
```powershell
> Если пользователь просит выгрузить конкретные объекты — используй `-Mode Partial` с `-Objects`. # Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
## Примеры
# Инкрементальная выгрузка
```powershell powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes
# Полная выгрузка (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full # Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
# Инкрементальная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Changes # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Частичная выгрузка
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" # Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Серверная база ```
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Password "secret" -ConfigDir "C:\WS\cfsrc" -Mode Full
# Выгрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-dump-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
```
+565 -223
View File
@@ -1,224 +1,566 @@
# db-dump-xml v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Выгрузка конфигурации 1С в XML-файлы .SYNOPSIS
Выгрузка конфигурации 1С в XML-файлы
.DESCRIPTION
Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах: .DESCRIPTION
- Full: полная выгрузка всей конфигурации Выполняет выгрузку конфигурации 1С в файлы в четырёх режимах:
- Changes: инкрементальная выгрузка изменённых объектов - Full: полная выгрузка всей конфигурации
- Partial: выгрузка конкретных объектов из списка - Changes: инкрементальная выгрузка изменённых объектов
- UpdateInfo: обновление только ConfigDumpInfo.xml - Partial: выгрузка конкретных объектов из списка
- UpdateInfo: обновление только ConfigDumpInfo.xml
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог для выгрузки конфигурации .PARAMETER ConfigDir
Каталог для выгрузки конфигурации
.PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes) .PARAMETER Mode
Режим выгрузки: Full, Changes, Partial, UpdateInfo (по умолчанию Changes)
.PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial) .PARAMETER Objects
Имена объектов метаданных через запятую (для режима Partial)
.PARAMETER Extension
Имя расширения для выгрузки .PARAMETER Extension
Имя расширения для выгрузки
.PARAMETER AllExtensions
Выгрузить все расширения .PARAMETER AllExtensions
Выгрузить все расширения
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$true)]
[string]$ConfigDir, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")] [Parameter(Mandatory=$false)]
[string]$Mode = "Changes", [string]$Password,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$true)]
[string]$Objects, [string]$ConfigDir,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$Extension, [ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "Changes",
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[string]$Objects,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$false)]
[string]$Format = "Hierarchical" [string]$Extension,
)
[Parameter(Mandatory=$false)]
$OutputEncoding = [System.Text.Encoding]::UTF8 [switch]$AllExtensions,
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Parameter(Mandatory=$false)]
# --- Resolve V8Path --- [ValidateSet("Hierarchical", "Plain")]
if (-not $V8Path) { [string]$Format = "Hierarchical",
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red # --- Additional platform arguments ---
exit 1 $script:V8OwnedKeys = @(
} 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
# --- Validate Partial mode --- '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
if ($Mode -eq "Partial" -and -not $Objects) { '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
exit 1 '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
} )
$script:IbcmdOwnedKeys = @(
# --- Create output dir if needed --- '--db-path', '--data', '--out', '--file', '--load', '--restore',
if (-not (Test-Path $ConfigDir)) { '--import', '--export', '--apply', '--force', '--create-database',
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null '--user', '--password'
Write-Host "Created output directory: $ConfigDir" )
} $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)" function Test-ArgKeyMatch {
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null # 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
try { # keeping /ClearCache distinct from /C.
# --- Build arguments --- param([string]$Token, [string]$Key)
$arguments = @("DESIGNER") if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
if ($InfoBaseServer -and $InfoBaseRef) { if ($Token.Length -eq $Key.Length) { return $true }
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" return -not [char]::IsLetter($Token[$Key.Length])
} else { }
$arguments += "/F", "`"$InfoBasePath`""
} function Get-ProjectExtraArgs {
# v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
if ($UserName) { $arguments += "/N`"$UserName`"" } param([string]$Name)
if ($Password) { $arguments += "/P`"$Password`"" } $dir = (Get-Location).Path
while ($dir) {
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`"" $pf = Join-Path $dir ".v8-project.json"
$arguments += "-Format", $Format if (Test-Path $pf) {
try {
switch ($Mode) { $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
"Full" { if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
Write-Host "Executing full configuration dump..." } catch {}
} return @()
"Changes" { }
Write-Host "Executing incremental configuration dump..." $parent = Split-Path $dir -Parent
$arguments += "-update" if (-not $parent -or $parent -eq $dir) { break }
$arguments += "-force" $dir = $parent
} }
"Partial" { return @()
Write-Host "Executing partial configuration dump..." }
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
function Assert-ExtraArgs {
$listFile = Join-Path $tempDir "dump_list.txt" # The platform accepts only one batch operation, and a duplicate connection or
$utf8Bom = New-Object System.Text.UTF8Encoding($true) # output key fails with an opaque 1C error — reject what the skill owns itself.
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom) param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$arguments += "-listFile", "`"$listFile`"" $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
Write-Host "Objects to dump: $($objectList.Count)" foreach ($tok in $ExtraArgs) {
foreach ($obj in $objectList) { Write-Host " $obj" } 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
"UpdateInfo" { exit 1
Write-Host "Updating ConfigDumpInfo.xml..." }
$arguments += "-configDumpInfoOnly" foreach ($k in $owned) {
} if (Test-ArgKeyMatch $tok $k) {
} $hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
# --- Extensions --- Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
if ($Extension) { exit 1
$arguments += "-Extension", "`"$Extension`"" }
} elseif ($AllExtensions) { }
$arguments += "-AllExtensions" }
} }
# --- Output --- function Resolve-ExtraArgs {
$outFile = Join-Path $tempDir "dump_log.txt" # Pick the argument list for the selected engine and validate it. An explicitly passed
$arguments += "/Out", "`"$outFile`"" # parameter for the other engine is an error; the same keys coming from .v8-project.json
$arguments += "/DisableStartupDialogs" # simply do not apply — a project may describe both engines.
param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
# --- Execute --- # powershell.exe -File — how skills are invoked — cannot bind an array parameter:
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" # space-separated values spill into positional ones, a comma-joined list arrives as a
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru # single token. So accept the repo's list convention (comma-separated) and split here;
$exitCode = $process.ExitCode # a native array call keeps working. A value containing a comma is not supported.
$V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
# --- Result --- $IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
if ($exitCode -eq 0) { if ($Engine -eq 'ibcmd' -and $V8Extra.Count -gt 0) {
Write-Host "Dump completed successfully" -ForegroundColor Green Write-Host "Error: -AdditionalV8Arguments applies to 1cv8 only; the selected engine is ibcmd (use -AdditionalIbcmdArguments)" -ForegroundColor Red
Write-Host "Configuration dumped to: $ConfigDir" exit 1
} else { }
Write-Host "Error dumping configuration (code: $exitCode)" -ForegroundColor Red 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 (Test-Path $outFile) { }
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if ($Engine -eq 'ibcmd') {
if ($logContent) { $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
Write-Host "--- Log ---" } else {
Write-Host $logContent $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
Write-Host "--- End ---" }
} 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.
exit $exitCode return $extra
}
} finally {
if (Test-Path $tempDir) { function Format-ArgsForDisplay {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue # 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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate Partial mode ---
if ($Mode -eq "Partial" -and -not $Objects) {
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
exit 1
}
# --- Create output dir if needed ---
if (-not (Test-Path $ConfigDir)) {
New-Item -ItemType Directory -Path $ConfigDir -Force | Out-Null
Write-Host "Created output directory: $ConfigDir"
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_dump_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "export", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "UpdateInfo") {
Write-Host "Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8" -ForegroundColor Red
exit 1
} elseif ($Mode -eq "Partial") {
$objList = @($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$arguments = @("infobase", "config", "export", "objects") + $objList
$arguments += "--out=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "export", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
$arguments += "-Format", $Format
switch ($Mode) {
"Full" {
Write-Host "Executing full configuration dump..."
}
"Changes" {
Write-Host "Executing incremental configuration dump..."
$arguments += "-update"
$arguments += "-force"
}
"Partial" {
Write-Host "Executing partial configuration dump..."
$objectList = $Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
$listFile = Join-Path $tempDir "dump_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($listFile, $objectList, $utf8Bom)
$arguments += "-listFile", "`"$listFile`""
Write-Host "Objects to dump: $($objectList.Count)"
foreach ($obj in $objectList) { Write-Host " $obj" }
}
"UpdateInfo" {
Write-Host "Updating ConfigDumpInfo.xml..."
$arguments += "-configDumpInfoOnly"
}
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+427 -31
View File
@@ -1,35 +1,356 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-dump-xml v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") while True:
if candidates: pf = os.path.join(d, ".v8-project.json")
candidates.sort() if os.path.isfile(pf):
return candidates[-1] try:
else: with open(pf, encoding="utf-8-sig") as f:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) data = json.load(f)
sys.exit(1) v = data.get("v8path")
elif os.path.isdir(v8path): if v:
v8path = os.path.join(v8path, "1cv8.exe") return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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) sys.exit(1)
return v8path
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
@@ -61,13 +382,41 @@ def main():
choices=["Hierarchical", "Plain"], choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)", 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 --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -81,6 +430,49 @@ def main():
os.makedirs(args.ConfigDir, exist_ok=True) os.makedirs(args.ConfigDir, exist_ok=True)
print(f"Created output directory: {args.ConfigDir}") print(f"Created output directory: {args.ConfigDir}")
# --- ibcmd branch (file infobase only; hierarchical Full/Changes) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "export", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "UpdateInfo":
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8", file=sys.stderr)
sys.exit(1)
elif args.Mode == "Partial":
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
arguments = ["infobase", "config", "export", "objects"] + obj_list
arguments += [f"--out={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "export", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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))
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: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_dump_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -90,16 +482,16 @@ def main():
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"] arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else: else:
arguments += ["/F", args.InfoBasePath] arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: 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] arguments += ["-Format", args.Format]
if args.Mode == "Full": if args.Mode == "Full":
@@ -116,7 +508,7 @@ def main():
with open(list_file, "w", encoding="utf-8-sig") as f: with open(list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(object_list)) f.write("\n".join(object_list))
arguments += ["-listFile", list_file] arguments += ["-listFile", f'"{list_file}"']
print(f"Objects to dump: {len(object_list)}") print(f"Objects to dump: {len(object_list)}")
for obj in object_list: for obj in object_list:
print(f" {obj}") print(f" {obj}")
@@ -126,28 +518,31 @@ def main():
# --- Extensions --- # --- Extensions ---
if args.Extension: if args.Extension:
arguments += ["-Extension", args.Extension] arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions: elif args.AllExtensions:
arguments.append("-AllExtensions") arguments.append("-AllExtensions")
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt") out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", out_file] arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- 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: if exit_code == 0:
print("Dump completed successfully") print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}") 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: else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr) print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
@@ -162,6 +557,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
+161 -158
View File
@@ -1,158 +1,161 @@
--- ---
name: db-list name: db-list
description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть description: Управление реестром баз данных 1С (.v8-project.json). Используй когда нужно работать с реестром баз — список баз, зарегистрировать базу в реестре, какие базы есть
argument-hint: "[add|remove|show]" argument-hint: "[add|remove|show]"
allowed-tools: allowed-tools:
- Read - Read
- Write - Write
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-list — Управление реестром баз данных # /db-list — Управление реестром баз данных
Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git. Управляет файлом `.v8-project.json` — реестром информационных баз проекта. Файл хранит параметры подключения, алиасы, привязку к веткам Git.
## Usage ## Usage
``` ```
/db-list — показать список баз /db-list — показать список баз
/db-list add — добавить базу (интерактивно) /db-list add — добавить базу (интерактивно)
/db-list remove <id> — удалить базу из реестра /db-list remove <id> — удалить базу из реестра
/db-list show <id|alias> — подробности по базе /db-list show <id|alias> — подробности по базе
``` ```
## Формат `.v8-project.json` ## Формат `.v8-project.json`
Файл размещается в корне проекта (рядом с `.git/`). Файл размещается в корне проекта (рядом с `.git/`).
```json ```json
{ {
"v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin", "v8path": "C:\\Program Files\\1cv8\\8.3.25.1257\\bin",
"databases": [ "v8args": ["/UseHwLicenses+"],
{ "databases": [
"id": "dev", {
"name": "Разработка", "id": "dev",
"type": "file", "name": "Разработка",
"path": "C:\\Bases\\MyApp_Dev", "type": "file",
"user": "Admin", "path": "C:\\Bases\\MyApp_Dev",
"password": "", "user": "Admin",
"aliases": ["dev", "разработка"], "password": "",
"branches": ["dev", "develop", "feature/*"], "aliases": ["dev", "разработка"],
"configSrc": "C:\\WS\\myapp\\cfsrc" "branches": ["dev", "develop", "feature/*"],
}, "configSrc": "C:\\WS\\myapp\\cfsrc"
{ },
"id": "test", {
"name": "Тестовая", "id": "test",
"type": "server", "name": "Тестовая",
"server": "srv01", "type": "server",
"ref": "MyApp_Test", "server": "srv01",
"user": "Admin", "ref": "MyApp_Test",
"password": "123", "user": "Admin",
"aliases": ["test", "тест"] "password": "123",
} "aliases": ["test", "тест"]
], }
"default": "dev" ],
} "default": "dev"
``` }
```
### Поля корневого объекта
### Поля корневого объекта
| Поле | Тип | Описание |
|------|-----|----------| | Поле | Тип | Описание |
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение | |------|-----|----------|
| `databases` | array | Массив баз данных | | `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `default` | string | id базы по умолчанию | | `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
### Поля объекта базы данных | `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
| Поле | Тип | Обязательное | Описание |
|------|-----|:------------:|----------| ### Поля объекта базы данных
| `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
| `name` | string | да | Человекочитаемое имя | | Поле | Тип | Обязательное | Описание |
| `type` | `"file"` / `"server"` | да | Тип подключения | |------|-----|:------------:|----------|
| `path` | string | для file | Путь к каталогу файловой базы | | `id` | string | да | Уникальный идентификатор (латиница, без пробелов) |
| `server` | string | для server | Адрес сервера 1С | | `name` | string | да | Человекочитаемое имя |
| `ref` | string | для server | Имя базы на сервере | | `type` | `"file"` / `"server"` | да | Тип подключения |
| `user` | string | нет | Имя пользователя 1С | | `path` | string | для file | Путь к каталогу файловой базы |
| `password` | string | нет | Пароль | | `server` | string | для server | Адрес сервера 1С |
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа | | `ref` | string | для server | Имя базы на сервере |
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе | | `user` | string | нет | Имя пользователя 1С |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации | | `password` | string | нет | Пароль |
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
## Алгоритм разрешения базы данных | `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
## Алгоритм разрешения базы данных
1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке: Этот алгоритм используется ВСЕМИ навыками (`db-*`, `epf-build`, `epf-dump`, `erf-build`, `erf-dump`) для определения целевой базы.
1. По `id` (точное совпадение)
2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой») 1. Если пользователь указал **параметры подключения** (путь, сервер) — используй напрямую
3. По `name` (нечёткое совпадение с учётом морфологии и регистра) 2. Если пользователь указал **базу по имени** — ищи совпадение в таком порядке:
3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`: 1. По `id` (точное совпадение)
- Точное совпадение: ветка `dev``"branches": ["dev"]` 2. По `aliases` (совпадение в массиве с учётом морфологии: «тестовую» = «тестовая» = «тестовой»)
- Glob-паттерн: ветка `release/2.1``"branches": ["release/*"]` 3. По `name` (нечёткое совпадение с учётом морфологии и регистра)
4. Если ветка не совпала — используй `default` 3. Если пользователь **не указал** базу — сопоставь текущую ветку Git с `databases[].branches`:
5. Если не найдено или неоднозначно — спроси пользователя - Точное совпадение: ветка `dev``"branches": ["dev"]`
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл - Glob-паттерн: ветка `release/2.1``"branches": ["release/*"]`
4. Если ветка не совпала — используй `default`
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`. 5. Если не найдено или неоднозначноспроси пользователя
6. Если файл `.v8-project.json` не найден — спроси параметры подключения и предложи создать файл
### Автоопределение платформы
После выполнения: если использованная база не зарегистрирована — предложи добавить через `/db-list add`.
Если `v8path` не задан в конфиге:
### Автоопределение платформы
```powershell
$v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1 Если `v8path` не задан в конфиге:
```
```powershell
## Операции $v8 = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort-Object -Descending | Select-Object -First 1
```
### Показать список баз
## Операции
Прочитай `.v8-project.json`, выведи таблицу:
### Показать список баз
```
ID Имя Тип Путь/Сервер По умолч. Прочитай `.v8-project.json`, выведи таблицу:
dev Разработка file C:\Bases\MyApp_Dev ✓
test Тестовая server srv01/MyApp_Test ```
``` ID Имя Тип Путь/Сервер По умолч.
dev Разработка file C:\Bases\MyApp_Dev ✓
### Добавить базу test Тестовая server srv01/MyApp_Test
```
Спроси у пользователя через AskUserQuestion:
- id, name, type (file/server) ### Добавить базу
- path (для file) или server + ref (для server)
- user, password (необязательно) Спроси у пользователя через AskUserQuestion:
- aliases, branches (необязательно) - id, name, type (file/server)
- path (для file) или server + ref (для server)
Добавь в массив `databases`. Если это первая база — установи как `default`. - user, password (необязательно)
- aliases, branches (необязательно)
### Удалить базу
Добавь в массив `databases`. Если это первая база — установи как `default`.
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
### Удалить базу
### Подробности по базе
Удали из массива `databases` по id. Если удаляемая была `default` — спросить новый default.
Выведи все поля конкретной базы.
### Подробности по базе
## Формирование строки подключения
Выведи все поля конкретной базы.
Для использования в шаблонах команд других навыков:
## Формирование строки подключения
**Файловая база:**
``` Для использования в шаблонах команд других навыков:
/F "<path>"
``` **Файловая база:**
```
**Серверная база:** /F "<path>"
``` ```
/S "<server>/<ref>"
``` **Серверная база:**
```
**Аутентификация** (добавляется если user задан): /S "<server>/<ref>"
``` ```
/N"<user>" /P"<password>"
``` **Аутентификация** (добавляется если user задан):
```
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком. /N"<user>" /P"<password>"
```
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
+75 -81
View File
@@ -1,81 +1,75 @@
--- ---
name: db-load-cf name: db-load-cf
description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF description: Загрузка конфигурации 1С из CF-файла. Используй когда нужно загрузить конфигурацию из CF, восстановить из бэкапа CF
argument-hint: <input.cf> [database] argument-hint: <input.cf> [database]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-load-cf — Загрузка конфигурации из CF-файла # /db-load-cf — Загрузка конфигурации из CF-файла
Загружает конфигурацию из бинарного CF-файла в информационную базу. Загружает конфигурацию из бинарного CF-файла в информационную базу.
## Usage ## Usage
``` ```
/db-load-cf <input.cf> [database] /db-load-cf <input.cf> [database]
/db-load-cf config.cf dev /db-load-cf config.cf dev
``` ```
> **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя. > **Внимание**: загрузка CF **полностью заменяет** конфигурацию в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к CF-файлу | | `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение | | `-Extension <имя>` | нет | Загрузить как расширение |
| `-AllExtensions` | нет | Загрузить все расширения из архива | | `-AllExtensions` | нет | Загрузить все расширения из архива |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Коды возврата > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
| Код | Описание | ## После выполнения
|-----|----------|
| 0 | Успешно | **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg`
| 1 | Ошибка (см. лог) |
## Примеры
## После выполнения
```powershell
1. Прочитай лог-файл и покажи результат # Файловая база
2. **Предложи выполнить `/db-update`** — загрузка CF обновляет только «основную» конфигурацию конфигуратора, для применения к БД нужен `/UpdateDBCfg` powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf"
## Примеры # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
```powershell
# Файловая база # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\config.cf" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
# Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "config.cf"
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "ext.cfe" -Extension "МоёРасширение"
```
+497 -166
View File
@@ -1,166 +1,497 @@
# db-load-cf v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Загрузка конфигурации 1С из CF-файла .SYNOPSIS
Загрузка конфигурации 1С из CF-файла
.DESCRIPTION
Загружает конфигурацию из бинарного CF-файла в информационную базу. .DESCRIPTION
Поддерживает загрузку расширений. Загружает конфигурацию из бинарного CF-файла в информационную базу.
Поддерживает загрузку расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к CF-файлу для загрузки .PARAMETER InputFile
Путь к CF-файлу для загрузки
.PARAMETER Extension
Загрузить как расширение .PARAMETER Extension
Загрузить как расширение
.PARAMETER AllExtensions
Загрузить все расширения из архива .PARAMETER AllExtensions
Загрузить все расширения из архива
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "config.cf"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$true)]
[string]$InputFile, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions [Parameter(Mandatory=$true)]
) [string]$InputFile,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [string]$Extension,
# --- Resolve V8Path --- [Parameter(Mandatory=$false)]
if (-not $V8Path) { [switch]$AllExtensions,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red function Get-ExitAnnotation {
exit 1 # 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;
# --- Validate input file --- # POSIX signals are handled in the .py port.)
if (-not (Test-Path $InputFile)) { param([int]$Code)
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red $win = @{
exit 1 -1073741819 = "0xC0000005 (access violation)"
} -1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
# --- Temp dir --- }
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)" if ($win.ContainsKey($Code)) {
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
}
try { return ""
# --- Build arguments --- }
$arguments = @("DESIGNER")
# --- Additional platform arguments ---
if ($InfoBaseServer -and $InfoBaseRef) { $script:V8OwnedKeys = @(
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
} else { '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
$arguments += "/F", "`"$InfoBasePath`"" '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
} '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
if ($UserName) { $arguments += "/N`"$UserName`"" } '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
if ($Password) { $arguments += "/P`"$Password`"" } )
$script:IbcmdOwnedKeys = @(
$arguments += "/LoadCfg", "`"$InputFile`"" '--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
# --- Extensions --- '--user', '--password'
if ($Extension) { )
$arguments += "-Extension", "`"$Extension`"" $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
} elseif ($AllExtensions) { $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
$arguments += "-AllExtensions"
} function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# --- Output --- # character is not a letter — catches glued /N"user" and --password=x, while
$outFile = Join-Path $tempDir "load_cf_log.txt" # keeping /ClearCache distinct from /C.
$arguments += "/Out", "`"$outFile`"" param([string]$Token, [string]$Key)
$arguments += "/DisableStartupDialogs" if ($Token.Length -lt $Key.Length) { return $false }
if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
# --- Execute --- if ($Token.Length -eq $Key.Length) { return $true }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" return -not [char]::IsLetter($Token[$Key.Length])
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru }
$exitCode = $process.ExitCode
function Get-ProjectExtraArgs {
# --- Result --- # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
if ($exitCode -eq 0) { param([string]$Name)
Write-Host "Configuration loaded successfully from: $InputFile" -ForegroundColor Green $dir = (Get-Location).Path
} else { while ($dir) {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red $pf = Join-Path $dir ".v8-project.json"
} if (Test-Path $pf) {
try {
if (Test-Path $outFile) { $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
if ($logContent) { } catch {}
Write-Host "--- Log ---" return @()
Write-Host $logContent }
Write-Host "--- End ---" $parent = Split-Path $dir -Parent
} if (-not $parent -or $parent -eq $dir) { break }
} $dir = $parent
}
exit $exitCode return @()
}
} finally {
if (Test-Path $tempDir) { function Assert-ExtraArgs {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue # 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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_cf_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config load does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "load", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$InputFile"
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadCfg", "`"$InputFile`""
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_cf_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+421 -29
View File
@@ -1,35 +1,376 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-cf v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -46,12 +387,40 @@ def main():
parser.add_argument("-InputFile", required=True) parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="") parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true") 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) 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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -60,6 +429,31 @@ def main():
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr) print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_cf_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -69,42 +463,39 @@ def main():
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]) arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else: else:
arguments.extend(["/F", args.InfoBasePath]) arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: 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 --- # --- Extensions ---
if args.Extension: if args.Extension:
arguments.extend(["-Extension", args.Extension]) arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions: elif args.AllExtensions:
arguments.append("-AllExtensions") arguments.append("-AllExtensions")
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "load_cf_log.txt") 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.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
if exit_code == 0: if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}") print(f"Configuration loaded successfully from: {args.InputFile}")
else: 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): if os.path.isfile(out_file):
try: try:
@@ -117,6 +508,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
+94
View File
@@ -0,0 +1,94 @@
---
name: db-load-dt
description: Загрузка информационной базы 1С из DT-файла — полная перезапись базы (конфигурация + данные). Используй когда нужно загрузить архив информационной базы, восстановить базу, загрузить dt
disable-model-invocation: true
argument-hint: <input.dt> [database]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-load-dt — Загрузка информационной базы из DT-файла
Восстанавливает информационную базу целиком (конфигурация **+ данные**) из DT-файла.
> ⚠️ **Необратимая операция.** Загрузка `.dt` **полностью перезаписывает базу** — и
> конфигурацию, и все данные. Текущее содержимое базы будет потеряно. После загрузки
> `/db-update` **не нужен** — конфигурация БД уже синхронна внутри снимка.
## Когда НЕ использовать
- Нужно создать **новую** базу из `.dt` → используй `/db-create` (из DT-шаблона), а не загрузку
в существующую.
- Нужно обновить только конфигурацию (без данных) → `/db-load-cf` или `/db-load-xml`.
## Usage
```
/db-load-dt <input.dt> [database]
/db-load-dt backup.dt dev
```
## Порядок действий перед загрузкой
1. Предложи пользователю сначала сделать `/db-dump-dt` текущего состояния базы — это точка
отката (восстановиться будет нечем, если не сохранить).
2. Запроси **явное подтверждение**: вся база (данные + конфигурация) будет перезаписана.
3. Только после подтверждения выполняй загрузку.
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" <параметры>
```
### Параметры скрипта
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к DT-файлу |
| `-JobsCount <N>` | нет | Число фоновых заданий загрузки (0 = по числу процессоров) |
| `-UnlockCode <код>` | нет | Код разблокировки (`/UC`), если заблокировано начало сеансов |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## После выполнения
Если база занята (активные сеансы), загрузка не выполнится — для серверной базы можно
передать `-UnlockCode`; иначе освободи базу и повтори.
## Примеры
```powershell
# Файловая база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -InputFile "C:\backup\base.dt"
# Серверная база с ускорением загрузки
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-dt.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Test" -UserName "Admin" -Password "secret" -InputFile "base.dt" -JobsCount 4
```
## Связанные навыки
- `/db-dump-dt` — выгрузка ИБ в DT (обратная операция, точка отката перед загрузкой)
- `/db-create` — создать новую базу (в т.ч. из DT-шаблона)
@@ -0,0 +1,487 @@
# 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 не исполняется.
<#
.SYNOPSIS
Загрузка информационной базы 1С из DT-файла
.DESCRIPTION
Загружает информационную базу целиком (конфигурация + данные) из DT-файла.
ВНИМАНИЕ: операция полностью перезаписывает базу.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к DT-файлу для загрузки
.PARAMETER JobsCount
Количество фоновых заданий для загрузки (0 = по числу процессоров)
.PARAMETER UnlockCode
Код разблокировки базы (/UC) если заблокировано начало сеансов
.PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$true)]
[string]$InputFile,
[Parameter(Mandatory=$false)]
[int]$JobsCount = 0,
[Parameter(Mandatory=$false)]
[string]$UnlockCode,
[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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_dt_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
$arguments = @("infobase", "restore", "--db-path=$InfoBasePath")
if (-not (Test-Path (Join-Path $InfoBasePath "1Cv8.1CD"))) { $arguments += "--create-database" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "$InputFile"
$arguments += "--data=$tempDir"
$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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UnlockCode) { $arguments += "/UC`"$UnlockCode`"" }
$arguments += "/RestoreIB", "`"$InputFile`""
if ($JobsCount -gt 0) { $arguments += "-JobsCount", "$JobsCount" }
# --- Output ---
$outFile = Join-Path $tempDir "load_dt_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
@@ -0,0 +1,515 @@
#!/usr/bin/env python3
# db-load-dt v1.12 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
import glob
import json
import os
import random
import re
import shutil
import subprocess
import sys
import tempfile
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
# --- 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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path):
print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(
description="Load 1C information base from DT file",
allow_abbrev=False,
)
parser.add_argument("-V8Path", default="")
parser.add_argument("-InfoBasePath", default="")
parser.add_argument("-InfoBaseServer", default="")
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-InputFile", required=True)
parser.add_argument("-JobsCount", type=int, default=0)
parser.add_argument("-UnlockCode", default="")
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:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1)
# --- Validate input file ---
if not os.path.isfile(args.InputFile):
print(f"Error: input file not found: {args.InputFile}", file=sys.stderr)
sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
arguments = ["infobase", "restore", f"--db-path={args.InfoBasePath}"]
if not os.path.isfile(os.path.join(args.InfoBasePath, "1Cv8.1CD")):
arguments.append("--create-database")
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(args.InputFile)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
arguments.append(f"--data={ib_data}")
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}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_dt_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True)
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
if args.UnlockCode:
arguments.append(f'/UC"{args.UnlockCode}"')
arguments.extend(["/RestoreIB", 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", f'"{out_file}"'])
arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute ---
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}){describe_exit(exit_code)}", file=sys.stderr)
if os.path.isfile(out_file):
try:
with open(out_file, "r", encoding="utf-8-sig") as f:
log_content = f.read()
if log_content:
print("--- Log ---")
print(log_content)
print("--- End ---")
except Exception:
pass
print_platform_output(result)
sys.exit(exit_code)
finally:
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+79 -78
View File
@@ -1,78 +1,79 @@
--- ---
name: db-load-git name: db-load-git
description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита description: Загрузка изменений из Git в базу 1С. Используй когда нужно загрузить изменения из гита, обновить базу из репозитория, partial load из коммита
argument-hint: "[database] [source]" argument-hint: "[database] [source]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-load-git — Загрузка изменений из Git # /db-load-git — Загрузка изменений из Git
Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу. Определяет изменённые файлы конфигурации по данным Git и выполняет частичную загрузку в информационную базу.
## Usage ## Usage
``` ```
/db-load-git [database] /db-load-git [database]
/db-load-git dev — все незафиксированные изменения /db-load-git dev — все незафиксированные изменения
/db-load-git dev -Source Staged — только staged /db-load-git dev -Source Staged — только staged
/db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD" /db-load-git dev -Source Commit -CommitRange "HEAD~3..HEAD"
/db-load-git dev -DryRun — только показать что будет загружено /db-load-git dev -DryRun — только показать что будет загружено
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог конфигурации. Если в записи базы указан `configSrc` — используй как каталог конфигурации.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) | | `-ConfigDir <путь>` | да | Каталог XML-выгрузки (git-репозиторий) |
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` | | `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) | | `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение | | `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения | | `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) | | `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## После выполнения > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
1. Показать список загруженных файлов и результат из лога ## После выполнения
2. Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
Если `-UpdateDB` не был указан — **предложить `/db-update`** для применения изменений к БД
## Примеры
## Примеры
```powershell
# Все незафиксированные изменения ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB # Все незафиксированные изменения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source All -UpdateDB
# Из диапазона коммитов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD" # Из диапазона коммитов
``` powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\WS\cfsrc" -Source Commit -CommitRange "HEAD~3..HEAD"
```
File diff suppressed because it is too large Load Diff
+457 -36
View File
@@ -1,9 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-git v1.3 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re import re
@@ -13,24 +15,327 @@ import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") while True:
if candidates: pf = os.path.join(d, ".v8-project.json")
candidates.sort() if os.path.isfile(pf):
return candidates[-1] try:
else: with open(pf, encoding="utf-8-sig") as f:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) data = json.load(f)
sys.exit(1) v = data.get("v8path")
elif os.path.isdir(v8path): if v:
v8path = os.path.join(v8path, "1cv8.exe") return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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) sys.exit(1)
return v8path
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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): def get_object_xml_from_subfile(relative_path):
@@ -44,7 +349,7 @@ def get_object_xml_from_subfile(relative_path):
def run_git(config_dir, git_args): def run_git(config_dir, git_args):
"""Run a git command in config_dir and return output lines on success.""" """Run a git command in config_dir and return output lines on success."""
result = subprocess.run( result = subprocess.run(
["git"] + git_args, ["git", "-c", "core.quotePath=false"] + git_args,
capture_output=True, capture_output=True,
text=True, text=True,
encoding="utf-8", encoding="utf-8",
@@ -55,6 +360,39 @@ def run_git(config_dir, git_args):
return [] 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(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -86,19 +424,48 @@ def main():
) )
parser.add_argument("-DryRun", action="store_true", help="Only show what would be loaded (no actual load)") 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") 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) --- # --- Resolve V8Path (skip if DryRun) ---
v8path = None v8path = None
if not args.DryRun: if not args.DryRun:
v8path = resolve_v8path(args.V8Path) v8path = resolve_v8path(args.V8Path)
# --- Validate connection (skip if DryRun) --- # --- Detect engine + validate connection (skip if DryRun) ---
engine = "1cv8"
if not args.DryRun: if not args.DryRun:
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): engine = "ibcmd" if os.path.basename(v8path).lower().startswith("ibcmd") else "1cv8"
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) 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 --- # --- Validate config dir ---
if not os.path.exists(args.ConfigDir): if not os.path.exists(args.ConfigDir):
print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr) print(f"Error: config directory not found: {args.ConfigDir}", file=sys.stderr)
@@ -146,14 +513,19 @@ def main():
# --- Filter and map to config files --- # --- Filter and map to config files ---
config_files = [] config_files = []
support_skipped = []
for file in changed_files: for file in changed_files:
file = file.strip().replace("\\", "/") file = file.strip().replace("\\", "/")
if not file: if not file:
continue continue
# Skip service files # Skip service files (not partially loadable). Support-state files are
if file == "ConfigDumpInfo.xml": # tracked to warn: support changes apply only via a full load.
if file.endswith("ParentConfigurations.bin"):
support_skipped.append(file)
continue
if file == "ConfigDumpInfo.xml" or file.endswith("/ConfigDumpInfo.xml"):
continue continue
full_path = os.path.join(args.ConfigDir, file) full_path = os.path.join(args.ConfigDir, file)
@@ -186,6 +558,12 @@ def main():
if rel_path not in config_files: if rel_path not in config_files:
config_files.append(rel_path) config_files.append(rel_path)
if support_skipped:
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
for sf in support_skipped:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
if len(config_files) == 0: if len(config_files) == 0:
print("No configuration files found in changes") print("No configuration files found in changes")
sys.exit(0) sys.exit(0)
@@ -205,6 +583,51 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch (file infobase only; import specific files) ---
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + config_files
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
print(f"Changes loaded successfully ({len(config_files)} files)")
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
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}){describe_exit(exit_code)}", file=sys.stderr)
print_platform_output(ar)
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) --- # --- Write list file (UTF-8 with BOM) ---
list_file = os.path.join(temp_dir, "load_list.txt") list_file = os.path.join(temp_dir, "load_list.txt")
with open(list_file, "w", encoding="utf-8-sig") as f: with open(list_file, "w", encoding="utf-8-sig") as f:
@@ -214,24 +637,24 @@ def main():
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"] arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else: else:
arguments += ["/F", args.InfoBasePath] arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: if args.Password:
arguments.append(f"/P{args.Password}") arguments.append(f'/P"{args.Password}"')
arguments += ["/LoadConfigFromFiles", args.ConfigDir] arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", list_file] arguments += ["-listFile", f'"{list_file}"']
arguments += ["-Format", args.Format] arguments += ["-Format", args.Format]
arguments.append("-partial") arguments.append("-partial")
arguments.append("-updateConfigDumpInfo") arguments.append("-updateConfigDumpInfo")
# --- Extensions --- # --- Extensions ---
if args.Extension: if args.Extension:
arguments += ["-Extension", args.Extension] arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions: elif args.AllExtensions:
arguments.append("-AllExtensions") arguments.append("-AllExtensions")
@@ -241,19 +664,16 @@ def main():
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt") out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", out_file] arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
print("") print("")
print("Executing partial configuration load...") 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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
@@ -261,7 +681,7 @@ def main():
if exit_code == 0: if exit_code == 0:
print("Load completed successfully") print("Load completed successfully")
else: 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): if os.path.isfile(out_file):
try: try:
@@ -274,6 +694,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
+103 -109
View File
@@ -1,109 +1,103 @@
--- ---
name: db-load-xml name: db-load-xml
description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles description: Загрузка конфигурации 1С из XML-файлов. Используй когда нужно загрузить конфигурацию из файлов, XML, исходников, LoadConfigFromFiles
argument-hint: <configDir> [database] argument-hint: <configDir> [database]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-load-xml — Загрузка конфигурации из XML # /db-load-xml — Загрузка конфигурации из XML
Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку. Загружает конфигурацию в информационную базу из XML-файлов (исходников). Поддерживает полную и частичную загрузку.
## Usage ## Usage
``` ```
/db-load-xml <configDir> [database] /db-load-xml <configDir> [database]
/db-load-xml src/config dev /db-load-xml src/config dev
/db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" /db-load-xml src/config dev -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
``` ```
> **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя. > **Внимание**: полная загрузка **заменяет всю конфигурацию** в базе. Перед выполнением запроси подтверждение у пользователя.
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию. Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-ConfigDir <путь>` | да | Каталог XML-исходников | | `-ConfigDir <путь>` | да | Каталог XML-исходников |
| `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` | | `-Mode <режим>` | нет | `Full` (по умолч.) / `Partial` |
| `-Files <список>` | для Partial | Относительные пути файлов через запятую | | `-Files <список>` | для Partial | Относительные пути файлов через запятую |
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) | | `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
| `-Extension <имя>` | нет | Загрузить в расширение | | `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения | | `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) | | `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
### Режимы загрузки > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
| Режим | Описание | ### Режимы загрузки
|-------|----------|
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML | | Режим | Описание |
| `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) | |-------|----------|
| `Full` | Полная загрузка — замена всей конфигурации из каталога XML |
### Формат файла списка (listFile) | `Partial` | Частичная — загрузка выбранных файлов (с `-partial -updateConfigDumpInfo`) |
Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**: ### Формат файла списка (listFile)
``` Файл содержит **относительные пути к файлам** в каталоге выгрузки (один на строку), кодировка **UTF-8 с BOM**:
Catalogs/Номенклатура.xml
Catalogs/Номенклатура/Ext/ObjectModule.bsl ```
Documents/Заказ.xml Catalogs/Номенклатура.xml
Documents/Заказ/Forms/ФормаДокумента.xml Catalogs/Номенклатура/Ext/ObjectModule.bsl
``` Documents/Заказ.xml
Documents/Заказ/Forms/ФормаДокумента.xml
## Коды возврата ```
| Код | Описание | ## После выполнения
|-----|----------|
| 0 | Успешно | Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД
| 1 | Ошибка (см. лог) |
## Примеры
## После выполнения
```powershell
1. Прочитай лог и покажи результат # Полная загрузка
2. Если `-UpdateDB` не был указан — **предложи выполнить `/db-update`** для применения изменений к БД powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full
## Примеры # Частичная загрузка конкретных файлов
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
```powershell
# Полная загрузка # Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -V8Path "C:\Program Files\1cv8\8.3.25.1257\bin" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Частичная загрузка конкретных файлов # Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
# Загрузка расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\ext_src" -Mode Full -Extension "МоёРасширение"
# Загрузка + обновление БД в одном запуске
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -ConfigDir "C:\WS\cfsrc" -Mode Full -UpdateDB
```
+664 -279
View File
@@ -1,279 +1,664 @@
# db-load-xml v1.3 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Загрузка конфигурации 1С из XML-файлов .SYNOPSIS
Загрузка конфигурации 1С из XML-файлов
.DESCRIPTION
Загружает конфигурацию в информационную базу из XML-файлов. .DESCRIPTION
Поддерживает полную и частичную загрузку. Загружает конфигурацию в информационную базу из XML-файлов.
Поддерживает полную и частичную загрузку.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER ConfigDir
Каталог XML-исходников конфигурации .PARAMETER ConfigDir
Каталог XML-исходников конфигурации
.PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full) .PARAMETER Mode
Режим загрузки: Full или Partial (по умолчанию Full)
.PARAMETER Files
Относительные пути файлов через запятую (для режима Partial) .PARAMETER Files
Относительные пути файлов через запятую (для режима Partial)
.PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial) .PARAMETER ListFile
Путь к файлу со списком файлов (альтернатива -Files, для режима Partial)
.PARAMETER Extension
Имя расширения для загрузки .PARAMETER Extension
Имя расширения для загрузки
.PARAMETER AllExtensions
Загрузить все расширения .PARAMETER AllExtensions
Загрузить все расширения
.PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат файлов: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Full
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$true)]
[string]$ConfigDir, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Partial")] [Parameter(Mandatory=$false)]
[string]$Mode = "Full", [string]$Password,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$true)]
[string]$Files, [string]$ConfigDir,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[string]$ListFile, [ValidateSet("Full", "Partial")]
[string]$Mode = "Full",
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$Files,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[string]$ListFile,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$false)]
[string]$Format = "Hierarchical", [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$UpdateDB, [switch]$AllExtensions,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$StrictLog [ValidateSet("Hierarchical", "Plain")]
) [string]$Format = "Hierarchical",
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [switch]$UpdateDB,
# --- Resolve V8Path --- [Parameter(Mandatory=$false)]
if (-not $V8Path) { [switch]$StrictLog,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red function Get-ExitAnnotation {
exit 1 # 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;
# --- Validate config dir --- # POSIX signals are handled in the .py port.)
if (-not (Test-Path $ConfigDir)) { param([int]$Code)
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red $win = @{
exit 1 -1073741819 = "0xC0000005 (access violation)"
} -1073741515 = "0xC0000135 (missing DLL)"
-1073740791 = "0xC0000409 (stack overrun)"
# --- Validate Partial mode --- }
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) { if ($win.ContainsKey($Code)) {
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
exit 1 }
} return ""
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)" # --- Additional platform arguments ---
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null $script:V8OwnedKeys = @(
'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
try { '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
# --- Build arguments --- '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
$arguments = @("DESIGNER") '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
if ($InfoBaseServer -and $InfoBaseRef) { '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" )
} else { $script:IbcmdOwnedKeys = @(
$arguments += "/F", "`"$InfoBasePath`"" '--db-path', '--data', '--out', '--file', '--load', '--restore',
} '--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
if ($UserName) { $arguments += "/N`"$UserName`"" } )
if ($Password) { $arguments += "/P`"$Password`"" } $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
function Test-ArgKeyMatch {
if ($Mode -eq "Full") { # A token matches a key when it equals the key, or starts with it and the next
Write-Host "Executing full configuration load..." # character is not a letter — catches glued /N"user" and --password=x, while
} else { # keeping /ClearCache distinct from /C.
Write-Host "Executing partial configuration load..." param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
# Build list file if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
$generatedListFile = $null if ($Token.Length -eq $Key.Length) { return $true }
if ($ListFile) { return -not [char]::IsLetter($Token[$Key.Length])
# Use provided list file }
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red function Get-ProjectExtraArgs {
exit 1 # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
} param([string]$Name)
$generatedListFile = $ListFile $dir = (Get-Location).Path
} else { while ($dir) {
# Generate from -Files parameter $pf = Join-Path $dir ".v8-project.json"
$fileList = $Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ } if (Test-Path $pf) {
$generatedListFile = Join-Path $tempDir "load_list.txt" try {
$utf8Bom = New-Object System.Text.UTF8Encoding($true) $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom) if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
Write-Host "Files to load: $($fileList.Count)" return @()
foreach ($f in $fileList) { Write-Host " $f" } }
} $parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$arguments += "-listFile", "`"$generatedListFile`"" $dir = $parent
$arguments += "-partial" }
$arguments += "-updateConfigDumpInfo" return @()
} }
$arguments += "-Format", $Format function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
# --- Extensions --- # output key fails with an opaque 1C error — reject what the skill owns itself.
if ($Extension) { param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$arguments += "-Extension", "`"$Extension`"" $paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
} elseif ($AllExtensions) { $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
$arguments += "-AllExtensions" 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
# --- UpdateDB --- exit 1
if ($UpdateDB) { }
$arguments += "/UpdateDBCfg" foreach ($k in $owned) {
} if (Test-ArgKeyMatch $tok $k) {
$hint = ''
# --- Output --- if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
$outFile = Join-Path $tempDir "load_log.txt" Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
$arguments += "/Out", "`"$outFile`"" exit 1
$arguments += "/DisableStartupDialogs" }
}
# --- Execute --- }
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru
$exitCode = $process.ExitCode function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
# --- Read log --- # parameter for the other engine is an error; the same keys coming from .v8-project.json
$logContent = $null # simply do not apply — a project may describe both engines.
if (Test-Path $outFile) { param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue # 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;
# --- Scan log for silent rejections --- # a native array call keeps working. A value containing a comma is not supported.
# Platform often writes load-time rejections into /Out but exits with code 0. $V8Extra = @($V8Extra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
# These patterns flag cases where metadata was dropped or rejected silently. $IbcmdExtra = @($IbcmdExtra | ForEach-Object { $_ -split ',' } | Where-Object { $_ -ne '' })
$fatalLogPatterns = @( 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') {
$silentFailures = @() $extra = @(Get-ProjectExtraArgs 'ibcmdargs') + @($IbcmdExtra)
if ($logContent) { } else {
foreach ($line in ($logContent -split "`r?`n")) { $extra = @(Get-ProjectExtraArgs 'v8args') + @($V8Extra)
foreach ($pat in $fatalLogPatterns) { }
if ($line -match [regex]::Escape($pat)) { if ($extra.Count -gt 0) { Assert-ExtraArgs $extra $Engine $Hints }
$silentFailures += $line.Trim() # Plain return, no comma trick: the caller re-collects with @(...), and ,@() there
break # 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.
# --- Result --- # Matching here is a plain prefix (no letter rule): over-masking costs nothing,
# Default: mirror platform's verdict via exit code. Log content (including any # a leaked password does.
# rejection warnings) is always printed to stdout for visibility. With -StrictLog, param([string[]]$ArgList, [string]$Engine)
# elevate exit code to 1 when rejection patterns are found even if platform said 0. $keys = if ($Engine -eq 'ibcmd') { $script:IbcmdSecretKeys } else { $script:V8SecretKeys }
if ($exitCode -eq 0) { $res = @()
Write-Host "Load completed successfully" -ForegroundColor Green $maskNext = $false
} else { foreach ($tok in $ArgList) {
Write-Host "Error loading configuration (code: $exitCode)" -ForegroundColor Red if ($maskNext) { $res += '***'; $maskNext = $false; continue }
} $hit = $null
foreach ($k in $keys) {
if ($logContent) { if ($tok.Length -ge $k.Length -and $tok.Substring(0, $k.Length).Equals($k, [System.StringComparison]::OrdinalIgnoreCase)) { $hit = $k; break }
Write-Host "--- Log ---" }
Write-Host $logContent if (-not $hit) { $res += $tok; continue }
Write-Host "--- End ---" if ($tok.Length -eq $hit.Length) { $res += $tok; $maskNext = $true }
} elseif ($tok[$hit.Length] -eq '=') { $res += ($hit + '=***') }
else { $res += ($hit + '***') }
if ($silentFailures.Count -gt 0) { }
$msg = "[warning] log contains $($silentFailures.Count) rejection(s) — platform loaded config but dropped properties/refs" return ,$res
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" } }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow } function ConvertTo-CleanPath {
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 } # 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
exit $exitCode # 1C answer with its opaque "Неверные или отсутствующие параметры соединения".
param([string]$Value, [string]$ParamName)
} finally { if (-not $Value) { return $Value }
if (Test-Path $tempDir) { $v = $Value.Trim()
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue 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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Validate config dir ---
if (-not (Test-Path $ConfigDir)) {
Write-Host "Error: config directory not found: $ConfigDir" -ForegroundColor Red
exit 1
}
# --- Validate Partial mode ---
if ($Mode -eq "Partial" -and -not $Files -and -not $ListFile) {
Write-Host "Error: -Files or -ListFile required for Partial mode" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_load_xml_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
# partial: import specific files (relative to ConfigDir)
$fileList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$fileList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} elseif ($Files) {
$fileList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
if ($fileList.Count -eq 0) {
Write-Host "Error: -Files or -ListFile required for partial import" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "import", "files") + $fileList
$arguments += "--base-dir=$ConfigDir", "--db-path=$InfoBasePath"
if ($Extension) { $arguments += "--extension=$Extension" }
} else {
$arguments = @("infobase", "config", "import", "--db-path=$InfoBasePath")
if ($Extension) { $arguments += "--extension=$Extension" }
$arguments += "$ConfigDir"
}
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
Write-PlatformOutput $output
exit $exitCode
}
Write-Host "Configuration loaded successfully from: $ConfigDir" -ForegroundColor Green
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"
$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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
Write-PlatformOutput $applyOut
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
if ($Mode -eq "Full") {
Write-Host "Executing full configuration load..."
} else {
Write-Host "Executing partial configuration load..."
# Build list file
$rawList = @()
if ($ListFile) {
if (-not (Test-Path $ListFile)) {
Write-Host "Error: list file not found: $ListFile" -ForegroundColor Red
exit 1
}
$rawList = @(Get-Content -Path $ListFile -Encoding UTF8 | ForEach-Object { $_.Trim() } | Where-Object { $_ })
} else {
$rawList = @($Files -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
}
# Support-state service files are NOT partially loadable — exclude with a hint.
$supportRe = 'ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$'
$supportFiles = @($rawList | Where-Object { $_ -match $supportRe })
$fileList = @($rawList | Where-Object { $_ -notmatch $supportRe })
if ($supportFiles.Count -gt 0) {
Write-Host "[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):" -ForegroundColor Yellow
foreach ($sf in $supportFiles) { Write-Host " - $sf" -ForegroundColor Yellow }
Write-Host " Смена состояния поддержки применяется только полной загрузкой: -Mode Full." -ForegroundColor Yellow
}
if ($fileList.Count -eq 0) {
Write-Host "Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full." -ForegroundColor Red
exit 1
}
$generatedListFile = Join-Path $tempDir "load_list.txt"
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllLines($generatedListFile, $fileList, $utf8Bom)
Write-Host "Files to load: $($fileList.Count)"
foreach ($f in $fileList) { Write-Host " $f" }
$arguments += "-listFile", "`"$generatedListFile`""
$arguments += "-partial"
$arguments += "-updateConfigDumpInfo"
}
$arguments += "-Format", $Format
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- UpdateDB ---
if ($UpdateDB) {
$arguments += "/UpdateDBCfg"
}
# --- Output ---
$outFile = Join-Path $tempDir "load_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
}
# --- Scan log for silent rejections ---
# Platform often writes load-time rejections into /Out but exits with code 0.
# These patterns flag cases where metadata was dropped or rejected silently.
$fatalLogPatterns = @(
'Неверное свойство объекта метаданных',
'не входит в состав объекта метаданных',
'Неизвестное имя типа',
'Неизвестный объект метаданных',
'Ни один из документов не является регистратором для регистра',
'Неверное значение перечисления',
'не может быть приведен к типу'
)
$silentFailures = @()
if ($logContent) {
foreach ($line in ($logContent -split "`r?`n")) {
foreach ($pat in $fatalLogPatterns) {
if ($line -match [regex]::Escape($pat)) {
$silentFailures += $line.Trim()
break
}
}
}
}
# --- Result ---
# Default: mirror platform's verdict via exit code. Log content (including any
# rejection warnings) is always printed to stdout for visibility. With -StrictLog,
# elevate exit code to 1 when rejection patterns are found even if platform said 0.
if ($exitCode -eq 0) {
Write-Host "Load completed successfully" -ForegroundColor Green
} else {
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if ($logContent) {
Write-Host "--- Log ---"
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"
if (-not $StrictLog) { $msg += " (pass -StrictLog to treat as error)" }
Write-Host $msg -ForegroundColor Yellow
foreach ($f in $silentFailures) { Write-Host " $f" -ForegroundColor Yellow }
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+484 -43
View File
@@ -1,35 +1,374 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-load-xml v1.3 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") while True:
if candidates: pf = os.path.join(d, ".v8-project.json")
candidates.sort() if os.path.isfile(pf):
return candidates[-1] try:
else: with open(pf, encoding="utf-8-sig") as f:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) data = json.load(f)
sys.exit(1) v = data.get("v8path")
elif os.path.isdir(v8path): if v:
v8path = os.path.join(v8path, "1cv8.exe") return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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) sys.exit(1)
return v8path
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
@@ -68,13 +407,43 @@ def main():
action="store_true", action="store_true",
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)", 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 --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -88,6 +457,70 @@ def main():
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr) print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only; hierarchical full-directory import) ---
if engine == "ibcmd":
if args.Format == "Plain":
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
if args.AllExtensions:
arguments = ["infobase", "config", "import", "all-extensions", args.ConfigDir, f"--db-path={args.InfoBasePath}"]
elif args.Mode == "Partial" or args.Files or args.ListFile:
# partial: import specific files (relative to ConfigDir)
if args.ListFile:
if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1)
with open(args.ListFile, encoding="utf-8-sig") as f:
file_list = [ln.strip() for ln in f if ln.strip()]
elif args.Files:
file_list = [p.strip() for p in args.Files.split(",") if p.strip()]
else:
file_list = []
if not file_list:
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + file_list
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
else:
arguments = ["infobase", "config", "import", f"--db-path={args.InfoBasePath}"]
if args.Extension:
arguments.append(f"--extension={args.Extension}")
arguments.append(args.ConfigDir)
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
print(f"Configuration loaded successfully from: {args.ConfigDir}")
exit_code = 0
if args.UpdateDB:
apply_args = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.UserName:
apply_args.append(f"--user={args.UserName}")
if args.Password:
apply_args.append(f"--password={args.Password}")
apply_args.append(f"--data={ib_data}")
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}){describe_exit(exit_code)}", file=sys.stderr)
print_platform_output(ar)
sys.exit(exit_code)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_load_xml_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -97,16 +530,16 @@ def main():
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"] arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else: else:
arguments += ["/F", args.InfoBasePath] arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: 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": if args.Mode == "Full":
print("Executing full configuration load...") print("Executing full configuration load...")
@@ -114,25 +547,35 @@ def main():
print("Executing partial configuration load...") print("Executing partial configuration load...")
# Build list file # Build list file
generated_list_file = None
if args.ListFile: if args.ListFile:
# Use provided list file
if not os.path.isfile(args.ListFile): if not os.path.isfile(args.ListFile):
print(f"Error: list file not found: {args.ListFile}", file=sys.stderr) print(f"Error: list file not found: {args.ListFile}", file=sys.stderr)
sys.exit(1) sys.exit(1)
generated_list_file = args.ListFile with open(args.ListFile, encoding="utf-8-sig") as f:
raw_list = [ln.strip() for ln in f if ln.strip()]
else: else:
# Generate from -Files parameter raw_list = [f.strip() for f in args.Files.split(",") if f.strip()]
file_list = [f.strip() for f in args.Files.split(",") if f.strip()]
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}") # Support-state service files are NOT partially loadable — exclude with a hint.
for fl in file_list: support_re = re.compile(r"ParentConfigurations\.bin$|(^|[\\/])ConfigDumpInfo\.xml$")
print(f" {fl}") support_files = [x for x in raw_list if support_re.search(x)]
file_list = [x for x in raw_list if not support_re.search(x)]
if support_files:
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):", file=sys.stderr)
for sf in support_files:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
if not file_list:
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
sys.exit(1)
generated_list_file = os.path.join(temp_dir, "load_list.txt")
with open(generated_list_file, "w", encoding="utf-8-sig") as f:
f.write("\n".join(file_list))
print(f"Files to load: {len(file_list)}")
for fl in file_list:
print(f" {fl}")
arguments += ["-listFile", generated_list_file] arguments += ["-listFile", f'"{generated_list_file}"']
arguments.append("-partial") arguments.append("-partial")
arguments.append("-updateConfigDumpInfo") arguments.append("-updateConfigDumpInfo")
@@ -140,7 +583,7 @@ def main():
# --- Extensions --- # --- Extensions ---
if args.Extension: if args.Extension:
arguments += ["-Extension", args.Extension] arguments += ["-Extension", f'"{args.Extension}"']
elif args.AllExtensions: elif args.AllExtensions:
arguments.append("-AllExtensions") arguments.append("-AllExtensions")
@@ -150,16 +593,13 @@ def main():
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "load_log.txt") out_file = os.path.join(temp_dir, "load_log.txt")
arguments += ["/Out", out_file] arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Read log --- # --- Read log ---
@@ -198,13 +638,14 @@ def main():
if exit_code == 0: if exit_code == 0:
print("Load completed successfully") print("Load completed successfully")
else: 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: if log_content:
print("--- Log ---") print("--- Log ---")
print(log_content) print(log_content)
print("--- End ---") print("--- End ---")
print_platform_output(result)
if silent_failures: if silent_failures:
suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)" suffix = "" if args.StrictLog else " (pass -StrictLog to treat as error)"
print( print(
+77 -76
View File
@@ -1,76 +1,77 @@
--- ---
name: db-run name: db-run
description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие description: Запуск 1С:Предприятие. Используй когда нужно запустить 1С, открыть базу, запустить предприятие
argument-hint: "[database]" argument-hint: "[database]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-run — Запуск 1С:Предприятие # /db-run — Запуск 1С:Предприятие
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
## Usage ## Usage
``` ```
/db-run [database] /db-run [database]
/db-run dev /db-run dev
/db-run dev /Execute process.epf /db-run dev /Execute process.epf
/db-run dev /C "параметр запуска" /db-run dev /C "параметр запуска"
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта | | `-Execute <файл.epf>` | нет | Запуск внешней обработки сразу после старта |
| `-CParam <строка>` | нет | Параметр запуска (/C) | | `-CParam <строка>` | нет | Параметр запуска (/C) |
| `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) | | `-URL <ссылка>` | нет | Навигационная ссылка (формат `e1cib/...`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
## Важно
## Важно
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
Скрипт запускает 1С в фоне (`Start-Process` без `-Wait`) — управление возвращается сразу.
## Примеры
## Примеры
```powershell
# Простой запуск ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" # Простой запуск
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf" # Запуск с обработкой
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Execute "C:\epf\МояОбработка.epf"
# Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура" # Открыть по навигационной ссылке
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -URL "e1cib/data/Справочник.Номенклатура"
# Серверная база с параметром запуска
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление" # Серверная база с параметром запуска
``` powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-run.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -CParam "ЗапуститьОбновление"
```
+369 -145
View File
@@ -1,145 +1,369 @@
# db-run v1.0 — Launch 1C:Enterprise # db-run v1.7 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Запуск 1С:Предприятие .SYNOPSIS
Запуск 1С:Предприятие
.DESCRIPTION
Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим). .DESCRIPTION
Запуск в фоне не ждёт завершения процесса. Запускает информационную базу в режиме 1С:Предприятие (пользовательский режим).
Запуск в фоне не ждёт завершения процесса.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER Execute
Путь к внешней обработке для запуска .PARAMETER Execute
Путь к внешней обработке для запуска
.PARAMETER CParam
Параметр запуска (/C) .PARAMETER CParam
Параметр запуска (/C)
.PARAMETER URL
Навигационная ссылка (e1cib/...) .PARAMETER URL
Навигационная ссылка (e1cib/...)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf" .PARAMETER AdditionalIbcmdArguments
Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
.EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление" .EXAMPLE
#> .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB"
[CmdletBinding()] .EXAMPLE
param( .\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -Execute "C:\epf\МояОбработка.epf"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$Execute, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[string]$CParam, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[string]$URL [Parameter(Mandatory=$false)]
) [string]$Execute,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [string]$CParam,
# --- Resolve V8Path --- [Parameter(Mandatory=$false)]
if (-not $V8Path) { [string]$URL,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red # --- Additional platform arguments ---
exit 1 $script:V8OwnedKeys = @(
} 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
# --- Build arguments as single string --- '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
# Note: Start-Process without -NoNewWindow uses ShellExecute. '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
# re-joins elements. Single string avoids this. '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
$argString = "ENTERPRISE" )
$script:IbcmdOwnedKeys = @(
if ($InfoBaseServer -and $InfoBaseRef) { '--db-path', '--data', '--out', '--file', '--load', '--restore',
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`"" '--import', '--export', '--apply', '--force', '--create-database',
} else { '--user', '--password'
$argString += " /F `"$InfoBasePath`"" )
} $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
if ($UserName) { $argString += " /N`"$UserName`"" }
if ($Password) { $argString += " /P`"$Password`"" } function Test-ArgKeyMatch {
# A token matches a key when it equals the key, or starts with it and the next
# --- Optional params --- # character is not a letter — catches glued /N"user" and --password=x, while
if ($Execute) { # keeping /ClearCache distinct from /C.
$ext = [System.IO.Path]::GetExtension($Execute).ToLower() param([string]$Token, [string]$Key)
if ($ext -eq ".erf") { if ($Token.Length -lt $Key.Length) { return $false }
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow if ($Token.Length -eq $Key.Length) { return $true }
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow return -not [char]::IsLetter($Token[$Key.Length])
$Execute = "" }
}
} function Get-ProjectExtraArgs {
if ($Execute) { # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
$argString += " /Execute `"$Execute`"" param([string]$Name)
} $dir = (Get-Location).Path
if ($CParam) { while ($dir) {
$argString += " /C `"$CParam`"" $pf = Join-Path $dir ".v8-project.json"
} if (Test-Path $pf) {
if ($URL) { try {
$argString += " /URL `"$URL`"" $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
} catch {}
$argString += " /DisableStartupDialogs" return @()
}
# --- Execute (background, no wait) --- $parent = Split-Path $dir -Parent
Write-Host "Running: 1cv8.exe $argString" if (-not $parent -or $parent -eq $dir) { break }
Start-Process -FilePath $V8Path -ArgumentList $argString $dir = $parent
Write-Host "1C:Enterprise launched" -ForegroundColor Green }
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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
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
exit 1
}
# --- Build arguments as single string ---
# Note: Start-Process without -NoNewWindow uses ShellExecute.
# Passing ArgumentList as array can corrupt Cyrillic when ShellExecute
# re-joins elements. Single string avoids this.
$argString = "ENTERPRISE"
if ($InfoBaseServer -and $InfoBaseRef) {
$argString += " /S `"$InfoBaseServer/$InfoBaseRef`""
} else {
$argString += " /F `"$InfoBasePath`""
}
if ($UserName) { $argString += " /N`"$UserName`"" }
if ($Password) { $argString += " /P`"$Password`"" }
# --- Optional params ---
if ($Execute) {
$ext = [System.IO.Path]::GetExtension($Execute).ToLower()
if ($ext -eq ".erf") {
Write-Host "[WARN] /Execute не поддерживает ERF-файлы (внешние отчёты)." -ForegroundColor Yellow
Write-Host " Откройте отчёт через «Файл -> Открыть»: $Execute" -ForegroundColor Yellow
Write-Host " Запускаю базу без /Execute." -ForegroundColor Yellow
$Execute = ""
}
}
if ($Execute) {
$argString += " /Execute `"$Execute`""
}
if ($CParam) {
$argString += " /C `"$CParam`""
}
if ($URL) {
$argString += " /URL `"$URL`""
}
$argString += " /DisableStartupDialogs"
# 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
+285 -14
View File
@@ -1,32 +1,264 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-run v1.0 — Launch 1C:Enterprise # db-run v1.7 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import glob import glob
import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import time
def _find_project_v8path():
"""Walk up from CWD to find .v8-project.json and read its v8path."""
d = os.getcwd()
while True:
pf = os.path.join(d, ".v8-project.json")
if os.path.isfile(pf):
try:
with open(pf, encoding="utf-8-sig") as f:
data = json.load(f)
v = data.get("v8path")
if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
d = parent
# --- 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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path): def resolve_v8path(v8path):
"""Resolve path to 1cv8.exe.""" """Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path: if not v8path:
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) v8path = _find_project_v8path()
if found: if not v8path:
return found[-1] if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else: else:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) # PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1) sys.exit(1)
elif os.path.isdir(v8path): if os.path.isdir(v8path):
v8path = os.path.join(v8path, "1cv8.exe") # PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return 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(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -43,10 +275,34 @@ def main():
parser.add_argument("-Execute", default="") parser.add_argument("-Execute", default="")
parser.add_argument("-CParam", default="") parser.add_argument("-CParam", default="")
parser.add_argument("-URL", 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) 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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
@@ -83,10 +339,25 @@ def main():
arguments.extend(["/URL", args.URL]) arguments.extend(["/URL", args.URL])
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(extra_args)
# --- Execute (background, no wait) --- # --- Execute (background) ---
print(f"Running: 1cv8.exe {' '.join(arguments)}") # Redact the password/user before printing the command line — never leak secrets.
subprocess.Popen([v8path] + arguments) 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") print("1C:Enterprise launched")
+88 -93
View File
@@ -1,93 +1,88 @@
--- ---
name: db-update name: db-update
description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg description: Обновление конфигурации базы данных 1С. Используй когда нужно обновить БД, применить конфигурацию, UpdateDBCfg
argument-hint: "[database]" argument-hint: "[database]"
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- AskUserQuestion - AskUserQuestion
--- ---
# /db-update — Обновление конфигурации БД # /db-update — Обновление конфигурации БД
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`. Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
## Usage ## Usage
``` ```
/db-update [database] /db-update [database]
/db-update dev /db-update dev
/db-update dev -Dynamic+ /db-update dev -Dynamic+
``` ```
## Параметры подключения ## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу: Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default` 4. Если ветка не совпала — используй `default`
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если файла нет — предложи `/db-list add`. Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-Extension <имя>` | нет | Обновить расширение | | `-Extension <имя>` | нет | Обновить расширение |
| `-AllExtensions` | нет | Обновить все расширения | | `-AllExtensions` | нет | Обновить все расширения |
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить | | `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-Server` | нет | Обновление на стороне сервера | | `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками | | `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef` | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
### Фоновое обновление (серверная база) > `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
| Параметр | Описание | ### Фоновое обновление (серверная база)
|----------|----------|
| `-BackgroundStart` | Начать фоновое обновление | | Параметр | Описание |
| `-BackgroundFinish` | Дождаться окончания | |----------|----------|
| `-BackgroundCancel` | Отменить | | `-BackgroundStart` | Начать фоновое обновление |
| `-BackgroundSuspend` | Приостановить | | `-BackgroundFinish` | Дождаться окончания |
| `-BackgroundResume` | Возобновить | | `-BackgroundCancel` | Отменить |
| `-BackgroundSuspend` | Приостановить |
## Коды возврата | `-BackgroundResume` | Возобновить |
| Код | Описание | ## Предупреждения
|-----|----------|
| 0 | Успешно | - Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
| 1 | Ошибка (см. лог) | - Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
## Предупреждения
## Примеры
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки ```powershell
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно # Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
## Примеры
# Динамическое обновление (серверная база)
```powershell powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" # Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
# Динамическое обновление (серверная база) ```
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
```
+515 -184
View File
@@ -1,184 +1,515 @@
# db-update v1.0 — Update 1C database configuration # db-update v1.13 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Обновление конфигурации базы данных 1С .SYNOPSIS
Обновление конфигурации базы данных 1С
.DESCRIPTION
Применяет изменения основной конфигурации к конфигурации базы данных. .DESCRIPTION
Поддерживает динамическое обновление, обновление расширений. Применяет изменения основной конфигурации к конфигурации базы данных.
Поддерживает динамическое обновление, обновление расширений.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER Extension
Имя расширения для обновления .PARAMETER Extension
Имя расширения для обновления
.PARAMETER AllExtensions
Обновить все расширения .PARAMETER AllExtensions
Обновить все расширения
.PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить .PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить
.PARAMETER Server
Обновление на стороне сервера .PARAMETER Server
Обновление на стороне сервера
.PARAMETER WarningsAsErrors
Предупреждения считать ошибками .PARAMETER WarningsAsErrors
Предупреждения считать ошибками
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\db-update.ps1 -InfoBasePath "C:\Bases\MyDB"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$false)]
[string]$Extension, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$false)]
[switch]$AllExtensions, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[ValidateSet("+", "-")] [Parameter(Mandatory=$false)]
[string]$Dynamic, [string]$Extension,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$Server, [switch]$AllExtensions,
[Parameter(Mandatory=$false)] [Parameter(Mandatory=$false)]
[switch]$WarningsAsErrors [ValidateSet("+", "-")]
) [string]$Dynamic,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$false)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [switch]$Server,
# --- Resolve V8Path --- [Parameter(Mandatory=$false)]
if (-not $V8Path) { [switch]$WarningsAsErrors,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Validate connection --- }
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red function Get-ExitAnnotation {
exit 1 # 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;
# --- Temp dir --- # POSIX signals are handled in the .py port.)
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)" param([int]$Code)
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null $win = @{
-1073741819 = "0xC0000005 (access violation)"
try { -1073741515 = "0xC0000135 (missing DLL)"
# --- Build arguments --- -1073740791 = "0xC0000409 (stack overrun)"
$arguments = @("DESIGNER") }
if ($win.ContainsKey($Code)) {
if ($InfoBaseServer -and $InfoBaseRef) { return " — abnormal termination, exception $($win[$Code]); the infobase may be left in an inconsistent state; verify it before retrying"
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" }
} else { return ""
$arguments += "/F", "`"$InfoBasePath`"" }
}
# --- Additional platform arguments ---
if ($UserName) { $arguments += "/N`"$UserName`"" } $script:V8OwnedKeys = @(
if ($Password) { $arguments += "/P`"$Password`"" } 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
'/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
$arguments += "/UpdateDBCfg" '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
'/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
# --- Options --- '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
if ($Dynamic) { '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
$arguments += "-Dynamic$Dynamic" )
} $script:IbcmdOwnedKeys = @(
if ($Server) { '--db-path', '--data', '--out', '--file', '--load', '--restore',
$arguments += "-Server" '--import', '--export', '--apply', '--force', '--create-database',
} '--user', '--password'
if ($WarningsAsErrors) { )
$arguments += "-WarningsAsErrors" $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
} $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
# --- Extensions --- function Test-ArgKeyMatch {
if ($Extension) { # A token matches a key when it equals the key, or starts with it and the next
$arguments += "-Extension", "`"$Extension`"" # character is not a letter — catches glued /N"user" and --password=x, while
} elseif ($AllExtensions) { # keeping /ClearCache distinct from /C.
$arguments += "-AllExtensions" 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 }
# --- Output --- if ($Token.Length -eq $Key.Length) { return $true }
$outFile = Join-Path $tempDir "update_log.txt" return -not [char]::IsLetter($Token[$Key.Length])
$arguments += "/Out", "`"$outFile`"" }
$arguments += "/DisableStartupDialogs"
function Get-ProjectExtraArgs {
# --- Execute --- # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" param([string]$Name)
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $dir = (Get-Location).Path
$exitCode = $process.ExitCode while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
# --- Result --- if (Test-Path $pf) {
if ($exitCode -eq 0) { try {
Write-Host "Database configuration updated successfully" -ForegroundColor Green $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} else { if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
Write-Host "Error updating database configuration (code: $exitCode)" -ForegroundColor Red } catch {}
} return @()
}
if (Test-Path $outFile) { $parent = Split-Path $dir -Parent
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue if (-not $parent -or $parent -eq $dir) { break }
if ($logContent) { $dir = $parent
Write-Host "--- Log ---" }
Write-Host $logContent return @()
Write-Host "--- End ---" }
}
} function Assert-ExtraArgs {
# The platform accepts only one batch operation, and a duplicate connection or
exit $exitCode # output key fails with an opaque 1C error — reject what the skill owns itself.
param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
} finally { $paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
if (Test-Path $tempDir) { $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue 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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
exit 1
}
} elseif (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef" -ForegroundColor Red
exit 1
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "db_update_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch (file infobase only) ---
if ($AllExtensions) {
Write-Host "Error: ibcmd config apply does not support -AllExtensions (use -Extension)" -ForegroundColor Red
exit 1
}
$arguments = @("infobase", "config", "apply", "--db-path=$InfoBasePath", "--force")
if ($Dynamic -eq "+") { $arguments += "--dynamic=auto" }
elseif ($Dynamic -eq "-") { $arguments += "--dynamic=disable" }
if ($Extension) { $arguments += "--extension=$Extension" }
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/UpdateDBCfg"
# --- Options ---
if ($Dynamic) {
$arguments += "-Dynamic$Dynamic"
}
if ($Server) {
$arguments += "-Server"
}
if ($WarningsAsErrors) {
$arguments += "-WarningsAsErrors"
}
# --- Extensions ---
if ($Extension) {
$arguments += "-Extension", "`"$Extension`""
} elseif ($AllExtensions) {
$arguments += "-AllExtensions"
}
# --- Output ---
$outFile = Join-Path $tempDir "update_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+423 -28
View File
@@ -1,35 +1,376 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# db-update v1.0 — Update 1C database configuration # db-update v1.13 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
found = sorted(glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")) while True:
if found: pf = os.path.join(d, ".v8-project.json")
return found[-1] if os.path.isfile(pf):
else: try:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) with open(pf, encoding="utf-8-sig") as f:
sys.exit(1) data = json.load(f)
elif os.path.isdir(v8path): v = data.get("v8path")
v8path = os.path.join(v8path, "1cv8.exe") if v:
return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
return v8path return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -48,15 +389,71 @@ def main():
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"]) parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
parser.add_argument("-Server", action="store_true") parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", 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) 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 --- # --- Validate connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
elif not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr) print("Error: specify -InfoBasePath or -InfoBaseServer + -InfoBaseRef", file=sys.stderr)
sys.exit(1) sys.exit(1)
# --- ibcmd branch (file infobase only) ---
if engine == "ibcmd":
if args.AllExtensions:
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)", file=sys.stderr)
sys.exit(1)
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.Dynamic == "+":
arguments.append("--dynamic=auto")
elif args.Dynamic == "-":
arguments.append("--dynamic=disable")
if args.Extension:
arguments.append(f"--extension={args.Extension}")
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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}){describe_exit(result.returncode)}", file=sys.stderr)
sys.exit(result.returncode)
# --- Temp dir --- # --- Temp dir ---
temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}") temp_dir = os.path.join(tempfile.gettempdir(), f"db_update_{random.randint(0, 999999)}")
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
@@ -66,14 +463,14 @@ def main():
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"]) arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else: else:
arguments.extend(["/F", args.InfoBasePath]) arguments.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: if args.Password:
arguments.append(f"/P{args.Password}") arguments.append(f'/P"{args.Password}"')
arguments.append("/UpdateDBCfg") arguments.append("/UpdateDBCfg")
@@ -87,29 +484,26 @@ def main():
# --- Extensions --- # --- Extensions ---
if args.Extension: if args.Extension:
arguments.extend(["-Extension", args.Extension]) arguments.extend(["-Extension", f'"{args.Extension}"'])
elif args.AllExtensions: elif args.AllExtensions:
arguments.append("-AllExtensions") arguments.append("-AllExtensions")
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "update_log.txt") 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.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- Result ---
if exit_code == 0: if exit_code == 0:
print("Database configuration updated successfully") print("Database configuration updated successfully")
else: 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): if os.path.isfile(out_file):
try: try:
@@ -122,6 +516,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
+196 -196
View File
@@ -1,196 +1,196 @@
--- ---
name: epf-bsp-add-command name: epf-bsp-add-command
description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП description: Определить команду в БСП‑описании обработки (`СведенияОВнешнейОбработке`) — открытие формы, вызов клиентского/серверного метода, заполнение объекта и т.п. Используй когда нужно зарегистрировать команду в дополнительной обработке БСП
argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление] argument-hint: <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
allowed-tools: allowed-tools:
- Read - Read
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /epf-bsp-add-command — Добавление команды БСП # /epf-bsp-add-command — Добавление команды БСП
Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик. Добавляет команду в существующую функцию `СведенияОВнешнейОбработке()` и генерирует соответствующий обработчик.
Предварительно обработка должна быть инициализирована через `/epf-bsp-init`. Предварительно обработка должна быть инициализирована через `/epf-bsp-init`.
## Usage ## Usage
``` ```
/epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление] /epf-bsp-add-command <ProcessorName> <Идентификатор> [ТипКоманды] [Представление]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|-----------------------|--------------------------------------------| |---------------|:------------:|-----------------------|--------------------------------------------|
| ProcessorName | да | — | Имя обработки | | ProcessorName | да | — | Имя обработки |
| Идентификатор | да | — | Внутреннее имя команды (латиница) | | Идентификатор | да | — | Внутреннее имя команды (латиница) |
| ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) | | ТипКоманды | нет | из вида обработки | Тип запуска команды (см. маппинг ниже) |
| Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя | | Представление | нет | = Идентификатор | Отображаемое имя команды для пользователя |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
## Маппинг типов команд ## Маппинг типов команд
Пользователь может указать тип в свободной форме: Пользователь может указать тип в свободной форме:
| Пользователь пишет | ТипКоманды | | Пользователь пишет | ТипКоманды |
|---------------------------------------|-----------------------------------------------------| |---------------------------------------|-----------------------------------------------------|
| открыть форму, форма | `ТипКомандыОткрытиеФормы()` | | открыть форму, форма | `ТипКомандыОткрытиеФормы()` |
| клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` | | клиентский метод, на клиенте | `ТипКомандыВызовКлиентскогоМетода()` |
| серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` | | серверный метод, на сервере | `ТипКомандыВызовСерверногоМетода()` |
| заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` | | заполнение формы, заполнить форму | `ТипКомандыЗаполнениеФормы()` |
| сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` | | сценарий, безопасный режим | `ТипКомандыСценарийВБезопасномРежиме()` |
Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`: Если пользователь не указал тип — определи по виду обработки из существующего кода `СведенияОВнешнейОбработке()`:
| Вид обработки (из кода) | ТипКоманды по умолчанию | | Вид обработки (из кода) | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------| |----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | | ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | | ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | | ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` | | Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | | ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | | СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон добавления команды ## Шаблон добавления команды
Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`: Вставляется в `СведенияОВнешнейОбработке()` **перед** строкой `Возврат ПараметрыРегистрации`:
```bsl ```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'"); НоваяКоманда.Представление = НСтр("ru = '{{Представление}}'");
НоваяКоманда.Идентификатор = "{{Идентификатор}}"; НоваяКоманда.Идентификатор = "{{Идентификатор}}";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
``` ```
Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также: Для печатных форм (ВидОбработкиПечатнаяФорма) добавь также:
```bsl ```bsl
НоваяКоманда.Модификатор = "ПечатьMXL"; НоваяКоманда.Модификатор = "ПечатьMXL";
``` ```
Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`. Примечание: в отличие от первой команды (из `/epf-bsp-init`), дополнительные команды используют строковые литералы `НСтр("ru = '...'")` для представления и строку для идентификатора, а не `Метаданные()`.
## Шаблоны обработчиков ## Шаблоны обработчиков
### ВызовСерверногоМетода — если обработчик уже есть ### ВызовСерверногоМетода — если обработчик уже есть
Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`: Если процедура `ВыполнитьКоманду` уже существует в модуле объекта, добавь ветку перед `КонецЕсли`:
```bsl ```bsl
ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда ИначеЕсли ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
``` ```
### ВызовСерверногоМетода — если обработчика нет ### ВызовСерверногоМетода — если обработчика нет
Для глобальных обработок (без `ОбъектыНазначения`): Для глобальных обработок (без `ОбъектыНазначения`):
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
Для назначаемых обработок (с `ОбъектыНазначения`): Для назначаемых обработок (с `ОбъектыНазначения`):
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
### ПечатнаяФорма — если процедура Печать уже есть ### ПечатнаяФорма — если процедура Печать уже есть
Добавь блок перед `КонецПроцедуры`: Добавь блок перед `КонецПроцедуры`:
```bsl ```bsl
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли; КонецЕсли;
``` ```
### ПечатнаяФорма — если процедуры Печать нет ### ПечатнаяФорма — если процедуры Печать нет
```bsl ```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}"); ПечатнаяФорма = УправлениеПечатью.СведенияОПечатнойФорме(КоллекцияПечатныхФорм, "{{Идентификатор}}");
Если ПечатнаяФорма <> Неопределено Тогда Если ПечатнаяФорма <> Неопределено Тогда
ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати); ПечатнаяФорма.ТабличныйДокумент = Сформировать{{Идентификатор}}(МассивОбъектов, ОбъектыПечати);
ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'"); ПечатнаяФорма.СинонимМакета = НСтр("ru = '{{Представление}}'");
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
### ВызовКлиентскогоМетода ### ВызовКлиентскогоМетода
Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`): Добавляется в **модуль формы** (`Forms/<FormName>/Ext/Form/Module.bsl`):
Для глобальных обработок: Для глобальных обработок:
```bsl ```bsl
&НаКлиенте &НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
Для назначаемых обработок: Для назначаемых обработок:
```bsl ```bsl
&НаКлиенте &НаКлиенте
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначенияМассив) Экспорт
Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда Если ИдентификаторКоманды = "{{Идентификатор}}" Тогда
// TODO: Реализация {{Идентификатор}} // TODO: Реализация {{Идентификатор}}
КонецЕсли; КонецЕсли;
КонецПроцедуры КонецПроцедуры
``` ```
Если процедура уже есть — добавь ветку `ИначеЕсли`. Если процедура уже есть — добавь ветку `ИначеЕсли`.
## Инструкции ## Инструкции
1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` 1. Найди и прочитай `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init` 2. Убедись что `СведенияОВнешнейОбработке()` существует. Если нет — предложи вызвать `/epf-bsp-init`
3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`) 3. Определи вид обработки из существующего кода (найди строку с `ВидОбработки...()`)
4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации` 4. Вставь блок команды **перед** `Возврат ПараметрыРегистрации`
5. Добавь обработчик: 5. Добавь обработчик:
- Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс` - Для серверных обработчиков — в `ObjectModule.bsl`, область `ПрограммныйИнтерфейс`
- Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`) - Для клиентских обработчиков — в модуль формы (найти через Glob: `src/{{ProcessorName}}/Forms/*/Ext/Form/Module.bsl`)
6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры 6. Если обработчик (`ВыполнитьКоманду` / `Печать`) уже есть — добавь ветку, не создавай дубль процедуры
7. Используй табы для отступов 7. Используй табы для отступов
## Пример ## Пример
Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"` Пользователь: `/epf-bsp-add-command МояОбработка ЗаказПокупателя серверный "Заказ покупателя"`
В `СведенияОВнешнейОбработке()` перед `Возврат` добавится: В `СведенияОВнешнейОбработке()` перед `Возврат` добавится:
```bsl ```bsl
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'"); НоваяКоманда.Представление = НСтр("ru = 'Заказ покупателя'");
НоваяКоманда.Идентификатор = "ЗаказПокупателя"; НоваяКоманда.Идентификатор = "ЗаказПокупателя";
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
``` ```
И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки. И в существующую процедуру `ВыполнитьКоманду` добавится блок обработки.
+208 -208
View File
@@ -1,208 +1,208 @@
--- ---
name: epf-bsp-init name: epf-bsp-init
description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки» description: Сформировать функцию `СведенияОВнешнейОбработке` в модуле объекта обработки — описание для подключения через подсистему БСП «Дополнительные отчёты и обработки». Используй когда нужно сделать обработку совместимой с БСП, подключаемой через «Дополнительные отчёты и обработки»
argument-hint: <ProcessorName> <Вид> argument-hint: <ProcessorName> <Вид>
allowed-tools: allowed-tools:
- Read - Read
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /epf-bsp-init — Регистрация обработки в БСП # /epf-bsp-init — Регистрация обработки в БСП
Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП. Добавляет в модуль объекта обработки функцию `СведенияОВнешнейОбработке()`, необходимую для регистрации в подсистеме «Дополнительные отчёты и обработки» БСП.
## Usage ## Usage
``` ```
/epf-bsp-init <ProcessorName> <Вид> [Назначение...] /epf-bsp-init <ProcessorName> <Вид> [Назначение...]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|---------------------------------------------------------| |---------------|:------------:|--------------|---------------------------------------------------------|
| ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) | | ProcessorName | да | — | Имя обработки (должна быть создана через `/epf-init`) |
| Вид | да | — | Вид обработки (см. маппинг ниже) | | Вид | да | — | Вид обработки (см. маппинг ниже) |
| Назначение | * | — | Объекты метаданных для назначаемых видов | | Назначение | * | — | Объекты метаданных для назначаемых видов |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
\* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов. \* Назначение обязательно для видов: ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов.
## Маппинг вида обработки ## Маппинг вида обработки
Пользователь может указать вид в свободной форме. Определи нужный по контексту: Пользователь может указать вид в свободной форме. Определи нужный по контексту:
| Пользователь пишет | Вид | API-метод | | Пользователь пишет | Вид | API-метод |
|-------------------------------------------|----------------------------|----------------------------------------------| |-------------------------------------------|----------------------------|----------------------------------------------|
| доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` | | доп обработка, обработка, глобальная | ДополнительнаяОбработка | `ВидОбработкиДополнительнаяОбработка()` |
| доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` | | доп отчёт, глобальный отчёт | ДополнительныйОтчет | `ВидОбработкиДополнительныйОтчет()` |
| заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` | | заполнение, заполнить | ЗаполнениеОбъекта | `ВидОбработкиЗаполнениеОбъекта()` |
| отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` | | отчёт (назначаемый, для объекта) | Отчет | `ВидОбработкиОтчет()` |
| печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` | | печатная форма, печать | ПечатнаяФорма | `ВидОбработкиПечатнаяФорма()` |
| создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` | | создание связанных объектов | СозданиеСвязанныхОбъектов | `ВидОбработкиСозданиеСвязанныхОбъектов()` |
## Тип команды по умолчанию ## Тип команды по умолчанию
| Вид | ТипКоманды по умолчанию | | Вид | ТипКоманды по умолчанию |
|----------------------------|-------------------------------------------| |----------------------------|-------------------------------------------|
| ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` | | ДополнительнаяОбработка | `ТипКомандыОткрытиеФормы()` |
| ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` | | ДополнительныйОтчет | `ТипКомандыОткрытиеФормы()` |
| ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` | | ЗаполнениеОбъекта | `ТипКомандыВызовСерверногоМетода()` |
| Отчет | `ТипКомандыОткрытиеФормы()` | | Отчет | `ТипКомандыОткрытиеФормы()` |
| ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` | | ПечатнаяФорма | `ТипКомандыВызовСерверногоМетода()` |
| СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` | | СозданиеСвязанныхОбъектов | `ТипКомандыВызовСерверногоМетода()` |
## Шаблон: СведенияОВнешнейОбработке ## Шаблон: СведенияОВнешнейОбработке
Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции. Базовый шаблон — одинаковый для всех видов, отличаются только вызовы API-методов и условные секции.
```bsl ```bsl
Функция СведенияОВнешнейОбработке() Экспорт Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные(); МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}}; ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ВидОбработки}};
ПараметрыРегистрации.Версия = "1.0"; ПараметрыРегистрации.Версия = "1.0";
{{СЕКЦИЯ_НАЗНАЧЕНИЕ}} {{СЕКЦИЯ_НАЗНАЧЕНИЕ}}
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление(); НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}}; НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.{{ТипКоманды}};
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
{{СЕКЦИЯ_МОДИФИКАТОР}} {{СЕКЦИЯ_МОДИФИКАТОР}}
Возврат ПараметрыРегистрации; Возврат ПараметрыРегистрации;
КонецФункции КонецФункции
``` ```
### Подстановки ### Подстановки
- `{{ВидОбработки}}` — API-метод из таблицы маппинга вида - `{{ВидОбработки}}` — API-метод из таблицы маппинга вида
- `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию - `{{ТипКоманды}}` — API-метод из таблицы типа команды по умолчанию
### Условные секции ### Условные секции
**`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект: **`{{СЕКЦИЯ_НАЗНАЧЕНИЕ}}`** — только для назначаемых видов (ЗаполнениеОбъекта, Отчет, ПечатнаяФорма, СозданиеСвязанныхОбъектов). Одна строка на каждый объект:
```bsl ```bsl
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
``` ```
Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`). Формат имени объекта: `ИмяКлассаОбъектаМетаданного.ИмяОбъекта` (например `Документ.СчетНаОплату`, `Справочник.Контрагенты`).
Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой. Для глобальных видов (ДополнительнаяОбработка, ДополнительныйОтчет) — секция не нужна, удалить вместе с пустой строкой.
**`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма: **`{{СЕКЦИЯ_МОДИФИКАТОР}}`** — только для ПечатнаяФорма:
```bsl ```bsl
НоваяКоманда.Модификатор = "ПечатьMXL"; НоваяКоманда.Модификатор = "ПечатьMXL";
``` ```
Для остальных видов — удалить вместе с пустой строкой. Для остальных видов — удалить вместе с пустой строкой.
## Шаблоны серверных обработчиков ## Шаблоны серверных обработчиков
Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`. Для видов с типом команды `ВызовСерверногоМетода` добавь соответствующую процедуру-обработчик в ту же область `ПрограммныйИнтерфейс`, после `СведенияОВнешнейОбработке`.
### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов ### Для ЗаполнениеОбъекта / СозданиеСвязанныхОбъектов
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ОбъектыНазначения, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
``` ```
### Для ПечатнаяФорма ### Для ПечатнаяФорма
```bsl ```bsl
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
``` ```
### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода) ### Для ДополнительнаяОбработка / ДополнительныйОтчет (с ВызовСерверногоМетода)
Если пользователь явно выбрал серверный метод вместо открытия формы: Если пользователь явно выбрал серверный метод вместо открытия формы:
```bsl ```bsl
Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт Процедура ВыполнитьКоманду(ИдентификаторКоманды, ПараметрыВыполненияКоманды) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
``` ```
Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`. Обрати внимание: у глобальных обработок нет параметра `ОбъектыНазначения`.
## Инструкции ## Инструкции
1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl` 1. Найди `ObjectModule.bsl` через Glob: `src/{{ProcessorName}}/Ext/ObjectModule.bsl`
2. Прочитай файл 2. Прочитай файл
3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй 3. Если `СведенияОВнешнейОбработке` уже есть — сообщи пользователю и не дублируй
4. Если файл не найден — предложи сначала вызвать `/epf-init` 4. Если файл не найден — предложи сначала вызвать `/epf-init`
5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти` 5. Найди область `#Область ПрограммныйИнтерфейс` ... `#КонецОбласти`
6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области 6. Вставь функцию `СведенияОВнешнейОбработке()` внутрь этой области
7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции 7. Если вид требует серверный обработчик — вставь его тоже в эту область, после функции
8. Используй табы для отступов (как в исходном файле) 8. Используй табы для отступов (как в исходном файле)
## Пример ## Пример
Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату` Пользователь: `/epf-bsp-init МояОбработка печатная форма для Документ.СчетНаОплату`
Результат в `ObjectModule.bsl`: Результат в `ObjectModule.bsl`:
```bsl ```bsl
#Область ОписаниеПеременных #Область ОписаниеПеременных
#КонецОбласти #КонецОбласти
#Область ПрограммныйИнтерфейс #Область ПрограммныйИнтерфейс
Функция СведенияОВнешнейОбработке() Экспорт Функция СведенияОВнешнейОбработке() Экспорт
МетаданныеОбработки = Метаданные(); МетаданныеОбработки = Метаданные();
ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1"); ПараметрыРегистрации = ДополнительныеОтчетыИОбработки.СведенияОВнешнейОбработке("2.2.2.1");
ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма(); ПараметрыРегистрации.Вид = ДополнительныеОтчетыИОбработкиКлиентСервер.ВидОбработкиПечатнаяФорма();
ПараметрыРегистрации.Версия = "1.0"; ПараметрыРегистрации.Версия = "1.0";
ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату"); ПараметрыРегистрации.Назначение.Добавить("Документ.СчетНаОплату");
НоваяКоманда = ПараметрыРегистрации.Команды.Добавить(); НоваяКоманда = ПараметрыРегистрации.Команды.Добавить();
НоваяКоманда.Представление = МетаданныеОбработки.Представление(); НоваяКоманда.Представление = МетаданныеОбработки.Представление();
НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя; НоваяКоманда.Идентификатор = МетаданныеОбработки.Имя;
НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода(); НоваяКоманда.Использование = ДополнительныеОтчетыИОбработкиКлиентСервер.ТипКомандыВызовСерверногоМетода();
НоваяКоманда.ПоказыватьОповещение = Ложь; НоваяКоманда.ПоказыватьОповещение = Ложь;
НоваяКоманда.Модификатор = "ПечатьMXL"; НоваяКоманда.Модификатор = "ПечатьMXL";
Возврат ПараметрыРегистрации; Возврат ПараметрыРегистрации;
КонецФункции КонецФункции
Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт Процедура Печать(МассивОбъектов, КоллекцияПечатныхФорм, ОбъектыПечати, ПараметрыВывода) Экспорт
// TODO: Реализация // TODO: Реализация
КонецПроцедуры КонецПроцедуры
#КонецОбласти #КонецОбласти
#Область СлужебныеПроцедурыИФункции #Область СлужебныеПроцедурыИФункции
#КонецОбласти #КонецОбласти
``` ```
## Дальнейшие шаги ## Дальнейшие шаги
- Добавить ещё команду: `/epf-bsp-add-command` - Добавить ещё команду: `/epf-bsp-add-command`
- Добавить форму: `/form-add` - Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить макет: `/template-add`
- Собрать EPF: `/epf-build` - Собрать EPF: `/epf-build`
+71 -69
View File
@@ -1,69 +1,71 @@
--- ---
name: epf-build name: epf-build
description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников description: Собрать внешнюю обработку 1С (EPF/ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать обработку или получить EPF/ERF файл из исходников
argument-hint: <ProcessorName> argument-hint: <ProcessorName>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /epf-build — Сборка обработки # /epf-build — Сборка обработки
## Usage ## Usage
``` ```
/epf-build <ProcessorName> [SrcDir] [OutDir] /epf-build <ProcessorName> [SrcDir] [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|---------------|:------------:|--------------|--------------------------------------| |---------------|:------------:|--------------|--------------------------------------|
| ProcessorName | да | — | Имя обработки (имя корневого XML) | | ProcessorName | да | — | Имя обработки (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата | | OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально) ## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для EPF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу | | `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Примеры > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
```powershell ## Примеры
# Сборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf" ```powershell
# Сборка обработки (файловая база)
# Серверная база powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
``` # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МояОбработка.xml" -OutputFile "build/МояОбработка.epf"
```
+510 -173
View File
@@ -1,173 +1,510 @@
# epf-build v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Сборка внешней обработки/отчёта 1С из XML-исходников .SYNOPSIS
Сборка внешней обработки/отчёта 1С из XML-исходников
.DESCRIPTION
Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С. .DESCRIPTION
Общий скрипт для epf-build и erf-build. Собирает EPF/ERF-файл из XML-исходников с помощью платформы 1С.
Общий скрипт для epf-build и erf-build.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER SourceFile
Путь к корневому XML-файлу исходников .PARAMETER SourceFile
Путь к корневому XML-файлу исходников
.PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу .PARAMETER OutputFile
Путь к выходному EPF/ERF-файлу
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МояОбработка.xml" -OutputFile "build\МояОбработка.epf"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$true)]
[string]$SourceFile, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$true)]
[string]$OutputFile [Parameter(Mandatory=$false)]
) [string]$Password,
$OutputEncoding = [System.Text.Encoding]::UTF8 [Parameter(Mandatory=$true)]
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [string]$SourceFile,
# --- Resolve V8Path --- [Parameter(Mandatory=$true)]
if (-not $V8Path) { [string]$OutputFile,
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { function Protect-Secrets {
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red # Redact literal secret values from a display string (String.Replace is literal, not regex).
exit 1 param([string]$Text, [string[]]$Secrets)
} foreach ($s in $Secrets) { if ($s) { $Text = $Text.Replace($s, '***') } }
return $Text
# --- Auto-create stub database if no connection specified --- }
$autoCreatedBase = $null
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { # --- Additional platform arguments ---
$sourceDir = Split-Path $SourceFile -Parent $script:V8OwnedKeys = @(
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)" 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1" '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
Write-Host "No database specified. Creating temporary stub database..." '/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
$stubArgs = "-SourceDir `"$sourceDir`" -V8Path `"$V8Path`" -TempBasePath `"$autoBasePath`"" '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
$stubProc = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -File `"$stubScript`" $stubArgs" -NoNewWindow -Wait -PassThru '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
if ($stubProc.ExitCode -ne 0) { '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
Write-Host "Error: failed to create stub database" -ForegroundColor Red )
exit 1 $script:IbcmdOwnedKeys = @(
} '--db-path', '--data', '--out', '--file', '--load', '--restore',
$InfoBasePath = $autoBasePath '--import', '--export', '--apply', '--force', '--create-database',
$autoCreatedBase = $autoBasePath '--user', '--password'
} )
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
# --- Validate source file --- $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
if (-not (Test-Path $SourceFile)) {
Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red function Test-ArgKeyMatch {
exit 1 # 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.
# --- Ensure output directory exists --- param([string]$Token, [string]$Key)
$outDir = Split-Path $OutputFile -Parent if ($Token.Length -lt $Key.Length) { return $false }
if ($outDir -and -not (Test-Path $outDir)) { if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
New-Item -ItemType Directory -Path $outDir -Force | Out-Null if ($Token.Length -eq $Key.Length) { return $true }
} return -not [char]::IsLetter($Token[$Key.Length])
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)" function Get-ProjectExtraArgs {
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
try { $dir = (Get-Location).Path
# --- Build arguments --- while ($dir) {
$arguments = @("DESIGNER") $pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
if ($InfoBaseServer -and $InfoBaseRef) { try {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" $j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
} else { if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
$arguments += "/F", "`"$InfoBasePath`"" } catch {}
} return @()
}
if ($UserName) { $arguments += "/N`"$UserName`"" } $parent = Split-Path $dir -Parent
if ($Password) { $arguments += "/P`"$Password`"" } if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`"" }
return @()
# --- Output --- }
$outFile = Join-Path $tempDir "build_log.txt"
$arguments += "/Out", "`"$outFile`"" function Assert-ExtraArgs {
$arguments += "/DisableStartupDialogs" # 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.
# --- Execute --- param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" $paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
$exitCode = $process.ExitCode foreach ($tok in $ExtraArgs) {
if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
# --- Result --- Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
if ($exitCode -eq 0) { exit 1
Write-Host "Build completed successfully: $OutputFile" -ForegroundColor Green }
} else { foreach ($k in $owned) {
Write-Host "Error building (code: $exitCode)" -ForegroundColor Red if (Test-ArgKeyMatch $tok $k) {
} $hint = ''
if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
if (Test-Path $outFile) { Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue exit 1
if ($logContent) { }
Write-Host "--- Log ---" }
Write-Host $logContent }
Write-Host "--- End ---" }
}
} function Resolve-ExtraArgs {
# Pick the argument list for the selected engine and validate it. An explicitly passed
exit $exitCode # 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.
} finally { param([string]$Engine, [string[]]$V8Extra, [string[]]$IbcmdExtra, [hashtable]$Hints)
if (Test-Path $tempDir) { # powershell.exe -File — how skills are invoked — cannot bind an array parameter:
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue # 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;
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { # a native array call keeps working. A value containing a comma is not supported.
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue $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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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
}
# --- Auto-create stub database if no connection specified ---
$autoCreatedBase = $null
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$sourceDir = Split-Path $SourceFile -Parent
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
$stubScript = Join-Path $PSScriptRoot "stub-db-create.ps1"
Write-Host "No database specified. Creating temporary stub database..."
# 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
}
$InfoBasePath = $autoBasePath
$autoCreatedBase = $autoBasePath
}
# --- Validate source file ---
if (-not (Test-Path $SourceFile)) {
Write-Host "Error: source file not found: $SourceFile" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
New-Item -ItemType Directory -Path $outDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_build_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: build EPF/ERF via config import --out ---
$srcDir = Split-Path $SourceFile -Parent
$arguments = @("infobase", "config", "import", "$srcDir", "--out=$OutputFile", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/LoadExternalDataProcessorOrReportFromFiles", "`"$SourceFile`"", "`"$OutputFile`""
# --- Output ---
$outFile = Join-Path $tempDir "build_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
+417 -32
View File
@@ -1,35 +1,356 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-build v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") while True:
if candidates: pf = os.path.join(d, ".v8-project.json")
candidates.sort() if os.path.isfile(pf):
return candidates[-1] try:
else: with open(pf, encoding="utf-8-sig") as f:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) data = json.load(f)
sys.exit(1) v = data.get("v8path")
elif os.path.isdir(v8path): if v:
v8path = os.path.join(v8path, "1cv8.exe") return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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) sys.exit(1)
return v8path
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
@@ -47,10 +368,38 @@ def main():
parser.add_argument("-Password", default="", help="1C user password") 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("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF 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 --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.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)
# --- Auto-create stub database if no connection specified --- # --- Auto-create stub database if no connection specified ---
auto_created_base = None auto_created_base = None
@@ -59,10 +408,16 @@ def main():
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}") 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") stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
print("No database specified. Creating temporary stub database...") print("No database specified. Creating temporary stub database...")
result = subprocess.run( stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
[sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path, "-TempBasePath", auto_base_path], "-TempBasePath", auto_base_path]
capture_output=False, # 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: if result.returncode != 0:
print("Error: failed to create stub database", file=sys.stderr) print("Error: failed to create stub database", file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -84,38 +439,67 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch: build EPF/ERF via config import --out ---
src_dir = os.path.dirname(os.path.abspath(args.SourceFile))
arguments = ["infobase", "config", "import", src_dir, f"--out={args.OutputFile}", f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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)
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: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"] arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else: else:
arguments += ["/F", args.InfoBasePath] arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: 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 --- # --- Output ---
out_file = os.path.join(temp_dir, "build_log.txt") out_file = os.path.join(temp_dir, "build_log.txt")
arguments += ["/Out", out_file] arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- 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: if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}") 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: else:
print(f"Error building (code: {exit_code})", file=sys.stderr) print(f"Error building (code: {exit_code})", file=sys.stderr)
@@ -130,6 +514,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
@@ -1,4 +1,4 @@
# stub-db-create v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -7,12 +7,162 @@ param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$V8Path, [string]$V8Path,
[string]$TempBasePath [string]$TempBasePath,
[string[]]$AdditionalV8Arguments = @(),
[string[]]$AdditionalIbcmdArguments = @()
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [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 --- # --- 1. Scan XML files for reference types ---
$typeMap = @{} # MetadataType -> @(Name1, Name2, ...) $typeMap = @{} # MetadataType -> @(Name1, Name2, ...)
@@ -1252,11 +1402,119 @@ $propsXml </Properties>$childObjLine
} }
} }
# --- 5a. Stub via ibcmd (one call: create [--import --apply]) ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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)"
New-Item -ItemType Directory -Path $ibData -Force | Out-Null
$ibArgs = @("infobase", "create", "--db-path=$TempBasePath", "--create-database")
if ($hasRefTypes) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$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) {
Write-PlatformOutput $ibOut
Write-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
Write-Host "[OK] Stub database created: $TempBasePath"
Write-Host $TempBasePath
exit 0
}
# --- 5. Create infobase --- # --- 5. Create infobase ---
Write-Host "Creating infobase: $TempBasePath" Write-Host "Creating infobase: $TempBasePath"
$createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" $createArgs = "CREATEINFOBASE File=`"$TempBasePath`" /DisableStartupDialogs" + $extraArgString
$proc = Start-Process -FilePath $V8Path -ArgumentList $createArgs -NoNewWindow -Wait -PassThru $proc = Invoke-PlatformProcess $V8Path @($createArgs) -PreQuoted
if ($proc.ExitCode -ne 0) { if ($proc.ExitCode -ne 0) {
Write-PlatformOutput $proc.Output
Write-Error "Failed to create infobase (code: $($proc.ExitCode))" Write-Error "Failed to create infobase (code: $($proc.ExitCode))"
exit 1 exit 1
} }
@@ -1267,10 +1525,11 @@ if ($hasRefTypes) {
# LoadConfigFromFiles # LoadConfigFromFiles
Write-Host "Loading configuration from files..." Write-Host "Loading configuration from files..."
$loadLog = Join-Path $env:TEMP "stub_load_log.txt" $loadLog = Join-Path $env:TEMP "stub_load_log.txt"
$loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" $loadArgs = "DESIGNER /F`"$TempBasePath`" /LoadConfigFromFiles `"$cfgDir`" /Out `"$loadLog`" /DisableStartupDialogs" + $extraArgString
$proc = Start-Process -FilePath $V8Path -ArgumentList $loadArgs -NoNewWindow -Wait -PassThru $proc = Invoke-PlatformProcess $V8Path @($loadArgs) -PreQuoted
if ($proc.ExitCode -ne 0) { if ($proc.ExitCode -ne 0) {
if (Test-Path $loadLog) { Get-Content $loadLog -Raw -ErrorAction SilentlyContinue | Write-Host } 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))" Write-Error "Failed to load config (code: $($proc.ExitCode))"
exit 1 exit 1
} }
@@ -1278,10 +1537,11 @@ if ($hasRefTypes) {
# UpdateDBCfg # UpdateDBCfg
Write-Host "Updating database configuration..." Write-Host "Updating database configuration..."
$updateLog = Join-Path $env:TEMP "stub_update_log.txt" $updateLog = Join-Path $env:TEMP "stub_update_log.txt"
$updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" $updateArgs = "DESIGNER /F`"$TempBasePath`" /UpdateDBCfg /Out `"$updateLog`" /DisableStartupDialogs" + $extraArgString
$proc = Start-Process -FilePath $V8Path -ArgumentList $updateArgs -NoNewWindow -Wait -PassThru $proc = Invoke-PlatformProcess $V8Path @($updateArgs) -PreQuoted
if ($proc.ExitCode -ne 0) { if ($proc.ExitCode -ne 0) {
if (Test-Path $updateLog) { Get-Content $updateLog -Raw -ErrorAction SilentlyContinue | Write-Host } 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))" Write-Error "Failed to update DB config (code: $($proc.ExitCode))"
exit 1 exit 1
} }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# stub-db-create v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
@@ -12,6 +12,256 @@ import tempfile
import uuid import uuid
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def new_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -781,7 +1031,17 @@ def main():
parser.add_argument('-SourceDir', required=True) parser.add_argument('-SourceDir', required=True)
parser.add_argument('-V8Path', required=True) parser.add_argument('-V8Path', required=True)
parser.add_argument('-TempBasePath', default='') 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) type_map = scan_ref_types(args.SourceDir)
register_columns = scan_register_columns(args.SourceDir) register_columns = scan_register_columns(args.SourceDir)
@@ -1034,13 +1294,43 @@ def main():
if register_columns: if register_columns:
print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.') print('WARNING: Register column categories (Dimension/Resource/Attribute) are guessed. Form field bindings may not survive round-trip through a real database.')
# Stub via ibcmd (one call: create [--import --apply])
stub_engine = "ibcmd" if os.path.basename(args.V8Path).lower().startswith("ibcmd") else "1cv8"
# --- 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}')
ib_data = tempfile.mkdtemp(prefix="stub_data_")
ib_args = [args.V8Path, 'infobase', 'create', f'--db-path={temp_base}', '--create-database']
if has_ref_types:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
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:
if result.stdout:
print(result.stdout)
if result.stderr:
print(result.stderr, file=sys.stderr)
print(f'Failed to create stub infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
if has_ref_types:
import shutil
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
print(f'[OK] Stub database created: {temp_base}')
print(temp_base)
sys.exit(0)
# Create infobase # Create infobase
print(f'Creating infobase: {temp_base}') print(f'Creating infobase: {temp_base}')
result = subprocess.run( result = run_v8(args.V8Path, ['CREATEINFOBASE', f'File="{temp_base}"', '/DisableStartupDialogs']
[args.V8Path, 'CREATEINFOBASE', f'File={temp_base}', '/DisableStartupDialogs'], + [quote_if_needed(a) for a in extra_args])
capture_output=True, text=True,
)
if result.returncode != 0: if result.returncode != 0:
print_platform_output(result)
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr) print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1) sys.exit(1)
@@ -1048,21 +1338,18 @@ def main():
cfg_dir = os.path.join(temp_base, 'cfg') cfg_dir = os.path.join(temp_base, 'cfg')
# LoadConfigFromFiles # LoadConfigFromFiles
print('Loading configuration from files...') print('Loading configuration from files...')
result = subprocess.run( result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/LoadConfigFromFiles', cfg_dir, '/DisableStartupDialogs'], '/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
capture_output=True, text=True,
)
if result.returncode != 0: if result.returncode != 0:
print_platform_output(result)
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr) print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
sys.exit(1) sys.exit(1)
# UpdateDBCfg # UpdateDBCfg
print('Updating database configuration...') print('Updating database configuration...')
update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt') update_log = os.path.join(tempfile.gettempdir(), 'stub_update_log.txt')
result = subprocess.run( result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/UpdateDBCfg', '/Out', f'"{update_log}"',
[args.V8Path, 'DESIGNER', f'/F{temp_base}', '/UpdateDBCfg', '/Out', update_log, '/DisableStartupDialogs'], '/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
capture_output=True, text=True,
)
if result.returncode != 0: if result.returncode != 0:
if os.path.isfile(update_log): if os.path.isfile(update_log):
try: try:
@@ -1070,6 +1357,7 @@ def main():
print(f.read()) print(f.read())
except Exception: except Exception:
pass pass
print_platform_output(result)
print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr) print(f'Failed to update DB config (code: {result.returncode})', file=sys.stderr)
sys.exit(1) sys.exit(1)
+71 -69
View File
@@ -1,69 +1,71 @@
--- ---
name: epf-dump name: epf-dump
description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла description: Разобрать EPF-файл обработки 1С (EPF/ERF) в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать обработку, получить исходники из EPF/ERF файла
argument-hint: <EpfFile> argument-hint: <EpfFile>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /epf-dump — Разборка обработки # /epf-dump — Разборка обработки
## Usage ## Usage
``` ```
/epf-dump <EpfFile> [OutDir] /epf-dump <EpfFile> [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------| |----------|:------------:|--------------|-------------------------------------|
| EpfFile | да | — | Путь к EPF-файлу | | EpfFile | да | — | Путь к EPF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников | | OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно) ## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы, или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к EPF/ERF-файлу | | `-InputFile <путь>` | да | Путь к EPF/ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | | `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Примеры > `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
```powershell ## Примеры
# Разборка обработки (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src" ```powershell
# Разборка обработки (файловая база)
# Серверная база powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МояОбработка.epf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
``` # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МояОбработка.epf" -OutputDir "src"
```
+497 -167
View File
@@ -1,167 +1,497 @@
# epf-dump v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
<# # NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
.SYNOPSIS <#
Разборка внешней обработки/отчёта 1С в XML-исходники .SYNOPSIS
Разборка внешней обработки/отчёта 1С в XML-исходники
.DESCRIPTION
Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С. .DESCRIPTION
Общий скрипт для epf-dump и erf-dump. Разбирает EPF/ERF-файл во XML-исходники с помощью платформы 1С.
Общий скрипт для epf-dump и erf-dump.
.PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe .PARAMETER V8Path
Путь к каталогу bin платформы или к 1cv8.exe
.PARAMETER InfoBasePath
Путь к файловой информационной базе .PARAMETER InfoBasePath
Путь к файловой информационной базе
.PARAMETER InfoBaseServer
Сервер 1С (для серверной базы) .PARAMETER InfoBaseServer
Сервер 1С (для серверной базы)
.PARAMETER InfoBaseRef
Имя базы на сервере .PARAMETER InfoBaseRef
Имя базы на сервере
.PARAMETER UserName
Имя пользователя 1С .PARAMETER UserName
Имя пользователя 1С
.PARAMETER Password
Пароль пользователя .PARAMETER Password
Пароль пользователя
.PARAMETER InputFile
Путь к EPF/ERF-файлу .PARAMETER InputFile
Путь к EPF/ERF-файлу
.PARAMETER OutputDir
Каталог для выгрузки исходников .PARAMETER OutputDir
Каталог для выгрузки исходников
.PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical) .PARAMETER Format
Формат выгрузки: Hierarchical или Plain (по умолчанию Hierarchical)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src" .PARAMETER AdditionalV8Arguments
Дополнительные аргументы запуска 1cv8.exe (например /UseHwLicenses+)
.EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src" .PARAMETER AdditionalIbcmdArguments
#> Дополнительные аргументы запуска ibcmd (форма --ключ=значение)
[CmdletBinding()] .EXAMPLE
param( .\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МояОбработка.epf" -OutputDir "src"
[Parameter(Mandatory=$false)]
[string]$V8Path, .EXAMPLE
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
[Parameter(Mandatory=$false)] #>
[string]$InfoBasePath,
[CmdletBinding()]
[Parameter(Mandatory=$false)] param(
[string]$InfoBaseServer, [Parameter(Mandatory=$false)]
[string]$V8Path,
[Parameter(Mandatory=$false)]
[string]$InfoBaseRef, [Parameter(Mandatory=$false)]
[string]$InfoBasePath,
[Parameter(Mandatory=$false)]
[string]$UserName, [Parameter(Mandatory=$false)]
[string]$InfoBaseServer,
[Parameter(Mandatory=$false)]
[string]$Password, [Parameter(Mandatory=$false)]
[string]$InfoBaseRef,
[Parameter(Mandatory=$true)]
[string]$InputFile, [Parameter(Mandatory=$false)]
[string]$UserName,
[Parameter(Mandatory=$true)]
[string]$OutputDir, [Parameter(Mandatory=$false)]
[string]$Password,
[Parameter(Mandatory=$false)]
[ValidateSet("Hierarchical", "Plain")] [Parameter(Mandatory=$true)]
[string]$Format = "Hierarchical" [string]$InputFile,
)
[Parameter(Mandatory=$true)]
$OutputEncoding = [System.Text.Encoding]::UTF8 [string]$OutputDir,
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Parameter(Mandatory=$false)]
# --- Resolve V8Path --- [ValidateSet("Hierarchical", "Plain")]
if (-not $V8Path) { [string]$Format = "Hierarchical",
$found = Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" -ErrorAction SilentlyContinue | Sort-Object FullName -Descending | Select-Object -First 1
if ($found) { [Parameter(Mandatory=$false)]
$V8Path = $found.FullName [string[]]$AdditionalV8Arguments = @(),
} else {
Write-Host "Error: 1cv8.exe not found. Specify -V8Path" -ForegroundColor Red [Parameter(Mandatory=$false)]
exit 1 [string[]]$AdditionalIbcmdArguments = @()
} )
} elseif (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe" $OutputEncoding = [System.Text.Encoding]::UTF8
} [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
if (-not (Test-Path $V8Path)) { # --- Additional platform arguments ---
Write-Host "Error: 1cv8.exe not found at $V8Path" -ForegroundColor Red $script:V8OwnedKeys = @(
exit 1 'DESIGNER', 'ENTERPRISE', 'CREATEINFOBASE', 'CONFIG',
} '/F', '/S', '/N', '/P', '/Out', '/DisableStartupDialogs',
'/UseTemplate', '/AddToList', '/Execute', '/C', '/URL', '/UC',
# --- Validate database connection --- '/DumpIB', '/RestoreIB', '/DumpCfg', '/LoadCfg',
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) { '/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red '/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow )
exit 1 $script:IbcmdOwnedKeys = @(
} '--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
# --- Validate input file --- '--user', '--password'
if (-not (Test-Path $InputFile)) { )
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red $script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
exit 1 $script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
}
function Test-ArgKeyMatch {
# --- Ensure output directory exists --- # A token matches a key when it equals the key, or starts with it and the next
if (-not (Test-Path $OutputDir)) { # character is not a letter — catches glued /N"user" and --password=x, while
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null # keeping /ClearCache distinct from /C.
} param([string]$Token, [string]$Key)
if ($Token.Length -lt $Key.Length) { return $false }
# --- Temp dir --- if (-not $Token.Substring(0, $Key.Length).Equals($Key, [System.StringComparison]::OrdinalIgnoreCase)) { return $false }
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)" if ($Token.Length -eq $Key.Length) { return $true }
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null return -not [char]::IsLetter($Token[$Key.Length])
}
try {
# --- Build arguments --- function Get-ProjectExtraArgs {
$arguments = @("DESIGNER") # v8args / ibcmdargs from .v8-project.json — same upward walk as v8path.
param([string]$Name)
if ($InfoBaseServer -and $InfoBaseRef) { $dir = (Get-Location).Path
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`"" while ($dir) {
} else { $pf = Join-Path $dir ".v8-project.json"
$arguments += "/F", "`"$InfoBasePath`"" if (Test-Path $pf) {
} try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($UserName) { $arguments += "/N`"$UserName`"" } if ($j.$Name) { return @($j.$Name | ForEach-Object { [string]$_ }) }
if ($Password) { $arguments += "/P`"$Password`"" } } catch {}
return @()
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`"" }
$arguments += "-Format", $Format $parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
# --- Output --- $dir = $parent
$outFile = Join-Path $tempDir "dump_log.txt" }
$arguments += "/Out", "`"$outFile`"" return @()
$arguments += "/DisableStartupDialogs" }
# --- Execute --- function Assert-ExtraArgs {
Write-Host "Running: 1cv8.exe $($arguments -join ' ')" # The platform accepts only one batch operation, and a duplicate connection or
$process = Start-Process -FilePath $V8Path -ArgumentList $arguments -NoNewWindow -Wait -PassThru # output key fails with an opaque 1C error — reject what the skill owns itself.
$exitCode = $process.ExitCode param([string[]]$ExtraArgs, [string]$Engine, [hashtable]$Hints)
$paramName = if ($Engine -eq 'ibcmd') { '-AdditionalIbcmdArguments' } else { '-AdditionalV8Arguments' }
# --- Result --- $owned = if ($Engine -eq 'ibcmd') { $script:IbcmdOwnedKeys } else { $script:V8OwnedKeys }
if ($exitCode -eq 0) { foreach ($tok in $ExtraArgs) {
Write-Host "Dump completed successfully to: $OutputDir" -ForegroundColor Green if ($Engine -eq 'ibcmd' -and $tok -notmatch '^-') {
} else { Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
Write-Host "Error dumping (code: $exitCode)" -ForegroundColor Red exit 1
} }
foreach ($k in $owned) {
if (Test-Path $outFile) { if (Test-ArgKeyMatch $tok $k) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue $hint = ''
if ($logContent) { if ($Hints -and $Hints.ContainsKey($k)) { $hint = " (use $($Hints[$k]))" }
Write-Host "--- Log ---" Write-Host "Error: $k is controlled by the skill and cannot be passed via $paramName$hint" -ForegroundColor Red
Write-Host $logContent exit 1
Write-Host "--- End ---" }
} }
} }
}
exit $exitCode
function Resolve-ExtraArgs {
} finally { # Pick the argument list for the selected engine and validate it. An explicitly passed
if (Test-Path $tempDir) { # parameter for the other engine is an error; the same keys coming from .v8-project.json
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue # 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
while ($dir) {
$pf = Join-Path $dir ".v8-project.json"
if (Test-Path $pf) {
try {
$j = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($j.v8path) { return [string]$j.v8path }
} catch {}
return $null
}
$parent = Split-Path $dir -Parent
if (-not $parent -or $parent -eq $dir) { break }
$dir = $parent
}
return $null
}
if (-not $V8Path) {
$V8Path = Find-ProjectV8Path
}
if (-not $V8Path) {
$found = Get-ChildItem @("C:\Program Files\1cv8\*\bin\1cv8.exe", "C:\Program Files (x86)\1cv8\*\bin\1cv8.exe") -ErrorAction SilentlyContinue |
Sort-Object { try { [version]$_.Directory.Parent.Name } catch { [version]"0.0" } } -Descending |
Select-Object -First 1
if ($found) {
$V8Path = $found.FullName
Write-Host "Auto-selected platform $($found.Directory.Parent.Name): $V8Path" -ForegroundColor Yellow
} else {
Write-Host "Error: 1C executable not found. Specify -V8Path" -ForegroundColor Red
exit 1
}
}
if (Test-Path $V8Path -PathType Container) {
$V8Path = Join-Path $V8Path "1cv8.exe"
}
if (-not (Test-Path $V8Path)) {
Write-Host "Error: 1C executable not found at $V8Path" -ForegroundColor Red
exit 1
}
# --- Validate database connection ---
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
Write-Host "Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef" -ForegroundColor Red
Write-Host "Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly." -ForegroundColor Yellow
exit 1
}
# --- Detect engine (ibcmd vs 1cv8) by exe name ---
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 = 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
$p = [System.Diagnostics.Process]::Start($psi)
$p.StandardInput.Close()
# 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
exit 1
}
if ($Format -eq "Plain") {
Write-Host "Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)" -ForegroundColor Red
exit 1
}
}
# --- Validate input file ---
if (-not (Test-Path $InputFile)) {
Write-Host "Error: input file not found: $InputFile" -ForegroundColor Red
exit 1
}
# --- Ensure output directory exists ---
if (-not (Test-Path $OutputDir)) {
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
}
# --- Temp dir ---
$tempDir = Join-Path $env:TEMP "epf_dump_$(Get-Random)"
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
try {
if ($engine -eq "ibcmd") {
# --- ibcmd branch: dump EPF/ERF via config export --file ---
$arguments = @("infobase", "config", "export", "--file=$InputFile", "$OutputDir", "--db-path=$InfoBasePath")
if ($UserName) { $arguments += "--user=$UserName" }
if ($Password) { $arguments += "--password=$Password" }
$arguments += "--data=$tempDir"
$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
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
$arguments += "/DumpExternalDataProcessorOrReportToFiles", "`"$OutputDir`"", "`"$InputFile`""
$arguments += "-Format", $Format
# --- Output ---
$outFile = Join-Path $tempDir "dump_log.txt"
$arguments += "/Out", "`"$outFile`""
$arguments += "/DisableStartupDialogs"
$arguments += $extraArgs
# --- Execute ---
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
}
if (Test-Path $outFile) {
$logContent = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($logContent) {
Write-Host "--- Log ---"
Write-Host $logContent
Write-Host "--- End ---"
}
}
Write-PlatformOutput $__v8.Output
exit $exitCode
} finally {
if (Test-Path $tempDir) {
Remove-Item -Path $tempDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
+410 -28
View File
@@ -1,35 +1,356 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-dump v1.0 — 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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import atexit
import glob import glob
import json
import os import os
import random import random
import re
import shutil import shutil
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
def resolve_v8path(v8path): def _find_project_v8path():
"""Resolve path to 1cv8.exe.""" """Walk up from CWD to find .v8-project.json and read its v8path."""
if not v8path: d = os.getcwd()
candidates = glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe") while True:
if candidates: pf = os.path.join(d, ".v8-project.json")
candidates.sort() if os.path.isfile(pf):
return candidates[-1] try:
else: with open(pf, encoding="utf-8-sig") as f:
print("Error: 1cv8.exe not found. Specify -V8Path", file=sys.stderr) data = json.load(f)
sys.exit(1) v = data.get("v8path")
elif os.path.isdir(v8path): if v:
v8path = os.path.join(v8path, "1cv8.exe") return v
except Exception:
pass
return None
parent = os.path.dirname(d)
if parent == d:
return None
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)
if os.path.basename(parent).lower() == "bin":
parent = os.path.dirname(parent)
return os.path.basename(parent)
def _version_key(p):
"""Numeric sort key from version dir name."""
return [int(x) for x in re.findall(r"\d+", _version_dir(p))]
def resolve_v8path(v8path):
"""Resolve path to a 1C executable (1cv8; ibcmd only when given explicitly)."""
if not v8path:
v8path = _find_project_v8path()
if not v8path:
if os.name == "nt":
candidates = (
glob.glob(r"C:\Program Files\1cv8\*\bin\1cv8.exe")
+ glob.glob(r"C:\Program Files (x86)\1cv8\*\bin\1cv8.exe")
)
else:
# PY-only: PS-порт на *nix не исполняется, поэтому *nix-раскладки нет в .ps1.
candidates = glob.glob("/opt/1cv8/*/1cv8")
if candidates:
v8path = max(candidates, key=_version_key)
print(f"Auto-selected platform {_version_dir(v8path)}: {v8path}")
else:
print("Error: 1C executable not found. Specify -V8Path", file=sys.stderr)
sys.exit(1)
if os.path.isdir(v8path):
# PY-only: на *nix исполняемый называется "1cv8" (без .exe); ibcmd — только явным путём.
exe = "1cv8.exe" if os.name == "nt" else "1cv8"
v8path = os.path.join(v8path, exe)
if not os.path.isfile(v8path): if not os.path.isfile(v8path):
print(f"Error: 1cv8.exe not found at {v8path}", file=sys.stderr) print(f"Error: 1C executable not found at {v8path}", file=sys.stderr)
sys.exit(1)
return v8path
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 "
"call may block instead of failing. If it does not return promptly, abort and "
"re-run with -UserName and -Password.\n"
)
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) sys.exit(1)
return v8path
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.
input="" closes stdin (EOF) so ibcmd's auth prompt fast-fails instead of hanging.
On Windows without -UserName ibcmd reads the console directly and may still block
that residual case is flagged via IBCMD_NOUSER_HINT (model-facing).
"""
if warn_no_user and os.name == "nt" and not has_username:
sys.stderr.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
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(): def main():
@@ -53,16 +374,48 @@ def main():
choices=["Hierarchical", "Plain"], choices=["Hierarchical", "Plain"],
help="Dump format (default: Hierarchical)", 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 --- # --- Resolve V8Path ---
v8path = resolve_v8path(args.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 --- # --- Validate database connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef): if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr) print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.") print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
sys.exit(1) sys.exit(1)
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
sys.exit(1)
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
sys.exit(1)
# --- Validate input file --- # --- Validate input file ---
if not os.path.isfile(args.InputFile): if not os.path.isfile(args.InputFile):
@@ -78,39 +431,67 @@ def main():
os.makedirs(temp_dir, exist_ok=True) os.makedirs(temp_dir, exist_ok=True)
try: try:
if engine == "ibcmd":
# --- ibcmd branch: dump EPF/ERF via config export --file ---
arguments = ["infobase", "config", "export", f"--file={args.InputFile}", args.OutputDir, f"--db-path={args.InfoBasePath}"]
ib_data = tempfile.mkdtemp(prefix="ibcmd_data_")
atexit.register(shutil.rmtree, ib_data, ignore_errors=True)
if args.UserName:
arguments.append(f"--user={args.UserName}")
if args.Password:
arguments.append(f"--password={args.Password}")
arguments.append(f"--data={ib_data}")
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)
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: {exit_code})", file=sys.stderr)
sys.exit(exit_code)
# --- Build arguments --- # --- Build arguments ---
arguments = ["DESIGNER"] arguments = ["DESIGNER"]
if args.InfoBaseServer and args.InfoBaseRef: if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f"{args.InfoBaseServer}/{args.InfoBaseRef}"] arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else: else:
arguments += ["/F", args.InfoBasePath] arguments += ["/F", f'"{args.InfoBasePath}"']
if args.UserName: if args.UserName:
arguments.append(f"/N{args.UserName}") arguments.append(f'/N"{args.UserName}"')
if args.Password: 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] arguments += ["-Format", args.Format]
# --- Output --- # --- Output ---
out_file = os.path.join(temp_dir, "dump_log.txt") out_file = os.path.join(temp_dir, "dump_log.txt")
arguments += ["/Out", out_file] arguments += ["/Out", f'"{out_file}"']
arguments.append("/DisableStartupDialogs") arguments.append("/DisableStartupDialogs")
arguments.extend(quote_if_needed(a) for a in extra_args)
# --- Execute --- # --- Execute ---
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( result = run_v8(v8path, arguments)
[v8path] + arguments,
capture_output=True,
text=True,
)
exit_code = result.returncode exit_code = result.returncode
# --- Result --- # --- 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: if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}") 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: else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr) print(f"Error dumping (code: {exit_code})", file=sys.stderr)
@@ -125,6 +506,7 @@ def main():
except Exception: except Exception:
pass pass
print_platform_output(result)
sys.exit(exit_code) sys.exit(exit_code)
finally: finally:
+42 -41
View File
@@ -1,41 +1,42 @@
--- ---
name: epf-init name: epf-init
description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля description: Создать пустую внешнюю обработку 1С (scaffold XML-исходников). Используй когда нужно создать новую внешнюю обработку с нуля
argument-hint: <Name> [Synonym] argument-hint: <Name> [Synonym]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /epf-init — Создание новой обработки # /epf-init — Создание новой обработки
Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки. Генерирует минимальный набор XML-исходников для внешней обработки 1С: корневой файл метаданных и каталог обработки.
## Usage ## Usage
``` ```
/epf-init <Name> [Synonym] [SrcDir] /epf-init <Name> [Synonym] [SrcDir] [FormatVersion]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-------------------------------------| |---------------|:------------:|--------------|------------------------------------------------|
| Name | да | — | Имя обработки (латиница/кириллица) | | Name | да | — | Имя обработки (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
## Команда
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] ```powershell
``` powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"]
```
## Дальнейшие шаги
## Дальнейшие шаги
- Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить форму: `/form-add`
- Добавить справку: `/help-add` - Добавить макет: `/template-add`
- Собрать EPF: `/epf-build` - Добавить справку: `/help-add`
- Собрать EPF: `/epf-build`
+118 -90
View File
@@ -1,90 +1,118 @@
# epf-init v1.1 — Init 1C external data processor scaffold # epf-init v1.4 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$SrcDir = "src" [string]$SrcDir = "src",
)
# Версия формата выгрузки. Своей конфигурации у автономной обработки нет, наследовать
$ErrorActionPreference = "Stop" # версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри обработки
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # навыки берут уже отсюда: их детектор читает version из корня этого файла.
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
$uuid1 = [guid]::NewGuid().ToString() )
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $ErrorActionPreference = "Stop"
$uuid4 = [guid]::NewGuid().ToString() [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$xml = @"
<?xml version="1.0" encoding="UTF-8"?> $uuid1 = [guid]::NewGuid().ToString()
<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"> $uuid2 = [guid]::NewGuid().ToString()
<ExternalDataProcessor uuid="$uuid1"> $uuid3 = [guid]::NewGuid().ToString()
<InternalInfo> $uuid4 = [guid]::NewGuid().ToString()
<xr:ContainedObject>
<xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId> # Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
<xr:ObjectId>$uuid2</xr:ObjectId> $xmlnsDecl = '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"'
</xr:ContainedObject> # 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
<xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object"> # платформа держит объявления по алфавиту, дописать в конец нельзя.
<xr:TypeId>$uuid3</xr:TypeId> if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
<xr:ValueId>$uuid4</xr:ValueId> $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
</xr:GeneratedType> }
</InternalInfo>
<Properties> $xml = @"
<Name>$Name</Name> <?xml version="1.0" encoding="UTF-8"?>
<Synonym> <MetaDataObject $xmlnsDecl version="$FormatVersion">
<v8:item> <ExternalDataProcessor uuid="$uuid1">
<v8:lang>ru</v8:lang> <InternalInfo>
<v8:content>$Synonym</v8:content> <xr:ContainedObject>
</v8:item> <xr:ClassId>c3831ec8-d8d5-4f93-8a22-f9bfae07327f</xr:ClassId>
</Synonym> <xr:ObjectId>$uuid2</xr:ObjectId>
<Comment/> </xr:ContainedObject>
<DefaultForm/> <xr:GeneratedType name="ExternalDataProcessorObject.$Name" category="Object">
<AuxiliaryForm/> <xr:TypeId>$uuid3</xr:TypeId>
</Properties> <xr:ValueId>$uuid4</xr:ValueId>
<ChildObjects/> </xr:GeneratedType>
</ExternalDataProcessor> </InternalInfo>
</MetaDataObject> <Properties>
"@ <Name>$Name</Name>
<Synonym>
$rootFile = Join-Path $SrcDir "$Name.xml" <v8:item>
$processorDir = Join-Path $SrcDir $Name <v8:lang>ru</v8:lang>
<v8:content>$Synonym</v8:content>
if (Test-Path $rootFile) { </v8:item>
Write-Error "Файл уже существует: $rootFile" </Synonym>
exit 1 <Comment/>
} <DefaultForm/>
<AuxiliaryForm/>
if (-not (Test-Path $SrcDir)) { </Properties>
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null <ChildObjects/>
} </ExternalDataProcessor>
$extDir = Join-Path $processorDir "Ext" </MetaDataObject>
New-Item -ItemType Directory -Path $extDir -Force | Out-Null "@
$enc = New-Object System.Text.UTF8Encoding($true) $rootFile = Join-Path $SrcDir "$Name.xml"
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) $processorDir = Join-Path $SrcDir $Name
# --- Модуль объекта --- if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile"
$moduleBsl = @" exit 1
#Область ОписаниеПеременных }
#КонецОбласти if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
#Область ПрограммныйИнтерфейс }
$extDir = Join-Path $processorDir "Ext"
#КонецОбласти New-Item -ItemType Directory -Path $extDir -Force | Out-Null
#Область СлужебныеПроцедурыИФункции $enc = New-Object System.Text.UTF8Encoding($true)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#КонецОбласти #
"@ # Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
$modulePath = Join-Path $extDir "ObjectModule.bsl" function Write-XmlFile([string]$path, [string]$text, $encoding) {
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) $t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
Write-Host "[OK] Создана обработка: $rootFile" }
Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath" Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
# --- Модуль объекта ---
$moduleBsl = @"
#Область ОписаниеПеременных
#КонецОбласти
#Область ПрограммныйИнтерфейс
#КонецОбласти
#Область СлужебныеПроцедурыИФункции
#КонецОбласти
"@
$modulePath = Join-Path $extDir "ObjectModule.bsl"
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
Write-Host "[OK] Создана обработка: $rootFile"
Write-Host " Каталог: $processorDir"
Write-Host " Модуль: $modulePath"
+57 -5
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# epf-init v1.1 — Init 1C external data processor scaffold # epf-init v1.4 — Init 1C external data processor scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external data processor.""" """Generates minimal XML source files for a 1C external data processor."""
import sys, os, argparse, uuid import sys, os, re, argparse, uuid
def esc_xml(s): def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
@@ -14,6 +14,22 @@ def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -21,6 +37,11 @@ def main():
parser.add_argument('-Name', dest='Name', required=True) parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None) parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src') parser.add_argument('-SrcDir', dest='SrcDir', default='src')
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
args = parser.parse_args() args = parser.parse_args()
name = args.Name name = args.Name
@@ -32,8 +53,36 @@ def main():
uuid3 = new_uuid() uuid3 = new_uuid()
uuid4 = new_uuid() uuid4 = new_uuid()
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
xmlns_decl = (
'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"'
)
format_version = args.FormatVersion
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
xml = f'''<?xml version="1.0" encoding="UTF-8"?> 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_decl} version="{format_version}">
\t<ExternalDataProcessor uuid="{uuid1}"> \t<ExternalDataProcessor uuid="{uuid1}">
\t\t<InternalInfo> \t\t<InternalInfo>
\t\t\t<xr:ContainedObject> \t\t\t<xr:ContainedObject>
@@ -72,7 +121,7 @@ def main():
ext_dir = os.path.join(processor_dir, "Ext") ext_dir = os.path.join(processor_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True) os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта --- # --- Модуль объекта ---
module_bsl = """\ module_bsl = """\
@@ -89,7 +138,10 @@ def main():
#КонецОбласти""" #КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl") module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl) # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
print(f"[OK] Создана обработка: {root_file}") print(f"[OK] Создана обработка: {root_file}")
print(f" Каталог: {processor_dir}") print(f" Каталог: {processor_dir}")
+30 -30
View File
@@ -1,30 +1,30 @@
--- ---
name: epf-validate name: epf-validate
description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности description: Валидация внешней обработки 1С (EPF). Используй после создания или модификации обработки для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30] argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /epf-validate — валидация внешней обработки (EPF) # /epf-validate — валидация внешней обработки (EPF)
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF). Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------| |------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу обработки | | ObjectPath | да | — | Путь к корневому XML или каталогу обработки |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | | OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-validate.ps1" -ObjectPath "src/МояОбработка/МояОбработка.xml"
``` ```
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects # Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
@@ -165,8 +165,9 @@ def main():
version = root.get("version", "") version = root.get("version", "")
if not version: if not version:
report_warn("1. Missing version attribute on MetaDataObject") report_warn("1. Missing version attribute on MetaDataObject")
elif version not in ("2.17", "2.20", "2.21"): elif version not in ("2.17", "2.18", "2.19", "2.20", "2.21"):
report_warn(f"1. Unusual version '{version}' (expected 2.17, 2.20 or 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 # Detect type
child_elements = [] child_elements = []
+73 -71
View File
@@ -1,71 +1,73 @@
--- ---
name: erf-build name: erf-build
description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников description: Собрать внешний отчёт 1С (ERF) из XML-исходников. Используй когда пользователь просит собрать, скомпилировать отчёт или получить ERF файл из исходников
argument-hint: <ReportName> argument-hint: <ReportName>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /erf-build — Сборка отчёта # /erf-build — Сборка отчёта
## Usage ## Usage
``` ```
/erf-build <ReportName> [SrcDir] [OutDir] /erf-build <ReportName> [SrcDir] [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|------------|:------------:|--------------|--------------------------------------| |------------|:------------:|--------------|--------------------------------------|
| ReportName | да | — | Имя отчёта (имя корневого XML) | | ReportName | да | — | Имя отчёта (имя корневого XML) |
| SrcDir | нет | `src` | Каталог исходников | | SrcDir | нет | `src` | Каталог исходников |
| OutDir | нет | `build` | Каталог для результата | | OutDir | нет | `build` | Каталог для результата |
## Параметры подключения (опционально) ## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы. Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки. 6. Если `.v8-project.json` нет или база не найдена — не указывай параметры подключения: скрипт автоматически создаст временную базу. Для ERF со ссылочными типами (CatalogRef, DocumentRef и т.д.) генерируются заглушки метаданных. Временная база удаляется после сборки.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
Используй общий скрипт из epf-build: Используй общий скрипт из epf-build:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников | | `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу | | `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Примеры > `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
```powershell ## Примеры
# Сборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf" ```powershell
# Сборка отчёта (файловая база)
# Серверная база powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBasePath "C:\Bases\MyDB" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
``` # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-build.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -SourceFile "src/МойОтчёт.xml" -OutputFile "build/МойОтчёт.erf"
```
+73 -71
View File
@@ -1,71 +1,73 @@
--- ---
name: erf-dump name: erf-dump
description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла description: Разобрать ERF-файл отчёта 1С в XML-исходники. Используй когда пользователь просит разобрать, декомпилировать отчёт, получить исходники из ERF файла
argument-hint: <ErfFile> argument-hint: <ErfFile>
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
- Grep - Grep
--- ---
# /erf-dump — Разборка отчёта # /erf-dump — Разборка отчёта
## Usage ## Usage
``` ```
/erf-dump <ErfFile> [OutDir] /erf-dump <ErfFile> [OutDir]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|----------|:------------:|--------------|-------------------------------------| |----------|:------------:|--------------|-------------------------------------|
| ErfFile | да | — | Путь к ERF-файлу | | ErfFile | да | — | Путь к ERF-файлу |
| OutDir | нет | `src` | Каталог для выгрузки исходников | | OutDir | нет | `src` | Каталог для выгрузки исходников |
## Параметры подключения (обязательно) ## Параметры подключения (обязательно)
Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются. Для разборки EPF/ERF требуется информационная база с конфигурацией. Без базы ссылочные типы безвозвратно теряются.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу: 1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую 2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json` 3. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
4. Если не указал — сопоставь текущую ветку Git с `databases[].branches` 4. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
5. Если ветка не совпала — используй `default` 5. Если ветка не совпала — используй `default`
6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`. 6. Если `.v8-project.json` нет или база не найдена — **сообщи пользователю об ошибке**. Для dump база обязательна: в пустой базе ссылочные типы (CatalogRef, DocumentRef и т.д.) безвозвратно сбрасываются в строки. Предложи указать базу или зарегистрировать через `/db-list add`.
Если `v8path` не задан — автоопределение: `Get-ChildItem "C:\Program Files\1cv8\*\bin\1cv8.exe" | Sort -Desc | Select -First 1` Если `v8path` не задан — скрипт сам попытается определить платформу (`.v8-project.json` → Program Files).
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`. Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
## Команда ## Команда
Используй общий скрипт из epf-dump: Используй общий скрипт из epf-dump:
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры> powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" <параметры>
``` ```
### Параметры скрипта ### Параметры скрипта
| Параметр | Обязательный | Описание | | Параметр | Обязательный | Описание |
|----------|:------------:|----------| |----------|:------------:|----------|
| `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) | | `-V8Path <путь>` | нет | Каталог bin платформы (или полный путь к 1cv8.exe) |
| `-InfoBasePath <путь>` | * | Файловая база | | `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) | | `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере | | `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Имя пользователя | | `-UserName <имя>` | нет | Имя пользователя |
| `-Password <пароль>` | нет | Пароль | | `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к ERF-файлу | | `-InputFile <путь>` | да | Путь к ERF-файлу |
| `-OutputDir <путь>` | да | Каталог для выгрузки исходников | | `-OutputDir <путь>` | да | Каталог для выгрузки исходников |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` | | `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
> `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы) | `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
## Примеры > `*` — обязательно хотя бы одно подключение. Без базы скрипт завершится с ошибкой (dump в пустой базе безвозвратно теряет ссылочные типы)
```powershell ## Примеры
# Разборка отчёта (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src" ```powershell
# Разборка отчёта (файловая база)
# Серверная база powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBasePath "C:\Bases\MyDB" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
``` # Серверная база
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-dump/scripts/epf-dump.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -InputFile "build/МойОтчёт.erf" -OutputDir "src"
```
+43 -42
View File
@@ -1,42 +1,43 @@
--- ---
name: erf-init name: erf-init
description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля description: Создать пустой внешний отчёт 1С (scaffold XML-исходников). Используй когда нужно создать новый внешний отчёт с нуля
argument-hint: <Name> [Synonym] [--with-skd] argument-hint: <Name> [Synonym] [--with-skd]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /erf-init — Создание нового отчёта # /erf-init — Создание нового отчёта
Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта. Генерирует минимальный набор XML-исходников для внешнего отчёта 1С: корневой файл метаданных и каталог отчёта.
## Usage ## Usage
``` ```
/erf-init <Name> [Synonym] [SrcDir] [--with-skd] /erf-init <Name> [Synonym] [SrcDir] [FormatVersion] [--with-skd]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|---------------------------------------| |---------------|:------------:|--------------|---------------------------------------|
| Name | да | — | Имя отчёта (латиница/кириллица) | | Name | да | — | Имя отчёта (латиница/кириллица) |
| Synonym | нет | = Name | Синоним (отображаемое имя) | | Synonym | нет | = Name | Синоним (отображаемое имя) |
| SrcDir | нет | `src` | Каталог исходников относительно CWD | | SrcDir | нет | `src` | Каталог исходников относительно CWD |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema | | FormatVersion | нет | `2.17` | Версия формата: 2.20 — платформа 8.3.27, 2.21 — 8.5. Дефолт открывается любой платформой |
| --WithSKD | нет | — | Создать пустую СКД и привязать к MainDataCompositionSchema |
## Команда
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-WithSKD] ```powershell
``` powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/init.ps1" -Name "<Name>" [-Synonym "<Synonym>"] [-SrcDir "<SrcDir>"] [-FormatVersion "<2.17|2.18|2.19|2.20|2.21>"] [-WithSKD]
```
## Дальнейшие шаги
## Дальнейшие шаги
- Добавить форму: `/form-add`
- Добавить макет: `/template-add` - Добавить форму: `/form-add`
- Добавить справку: `/help-add` - Добавить макет: `/template-add`
- Собрать ERF: `/erf-build` - Добавить справку: `/help-add`
- Собрать ERF: `/erf-build`
+208 -180
View File
@@ -1,180 +1,208 @@
# erf-init v1.1 — Init 1C external report scaffold # erf-init v1.4 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
[string]$Name, [string]$Name,
[string]$Synonym = $Name, [string]$Synonym = $Name,
[string]$SrcDir = "src", [string]$SrcDir = "src",
[switch]$WithSKD [switch]$WithSKD,
)
# Версия формата выгрузки. Своей конфигурации у автономного отчёта нет, наследовать
$ErrorActionPreference = "Stop" # версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри отчёта
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 # навыки берут уже отсюда: их детектор читает version из корня этого файла.
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [ValidateSet("2.17", "2.18", "2.19", "2.20", "2.21")]
[string]$FormatVersion = "2.17"
$uuid1 = [guid]::NewGuid().ToString() )
$uuid2 = [guid]::NewGuid().ToString()
$uuid3 = [guid]::NewGuid().ToString() $ErrorActionPreference = "Stop"
$uuid4 = [guid]::NewGuid().ToString() [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Формируем Properties ---
$uuid1 = [guid]::NewGuid().ToString()
$mainDCSValue = "" $uuid2 = [guid]::NewGuid().ToString()
$childObjectsContent = "" $uuid3 = [guid]::NewGuid().ToString()
$uuid4 = [guid]::NewGuid().ToString()
if ($WithSKD) {
$mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных" # Объявления пространств имён — одной переменной: места эмиссии её только интерполируют.
$childObjectsContent = @" $xmlnsDecl = '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"'
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед style):
<Template>ОсновнаяСхемаКомпоновкиДанных</Template> # платформа держит объявления по алфавиту, дописать в конец нельзя.
if (($FormatVersion -match '^(\d+)\.(\d+)$') -and ([int]$Matches[1] * 100 + [int]$Matches[2]) -ge 221) {
"@ $xmlnsDecl = $xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
} }
$mainDCSElement = if ($mainDCSValue) { # --- Формируем Properties ---
"<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
} else { $mainDCSValue = ""
"<MainDataCompositionSchema/>" $childObjectsContent = ""
}
if ($WithSKD) {
$childObjectsXml = if ($childObjectsContent) { $mainDCSValue = "ExternalReport.$Name.Template.ОсновнаяСхемаКомпоновкиДанных"
"<ChildObjects>$childObjectsContent</ChildObjects>" $childObjectsContent = @"
} else {
"<ChildObjects/>" <Template>ОсновнаяСхемаКомпоновкиДанных</Template>
}
"@
$xml = @" }
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> $mainDCSElement = if ($mainDCSValue) {
<ExternalReport uuid="$uuid1"> "<MainDataCompositionSchema>$mainDCSValue</MainDataCompositionSchema>"
<InternalInfo> } else {
<xr:ContainedObject> "<MainDataCompositionSchema/>"
<xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId> }
<xr:ObjectId>$uuid2</xr:ObjectId>
</xr:ContainedObject> $childObjectsXml = if ($childObjectsContent) {
<xr:GeneratedType name="ExternalReportObject.$Name" category="Object"> "<ChildObjects>$childObjectsContent</ChildObjects>"
<xr:TypeId>$uuid3</xr:TypeId> } else {
<xr:ValueId>$uuid4</xr:ValueId> "<ChildObjects/>"
</xr:GeneratedType> }
</InternalInfo>
<Properties> $xml = @"
<Name>$Name</Name> <?xml version="1.0" encoding="UTF-8"?>
<Synonym> <MetaDataObject $xmlnsDecl version="$FormatVersion">
<v8:item> <ExternalReport uuid="$uuid1">
<v8:lang>ru</v8:lang> <InternalInfo>
<v8:content>$Synonym</v8:content> <xr:ContainedObject>
</v8:item> <xr:ClassId>e41aff26-25cf-4bb6-b6c1-3f478a75f374</xr:ClassId>
</Synonym> <xr:ObjectId>$uuid2</xr:ObjectId>
<Comment/> </xr:ContainedObject>
<DefaultForm/> <xr:GeneratedType name="ExternalReportObject.$Name" category="Object">
<AuxiliaryForm/> <xr:TypeId>$uuid3</xr:TypeId>
$mainDCSElement <xr:ValueId>$uuid4</xr:ValueId>
<DefaultSettingsForm/> </xr:GeneratedType>
<AuxiliarySettingsForm/> </InternalInfo>
<DefaultVariantForm/> <Properties>
<VariantsStorage/> <Name>$Name</Name>
<SettingsStorage/> <Synonym>
</Properties> <v8:item>
$childObjectsXml <v8:lang>ru</v8:lang>
</ExternalReport> <v8:content>$Synonym</v8:content>
</MetaDataObject> </v8:item>
"@ </Synonym>
<Comment/>
$rootFile = Join-Path $SrcDir "$Name.xml" <DefaultForm/>
$reportDir = Join-Path $SrcDir $Name <AuxiliaryForm/>
$mainDCSElement
if (Test-Path $rootFile) { <DefaultSettingsForm/>
Write-Error "Файл уже существует: $rootFile" <AuxiliarySettingsForm/>
exit 1 <DefaultVariantForm/>
} <VariantsStorage/>
<SettingsStorage/>
if (-not (Test-Path $SrcDir)) { </Properties>
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null $childObjectsXml
} </ExternalReport>
$extDir = Join-Path $reportDir "Ext" </MetaDataObject>
New-Item -ItemType Directory -Path $extDir -Force | Out-Null "@
$enc = New-Object System.Text.UTF8Encoding($true) $rootFile = Join-Path $SrcDir "$Name.xml"
[System.IO.File]::WriteAllText((Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml"), $xml, $enc) $reportDir = Join-Path $SrcDir $Name
# --- Модуль объекта --- if (Test-Path $rootFile) {
Write-Error "Файл уже существует: $rootFile"
$moduleBsl = @" exit 1
#Область ОписаниеПеременных }
#КонецОбласти if (-not (Test-Path $SrcDir)) {
New-Item -ItemType Directory -Path $SrcDir -Force | Out-Null
#Область ПрограммныйИнтерфейс }
$extDir = Join-Path $reportDir "Ext"
#КонецОбласти New-Item -ItemType Directory -Path $extDir -Force | Out-Null
#Область СлужебныеПроцедурыИФункции $enc = New-Object System.Text.UTF8Encoding($true)
# XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#КонецОбласти #
"@ # Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
$modulePath = Join-Path $extDir "ObjectModule.bsl" function Write-XmlFile([string]$path, [string]$text, $encoding) {
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc) $t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
Write-Host "[OK] Создан отчёт: $rootFile" }
Write-Host " Каталог: $reportDir"
Write-Host " Модуль: $modulePath" Write-XmlFile (Resolve-Path $SrcDir | Join-Path -ChildPath "$Name.xml") $xml $enc
# --- СКД-макет (если --WithSKD) --- # --- Модуль объекта ---
if ($WithSKD) { $moduleBsl = @"
$templatesDir = Join-Path $reportDir "Templates" #Область ОписаниеПеременных
$skdName = "ОсновнаяСхемаКомпоновкиДанных"
$skdMetaPath = Join-Path $templatesDir "$skdName.xml" #КонецОбласти
$skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null #Область ПрограммныйИнтерфейс
$skdUuid = [guid]::NewGuid().ToString() #КонецОбласти
$skdMetaXml = @" #Область СлужебныеПроцедурыИФункции
<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17"> #КонецОбласти
<Template uuid="$skdUuid"> "@
<Properties>
<Name>$skdName</Name> $modulePath = Join-Path $extDir "ObjectModule.bsl"
<Synonym> # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
<v8:item> # чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
<v8:lang>ru</v8:lang> # неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
<v8:content>Основная схема компоновки данных</v8:content> # самого скрипта, а он в репозитории хранится с LF.
</v8:item> $moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
</Synonym> [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $enc)
<Comment/>
<TemplateType>DataCompositionSchema</TemplateType> Write-Host "[OK] Создан отчёт: $rootFile"
</Properties> Write-Host " Каталог: $reportDir"
</Template> Write-Host " Модуль: $modulePath"
</MetaDataObject>
"@ # --- СКД-макет (если --WithSKD) ---
[System.IO.File]::WriteAllText($skdMetaPath, $skdMetaXml, $enc) if ($WithSKD) {
$templatesDir = Join-Path $reportDir "Templates"
$skdContent = @" $skdName = "ОсновнаяСхемаКомпоновкиДанных"
<?xml version="1.0" encoding="UTF-8"?> $skdMetaPath = Join-Path $templatesDir "$skdName.xml"
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" $skdExtDir = Join-Path (Join-Path $templatesDir $skdName) "Ext"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common" New-Item -ItemType Directory -Path $skdExtDir -Force | Out-Null
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" $skdUuid = [guid]::NewGuid().ToString()
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" $skdMetaXml = @"
xmlns:xs="http://www.w3.org/2001/XMLSchema" <?xml version="1.0" encoding="UTF-8"?>
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <MetaDataObject $xmlnsDecl version="$FormatVersion">
<dataSource> <Template uuid="$skdUuid">
<name>ИсточникДанных1</name> <Properties>
<dataSourceType>Local</dataSourceType> <Name>$skdName</Name>
</dataSource> <Synonym>
</DataCompositionSchema> <v8:item>
"@ <v8:lang>ru</v8:lang>
<v8:content>Основная схема компоновки данных</v8:content>
$skdFilePath = Join-Path $skdExtDir "Template.xml" </v8:item>
[System.IO.File]::WriteAllText($skdFilePath, $skdContent, $enc) </Synonym>
<Comment/>
Write-Host " СКД: $skdMetaPath" <TemplateType>DataCompositionSchema</TemplateType>
Write-Host " Тело: $skdFilePath" </Properties>
} </Template>
</MetaDataObject>
"@
Write-XmlFile $skdMetaPath $skdMetaXml $enc
$skdContent = @"
<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
xmlns:dcscom="http://v8.1c.ru/8.1/data-composition-system/common"
xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"
xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"
xmlns:v8="http://v8.1c.ru/8.1/data/core"
xmlns:v8ui="http://v8.1c.ru/8.1/data/ui"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dataSource>
<name>ИсточникДанных1</name>
<dataSourceType>Local</dataSourceType>
</dataSource>
</DataCompositionSchema>
"@
$skdFilePath = Join-Path $skdExtDir "Template.xml"
Write-XmlFile $skdFilePath $skdContent $enc
Write-Host " СКД: $skdMetaPath"
Write-Host " Тело: $skdFilePath"
}
+60 -8
View File
@@ -1,8 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# erf-init v1.1 — Init 1C external report scaffold # erf-init v1.4 — Init 1C external report scaffold
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C external report.""" """Generates minimal XML source files for a 1C external report."""
import sys, os, argparse, uuid import sys, os, re, argparse, uuid
def esc_xml(s): def esc_xml(s):
return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;') return s.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;').replace('"','&quot;')
@@ -14,6 +14,22 @@ def write_utf8_bom(path, content):
with open(path, 'w', encoding='utf-8-sig', newline='') as f: with open(path, 'w', encoding='utf-8-sig', newline='') as f:
f.write(content) f.write(content)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_utf8_bom(path, text)
def format_rank(ver):
""""2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
m = re.match(r'^(\d+)\.(\d+)$', ver or '')
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -21,6 +37,11 @@ def main():
parser.add_argument('-Name', dest='Name', required=True) parser.add_argument('-Name', dest='Name', required=True)
parser.add_argument('-Synonym', dest='Synonym', default=None) parser.add_argument('-Synonym', dest='Synonym', default=None)
parser.add_argument('-SrcDir', dest='SrcDir', default='src') parser.add_argument('-SrcDir', dest='SrcDir', default='src')
# Версия формата выгрузки. Своей конфигурации у автономного объекта нет, наследовать
# версию неоткуда — поэтому её задают явно. Формы, макеты и справку внутри объекта
# навыки берут уже отсюда: их детектор читает version из корня этого файла.
parser.add_argument('-FormatVersion', dest='FormatVersion', default='2.17',
choices=['2.17', '2.18', '2.19', '2.20', '2.21'])
parser.add_argument('-WithSKD', dest='WithSKD', action='store_true') parser.add_argument('-WithSKD', dest='WithSKD', action='store_true')
args = parser.parse_args() args = parser.parse_args()
@@ -33,6 +54,34 @@ def main():
uuid3 = new_uuid() uuid3 = new_uuid()
uuid4 = new_uuid() uuid4 = new_uuid()
# Объявления пространств имён — одной переменной: места эмиссии её только подставляют.
xmlns_decl = (
'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"'
)
format_version = args.FormatVersion
# 2.21 (8.5) добавила в шапку пространство палитры. Вставляем НА МЕСТО (после lf, перед
# style): платформа держит объявления по алфавиту, дописать в конец нельзя.
if format_rank(format_version) >= 221:
xmlns_decl = xmlns_decl.replace(
' xmlns:style=',
' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style=')
# --- Properties --- # --- Properties ---
main_dcs_value = "" main_dcs_value = ""
child_objects_content = "" child_objects_content = ""
@@ -45,7 +94,7 @@ def main():
child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>" child_objects_xml = f"<ChildObjects>{child_objects_content}\t\t</ChildObjects>" if child_objects_content else "<ChildObjects/>"
xml = f'''<?xml version="1.0" encoding="UTF-8"?> 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_decl} version="{format_version}">
\t<ExternalReport uuid="{uuid1}"> \t<ExternalReport uuid="{uuid1}">
\t\t<InternalInfo> \t\t<InternalInfo>
\t\t\t<xr:ContainedObject> \t\t\t<xr:ContainedObject>
@@ -90,7 +139,7 @@ def main():
ext_dir = os.path.join(report_dir, "Ext") ext_dir = os.path.join(report_dir, "Ext")
os.makedirs(ext_dir, exist_ok=True) os.makedirs(ext_dir, exist_ok=True)
write_utf8_bom(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml) write_xml_file(os.path.join(os.path.abspath(src_dir), f"{name}.xml"), xml)
# --- Модуль объекта --- # --- Модуль объекта ---
module_bsl = """\ module_bsl = """\
@@ -107,7 +156,10 @@ def main():
#КонецОбласти""" #КонецОбласти"""
module_path = os.path.join(ext_dir, "ObjectModule.bsl") module_path = os.path.join(ext_dir, "ObjectModule.bsl")
write_utf8_bom(module_path, module_bsl) # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_utf8_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
print(f"[OK] Создан отчёт: {root_file}") print(f"[OK] Создан отчёт: {root_file}")
print(f" Каталог: {report_dir}") print(f" Каталог: {report_dir}")
@@ -124,7 +176,7 @@ def main():
skd_uuid = new_uuid() skd_uuid = new_uuid()
skd_meta_xml = f'''<?xml version="1.0" encoding="UTF-8"?> skd_meta_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_decl} version="{format_version}">
\t<Template uuid="{skd_uuid}"> \t<Template uuid="{skd_uuid}">
\t\t<Properties> \t\t<Properties>
\t\t\t<Name>{skd_name}</Name> \t\t\t<Name>{skd_name}</Name>
@@ -140,7 +192,7 @@ def main():
\t</Template> \t</Template>
</MetaDataObject>''' </MetaDataObject>'''
write_utf8_bom(skd_meta_path, skd_meta_xml) write_xml_file(skd_meta_path, skd_meta_xml)
skd_content = '''<?xml version="1.0" encoding="UTF-8"?> skd_content = '''<?xml version="1.0" encoding="UTF-8"?>
<DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema" <DataCompositionSchema xmlns="http://v8.1c.ru/8.1/data-composition-system/schema"
@@ -158,7 +210,7 @@ def main():
</DataCompositionSchema>''' </DataCompositionSchema>'''
skd_file_path = os.path.join(skd_ext_dir, "Template.xml") skd_file_path = os.path.join(skd_ext_dir, "Template.xml")
write_utf8_bom(skd_file_path, skd_content) write_xml_file(skd_file_path, skd_content)
print(f" СКД: {skd_meta_path}") print(f" СКД: {skd_meta_path}")
print(f" Тело: {skd_file_path}") print(f" Тело: {skd_file_path}")
+32 -32
View File
@@ -1,32 +1,32 @@
--- ---
name: erf-validate name: erf-validate
description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности description: Валидация внешнего отчёта 1С (ERF). Используй после создания или модификации отчёта для проверки корректности
argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30] argument-hint: <ObjectPath> [-Detailed] [-MaxErrors 30]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Glob - Glob
--- ---
# /erf-validate — валидация внешнего отчёта (ERF) # /erf-validate — валидация внешнего отчёта (ERF)
Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Проверяет структурную корректность XML-исходников внешнего отчёта: корневую структуру, InternalInfo, свойства (включая MainDataCompositionSchema), ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов.
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport). Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
## Параметры ## Параметры
| Параметр | Обяз. | Умолч. | Описание | | Параметр | Обяз. | Умолч. | Описание |
|------------|:-----:|---------|-------------------------------------------------| |------------|:-----:|---------|-------------------------------------------------|
| ObjectPath | да | — | Путь к корневому XML или каталогу отчёта | | ObjectPath | да | — | Путь к корневому XML или каталогу отчёта |
| Detailed | нет | — | Подробный вывод (все проверки, включая успешные) | | Detailed | нет | — | Подробный вывод (все проверки, включая успешные) |
| MaxErrors | нет | 30 | Остановиться после N ошибок | | MaxErrors | нет | 30 | Остановиться после N ошибок |
| OutFile | нет | — | Записать результат в файл (UTF-8 BOM) | | OutFile | нет | — | Записать результат в файл (UTF-8 BOM) |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт"
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml" powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-validate/scripts/epf-validate.ps1" -ObjectPath "src/МойОтчёт/МойОтчёт.xml"
``` ```
+71 -71
View File
@@ -1,71 +1,71 @@
--- ---
name: form-add name: form-add
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default] argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
allowed-tools: allowed-tools:
- Bash - Bash
- Read - Read
- Write - Write
- Edit - Edit
- Glob - Glob
- Grep - Grep
--- ---
# /form-add — Добавление формы к объекту конфигурации # /form-add — Добавление формы к объекту конфигурации
Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.). Создаёт управляемую форму (metadata XML + Form.xml + Module.bsl) и регистрирует её в корневом XML объекта конфигурации (Document, Catalog, InformationRegister и др.).
## Usage ## Usage
``` ```
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default] /form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
``` ```
| Параметр | Обязательный | По умолчанию | Описание | | Параметр | Обязательный | По умолчанию | Описание |
|-------------|:------------:|--------------|----------------------------------------------| |-------------|:------------:|--------------|----------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) | | ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
| FormName | да | — | Имя формы (ФормаДокумента) | | FormName | да | — | Имя формы (ФормаДокумента) |
| Purpose | нет | Object | Назначение: Object, List, Choice, Record | | Purpose | нет | Object | Назначение: Object, List, Choice, Record |
| Synonym | нет | = FormName | Синоним формы | | Synonym | нет | = FormName | Синоним формы |
| --set-default | нет | авто | Установить как форму по умолчанию | | --set-default | нет | авто | Установить как форму по умолчанию |
## Команда ## Команда
```powershell ```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault] powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -ObjectPath "<ObjectPath>" -FormName "<FormName>" [-Purpose "<Purpose>"] [-Synonym "<Synonym>"] [-SetDefault]
``` ```
## Purpose — назначение формы ## Purpose — назначение формы
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство | | Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|---------|-------------------------|-------------------|---------------------| |---------|-------------------------|-------------------|---------------------|
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) | | Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm | | List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm | | Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm | | Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
## Примеры ## Примеры
``` ```
# Форма документа # Форма документа
/form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object /form-add Documents/АвансовыйОтчет.xml ФормаДокумента --purpose Object
# Форма списка каталога # Форма списка каталога
/form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List /form-add Catalogs/Контрагенты.xml ФормаСписка --purpose List
# Форма записи регистра сведений # Форма записи регистра сведений
/form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record /form-add InformationRegisters/КурсыВалют.xml ФормаЗаписи --purpose Record
# Форма выбора с синонимом # Форма выбора с синонимом
/form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры" /form-add Catalogs/Номенклатура.xml ФормаВыбора --purpose Choice --synonym "Выбор номенклатуры"
# Установить как форму по умолчанию # Установить как форму по умолчанию
/form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default /form-add Documents/Заказ.xml ФормаДокументаНовая --purpose Object --set-default
``` ```
## Workflow ## Workflow
1. `/form-add` — создать каркас формы 1. `/form-add` — создать каркас формы
2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами 2. `/form-compile` или `/form-edit` — наполнить Form.xml элементами
3. `/form-validate` — проверить корректность 3. `/form-validate` — проверить корректность
4. `/form-info` — проанализировать результат 4. `/form-info` — проанализировать результат
+232 -19
View File
@@ -1,4 +1,4 @@
# form-add v1.5 — Add managed form to 1C config object # form-add v1.23 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param( param(
[Parameter(Mandatory)] [Parameter(Mandatory)]
@@ -18,14 +18,156 @@ $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8 [Console]::InputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
# read-only configs unless allowed. Trigger = bin present; reaction from
# .v8-project.json editingAllowedCheck (deny|warn|off, default deny). Never
# throws — guard errors degrade to allow.
function Get-RootUuid([string]$xmlPath) {
if (-not (Test-Path $xmlPath)) { return $null }
try {
[xml]$mx = Get-Content -Path $xmlPath -Encoding UTF8
$el = $mx.DocumentElement.FirstChild
while ($el -and $el.NodeType -ne 'Element') { $el = $el.NextSibling }
if ($el) { $u = $el.GetAttribute("uuid"); if ($u) { return $u } }
} catch {}
return $null
}
function 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++) {
$pj = Join-Path $d ".v8-project.json"
if (Test-Path $pj) { return $pj }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
return $null
}
function Get-EditMode([string]$cfgDir) {
try {
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project $cfgDir }
if (-not $pj) { return 'deny' }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc) {
$src = [System.IO.Path]::GetFullPath($db.configSrc).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ($db.editingAllowedCheck) { return $db.editingAllowedCheck }
}
}
}
}
if ($proj.editingAllowedCheck) { return $proj.editingAllowedCheck }
return 'deny'
} catch { return 'deny' }
}
function Assert-EditAllowed([string]$targetPath, [string]$require) {
try {
$rp = $targetPath
try { $rp = (Resolve-Path $targetPath -ErrorAction Stop).Path } catch {}
# 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"
if ((Test-Path $cand) -or (Test-Path (Join-Path $d "Configuration.xml"))) { $cfgDir = $d; $binPath = $cand }
}
if ($elemUuid -and $cfgDir) { break }
$parent = [System.IO.Path]::GetDirectoryName($d)
if ($parent -eq $d) { break }
$d = $parent
}
# New object (no element file): fall back to config root uuid.
if (-not $elemUuid -and $cfgDir) { $elemUuid = Get-RootUuid (Join-Path $cfgDir "Configuration.xml") }
if (-not $binPath -or -not (Test-Path $binPath)) { return }
$bytes = [System.IO.File]::ReadAllBytes($binPath)
if ($bytes.Length -le 32) { return }
$start = 0
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 }
$text = [System.Text.Encoding]::UTF8.GetString($bytes, $start, $bytes.Length - $start)
$hm = [regex]::Match($text, '^\{6,(\d+),(\d+),')
if (-not $hm.Success) { return }
$G = [int]$hm.Groups[1].Value
$K = [int]$hm.Groups[2].Value
if ($K -eq 0) { return }
$best = $null
if ($elemUuid) {
$u = [regex]::Escape($elemUuid.ToLower())
foreach ($m in [regex]::Matches($text, "([0-2]),0,$u")) {
$f1 = [int]$m.Groups[1].Value
if ($null -eq $best -or $f1 -lt $best) { $best = $f1 }
}
}
$blocked = $false; $code = ""; $reason = ""
if ($G -eq 1) { $blocked = $true; $code = "capability-off"; $reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)" }
elseif ($require -eq 'removed') {
if ($null -ne $best -and $best -ne 2) { $blocked = $true; $code = "not-removed"; $reason = "объект не снят с поддержки — удаление сломает обновления" }
}
else {
if ($null -ne $best -and $best -eq 0) { $blocked = $true; $code = "locked"; $reason = "объект на замке — редактирование сломает обновления" }
}
if (-not $blocked) { return }
$mode = Get-EditMode $cfgDir
if ($mode -eq 'off') { return }
# Use Console.Error (not Write-Error) — under ErrorActionPreference=Stop the
# latter throws and would be swallowed by this function's own catch.
if ($mode -eq 'warn') { [Console]::Error.WriteLine("[support-guard] ПРЕДУПРЕЖДЕНИЕ: $reason. Цель: $rp"); return }
$head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
$cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
$offNote = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if ($code -eq "capability-off") {
$state = "Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «$rp» редактировать нельзя."
$fix = "Либо снять защиту явно (навык support-edit, два шага):`n 1. support-edit -Path ""$cfgDir"" -Capability on — включить возможность изменения (объекты пока остаются на замке);`n 2. support-edit -Path ""$rp"" -Set editable — открыть этот объект для редактирования.`n Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
} elseif ($code -eq "not-removed") {
$state = "Состояние: объект «$rp» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
$fix = "Либо сначала снять объект с поддержки, затем удалять:`n support-edit -Path ""$rp"" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно."
} else {
$state = "Состояние: объект «$rp» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
$fix = "Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):`n support-edit -Path ""$rp"" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);`n support-edit -Path ""$rp"" -Set off-support — снять с поддержки: обновления по объекту больше не приходят."
}
[Console]::Error.WriteLine("$head`n$state`n$cfe`n$fix`n$offNote")
exit 1
} catch { return }
}
# --- Detect XML format version --- # --- Detect XML format version ---
function Detect-FormatVersion([string]$dir) { function Detect-FormatVersion([string]$dir) {
$d = $dir $d = $dir
while ($d) { while ($d) {
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
$extPath = "$d.xml"
if (Test-Path $extPath) {
$extText = [System.IO.File]::ReadAllText($extPath, [System.Text.Encoding]::UTF8)
$extHead = $extText.Substring(0, [Math]::Min(2000, $extText.Length))
if ($extHead -match '<(ExternalDataProcessor|ExternalReport)[ >]' -and $extHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$cfgPath = Join-Path $d "Configuration.xml" $cfgPath = Join-Path $d "Configuration.xml"
if (Test-Path $cfgPath) { 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] } if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
} }
$parent = Split-Path $d -Parent $parent = Split-Path $d -Parent
@@ -35,6 +177,13 @@ function Detect-FormatVersion([string]$dir) {
return "2.17" return "2.17"
} }
# Версия формата как число для сравнений: "2.20" → 220, "2.9" → 209.
# Строковое сравнение здесь неверно ("2.9" > "2.17" лексикографически) — известная ловушка.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
# --- Фаза 1: Определение типа объекта --- # --- Фаза 1: Определение типа объекта ---
# Resolve ObjectPath (directory → .xml) # Resolve ObjectPath (directory → .xml)
@@ -55,7 +204,27 @@ if (-not (Test-Path $ObjectPath)) {
} }
$objectXmlFull = Resolve-Path $ObjectPath $objectXmlFull = Resolve-Path $ObjectPath
$script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) Assert-EditAllowed $objectXmlFull.Path 'editable'
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
$script:formatVersion = $null
$objHead = [System.IO.File]::ReadAllText($objectXmlFull.Path, [System.Text.Encoding]::UTF8)
$objHead = $objHead.Substring(0, [Math]::Min(2000, $objHead.Length))
if ($objHead -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { $script:formatVersion = $Matches[1] }
if (-not $script:formatVersion) { $script:formatVersion = Detect-FormatVersion (Split-Path $objectXmlFull.Path -Parent) }
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
# интерполируют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
$script:xmlnsDecl = '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"'
$script:formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns: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: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"'
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$script:xmlnsDecl = $script:xmlnsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
$script:formNsDecl = $script:formNsDecl -replace ' xmlns:style=', ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
}
$xmlDoc = New-Object System.Xml.XmlDocument $xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true $xmlDoc.PreserveWhitespace = $true
@@ -76,7 +245,7 @@ $supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task" "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
) )
$objectType = $null $objectType = $null
@@ -178,9 +347,16 @@ if ($objectType -in $processorLikeTypes) {
$extPresentationLine = "`n`t`t`t<ExtendedPresentation/>" $extPresentationLine = "`n`t`t`t<ExtendedPresentation/>"
} }
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
$useInIfcLine = ""
if ((Get-FormatRank $script:formatVersion) -ge 221) {
$useInIfcLine = "`n`t`t`t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>"
}
$formMetaXml = @" $formMetaXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?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="$($script:formatVersion)"> <MetaDataObject $($script:xmlnsDecl) version="$($script:formatVersion)">
<Form uuid="$formUuid"> <Form uuid="$formUuid">
<Properties> <Properties>
<Name>$FormName</Name> <Name>$FormName</Name>
@@ -196,20 +372,29 @@ $formMetaXml = @"
<UsePurposes> <UsePurposes>
<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>
<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value> <v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>
</UsePurposes>$extPresentationLine </UsePurposes>$useInIfcLine$extPresentationLine
</Properties> </Properties>
</Form> </Form>
</MetaDataObject> </MetaDataObject>
"@ "@
[System.IO.File]::WriteAllText($formMetaPath, $formMetaXml, $encBom) # XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
#
# Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
# копии одинаковыми — сознательно: разошедшиеся копии сводят на нет весь смысл.
#
# Модуль .bsl сюда НЕ идёт — он пишется отдельно.
function Write-XmlFile([string]$path, [string]$text, $encoding) {
$t = ($text -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($path, $t.TrimEnd("`r", "`n"), $encoding)
}
Write-XmlFile $formMetaPath $formMetaXml $encBom
# --- 3b. Form.xml --- # --- 3b. Form.xml ---
$formXmlPath = Join-Path $formExtDir "Form.xml" $formXmlPath = Join-Path $formExtDir "Form.xml"
$formNsDecl = 'xmlns="http://v8.1c.ru/8.3/xcf/logform" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core" xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings" xmlns: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: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"'
if ($Purpose -eq "List" -or $Purpose -eq "Choice") { if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
# Динамический список # Динамический список
# MainTable: тип.имя # MainTable: тип.имя
@@ -217,7 +402,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)"> <Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
@@ -242,7 +427,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)"> <Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
@@ -289,7 +474,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
$formXml = @" $formXml = @"
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Form $formNsDecl version="$($script:formatVersion)"> <Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1"> <AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill> <Autofill>true</Autofill>
</AutoCommandBar> </AutoCommandBar>
@@ -309,7 +494,7 @@ if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
if (Test-Path $formXmlPath) { if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting" Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else { } else {
[System.IO.File]::WriteAllText($formXmlPath, $formXml, $encBom) Write-XmlFile $formXmlPath $formXml $encBom
} }
# --- 3c. Module.bsl --- # --- 3c. Module.bsl ---
@@ -341,6 +526,11 @@ $moduleBsl = @"
if (Test-Path $modulePath) { if (Test-Path $modulePath) {
Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting" Write-Host "[SKIP] Module.bsl already exists: $modulePath — not overwriting"
} else { } else {
# Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без). Шаблон берёт переводы строк из
# самого скрипта, а он в репозитории хранится с LF.
$moduleBsl = ($moduleBsl -replace "`r`n", "`n") -replace "`n", "`r`n"
[System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom) [System.IO.File]::WriteAllText($modulePath, $moduleBsl, $encBom)
} }
@@ -352,7 +542,10 @@ if (-not $childObjects) {
exit 1 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 = $xmlDoc.CreateElement("Form", "http://v8.1c.ru/8.3/MDClasses")
$formElem.InnerText = $FormName $formElem.InnerText = $FormName
@@ -406,6 +599,7 @@ if ($insertBefore) {
} }
} }
} }
}
# --- SetDefault --- # --- SetDefault ---
@@ -446,12 +640,27 @@ if ($SetDefault -or $isFirstFormForPurpose) {
$settings = New-Object System.Xml.XmlWriterSettings $settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom $settings.Encoding = $encBom
$settings.Indent = $false $settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$stream = New-Object System.IO.FileStream($objectXmlFull.Path, [System.IO.FileMode]::Create) # Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$writer = [System.Xml.XmlWriter]::Create($stream, $settings) $memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer) $xmlDoc.Save($writer)
$writer.Close() $writer.Flush(); $writer.Close()
$stream.Close()
$xmlText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($xmlText.Length -gt 0 -and $xmlText[0] -eq [char]0xFEFF) { $xmlText = $xmlText.Substring(1) }
$xmlText = $xmlText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$xmlText = [regex]::Replace($xmlText, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objectXmlFull.Path) -and ([System.IO.File]::ReadAllText($objectXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objectXmlFull.Path, $xmlText, $encBom)
# --- Фаза 5: Вывод --- # --- Фаза 5: Вывод ---
@@ -471,7 +680,11 @@ Write-Host " Metadata: $objDirName\$objBaseName\Forms\$FormName.xml"
Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml" Write-Host " Form: $objDirName\$objBaseName\Forms\$FormName\Ext\Form.xml"
Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl" Write-Host " Module: $objDirName\$objBaseName\Forms\$FormName\Ext\Form\Module.bsl"
Write-Host "" 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) { if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue" Write-Host "${defaultPropName}: $defaultValue"
} }
+366 -86
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# form-add v1.5 — Add managed form to 1C config object # form-add v1.23 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse import argparse
import json
import os import os
import re import re
import sys import sys
@@ -10,6 +11,183 @@ import uuid
from lxml import etree from lxml import etree
# ============================================================
# Support guard (Ext/ParentConfigurations.bin) — see docs/1c-support-state-spec.md
# Blocks edits of vendor objects "на замке" / read-only configs. Trigger = bin
# present; reaction from .v8-project.json editingAllowedCheck (deny|warn|off,
# default deny). Never throws (except sys.exit on deny) — errors degrade to allow.
# ============================================================
def _sg_root_uuid(xml_path):
if not os.path.isfile(xml_path):
return None
try:
mx = etree.parse(xml_path).getroot()
for child in mx:
if isinstance(child.tag, str) and child.get("uuid"):
return child.get("uuid")
except Exception:
return None
return None
def _sg_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):
if not d:
break
pj = os.path.join(d, ".v8-project.json")
if os.path.isfile(pj):
return pj
parent = os.path.dirname(d)
if parent == d:
break
d = parent
return None
def _sg_get_edit_mode(cfg_dir):
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(cfg_dir)
if not pj:
return "deny"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
cfg_full = os.path.normcase(os.path.abspath(cfg_dir)).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src:
src_full = os.path.normcase(os.path.abspath(src)).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
if db.get("editingAllowedCheck"):
return db["editingAllowedCheck"]
if proj.get("editingAllowedCheck"):
return proj["editingAllowedCheck"]
return "deny"
except Exception:
return "deny"
def assert_edit_allowed(target_path, require):
try:
rp = os.path.abspath(target_path)
# 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
d = rp if os.path.isdir(rp) else os.path.dirname(rp)
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:
cand = os.path.join(d, "Ext", "ParentConfigurations.bin")
if os.path.exists(cand) or os.path.exists(os.path.join(d, "Configuration.xml")):
cfg_dir = d
bin_path = cand
if elem_uuid and cfg_dir:
break
parent = os.path.dirname(d)
if parent == d:
break
d = parent
if not elem_uuid and cfg_dir:
elem_uuid = _sg_root_uuid(os.path.join(cfg_dir, "Configuration.xml"))
if not bin_path or not os.path.exists(bin_path):
return
data = open(bin_path, "rb").read()
if len(data) <= 32:
return
if data[:3] == b"\xef\xbb\xbf":
data = data[3:]
text = data.decode("utf-8", "replace")
h = re.match(r"\{6,(\d+),(\d+),", text)
if not h:
return
g = int(h.group(1))
k = int(h.group(2))
if k == 0:
return
best = None
if elem_uuid:
for m in re.finditer(r"([0-2]),0," + re.escape(elem_uuid.lower()), text):
f1 = int(m.group(1))
if best is None or f1 < best:
best = f1
blocked = False
code = ""
reason = ""
if g == 1:
blocked = True
code = "capability-off"
reason = "возможность изменения конфигурации выключена (вся конфигурация read-only)"
elif require == "removed":
if best is not None and best != 2:
blocked = True
code = "not-removed"
reason = "объект не снят с поддержки — удаление сломает обновления"
else:
if best is not None and best == 0:
blocked = True
code = "locked"
reason = "объект на замке — редактирование сломает обновления"
if not blocked:
return
mode = _sg_get_edit_mode(cfg_dir)
if mode == "off":
return
if mode == "warn":
sys.stderr.write(f"[support-guard] ПРЕДУПРЕЖДЕНИЕ: {reason}. Цель: {rp}\n")
return
head = "[support-guard] Редактирование отклонено: это объект типовой конфигурации на поддержке поставщика, прямое редактирование молча сломает будущие обновления."
cfe = "Рекомендуемый путь: внести доработку в расширение (навыки cfe-borrow / cfe-patch-method) — состояние поддержки менять не нужно, обновления вендора сохраняются."
off_note = "Снять проверку для этой базы: editingAllowedCheck = warn|off в .v8-project.json."
if code == "capability-off":
state = f"Состояние: у всей конфигурации выключена возможность изменения (режим read-only «из коробки») — поэтому объект «{rp}» редактировать нельзя."
fix = (
"Либо снять защиту явно (навык support-edit, два шага):\n"
f' 1. support-edit -Path "{cfg_dir}" -Capability on — включить возможность изменения (объекты пока остаются на замке);\n'
f' 2. support-edit -Path "{rp}" -Set editable — открыть этот объект для редактирования.\n'
" Изменение применяется в базу полной загрузкой выгрузки и обходит механизм обновлений вендора."
)
elif code == "not-removed":
state = f"Состояние: объект «{rp}» на поддержке (не снят с поддержки) — его удаление разорвёт обновления вендора."
fix = (
"Либо сначала снять объект с поддержки, затем удалять:\n"
f' support-edit -Path "{rp}" -Set off-support — объект уходит из-под обновлений, после этого удаление безопасно.'
)
else:
state = f"Состояние: объект «{rp}» на замке (возможность изменения конфигурации включена, но сам объект не редактируется)."
fix = (
"Либо разрешить редактирование этого объекта (навык support-edit, выбрать одно):\n"
f' support-edit -Path "{rp}" -Set editable — редактировать и дальше получать обновления вендора (возможны конфликты слияния);\n'
f' support-edit -Path "{rp}" -Set off-support — снять с поддержки: обновления по объекту больше не приходят.'
)
sys.stderr.write(head + "\n" + state + "\n" + cfe + "\n" + fix + "\n" + off_note + "\n")
sys.exit(1)
except SystemExit:
raise
except Exception:
return
NSMAP = { NSMAP = {
"md": "http://v8.1c.ru/8.3/MDClasses", "md": "http://v8.1c.ru/8.3/MDClasses",
"v8": "http://v8.1c.ru/8.1/data/core", "v8": "http://v8.1c.ru/8.1/data/core",
@@ -18,6 +196,16 @@ NSMAP = {
def detect_format_version(d): def detect_format_version(d):
while d: while d:
# Автономная внешняя обработка/отчёт: своего Configuration.xml у неё нет, версию несёт
# корень самой обработки. Без этого форма и макет внутри обработки 2.21 писались бы 2.17.
ext_path = d + ".xml"
if os.path.isfile(ext_path):
with open(ext_path, "r", encoding="utf-8-sig") as f:
ext_head = f.read(2000)
if re.search(r'<(ExternalDataProcessor|ExternalReport)[ >]', ext_head):
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', ext_head)
if m:
return m.group(1)
cfg_path = os.path.join(d, "Configuration.xml") cfg_path = os.path.join(d, "Configuration.xml")
if os.path.isfile(cfg_path): if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f: with open(cfg_path, "r", encoding="utf-8-sig") as f:
@@ -32,23 +220,80 @@ def detect_format_version(d):
return "2.17" return "2.17"
def save_xml_with_bom(tree, path): def format_rank(ver):
"""Save XML tree to file with UTF-8 BOM.""" """"2.20" → 220, "2.9" → 209. Строковое сравнение неверно ("2.9" > "2.17")."""
xml_bytes = etree.tostring(tree, xml_declaration=True, encoding="UTF-8") m = re.match(r'^(\d+)\.(\d+)$', ver or '')
xml_bytes = xml_bytes.replace(b"<?xml version='1.0' encoding='UTF-8'?>", b'<?xml version="1.0" encoding="utf-8"?>') return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
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) — к канону
выгрузки Конфигуратора: encoding="UTF-8", CRLF в разделителях, без перевода в конце."""
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"))
# Финальный перенос — как в оригинале (новый файл → нет, канон #57)
want_final_nl = style["final_nl"] if style else False
xml_bytes = xml_bytes.rstrip(b"\n")
if want_final_nl:
xml_bytes += b"\n" xml_bytes += b"\n"
# EOL — как в оригинале (новый файл → CRLF, канон #57)
if (style["crlf"] if style else True):
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: 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) f.write(xml_bytes)
def write_text_with_bom(path, text): def write_text_with_bom(path, text):
"""Write text to file with UTF-8 BOM.""" """Write text to file with UTF-8 BOM."""
with open(path, "w", encoding="utf-8-sig") as f: # newline="" => без трансляции: в текстовом режиме Python на Windows превратил
# бы \n в \r\n, а на macOS оставил \n — вывод зависел бы от ОС.
with open(path, "w", encoding="utf-8-sig", newline="") as f:
f.write(text) f.write(text)
def write_xml_file(path, content):
"""XML в каноне выгрузки Конфигуратора: CRLF в разделителях, без перевода в конце.
Копия этой функции есть в каждом навыке-эмиттере (навыки автономны). Держать
копии одинаковыми сознательно: разошедшиеся копии сводят на нет весь смысл.
Модуль .bsl сюда НЕ идёт он пишется отдельно.
"""
text = content.replace('\r\n', '\n').replace('\n', '\r\n').rstrip('\r\n')
write_text_with_bom(path, text)
def main(): def main():
sys.stdout.reconfigure(encoding="utf-8") sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8")
@@ -84,7 +329,65 @@ def main():
sys.exit(1) sys.exit(1)
object_xml_full = os.path.abspath(object_path) object_xml_full = os.path.abspath(object_path)
format_version = detect_format_version(os.path.dirname(object_xml_full)) assert_edit_allowed(object_xml_full, "editable")
# Версию берём прежде всего из корня самого объекта — он её несёт всегда, а у автономной
# внешней обработки/отчёта подниматься к Configuration.xml просто некуда.
format_version = None
with open(object_xml_full, "r", encoding="utf-8-sig") as f:
obj_head = f.read(2000)
m_ver = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', obj_head)
if m_ver:
format_version = m_ver.group(1)
if not format_version:
format_version = detect_format_version(os.path.dirname(object_xml_full))
# Объявления пространств имён — одной переменной на корень: места эмиссии их только
# подставляют. Правки шапки (как xmlns:pal в формате 2.21) делаются здесь, в одном месте.
xmlns_decl = (
'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"'
)
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns: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: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"'
)
# 2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
# Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
# дописать в конец нельзя.
if format_rank(format_version) >= 221:
pal = ' xmlns:pal="http://v8.1c.ru/8.1/data/ui/colors/palette" xmlns:style='
xmlns_decl = xmlns_decl.replace(' xmlns:style=', pal)
form_ns_decl = form_ns_decl.replace(' xmlns:style=', pal)
parser_xml = etree.XMLParser(remove_blank_text=False) parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(object_xml_full, parser_xml) tree = etree.parse(object_xml_full, parser_xml)
@@ -94,7 +397,7 @@ def main():
"Document", "Catalog", "DataProcessor", "Report", "Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport", "ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes", "InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task", "ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
] ]
object_type = None object_type = None
@@ -173,24 +476,7 @@ def main():
form_meta_xml = ( form_meta_xml = (
'<?xml version="1.0" encoding="UTF-8"?>\n' '<?xml version="1.0" encoding="UTF-8"?>\n'
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses"' f'<MetaDataObject {xmlns_decl} version="{format_version}">\n'
' 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"'
f' version="{format_version}">\n'
f'\t<Form uuid="{form_uuid}">\n' f'\t<Form uuid="{form_uuid}">\n'
'\t\t<Properties>\n' '\t\t<Properties>\n'
f'\t\t\t<Name>{form_name}</Name>\n' f'\t\t\t<Name>{form_name}</Name>\n'
@@ -207,37 +493,22 @@ def main():
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n' '\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">PlatformApplication</v8:Value>\n'
'\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n' '\t\t\t\t<v8:Value xsi:type="app:ApplicationUsePurpose">MobilePlatformApplication</v8:Value>\n'
'\t\t\t</UsePurposes>\n' '\t\t\t</UsePurposes>\n'
# Использование в режиме совместимости интерфейса — свойство формата 2.21 (8.5),
# сразу после UsePurposes (проверено по выгрузке 8.5, до ExtendedPresentation).
+ ('\t\t\t<UseInInterfaceCompatibilityMode>Any</UseInInterfaceCompatibilityMode>\n'
if format_rank(format_version) >= 221 else '')
+ ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '') + ('\t\t\t<ExtendedPresentation/>\n' if object_type in processor_like_types else '')
+ '\t\t</Properties>\n' + '\t\t</Properties>\n'
'\t</Form>\n' '\t</Form>\n'
'</MetaDataObject>' '</MetaDataObject>'
) )
write_text_with_bom(form_meta_path, form_meta_xml) write_xml_file(form_meta_path, form_meta_xml)
# --- 3b. Form.xml --- # --- 3b. Form.xml ---
form_xml_path = os.path.join(form_ext_dir, "Form.xml") form_xml_path = os.path.join(form_ext_dir, "Form.xml")
form_ns_decl = (
'xmlns="http://v8.1c.ru/8.3/xcf/logform"'
' xmlns:app="http://v8.1c.ru/8.2/managed-application/core"'
' xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config"'
' xmlns:dcscor="http://v8.1c.ru/8.1/data-composition-system/core"'
' xmlns:dcsset="http://v8.1c.ru/8.1/data-composition-system/settings"'
' xmlns: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: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"'
)
if purpose in ("List", "Choice"): if purpose in ("List", "Choice"):
# Dynamic list # Dynamic list
main_table = f"{object_type}.{object_name}" main_table = f"{object_type}.{object_name}"
@@ -336,7 +607,7 @@ def main():
if os.path.exists(form_xml_path): if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting") print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
else: else:
write_text_with_bom(form_xml_path, form_xml) write_xml_file(form_xml_path, form_xml)
# --- 3c. Module.bsl --- # --- 3c. Module.bsl ---
@@ -367,7 +638,10 @@ def main():
if os.path.exists(module_path): if os.path.exists(module_path):
print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting") print(f"[SKIP] Module.bsl already exists: {module_path} — not overwriting")
else: else:
write_text_with_bom(module_path, module_bsl) # Модуль пишем в каноне платформы: CRLF в разделителях строк (корпус: 2643 CRLF,
# чисто-LF 0 из 3001). Хвостовой перевод НЕ навязываем — у платформы он
# неканоничен (1235 модулей с ним, 766 без).
write_text_with_bom(module_path, module_bsl.replace('\r\n', '\n').replace('\n', '\r\n'))
# --- Phase 4: Register in parent object --- # --- Phase 4: Register in parent object ---
@@ -377,47 +651,50 @@ def main():
print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr) print(f"Не найден элемент ChildObjects в {object_path}", file=sys.stderr)
sys.exit(1) sys.exit(1)
# Add <Form>$FormName</Form> # Add <Form>$FormName</Form> — idempotent (do not duplicate already-registered form)
form_elem = etree.Element(f"{{{ns}}}Form") already_registered = child_objects.find(f"md:Form[.='{form_name}']", NSMAP) is not None
form_elem.text = form_name
# Find first <Template> to insert before it if not already_registered:
first_template = child_objects.find("md:Template", NSMAP) form_elem = etree.Element(f"{{{ns}}}Form")
# Find first <TabularSection> to insert before it (if no Template) form_elem.text = form_name
first_tabular = child_objects.find("md:TabularSection", NSMAP)
# Determine insertion point: before Template, before TabularSection, or at end # Find first <Template> to insert before it
insert_before = None first_template = child_objects.find("md:Template", NSMAP)
if first_template is not None: # Find first <TabularSection> to insert before it (if no Template)
insert_before = first_template first_tabular = child_objects.find("md:TabularSection", NSMAP)
elif first_tabular is not None:
insert_before = first_tabular
if insert_before is not None: # Determine insertion point: before Template, before TabularSection, or at end
# Insert before the found element insert_before = None
idx = list(child_objects).index(insert_before) if first_template is not None:
child_objects.insert(idx, form_elem) insert_before = first_template
# Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before) elif first_tabular is not None:
form_elem.tail = "\n\t\t\t" insert_before = first_tabular
else:
# Add to end of ChildObjects if insert_before is not None:
children = list(child_objects) # Insert before the found element
if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""): idx = list(child_objects).index(insert_before)
# Empty ChildObjects (self-closing) child_objects.insert(idx, form_elem)
child_objects.text = "\n\t\t\t" # Whitespace: form_elem gets "\n\t\t\t" as tail (indent before insert_before)
child_objects.append(form_elem) form_elem.tail = "\n\t\t\t"
form_elem.tail = "\n\t\t"
else: else:
if len(children) > 0: # Add to end of ChildObjects
last_child = children[-1] children = list(child_objects)
old_tail = last_child.tail if len(children) == 0 and (child_objects.text is None or child_objects.text.strip() == ""):
last_child.tail = "\n\t\t\t" # Empty ChildObjects (self-closing)
child_objects.append(form_elem) child_objects.text = "\n\t\t\t"
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) child_objects.append(form_elem)
form_elem.tail = "\n\t\t" 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 --- # --- SetDefault ---
@@ -462,7 +739,10 @@ def main():
print(f" Form: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form.xml") 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(f" Module: {obj_dir_name}\\{obj_base_name}\\Forms\\{form_name}\\Ext\\Form\\Module.bsl")
print() 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: if default_updated:
print(f"{default_prop_name}: {default_value}") print(f"{default_prop_name}: {default_value}")
print() print()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,123 @@
# Оформление
Два независимых механизма: **оформление элемента** (постоянные цвета/шрифт/граница на конкретном элементе) и **условное оформление формы** (`conditionalAppearance` — правила, применяемые при выполнении условия).
## Оформление элемента (цвета / шрифты / граница)
Свойства задаются прямо на элементе. Применимо к полям (`input`/`check`/`radio`/`labelField`/`picField`/`calendar`), декорациям (`label`/`picture`), кнопкам (`button`), группам (`group`/`columnGroup`), страницам (`page`/`pages`), попапам (`popup`) и таблицам (`table`). Каждое свойство необязательно.
| Ключ | Что задаёт |
|------|-----------|
| `textColor` | Цвет текста |
| `backColor` | Цвет фона |
| `borderColor` | Цвет рамки |
| `font` | Шрифт |
| `border` | Граница |
| `titleTextColor` / `titleBackColor` / `titleFont` | Цвет текста / цвет фона / шрифт заголовка колонки (`labelField`, колонки таблицы); у `page`/`pages`/`popup``titleTextColor`/`titleFont` заголовка страницы/попапа |
| `footerTextColor` / `footerBackColor` / `footerFont` | Цвет текста / цвет фона / шрифт подвала колонки |
Те же свойства доступны и через словарь `appearance` элемента — под русскими именами параметров платформы: `ЦветТекста`, `ЦветФона`, `ЦветРамки`, `Шрифт`, `Граница`, `ЦветТекстаЗаголовка`, `ЦветФонаЗаголовка`, `ШрифтЗаголовка`, `ЦветТекстаПодвала`, `ЦветФонаПодвала`, `ШрифтПодвала`. Это та же запись, что и в правилах условного оформления (ниже) и в `appearance` поля дин-списка.
### Цвет
Строка в одной из форм:
| Форма | Значение |
|-------|----------|
| `web:Имя` | Цвет из web-палитры, напр. `web:Red`, `web:FireBrick`, `web:HoneyDew` |
| `win:Имя` | Системный цвет Windows, напр. `win:MenuBar`, `win:ButtonText`, `win:DisabledText` |
| `style:ИмяСтиля` | Ссылка на элемент стиля конфигурации/платформы, напр. `style:FormBackColor`, `style:BorderColor` |
| `#RRGGBB` | RGB-hex, напр. `#FF0000` |
Имя должно существовать в своей палитре (несуществующий web-/win-цвет или ссылка на отсутствующий `style:`-элемент — ошибка загрузки формы).
### Шрифт (`font` / `titleFont` / `footerFont`)
- Строка `"style:ИмяСтиля"` — шрифт из элемента стиля. Минимальная форма.
- Объект — задаются только нужные атрибуты:
| Ключ | Назначение |
|------|-----------|
| `ref` | Ссылка на стиль (`"style:X"`) или системный шрифт (`"sys:…"`) |
| `faceName` | Имя гарнитуры (для собственного шрифта) |
| `height` | Размер |
| `bold` / `italic` / `underline` / `strikeout` | `true`/`false` — начертание |
| `scale` | Масштаб, % |
| `kind` | `Absolute` (собственный шрифт — с `faceName`+`height`) / `WindowsFont` (системный — с `ref:"sys:…"`) |
```json
{ "label": "Внимание!", "textColor": "web:FireBrick",
"font": { "faceName": "Arial", "height": 12, "bold": true, "kind": "Absolute", "scale": 100 } }
```
### Граница (`border`)
- Строка `"style:ИмяСтиля"` (или объект `{ "ref": "style:X" }`) — граница из стиля.
- Объект `{ "width": N, "style": "..." }` — собственная граница. `style` — один из: `Single`, `Double`, `Underline`, `DoubleUnderline`, `Overline`, `Embossed`, `Indented`, `WithoutBorder`.
```json
{ "input": "Цена", "path": "Объект.Цена", "textColor": "#FF0000",
"borderColor": "style:BorderColor", "border": { "width": 1, "style": "Single" } }
{ "labelField": "Код", "titleTextColor": "web:HoneyDew", "border": "style:ControlBorder" }
```
## Условное оформление формы (`conditionalAppearance`)
Форменный ключ верхнего уровня — массив правил. Каждое правило применяет оформление к перечисленным полям, когда выполняется его условие.
```json
"conditionalAppearance": [
{ "selection": ["ОбычноеПоле"], "filter": ["ЧисловоеПоле > 100"],
"appearance": { "ЦветФона": "style:FormBackColor" },
"presentation": { "ru": "Подсветка", "en": "Highlight" } }
]
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `selection` | array | Имена форматируемых полей формы |
| `filter` | array | Условие применения (грамматика — ниже) |
| `appearance` | object | Словарь «параметр платформы: значение» |
| `presentation` | string / object | Подпись правила в списке настроек |
| `use` | bool | `false` — правило отключено |
| `viewMode` | string | Режим отображения настройки |
| `userSettingID` | string | Идентификатор пользовательской настройки; `"auto"` — сгенерировать |
### filter
Та же грамматика, что в отборе списка — shorthand `"Поле оператор значение @флаги"` или объект:
```json
"filter": [
"Статус = 3",
{ "field": "Сумма", "op": ">=", "value": 1000 },
{ "group": "Or", "items": [ "Просрочено = true", "Заблокирован = true" ] }
]
```
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
- **Флаги:** `@off` (отключён), `@user`, `@quickAccess`; `_` = пустое значение.
- **Группа:** `{ "group": "And"|"Or"|"Not", "items": [...], "use"? }`.
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата; именованный относительный период — строкой `"BeginningOfThisWeek"` с `"valueType": "v8:StandardBeginningDate"` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
### appearance
Словарь «параметр платформы: значение». Имена параметров — русские: `ЦветТекста`, `ЦветФона`, `Шрифт`, `Граница`, `Текст`, `Заголовок`, `Формат`, `ВидимостьЭлемента`, `Доступность` и другие параметры оформления компоновки.
Значения:
- **Цвет** (`ЦветТекста`/`ЦветФона`/…) и **шрифт** (`Шрифт`) — те же формы, что в оформлении элемента выше (`web:`/`win:`/`style:`/`#RRGGBB`; шрифт — строка `"style:X"` или объект).
- **Текстовые параметры** (`Текст`/`Заголовок`/`Формат`) — по форме значения:
- голая строка → нелокализованный литерал (`""` → пустое значение);
- объект `{ "ru": "...", "en": "..." }` → локализуемая строка;
- объект `{ "field": "путь" }` → ссылка на поле компоновки.
```json
"conditionalAppearance": [
{ "selection": ["Остаток"], "filter": ["Остаток < 0"],
"appearance": { "ЦветТекста": "web:Red", "Шрифт": { "bold": true } } },
{ "selection": ["Комментарий"], "filter": ["Комментарий notFilled"],
"appearance": { "Текст": { "ru": "— нет данных —" }, "ЦветТекста": "win:DisabledText" } }
]
```
> Условное оформление **самого дин-списка** задаётся не здесь, а в `settings.conditionalAppearance` реквизита-списка — см. `references/dynamic-list.md`.
@@ -0,0 +1,143 @@
# Диаграммы, диаграмма Ганта, планировщик
Поле-диаграмма (`chart` / `ganttChart`), поле-планировщик (`planner`) и дендрограмма выводят значение из реквизита соответствующего типа. Конструкция всегда двойная:
1. **Реквизит** chart/planner-типа (несёт данные и, при необходимости, design-time конфиг).
2. **Элемент** формы, привязанный к реквизиту через `path`.
Минимум — реквизит нужного типа плюс элемент с тем же `path`:
```json
"attributes": [ { "name": "Диаграмма", "type": "d5p1:Chart" } ],
"items": [ { "chart": "ПолеДиаграммы", "path": "Диаграмма" } ]
```
Реквизит, заполняемый в коде (без встроенной настройки), достаточно объявить типом — элемент привязывается и работает.
## Типы реквизита и элемента
| Элемент | Ключ типа | Тип реквизита | Что несёт элемент дополнительно |
|---------|-----------|---------------|---------------------------------|
| Диаграмма | `chart` | `d5p1:Chart` | — |
| Диаграмма Ганта | `ganttChart` | `d5p1:GanttChart` | `ganttTable` — вложенная таблица (см. ниже) |
| Планировщик | `planner` | `pl:Planner` | — |
| График. схема | `graphicalSchema` | `d5p1:FlowchartContextType` | `edit`, `warningOnEditRepresentation` |
| Период | `periodField` | `v8:StandardPeriod` | — |
| Дендрограмма | `dendrogram` | — | — |
Имя элемента — значение ключа (`"chart": "ПолеДиаграммы"`); `path` — короткое имя реквизита.
### Элемент диаграммы Ганта (`ganttTable`)
У поля Ганта внутри лежит полноценная таблица — задаётся ключом `ganttTable` (та же грамматика, что у обычной `table`):
```json
{ "ganttChart": "Ганта", "path": "Ганта",
"ganttTable": { "table": "ТаблицаГанта", "path": "Ганта", "height": 3 } }
```
## Design-time конфиг диаграммы (`chart`)
Реквизит типа `d5p1:Chart` / `d5p1:GanttChart` может нести встроенную настройку диаграммы — объект `chart` на реквизите. Платформа всегда пишет полный набор свойств (~127: тип, серии, легенда, заголовок, шкалы, цвета, шрифты, оси), поэтому **авторинг с нуля непрактичен** — возьмите рабочую диаграмму за основу и правьте смысловое ядро.
Ключи `chart` = канонические имена свойств диаграммы; задавайте только те, что меняете:
```json
{ "name": "Диаграмма", "type": "d5p1:Chart", "chart": {
"chartType": "Line",
"isSeriesDesign": true, "realSeriesCount": "2",
"realSeriesData": [
{ "id": "1", "color": "auto", "line": {"width":2,"gap":false,"style":"Solid"},
"marker": "Auto", "text": "Серия 1", "strIsChanged": false, "isExpand": false,
"isIndicator": false, "colorPriority": false }
],
"isShowTitle": true, "title": "Продажи",
"isShowLegend": true, "legendPlacement": "Bottom",
"paletteKind": "Auto"
} }
```
Смысловое ядро для правки:
| Ключ | Назначение |
|------|------------|
| `chartType` | Тип: `Line` / `Pie` / `Bar` / `Histogram` / `Column` / `Area` / … |
| `realSeriesData` | Массив серий — объекты `{ id, text, color, line, marker, … }` |
| `isShowTitle` + `title` | Показ и текст заголовка |
| `isShowLegend` + `legendPlacement` | Показ и расположение легенды (`Bottom` / `Right` / …) |
| `paletteKind` | Палитра (`Auto` / …) |
| `bkgColor` / `labelsColor` / … | Базовые цвета |
Формы значений внутри `chart`:
- **Цвета** — verbatim: `auto`, `style:ИмяСтиля`, `web:Red`, `#hex`.
- **`line`** — `{ width, gap, style }` (стиль линии: `Solid` / …).
- **`border`** — `{ width, style }`.
- **`font`** — `{ kind: "AutoFont" }` либо атрибуты шрифта.
- **Локализуемые строки** (`title`, `vsFormat`, `lbFormat`, `labelFormat`, серия `text`, …) — голая строка либо `{ "ru": "…", "en": "…" }`.
- **Области** (`elementsChart` / `elementsLegend` / `elementsTitle`) — `{ left, right, top, bottom }`.
- **Серии** (`realSeriesData` / `realExSeriesData`) — массивы объектов.
Любое из ~127 свойств переопределяется по каноническому имени; остальное оставляйте дефолтным (не указывайте — берётся из основы).
### Диаграмма Ганта (`d5p1:GanttChart`)
Реквизит типа `d5p1:GanttChart` использует **тот же** ключ `chart`. Внутри — вложенный полный `chart`-блок плюс гант-специфика (`points` / `series` / `timeScale` / `drawEmpty` / …). Так же берите рабочую диаграмму Ганта за основу.
> **Ограничение.** Диаграммы (Chart/Gantt) с заполненными **точками/осями** (`realPointData` / `realDataItems`, заполненные `valuesAxis` / `pointsAxis`) генерик-движком не поддержаны — это редкий вариант. Частые дашборд-диаграммы и диаграммы Ганта (серии / легенда / оформление / шкалы) поддержаны полностью.
## Design-time конфиг планировщика (`planner`)
Реквизит типа `pl:Planner` несёт встроенную настройку планировщика — объект `planner`. Компилятор подставляет умолчания для пропущенных ключей, поэтому авторинг может быть кратким:
```json
{ "name": "Планировщик", "type": "pl:Planner", "planner": {
"items": [
{ "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00",
"borderColor": "auto", "backColor": "auto", "deleted": false, "editMode": "EnableEdit" }
],
"period": { "begin": "2026-06-09T00:00:00", "end": "2026-06-09T23:59:59" },
"displayCurrentDate": true, "itemsTimeRepresentation": "BeginTime",
"timeScale": { "placement": "Left", "levels": [ { "measure": "Hour", "interval": 1 } ] }
} }
```
Минимум — один `item`:
```json
"planner": { "items": [ { "text": "Встреча", "begin": "2026-06-09T01:00:00", "end": "2026-06-09T04:00:00" } ] }
```
| Ключ `planner` | Тип | Назначение |
|----------------|-----|------------|
| `items` | array | Элементы расписания. Поля элемента: `text`, `tooltip`, `begin`, `end`, `value`, `borderColor`, `backColor`, `textColor`, `font`, `border`, `replacementDate`, `deleted` (bool), `editMode` (`EnableEdit` / …), `id` (необязательно — авто-GUID), `textFormatted` |
| `dimensions` | array | Измерения (разрезы) планировщика. Поля: `value` (объект разреза — ссылка `Enum.X.EnumValue.Y` / `Справочник.X`; опустить → пусто), `text` (заголовок), `borderColor`, `backColor`, `textColor`, `font`, `textFormatted`, `elements`. `elements` — элементы измерения, рекурсивны (могут нести вложенные `elements`): `value`, `text`, цвета, `font`, `showOnlySubordinatesAreas` (bool), `textFormatted` |
| `period` | object | Отображаемый период `{ begin, end }` (необязательно) |
| `timeScale` | object | Шкала времени (см. ниже) |
| `borderColor` / `backColor` / `textColor` / `lineColor` | color | Цвета (умолч. `auto`) |
| `font` | font | Шрифт (умолч. `{ kind: "AutoFont" }`) |
| `border` | border | Рамка `{ width, style }` |
| `beginOfRepresentationPeriod` / `endOfRepresentationPeriod` | dateTime | Период представления |
| `displayCurrentDate` / `displayWrapHeaders` / `displayTimeScaleWrapHeaders` / `alignElementsOfTimeScale` | bool | Флаги отображения |
| `timeScaleWrapHeadersFormat` | ML | Формат перенесённых заголовков шкалы |
| `timeScaleWrapBeginIndent` / `timeScaleWrapEndIndent` | int | Отступы переноса шкалы |
| `periodicVariantUnit` / `periodicVariantRepetition` | value / int | Единица и кратность периодического варианта |
| `itemsTimeRepresentation` | value | Представление времени элементов (`BeginTime` / …) |
| `itemsBehaviorWhenSpaceInsufficient` / `newItemsTextType` / `fixDimensionsHeader` / `fixTimeScaleHeader` | value | Поведение элементов и заголовков |
| `autoMinColumnWidth` / `autoMinRowHeight` | bool | Авто-минимум размеров |
| `minColumnWidth` / `minRowHeight` | int | Минимальные размеры |
Шкала времени (`timeScale`):
```json
"timeScale": {
"placement": "Left",
"levels": [ { "measure": "Hour", "interval": 1 } ]
}
```
Ключи: `placement`, `levels` (массив уровней), `transparent`, `backColor`, `textColor`, `currentLevel`. Уровень: `measure` (`Hour` / `Day` / …), `interval`, `show`, `line` (`{ width, gap, style }`), `scaleColor`, `dayFormatRule`, `format` (ML), `labels` (`{ ticks }`), `backColor`, `textColor`, `showPereodicalLabels`.
Формы значений в `planner` те же, что у диаграммы: цвета verbatim (`auto` / `style:X` / `web:Red` / `#hex`); шрифт `{ kind: "AutoFont" }` либо ref-строка; граница `{ width, style }`; ML-форматы — строка или `{ "ru": …, "en": … }`.
> **Ограничение.** Привязка элемента расписания к элементам измерений (`item.dimensionValues`) пока всегда пустая. Сами измерения (`dimensions`) задавать можно.
@@ -0,0 +1,73 @@
# Параметры выбора и связь по типу
Свойства поля ввода (`input`), управляющие выбором значения: чем ограничен список выбора и каким будет тип значения. Имена параметров — строки 1С как есть (`"Отбор.Х"`).
```json
{ "input": "Контрагент", "path": "Объект.Контрагент",
"choiceParameters": [
{ "name": "Отбор.Активный", "value": true },
{ "name": "Отбор.ВидПродукции", "value": ["Enum.Виды.Агрохимикат", "Enum.Виды.Пестицид"] }
],
"choiceParameterLinks": [
{ "name": "Отбор.Организация", "dataPath": "Объект.Организация" },
{ "name": "Отбор.Тип", "dataPath": "Объект.Тип", "valueChange": "DontChange" }
],
"typeLink": { "dataPath": "Объект.ЗначениеДата", "linkItem": 0 }
}
```
## Параметры выбора (`choiceParameters`)
Фиксированные значения параметров выбора, отбирающие список значений независимо от данных формы. Массив объектов `{ name, value }`:
- `name` — имя параметра (`"Отбор.Активный"`).
- `value` — значение. Допустимы: bool, число, строка, ISO-дата (`"2020-01-01T00:00:00"`), ссылка-путь (`Enum.X.Y`, `Catalog.X`). **Массив** значений задаёт фиксированный массив.
Короткая форма — строки `"name=value"`; значение с запятыми становится массивом, `true`/`false` → bool, число → число, остальное → строка/ссылка:
```json
"choiceParameters": [
"Отбор.Активный=true",
"Отбор.ВидПродукции=Enum.Виды.Агрохимикат, Enum.Виды.Пестицид"
]
```
## Связи параметров выбора (`choiceParameterLinks`)
Параметры выбора, значение которых берётся из **другого поля формы** (а не задано фиксированно). Типовой случай — отбор списка договоров по выбранному контрагенту. Массив объектов `{ name, dataPath, valueChange? }`:
- `name` — имя параметра выбора.
- `dataPath` — путь к полю формы, чьё значение подставляется в параметр.
- `valueChange` — что делать с уже выбранным значением при смене источника: `Clear` (очистить, необязательно — поведение по умолчанию) / `DontChange` (не менять).
```json
{ "input": "Договор", "path": "Объект.Договор",
"choiceParameterLinks": [
{ "name": "Отбор.Владелец", "dataPath": "Объект.Контрагент" }
]
}
```
Короткая форма — строки `"name=dataPath"`, опциональный хвост `:Clear` / `:DontChange`:
```json
"choiceParameterLinks": [ "Отбор.Организация=Объект.Организация", "Отбор.Тип=Объект.Тип:DontChange" ]
```
## Связь по типу (`typeLink`)
Тип значения поля определяется другим полем формы (напр. поле «Значение» субконто, тип которого задаётся выбранным видом субконто). Объект `{ dataPath, linkItem }`:
- `dataPath` — путь к полю, задающему тип.
- `linkItem` — индекс элемента связи (необязательно, по умолчанию `0`).
```json
"typeLink": { "dataPath": "Объект.ВидСубконто", "linkItem": 0 }
```
Короткая форма — строка `"dataPath"` либо `"dataPath#linkItem"`:
```json
"typeLink": "Объект.ВидСубконто"
"typeLink": "Объект.ВидСубконто#1"
```
@@ -0,0 +1,86 @@
# Командный интерфейс формы
Форменный ключ `commandInterface` управляет расстановкой команд по двум панелям формы:
- `commandBar` — командная панель формы;
- `navigationPanel` — панель навигации.
Указывать нужно **только команды, у которых меняется расстановка по умолчанию** (видимость, группа, порядок). Команды, которые платформа размещает автоматически и без изменений, в блок не включают.
```json
"commandInterface": {
"commandBar": [
{ "command": "Form.Command.Печать", "defaultVisible": false, "group": "FormCommandBarImportant",
"visible": { "common": false, "roles": { "Бухгалтер": true } } },
"CommonCommand.История"
],
"navigationPanel": {
"important": [ { "command": "CommonCommand.СвязанныеДокументы", "defaultVisible": false, "visible": false } ],
"seeAlso": [ { "command": "CommonCommand.Заметки", "defaultVisible": false, "visible": false } ]
}
}
```
## Элемент-команда
Каждый элемент панели — объект, либо строка-shorthand (= голый `command` со всеми остальными свойствами по умолчанию):
```json
"CommonCommand.История"
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `command` | string | Ссылка на команду дословно: `CommonCommand.X`, `Document.X.StandardCommand.Y`, `Form.Command.X`, `Form.StandardCommand.OK`, `"0"` (пустой / разделитель) |
| `type` | string | `Auto` (по умолчанию, необязательно) или `Added` |
| `defaultVisible` | bool | Видимость по умолчанию. На практике задаётся только `false` — чтобы скрыть команду, которая иначе видна |
| `visible` | bool / object | Видимость с исключениями по ролям: `bool` либо `{ "common": bool, "roles": { "Имя": bool } }` |
| `group` | string | Группа размещения дословно: предопределённая (`FormCommandBarImportant`, `FormNavigationPanelGoTo`, …), именованная (`CommandGroup.X`) или GUID-группа расширения |
| `index` | int | Порядок команды внутри группы |
| `attribute` | string | Путь реквизита для элемента панели навигации |
## Две формы записи панели
Панель можно описать **плоским массивом** или **деревом по группам** — выбирайте любую.
**Плоский массив** — каждый элемент при необходимости несёт собственный `group`:
```json
"commandBar": [
{ "command": "Form.Command.Печать", "group": "FormCommandBarImportant", "defaultVisible": false },
{ "command": "CommonCommand.История", "group": "FormCommandBarImportant", "index": 1 }
]
```
**Дерево** — объект `{ группа: [команды] }`; группа берётся из ключа, элементы её не повторяют:
```json
"navigationPanel": {
"important": [ "CommonCommand.СвязанныеДокументы" ],
"goTo": [ { "command": "Document.Заказ.StandardCommand.Movements", "defaultVisible": false, "visible": false } ],
"seeAlso": [ "CommonCommand.Заметки" ]
}
```
Ключи-группы дерева зависят от панели:
- `navigationPanel`: `important`, `goTo`, `seeAlso` (можно по-русски — `важное`, `перейти`, `смТакже`);
- `commandBar`: `important`, `createBasedOn`;
- любой другой ключ (`CommandGroup.X` или GUID) подставляется в группу дословно.
## Скрыть видимую команду
Самый частый случай — убрать команду, которую платформа показывает по умолчанию:
```json
"commandBar": [
{ "command": "Form.Command.Печать", "defaultVisible": false, "visible": false }
]
```
Показать команду только некоторым ролям:
```json
{ "command": "Form.Command.Печать", "defaultVisible": false,
"visible": { "common": false, "roles": { "Бухгалтер": true } } }
```
@@ -0,0 +1,131 @@
# Companion-панели и расширенная подсказка элемента
Любой элемент формы может нести свой собственный контент в трёх companion-свойствах: расширенную подсказку (`extendedTooltip`), командную панель (`commandBar`) и контекстное меню (`contextMenu`). Все три задаются ключами прямо на объекте элемента.
```jsonc
{ "table": "Список", "path": "Список",
"commandBar": { "children": [ … ] },
"contextMenu": { "children": [ … ] },
"extendedTooltip": "Двойной клик открывает карточку" }
```
## Расширенная подсказка (`extendedTooltip`)
Подсказка-надпись рядом с элементом. Две формы записи.
**Текст-форма** — просто текст подсказки:
```jsonc
"extendedTooltip": "Укажите ИНН контрагента"
"extendedTooltip": { "ru": "Сумма с НДС", "en": "Amount incl. VAT" }
"extendedTooltip": { "text": "Всего <b>с НДС</b>", "formatted": true }
```
- строка — ru-текст;
- `{ "ru": …, "en": … }` — многоязычный (как `title`);
- `{ "text": …, "formatted": true }` — форматированный текст (inline-разметка 1С: `<b>…</>`, `<i>`, `<u>`, `<color web:Red>…</>`, `<bgColor …>`, `<font …>`, `<fontSize …>`, `<link URL>…</>`, `<img …>`; закрывающий тег — `</>`). `formatted` нужен только когда текст содержит такую разметку.
**Own-content форма** — объект с раскладкой/оформлением/флагами, когда подсказке нужны размеры, цвет, гиперссылка и т.п.:
```jsonc
"extendedTooltip": {
"text": "Перейти к инструкции",
"hyperlink": true,
"textColor": "web:Blue",
"events": { "URLProcessing": "ПодсказкаОбработкаНавигационнойСсылки" }
}
```
Ключи own-content объекта (все необязательны):
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `text` | string/ML | Текст подсказки (с `formatted` — форматированный) |
| `formatted` | bool | Интерпретировать inline-разметку в `text` |
| `tooltip` | string/ML | Всплывающая подсказка самой расширенной подсказки (редко; ≠ обычному `tooltip` элемента) |
| `hyperlink` | bool | Сделать подсказку гиперссылкой |
| `visible` / `enabled` | bool | Видимость / доступность подсказки |
| `width` / `height` | number | Размеры |
| `maxWidth` / `autoMaxWidth` | number / bool | Максимальная ширина / авто-максимум |
| `titleHeight` | number | Высота заголовка |
| `horizontalStretch` | bool | Горизонтальное растяжение |
| `verticalAlign` | string | Вертикальное выравнивание |
| `textColor` / `font` | string/object | Цвет текста / шрифт (см. `references/appearance.md`) |
| `events` | object | Обработчики событий подсказки, напр. `{ "URLProcessing": "Имя" }` у гиперссылочной подсказки |
## Командная панель (`commandBar`)
Собственная командная панель элемента (обычно таблицы или группы).
**Значение** — массив или объект:
```jsonc
"commandBar": [ { "button": "Создать", "command": "СоздатьЭлемент" } ]
"commandBar": {
"autofill": false,
"horizontalAlign": "Right",
"children": [
{ "button": "Создать", "command": "СоздатьЭлемент" },
{ "buttonGroup": "Печать", "children": [ … ] }
]
}
```
- массив `[ … ]` — краткая запись для `{ "children": [ … ] }`;
- объект — `children` плюс необязательные `autofill` и `horizontalAlign`.
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `children` | array | Содержимое панели — обычная грамматика кнопок (см. основную инструкцию) |
| `autofill` | bool | `false` — подавить автозаполнение панели стандартными командами. Необязательно (по умолчанию панель автозаполняется) |
| `horizontalAlign` | string | Горизонтальное выравнивание содержимого: `Left` / `Center` / `Right`. Необязательно |
`children` — кнопки: `button` (с `command` / `commandName` / `stdCommand`), `buttonGroup`, `popup` — как в основной инструкции по кнопкам.
> Для таблицы динамического списка панель по умолчанию подавлена (чтобы не дублировать командную панель формы). Чтобы оставить автозаполняемую панель у самой таблицы — задайте `commandBar: { "autofill": true }`.
## Контекстное меню (`contextMenu`)
Собственное контекстное меню элемента. Грамматика та же, что у `commandBar`, но без `horizontalAlign`.
```jsonc
"contextMenu": [ { "button": "Карта маршрута", "commandName": "CommonCommand.КартаМаршрута" } ]
"contextMenu": {
"autofill": false,
"children": [
{ "button": "Скопировать ссылку", "command": "СкопироватьСсылку" }
]
}
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `children` | array | Пункты меню — обычная грамматика кнопок |
| `autofill` | bool | `false` — подавить автозаполнение меню. Необязательно |
## Пример: таблица со своим меню и инфо-баннером
```jsonc
{ "table": "Заказы", "path": "Объект.Заказы",
"extendedTooltip": {
"text": "Строки с просрочкой выделены <color web:FireBrick>красным</>",
"formatted": true
},
"commandBar": {
"autofill": false,
"horizontalAlign": "Right",
"children": [
{ "button": "Добавить", "command": "ДобавитьЗаказ" },
{ "button": "Удалить", "command": "УдалитьЗаказ" }
]
},
"contextMenu": {
"children": [
{ "button": "Открыть документ", "command": "ОткрытьЗаказ" },
{ "buttonGroup": "Экспорт", "children": [
{ "button": "В Excel", "command": "ВыгрузитьВExcel" } ] }
]
} }
```
@@ -0,0 +1,144 @@
# Динамический список
Реквизит с `type: "DynamicList"` (обычно `main: true`) — основа формы списка. Объект `settings` описывает источник данных и настройки списка. Минимум — указать источник:
```json
{ "name": "Список", "type": "DynamicList", "main": true,
"settings": { "mainTable": "Catalog.Контрагенты" } }
```
К списку привязывается таблица-элемент (`table`), ссылающаяся на реквизит через `path` — см. основную инструкцию.
## Источник данных
Два взаимоисключающих режима:
**Таблично-ориентированный** — основная таблица метаданных:
```json
"settings": { "mainTable": "Catalog.Контрагенты" }
```
**Запросный** — произвольный запрос:
```json
"settings": {
"query": "ВЫБРАТЬ Т.Ссылка, Т.Наименование, Т.Сумма ИЗ Документ.Заказ КАК Т ГДЕ Т.Сумма > &Порог",
"mainTable": "Document.Заказ"
}
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `mainTable` | string | Основная таблица (`Catalog.X` / `Document.X` / …). Можно вместе с `query` |
| `query` | string | Текст запроса. Поддерживает `@file.sql` (путь к файлу запроса рядом с JSON) |
| `keyType` | string | Запросный список без `mainTable`: тип ключа набора — `FieldValue` / `RowKey` / `RowNumber` |
| `keyFields` | array | Поля ключа набора (для `keyType` без `mainTable`) |
Параметры запроса (`&Имя`) задаются в `parameters` (ниже).
`"dynamicDataRead": false` отключает динамическое считывание (список читается обычным запросом, без фонового обновления) — нужно для тяжёлых/агрегатных запросов.
## Параметры запроса (`parameters`)
Значения для `&параметров` текста запроса. Shorthand `"Имя [Заголовок]: тип = Значение"` (всё кроме имени необязательно) либо объект:
```json
"settings": {
"query": "… ГДЕ Т.Артикул = &Артикул И Т.Цена ПОДОБНО &Маска",
"parameters": [
"Артикул",
"Маска: string = %",
{ "name": "ВидЦен", "valueListAllowed": true },
{ "name": "Период", "type": "dateTime" }
]
}
```
Ключи объекта: `name`, `title`, `type` (грамматика типов — см. основную инструкцию), `value`, `valueListAllowed` (разрешить список значений), `availableValues` (`[{ value, presentation }]`), `expression`, `use`.
## Значения параметров в настройках (`dataParameters`)
Предустановленные значения параметров на уровне настроек списка. Shorthand `"Имя = Значение"` или объект `{ parameter, value?, use?, viewMode? }`:
```json
"dataParameters": [ "Организация = _", "ВидЦен" ]
```
## Поля набора (`fields`)
Обычно поля выводятся из источника сами — `fields` нужен **только чтобы переопределить** свойства отдельного поля:
```json
"fields": [
{ "field": "Сумма", "title": "Сумма, руб", "appearance": { "Формат": "ЧДЦ=2" } },
{ "field": "Остаток", "valueType": "number(15,2)" }
]
```
Ключи поля: `field`, `dataPath`, `title`, `valueType`, `appearance` (как в условном оформлении), `presentationExpression`, `inputParameters` (связь по параметрам выбора), `typeLink` (`{ field, linkItem }` — связь по типу, напр. субконто).
## Вычисляемые поля (`calculatedFields`)
Поля, считаемые выражением. Shorthand `"Имя [Заголовок]: тип = Выражение"`:
```json
"calculatedFields": [
"Метка = Code + \" \" + Description",
"Маржа [Маржа, руб]: number(15,2) = Цена - Закупка"
]
```
Объектная форма — для `presentationExpression` / `orderExpression`:
```json
{ "dataPath": "Сорт", "expression": "Code", "title": "Сорт",
"valueType": "string(10)", "presentationExpression": "Code" }
```
## Отбор (`filter`)
Shorthand `"Поле оператор значение @флаги"` или объект:
```json
"filter": [
"Организация = _ @off @user",
"Сумма > 1000",
{ "field": "Дата", "op": ">=", "value": "2024-01-01T00:00:00" },
{ "group": "Or", "items": [ "Статус = 1", "Статус = 2" ] }
]
```
- **Операторы:** `=` `<>` `>` `>=` `<` `<=`, `in` / `notIn`, `inHierarchy`, `contains` / `notContains`, `beginsWith` / `notBeginsWith`, `like` / `notLike` (`%`-шаблон), `filled` / `notFilled`.
- **Флаги:** `@off` (отключён), `@user` (в пользовательских настройках), `@quickAccess`; `_` = пустое значение.
- **Группа:** `{ group: "And"|"Or"|"Not", items: [...] }`.
- **Дата-значение:** ISO-дата `"2024-01-01T00:00:00"` — фиксированная дата. Именованный относительный период — строкой с типом: `{ "value": "BeginningOfThisWeek", "valueType": "v8:StandardBeginningDate" }` (варианты `BeginningOfThisDay`/`BeginningOfThisWeek`/`BeginningOfThisMonth`/`BeginningOfThisYear`/…).
## Сортировка (`order`)
Строка `"Поле"` (по возр.) / `"Поле desc"`, либо объект `{ field, direction? }`. `"Auto"` — автосортировка:
```json
"order": [ "Дата desc", "Наименование", "Auto" ]
```
## Группировка строк (`grouping`)
Линейная цепочка уровней (внешний → внутренний). Шорткат `>` или массив:
```json
"grouping": "Контрагент > Договор"
"grouping": [ "Контрагент", { "field": "Дата", "groupType": "Hierarchy" } ]
```
Ключи уровня-объекта: `field`, `groupType` (`Items` / `Hierarchy`).
## Условное оформление (`conditionalAppearance`)
```json
"conditionalAppearance": [
{ "filter": [ "Просрочено = true" ], "appearance": { "ЦветТекста": "web:Red" } }
]
```
`filter` — та же грамматика, что выше. `appearance` — словарь «параметр платформы: значение» (`ЦветТекста`, `ЦветФона`, `Шрифт`, `Текст`, `Формат`, …). Значение `Текст`/`Заголовок`/`Формат`: голая строка — нелокализованный литерал; `{ru,en}` — локализуемая строка; `{ field: "путь" }` — ссылка на поле. Подробнее об оформлении — `references/appearance.md`.
@@ -0,0 +1,111 @@
# Продвинутая раскладка
Тонкая настройка размещения элемента внутри родителя сверх базовой геометрии (`width`/`height`/`horizontalStretch`/`verticalStretch`/`visible`/`enabled` и ориентации групп/страниц — они в основной инструкции). Все ключи ниже задаются прямо на элементе и **необязательны** — без них действует поведение платформы по умолчанию.
## Выравнивание внутри родителя
Различают **выравнивание самого элемента** в отведённой ему ячейке и **выравнивание содержимого** элемента.
| Ключ | Значения | Что выравнивает |
|------|----------|-----------------|
| `groupHorizontalAlign` | `Left` / `Center` / `Right` | Положение **элемента** по горизонтали в родительской группе (когда элемент у́же доступного места) |
| `groupVerticalAlign` | `Top` / `Center` / `Bottom` | Положение **элемента** по вертикали в родительской группе |
| `horizontalAlign` | `Left` / `Center` / `Right` | Выравнивание **содержимого** (текста/значения) внутри самого элемента |
| `verticalAlign` | `Top` / `Center` / `Bottom` | Выравнивание содержимого по вертикали внутри элемента |
`group*Align` отвечает на вопрос «куда сдвинуть нерастянутый элемент в его ячейке», `horizontalAlign`/`verticalAlign` — «как разместить текст внутри элемента». Это разные оси настройки, их часто комбинируют.
```json
{ "button": "ОК", "groupHorizontalAlign": "Right" }
{ "input": "Сумма", "path": "Объект.Сумма", "horizontalAlign": "Right" }
{ "label": "Итого", "groupHorizontalAlign": "Center", "horizontalAlign": "Center" }
```
## Ограничение максимального размера
По умолчанию растягивающийся элемент имеет авто-вычисляемый предел ширины/высоты. Чтобы задать жёсткий предел или вовсе снять авто-предел:
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `maxWidth` | число | Жёсткий максимум ширины элемента |
| `maxHeight` | число | Жёсткий максимум высоты элемента |
| `autoMaxWidth` | `false` | Отключить авто-предел ширины (элемент тянется без ограничения сверху) |
| `autoMaxHeight` | `false` | Отключить авто-предел высоты |
`autoMaxWidth: false` нужен, например, для широкого многострочного поля или растянутого по всей форме поля ввода, чтобы платформа не «прижимала» его к авто-пределу. Указывают именно отклонение от дефолта; обычное значение `true` писать не нужно.
```json
{ "input": "Комментарий", "path": "Объект.Комментарий", "multiLine": true,
"horizontalStretch": true, "autoMaxWidth": false }
{ "input": "Поиск", "path": "СтрокаПоиска", "horizontalStretch": true, "maxWidth": 600 }
```
## Поведение при вводе и активации
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `skipOnInput` | `true` / `false` | Пропускать элемент при обходе по Enter/Tab (фокус через него не проходит). Указывают явно, в т.ч. `false` чтобы вернуть в обход поле, которое платформа пропустила бы |
| `defaultItem` | `true` | Элемент получает фокус по умолчанию при открытии формы (поле/таблица для немедленного ввода) |
```json
{ "input": "Идентификатор", "path": "Объект.Идентификатор", "skipOnInput": true }
{ "input": "Штрихкод", "path": "Штрихкод", "defaultItem": true }
```
`skipOnInput: true` — для служебных/расчётных полей, которые видны, но не редактируются вводом с клавиатуры в общем потоке. `defaultItem: true` ставят на одном элементе формы — точке, с которой пользователь начнёт работу.
## Перетаскивание
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `enableStartDrag` | `true` | Разрешить начинать перетаскивание из элемента (источник drag-n-drop) |
Для таблиц приём/перемещение строк управляется ключами таблицы (`enableDrag`, `changeRowOrder`) — см. основную инструкцию; `enableStartDrag` — общий низкоуровневый флаг «этот элемент может быть источником перетаскивания».
## Закрепление колонки в таблице (`fixingInTable`)
Свойство поля-колонки внутри таблицы: закрепить колонку у края, чтобы она не уходила при горизонтальной прокрутке.
| Значения |
|----------|
| `None` (по умолчанию — не закреплена) / `Left` / `Right` |
```json
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
{ "input": "Количество", "path": "Объект.Товары.Количество" },
{ "input": "Сумма", "path": "Объект.Товары.Сумма", "fixingInTable": "Right" } ] }
```
Закрепляют ключевые колонки (идентифицирующую слева, итоговую справа), чтобы они оставались видны при прокрутке широкой таблицы.
## Ячейки колонок: шапка и подвал
Для поля-колонки внутри таблицы (и `columnGroup`) — размещение в шапке/подвале и выравнивание текста ячеек. Применять только к элементам внутри `columns` таблицы.
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `showInHeader` | `true` / `false` | Показывать колонку в шапке таблицы |
| `showInFooter` | `true` / `false` | Показывать колонку в подвале (нужно для итогов; подвал самой таблицы включается `footer: true`) |
| `headerHorizontalAlign` | `Left` / `Right` / `Center` / `Auto` | Выравнивание текста в шапке колонки |
| `footerHorizontalAlign` | `Left` / `Right` / `Center` | Выравнивание текста в подвале колонки |
| `autoCellHeight` | `true` / `false` | Авто-высота ячейки (перенос содержимого на несколько строк) |
```json
{ "table": "Товары", "path": "Объект.Товары", "footer": true, "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "autoCellHeight": true },
{ "input": "Сумма", "path": "Объект.Товары.Сумма",
"headerHorizontalAlign": "Right", "showInFooter": true, "footerHorizontalAlign": "Right" } ] }
```
## Адаптивная важность (`displayImportance`)
| Значения |
|----------|
| `VeryHigh` / `High` / `Usual` / `VeryLow` / `Low` |
Приоритет элемента при адаптивной перекомпоновке формы на узких/мобильных экранах: элементы с меньшей важностью сворачиваются/прячутся первыми. Применимо к любому элементу.
```json
{ "input": "Комментарий", "path": "Объект.Комментарий", "displayImportance": "Low" }
```
@@ -0,0 +1,79 @@
# Форма отчёта
Форма, подключённая к объекту-отчёту (`Report`). Кроме обычных свойств формы у неё есть несколько свойств в `properties`, связывающих форму с механизмом компоновки (СКД): куда выводится результат, где данные расшифровки, какого она типа. Все они задаются в блоке `properties` верхнего уровня.
```json
"properties": {
"reportFormType": "Main",
"reportResult": "РезультатОтчета",
"detailsData": "ДанныеРасшифровки"
}
```
Ни одно из этих свойств не обязательно — указывайте только те, что нужны конкретной форме.
## Тип формы отчёта (`reportFormType`)
Роль формы в составе отчёта:
| Значение | Назначение |
|----------|-----------|
| `Main` | Основная форма отчёта (результат + настройки) |
| `Settings` | Форма настроек |
| `Variant` | Форма варианта |
```json
"reportFormType": "Main"
```
## Привязка к компоновке
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `reportResult` | string | Имя реквизита-результата формы (табличный документ, куда выводится отчёт) |
| `detailsData` | string | Имя реквизита данных расшифровки |
| `variantAppearance` | string | Имя реквизита оформления варианта |
Значение каждого ключа — имя реквизита формы (а не путь к данным). Реквизит с таким именем должен присутствовать в `attributes` формы.
## Группа пользовательских настроек (`customSettingsFolder`)
Группа-элемент формы, в которую генерируются пользовательские настройки компоновщика. Задаётся **по имени** элемента-группы:
```json
"customSettingsFolder": "ГруппаПользовательскихНастроек"
```
## Прочие свойства компоновки
Редкие, задавайте только при явной необходимости:
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `autoShowState` | `Auto`, `DontShow`, `ShowOnComposition` | Автопоказ состояния формирования |
| `reportResultViewMode` | `Auto` | Режим просмотра результата |
| `viewModeApplicationOnSetReportResult` | `Auto` | Применение режима просмотра при установке результата |
## Реалистичный пример
Основная форма отчёта со СКД: реквизит-результат, данные расшифровки и группа пользовательских настроек.
```json
{
"properties": {
"reportFormType": "Main",
"reportResult": "РезультатОтчета",
"detailsData": "ДанныеРасшифровки",
"customSettingsFolder": "ГруппаПользовательскихНастроек"
},
"attributes": [
{ "name": "РезультатОтчета", "type": "SpreadsheetDocument" },
{ "name": "ДанныеРасшифровки", "type": "DataCompositionDetailsData" }
],
"elements": [
{ "group": "vertical", "name": "ГруппаПользовательскихНастроек" },
{ "spreadsheet": "РезультатОтчета", "path": "РезультатОтчета",
"titleLocation": "none" }
]
}
```
@@ -0,0 +1,73 @@
# Доступ по ролям
Единый механизм платформы для разграничения по ролям: задаётся общее значение для всех ролей плюс исключения для конкретных ролей. Один и тот же формат значения у четырёх ключей — каждый на своём владельце:
| Ключ | Владелец | Смысл |
|------|----------|-------|
| `userVisible` | элемент формы | пользовательская видимость элемента |
| `view` | реквизит формы | право просмотра |
| `edit` | реквизит формы | право редактирования |
| `use` | команда формы | доступность команды |
Ключ необязателен: его отсутствие = полный доступ для всех ролей.
## Значение
Две формы (одинаковы для всех четырёх ключей):
**Скаляр** `true` / `false` — общее значение для всех ролей, без исключений:
```json
{ "input": "Поле", "userVisible": false }
```
**Объект** `{ "common": <bool>, "roles": { "ИмяРоли": <bool>, … } }` — общее значение `common` плюс явные исключения по ролям:
```json
{ "name": "Реквизит",
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
```
Роль, **не указанная** в `roles`, наследует `common`. Указанная — задаёт явный `true`/`false` (может и совпадать с `common`).
## Имя роли
Ключи в `roles` — имена ролей конфигурации (`ПолныеПрава`, `Бухгалтер`, …).
## Примеры
Элемент скрыт у всех пользователей:
```json
{ "input": "Комментарий", "userVisible": false }
```
Реквизит не виден никому и редактируется только одной ролью:
```json
{ "name": "СуммаБонуса",
"view": false,
"edit": { "common": false, "roles": { "ПолныеПрава": true } } }
```
Поле доступно для просмотра всем, но редактируемо только администратору:
```json
{ "name": "Статус",
"view": true,
"edit": { "common": false, "roles": { "Администратор": true } } }
```
Команда недоступна по умолчанию, разрешена только бухгалтеру:
```json
{ "name": "ПровестиЗакрытие",
"use": { "common": false, "roles": { "Бухгалтер": true } } }
```
Обратный случай — доступно всем, кроме одной роли:
```json
{ "name": "РедактироватьЦену",
"edit": { "common": true, "roles": { "Кладовщик": false } } }
```
@@ -0,0 +1,109 @@
# Спец-поля «документ/датчик»
Поля для отображения специальных данных: табличный документ, HTML, текст, форматированный документ, индикатор, ползунок. Каждое привязывается к реквизиту своего платформенного типа.
Структурно это обычные поля — поддерживают общий скелет поля (`path`, `title`, `titleLocation`, флаги `readOnly`/`enabled`/`visible`, `layout`, оформление, события). Ниже — только ключ `type` (имя элемента задаётся значением ключа) и собственные скаляры каждого семейства. Все скаляры необязательны.
| Ключ типа | Тип реквизита |
|-----------|---------------|
| `spreadsheet` | `mxl:SpreadsheetDocument` (ТабличныйДокумент) |
| `html` | `string` |
| `textDoc` | `d5p1:TextDocument` (ТекстовыйДокумент) |
| `formattedDoc` | `fd:FormattedDocument` (ФорматированныйДокумент) |
| `progressBar` | число |
| `trackBar` | число |
## spreadsheet — поле табличного документа
Просмотр/редактирование табличного документа (отчёт, печатная форма).
```json
{ "spreadsheet": "ТаблицаОтчета", "path": "ТаблицаОтчета",
"titleLocation": "none", "readOnly": true,
"output": "Disable", "protection": true }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `output` | string | Использование вывода: `Enable` / `Disable` |
| `protection` | bool | Защита от изменений |
| `edit` | bool | Разрешить редактирование |
| `showGrid` | bool | Показывать сетку |
| `showHeaders` | bool | Показывать заголовки строк/колонок |
| `showGroups` | bool | Показывать группировки |
| `showRowAndColumnNames` | bool | Показывать имена строк и колонок |
| `showCellNames` | bool | Показывать имена ячеек |
| `verticalScrollBar` / `horizontalScrollBar` | string | Режим полос прокрутки |
| `viewScalingMode` | string | Режим масштабирования просмотра |
| `selectionShowMode` | string | Режим отображения выделения |
| `pointerType` | string | Тип указателя |
| `enableDrag` / `enableStartDrag` | bool | Разрешить перетаскивание / начало перетаскивания |
## html — поле HTML-документа
Просмотр HTML. Реквизит — строка (содержит HTML-текст или адрес).
```json
{ "html": "Просмотр", "path": "СодержимоеHTML", "titleLocation": "none",
"output": "Enable", "warningOnEditRepresentation": false }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `output` | string | Использование вывода: `Enable` / `Disable` |
| `warningOnEditRepresentation` | bool | Предупреждать при изменении представления |
## textDoc — поле текстового документа
Просмотр/редактирование текстового документа.
```json
{ "textDoc": "Текст", "path": "ТекстДокумента", "editMode": "Edit" }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
## formattedDoc — поле форматированного документа
Просмотр/редактирование форматированного документа.
```json
{ "formattedDoc": "Описание", "path": "ФорматированноеОписание", "editMode": "Edit" }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `editMode` | string | Режим редактирования (напр. `Edit` / `View`) |
## progressBar — поле индикатора
Индикатор прогресса. Реквизит — числовой.
```json
{ "progressBar": "Прогресс", "path": "Прогресс",
"minValue": 0, "maxValue": 100, "showPercent": true }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
| `showPercent` | bool | Показывать проценты |
## trackBar — поле ползунка
Регулятор-ползунок. Реквизит — числовой.
```json
{ "trackBar": "Масштаб", "path": "Масштаб",
"minValue": 20, "maxValue": 400, "markingStep": 20 }
```
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `minValue` / `maxValue` | число | Минимальное / максимальное значение |
| `step` | число | Шаг изменения |
| `largeStep` | число | Крупный шаг |
| `markingStep` | число | Шаг разметки |
| `markingAppearance` | string | Оформление разметки |
@@ -0,0 +1,132 @@
# Таблица — продвинутые возможности
Базовый элемент таблицы (`type: "table"`, колонки, основные свойства) описан в основной инструкции, раздел «Таблица (table)». Здесь — продвинутые возможности: дополнения командной панели, специфика таблицы динамического списка и неочевидные свойства/режимы.
## Представление (`representation`)
Как таблица рисует строки:
```json
{ "table": "Список", "path": "Список", "representation": "Tree" }
```
`List` — плоский список (по умолчанию), `Tree` — дерево, `HierarchicalList` — иерархический список (группы + элементы на одном уровне).
Для дерева/иерархии управляйте раскрытием уровней через `initialTreeView` (`ExpandTopLevel` / `ExpandAllLevels` / `NoExpand`).
## Выделение и текущая строка
| Ключ | Значения | Назначение |
|------|----------|-----------|
| `selectionMode` | `SingleRow` / `MultiRow` | Режим выделения строк |
| `multipleChoice` | bool | Разрешить множественный выбор (для форм выбора) |
| `currentRowUse` | `DontUse` / `Use` / `SelectionPresentation` / `SelectionPresentationAndChoice` / `Choice` | Использование текущей строки таблицы |
```json
{ "table": "Список", "path": "Список", "selectionMode": "MultiRow", "multipleChoice": true }
```
## Поиск при вводе (`searchOnInput`)
Поведение встроенного поиска при наборе текста в таблице:
```json
{ "table": "Список", "path": "Список", "searchOnInput": "Use" }
```
`Auto` (по умолчанию) / `Use` (искать) / `DontUse` (не искать).
Где располагать сами элементы поиска — управляется `searchStringLocation` / `viewStatusLocation` / `searchControlLocation` (`None` / `Top` / `Bottom` / `CommandBar` / `Auto`).
## Прочие свойства таблицы
| Ключ | Тип | Назначение |
|------|-----|-----------|
| `useAlternationRowColor` | bool | Чередование цвета строк |
| `verticalLines` / `horizontalLines` | bool | Линии сетки (укажите `false`, чтобы скрыть) |
| `markIncomplete` | bool | Автоотметка незаполненных ячеек |
| `heightInTableRows` | int | Высота элемента в строках (отдельно от `height`) |
| `autoInsertNewRow` | bool | Автодобавление новой строки при вводе в последнюю |
| `rowsPicture` | string \| object | Картинка строк. Ссылка (`"CommonPicture.X"`, `"abs:..."`) либо объект `{ src, loadTransparent?, transparentPixel? }` |
| `tooltipRepresentation` | string | Режим показа подсказки таблицы: `None`, `Button`, `ShowBottom`, `ShowTop`, `ShowLeft`, `ShowRight`, `ShowAuto`, `Balloon` |
## Фиксация колонки (`fixingInTable`)
Свойство **колонки** (на `input` / `labelField` / `check` / `picField` внутри `columns`), а не самой таблицы. Закрепляет колонку у края при горизонтальной прокрутке:
```json
{ "table": "Товары", "path": "Объект.Товары", "columns": [
{ "input": "Номенклатура", "path": "Объект.Товары.Номенклатура", "fixingInTable": "Left" },
{ "input": "Количество", "path": "Объект.Товары.Количество" }
]}
```
`Left` / `Right` / `None`.
## Исключённые команды (`excludedCommands`)
Убрать стандартные команды редактора таблицы (кнопки добавления/перемещения/сортировки):
```json
{ "table": "Товары", "path": "Объект.Товары",
"excludedCommands": [ "Add", "Delete", "MoveUp", "SortListAsc" ] }
```
Свойство работает на любом поле и на уровне формы; для таблицы значимы команды вида `Add` / `Delete` / `MoveUp` / `MoveDown` / `SortListAsc` / `SortListDesc`.
## Дополнения командной панели (`additions`)
Дополнения — это «представления» встроенного поиска таблицы:
- `searchString` — отображение строки поиска,
- `viewStatus` — состояние просмотра,
- `searchControl` — управление поиском.
Каждое дополнение — полноценный элемент (полный набор свойств поля). Размещать их можно двумя способами.
**(1) Стандартные дополнения** генерирует платформа на уровне таблицы. В DSL указывайте **только отклонения** от стандартного вида — через карту `additions` (ключ = тип дополнения):
```json
{ "table": "Список", "path": "Список",
"additions": { "viewStatus": { "horizontalLocation": "left" } } }
```
**(2) Кастомное дополнение**, размещённое прямо в командной панели — обычный элемент в `commandBar` с ключом-типом:
```json
{ "table": "Список", "path": "Список", "commandBar": [
{ "searchString": "ПоискСписка", "source": "Список", "width": 15, "horizontalStretch": true }
]}
```
- Тип-ключ: `searchString` / `viewStatus` / `searchControl`.
- `source` — имя таблицы-источника; необязательно, по умолчанию = имя родительской таблицы.
- `horizontalLocation`: `auto` (по умолчанию) / `left` / `right`. Применимо и к обычным элементам командных панелей.
- Прочие свойства как у поля: `title`, `visible`, `userVisible`, `enabled`, `tooltip`, оформление, `width` / `maxWidth` / `autoMaxWidth` / `horizontalStretch` / `groupHorizontalAlign` и др.
## Таблица динамического списка
Когда `path` таблицы указывает на реквизит `type: "DynamicList"` (см. `references/dynamic-list.md`), доступен блок специфичных свойств. Указывайте **только отличия** от умолчания.
| Ключ | Тип | Умолчание | Назначение |
|------|-----|-----------|-----------|
| `rowPictureDataPath` | string | картинка осн. таблицы | Путь к картинке строки. `""` — подавить картинку |
| `rowsPicture` | string | — | Картинка строк (`"CommonPicture.X"`) |
| `autoRefresh` | bool | `false` | Автообновление списка |
| `autoRefreshPeriod` | int | `60` | Период автообновления, сек |
| `updateOnDataChange` | string | `Auto` | Обновлять при изменении данных: `Auto` / `DontUpdate` |
| `choiceFoldersAndItems` | string | `Items` | Что выбирать: `Items` / `Folders` / `FoldersAndItems` |
| `restoreCurrentRow` | bool | `false` | Восстанавливать текущую строку при обновлении |
| `showRoot` | bool | `true` | Показывать корень |
| `allowRootChoice` | bool | `false` | Разрешить выбор корня |
| `allowGettingCurrentRowURL` | bool | `true` | Разрешить получение URL текущей строки |
| `userSettingsGroup` | string | — | Группа пользовательских настроек (привязка к одноимённой группе настроек) |
```json
{ "table": "Список", "path": "Список",
"representation": "Tree",
"rowPictureDataPath": "Список.DefaultPicture",
"choiceFoldersAndItems": "FoldersAndItems",
"allowRootChoice": true,
"updateOnDataChange": "DontUpdate" }
```
@@ -0,0 +1,77 @@
# Продвинутые конструкции типов
Примитивы (`string(n)`, `number(p,s)`, `boolean`, `date`/`dateTime`, …) и одиночные ссылки (`CatalogRef.Контрагенты`, `DocumentRef.Заказ`, `EnumRef.X`, …) описаны в основной инструкции. Здесь — типы, которые нельзя выразить одним именем: составные типы, наборы типов и платформенные наборы ссылок.
Любая из этих конструкций пишется в поле `type` реквизита, реквизита-параметра или поля.
## Составные типы
Несколько типов на одном реквизите — части перечисляются через разделитель `" | "` (можно `+`). Реквизит сможет принимать значение любого из перечисленных типов:
```json
{ "name": "Плательщик",
"type": "CatalogRef.Организации | CatalogRef.ИндивидуальныеПредприниматели" }
```
Смешивать можно типы из разных категорий — ссылки, примитивы, наборы типов:
```json
{ "name": "Источник",
"type": "CatalogRef.Контрагенты | DocumentRef.Заказ | string(150)" }
```
Каждая часть — самостоятельный токен из этого файла или из основной инструкции. Порядок частей произвольный.
## Наборы типов (TypeSet)
«Набор типов» подставляется вместо конкретного типа — это один токен, а не перечисление. Применимо и в составном типе как одна из частей.
| Токен `type` | Смысл |
|------|-------|
| `"DefinedType.ИмяТипа"` | определяемый тип конфигурации |
| `"Characteristic.ИмяПлана"` | тип значения характеристики (по плану видов характеристик) |
| `"AnyRef"` | любая ссылка |
| `"AnyIBRef"` | любая ссылка информационной базы |
Определяемый тип — реквизит принимает то, что задано в определяемом типе конфигурации (например `DefinedType.ДенежнаяСумма`):
```json
{ "name": "Сумма", "type": "DefinedType.ДенежнаяСумма" }
```
Характеристика — тип значения берётся из плана видов характеристик:
```json
{ "name": "Значение", "type": "Characteristic.ДополнительныеРеквизиты" }
```
## Платформенные наборы ссылок
«Голый» ссылочный токен **без `.Имя`** означает «любая ссылка этой категории объектов»:
| Токен `type` | Смысл |
|------|-------|
| `"CatalogRef"` | любая ссылка справочника |
| `"DocumentRef"` | любая ссылка документа |
| `"EnumRef"` | любая ссылка перечисления |
| `"ExchangePlanRef"` | любая ссылка плана обмена |
| `"TaskRef"` | любая ссылка задачи |
| `"BusinessProcessRef"` | любая ссылка бизнес-процесса |
| `"ChartOfCharacteristicTypesRef"` | любая ссылка плана видов характеристик |
| `"ChartOfAccountsRef"` | любая ссылка плана счетов |
| `"ChartOfCalculationTypesRef"` | любая ссылка плана видов расчёта |
Различие с одиночной ссылкой — только в наличии `.Имя`:
- `"CatalogRef.Валюты"` — конкретный справочник «Валюты»;
- `"CatalogRef"` — любой справочник.
```json
{ "name": "ЛюбойСправочник", "type": "CatalogRef" }
```
Эти наборы тоже комбинируются в составном типе:
```json
{ "name": "Объект", "type": "CatalogRef | DocumentRef" }
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
---
name: form-decompile
description: Декомпиляция управляемой формы 1С (Form.xml) в JSON-черновик в формате form-compile. Используй для scaffold новой формы по образцу или структурного рефакторинга. Не для точечных правок
argument-hint: <FormPath> [-OutputPath <out.json>]
disable-model-invocation: true
allowed-tools:
- Bash
- Read
- Write
- Glob
---
# /form-decompile — JSON-черновик из Form.xml управляемой формы
Читает Form.xml и эмитит компактный JSON в формате `form-compile`. **Результат — черновик**, а не обратимое представление: см. раздел «Что получаешь».
## Когда использовать
- **Scaffold новой формы по образцу** — взять существующую форму, получить JSON, поправить и скомпилировать в новую.
- **Структурный рефакторинг** — перебрать дерево элементов, реквизиты, команды.
## Когда **не** использовать
- **Точечные правки готовой формы** (добавить элемент, реквизит, команду) → `/form-edit`. Цикл «декомпиляция → правка JSON → компиляция» переписывает форму целиком, может терять непокрытые конструкции и даёт большой diff. `/form-edit` правит адресно.
## Параметры
| Параметр | Описание |
|----------|----------|
| `FormPath` | Путь к Form.xml (обязательный) |
| `OutputPath` | Путь к выходному JSON. Если не задан — JSON в stdout |
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-decompile.ps1" -FormPath "<Form.xml>" -OutputPath "<out.json>"
```
## Что получаешь
JSON-черновик в формате `/form-compile`**не полное обратимое представление**: раундтрип `xml → json → xml` не гарантируется, часть конструкций DSL не покрывает и **теряет молча**.
Критичные конструкции (`ConditionalAppearance` со scope, design-time диаграммы/планировщики на реквизите, неизвестный тип элемента, не-Form root) → скрипт падает с ненулевым кодом и сообщением в stderr; для правок такой формы — `/form-edit`.
## Workflow
1. `/form-decompile <Form.xml> -OutputPath draft.json` — получить черновик.
2. Поправить JSON под задачу.
3. `/form-compile -JsonPath draft.json -OutputPath new/Form.xml` — собрать обратно.
4. `/form-validate` + `/form-info` — проверить результат.

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