Compare commits

..
Author SHA1 Message Date
Nick ShirokovandClaude Opus 5 41e2eca638 fix(meta-decompile): слоты форм таблицы внешнего источника терялись при разборе
Раундтрип B на стенде вскрыл пробел: у таблицы внешнего источника декомпилятор не снимал
DefaultObjectForm/DefaultRecordForm/DefaultListForm/DefaultChoiceForm, хотя DSL их поддерживает,
компилятор эмитит, а у прочих объектов они снимаются. Пересборка стенда молча теряла назначенную
форму списка.

Сами формы вне скоупа раундтрипа (это отдельные файлы, их делает навык form-add), поэтому
собранная из такого DSL конфигурация не загрузится — ровно как и у справочника с формой.
Договорённость одна и та же, записана в docs/meta-dsl-spec.md.

Раундтрип B после правки: из выгрузки стенда → DSL → meta-compile → загрузка в базу →
выгрузка платформой = побайтово то же, что собрал компилятор. Против исходной выгрузки стенда
остаются два известных расхождения: UnfilledParentValue (загрузкой XML не задаётся, замерено
тремя опытами) и регистрация <Form> (вне скоупа).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 20:34:23 +03:00
Nick ShirokovandClaude Opus 5 5c79b494cd fix(meta-edit): первый ребёнок в пустом ChildObjects вставал вровень с контейнером
Get-ChildIndent брал отступ из первого пробельного узла контейнера. В контейнере с детьми это
отступ ПЕРЕД первым ребёнком — верно; в пустом (только что раскрытом) единственный пробельный
узел — отступ ЗАКРЫВАЮЩЕГО тега, то есть уровень самого контейнера. Первый добавленный реквизит,
команда или таблица получали отступ <ChildObjects>, а не на табуляцию глубже. То же во вложенном
ChildObjects табличной части.

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

Эталоны четырёх кейсов пересняты: git diff -w пуст, меняются только отступы. Все 23 снэпшота
meta-edit, доезжающие до платформы, приняты 8.3.24.1691.

Кейс eds-add-table-twice объявил skipPlatformVerify: он нарочно удаляет файл таблицы, оставляя
висячую регистрацию, — платформа отказывает по условию кейса, а не из-за дефекта навыка.

Радиус проверен: та же эвристика есть в cf-edit, form-edit, interface-edit и cfe-borrow (семья
get_child_indent из списка долга check-inline-drift, не сведена). У form-edit пустой контейнер
обрабатывается верно — проверено добавлением элемента в форму с <ChildItems/>; у cf-edit и
cfe-borrow ветка недостижима: в ChildObjects конфигурации всегда есть <Language>.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 20:15:26 +03:00
Nick ShirokovandClaude Opus 5 11c0450d2a fix(meta-info,meta-decompile): xs:base64Binary без квалификаторов — двоичные данные, не хранилище
Вчерашняя правка исходила из того, что xs:base64Binary без BinaryDataQualifiers — вторая
форма ХранилищаЗначения. Спросили платформу: конфигурация с таким узлом загружается и
выгружается обратно как xs:base64Binary с Length 0 / AllowedLength Variable, то есть как
БЕЗЛИМИТНЫЕ двоичные данные. ХранилищеЗначения — отдельный узел v8:ValueStorage.

- meta-info: xs:base64Binary снова всегда ДвоичныеДанные (правка отменена, замер записан
  в комментарий).
- meta-decompile: узел без квалификаторов теперь возвращается как BinaryData(0), а не
  ValueStorage — раньше раундтрип молча менял тип реквизита. Проверено: пересборка из
  полученного DSL даёт байт в байт то же, что выгрузила платформа.
- Кейс и фикстура переименованы по факту и стали полной конфигурацией: теперь кейс доезжает
  до платформы в verify-snapshots (раньше падал с «конфигурации для загрузки нет»).

check-uuid-invariant: добавлен сценарий внешнего источника — правки идут в два файла, и
пересборка файла источника дала бы ему новый uuid, осиротив существующие таблицы. Проверка
не холостая: намеренная порча uuid источника гард роняет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 20:02:05 +03:00
Nick ShirokovandClaude Opus 5 b4bc02bc98 docs(meta-edit): каскад инструкций по задачам вместо одной кучи
child-operations.md был на 191 строку и держал в одном файле реквизиты, поля внешнего
источника, таблицы, функции, ТЧ, предопределённые и значения перечисления: модель, добавляющая
реквизит, читала заодно про функции внешнего источника. Ось резки была механизмом (add/remove/
modify), а нужна сущность — как в meta-compile.

SKILL.md: частое (команда, сводная таблица операций, быстрые примеры) плюс индекс
«что правишь → файл». Остальное — reference/: attributes, tabular-sections, predefined,
external-data-source, other-children, properties (бывший properties-reference), json-dsl.

Попутно: везде «навык form-add», а не голое имя — иначе читается как команда оболочки или
значение -Operation. Тексты предупреждений в обоих портах тоже.

scripts/switch.py собирал .md только из корня навыка, подкаталоги не видел: команды запуска
в reference/*.md молча остались бы на старом рантайме. Обход стал рекурсивным.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 19:41:57 +03:00
Nick ShirokovandClaude Opus 5 fb211a61ac feat(form-add): формы таблиц внешних источников данных
form-add отказывал видом объекта 'Table', хотя платформа такие формы делает, а в DSL
таблицы есть слоты defaultObjectForm/defaultRecordForm/defaultListForm/defaultChoiceForm.
Инструкция при этом обещала обратное — строка исправлена.

Таблица — единственный вид, чьи ссылки трёхчастные: слот
ExternalDataSource.<И>.Table.<Т>.Form.<Ф>, главный реквизит ExternalDataSourceTableObject.<И>.<Т>,
MainTable динамического списка ExternalDataSource.<И>.Table.<Т>. Имя источника в файле таблицы
не хранится — берётся из пути ExternalDataSources/<И>/Tables/<Т>.xml, единственное место,
где навык смотрит на путь.

Набор назначений зависит от вида данных таблицы — замерено загрузками на 8.3.24.1691:
ObjectData даёт Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
схемой формы не отвергается: она валит загрузку ВСЕЙ конфигурации «Исключением XDTO при
чтении файла» без указания причины, поэтому form-add проверяет её сам и объясняет, а без
-Purpose у таблицы с составным ключом основной становится форма записи.

form-validate: типы ExternalDataSourceTable* добавлены в список известных cfg-префиксов —
иначе корректная форма получала предупреждение «unrecognized cfg prefix».

Проверено: form-add и form-validate зелёные в обоих рантаймах, гарды прошли (в том числе
check-form-purposes: спека и оба порта сходятся), снэпшот eds-table-forms принят платформой,
формы всех четырёх назначений загружены в базу и выгружены обратно со ссылками без потерь.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 19:26:43 +03:00
Nick ShirokovandClaude Opus 5 a38f30c9dd fix(meta-*): двоичные данные с длиной, корень реестра в py, ХранилищеЗначения в meta-info
Мелкие находки ревью, по одному кейсу на каждую:

- meta-compile: BinaryData(N[,fixed|variable]) — фиксированная длина больше не теряется;
  голая форма по-прежнему 4294967292/Fixed, как её пишет платформа при импорте из СУБД.
  Посторонний текст в скобках (BinaryData(abc)) отвергается, а не даёт тихий безлимит —
  PS принимал его и молча подставлял дефолт, py такой тип не понимал вовсе.
- meta-decompile: сворачивает в голый BinaryData только точное совпадение с этим дефолтом.
- meta-info: xs:base64Binary без квалификаторов — вторая форма ХранилищаЗначения, а не
  двоичные данные; раньше два навыка читали один и тот же узел по-разному.
- meta-remove (py): в ветке «файлов нет» корень реестра был жёстко Configuration, поэтому
  осиротевшая таблица внешнего источника не находилась и навык падал, тогда как PS её
  разрегистрировал. Сообщение в обоих портах теперь называет реальный реестр.

Проверено: наборы meta-compile/decompile/info/remove/edit/validate зелёные в обоих
рантаймах, гарды прошли, снэпшот eds-binary-data принят платформой 8.3.24.1691.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 19:11:29 +03:00
Nick ShirokovandClaude Opus 5 7dc401718f fix(meta-edit): формы и макеты отданы form-add/template-add, команда приведена к платформенной
Навык регистрировал форму, макет и команду одинаково — узлом с uuid и <Properties>.
Платформа так пишет только команду: форма и макет регистрируются голым текстом
(<Form>Имя</Form>) и требуют собственных файлов. В корпусе acc_8.3.24 ни одного
<Form uuid и <Template uuid, при 81 голом <Template> и 46 <Command uuid.
Подменённая конфигурация валила загрузку с уходом 1cv8 в бесконечное выделение памяти.

Форму и макет добавляет form-add / template-add (они делают и файл, и запись),
удаляет form-remove / template-remove — meta-edit теперь отсылает к ним вместо
тихой порчи. Команда осталась за meta-edit: дописаны CommandParameterType,
ParameterUseMode, ModifiesData и OnMainServerUnavalableBehavior, плюс заготовка
Commands/<Имя>/Ext/CommandModule.bsl (в корпусе модуль есть у всех 97 команд).

Заодно: Table в childOrder, и пустой ChildObjects в PS схлопывается в
самозакрывающийся, как его пишет платформа (722 из 722 в корпусе) и py-порт.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 18:33:45 +03:00
Nick ShirokovandClaude Opus 5 0dac85946f fix(meta-edit): проверка «ребёнок уже есть» не видела узлов без Properties
Get-AllChildNames собирал имена только из Properties/Name и пропускал детей, которые
регистрируются голым текстом: <Table>Имя</Table>, <Form>Имя</Form>, <Template>Имя</Template>.
Из-за этого проверка на дубликат была мёртвой: после удаления файла таблицы повторное
добавление отвечало «Added: 1» и клало в ChildObjects второй такой же <Table>.

Теперь имя берётся из текста узла, если у ребёнка нет Properties. Кейс воспроизводит
исходный сценарий: добавить таблицу, удалить её файл, добавить снова — второй записи
быть не должно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AtG4qGvZocJSWLAsbHpBCQ
2026-09-06 17:59:11 +03:00
Nick ShirokovandClaude Opus 5 8d1670468a fix(meta-edit): копия эмиттера звала функции, которых в навыке нет
Скопированные из meta-compile Emit-FormRef и Emit-Characteristics тянули за собой хвост
хелперов (Normalize-FormRef, Normalize-CharFrom, Expand-CharField, Get-CharIntField), которых
в meta-edit нет. Добавление таблицы с ключом defaultListForm или characteristics давало в PS
CommandNotFoundException при коде возврата 0: навык отчитывался успехом, а файл таблицы
не создавался и в ChildObjects ничего не появлялось. В python то же место падало трейсбеком —
порты расходились ещё и потоком ошибки.

Причина не в копировании как таковом: копия обязана быть не только идентичной, но и
самодостаточной. Форма функции для этого не годилась, поэтому изменена в эталоне и
перекопирована — тем же приёмом, что уже применён в этой семье дважды: Emit-EdsTableProperties
принимает готовые блоки <Characteristics> и четырёх слотов <Default*Form>, рендерит их
вызывающий навык своим эмиттером. Зависимостей у тела не осталось.

meta-edit передаёт пустые блоки и отвергает ключи characteristics и default*Form с объяснением,
куда идти: форму назначает form-add, характеристики — meta-compile.

Попутно исправлено то, что вскрылось при разборе: короткое имя формы в defaultListForm
эмитилось как есть, а платформа отвечает «Неизвестный объект метаданных». Теперь оно
разворачивается в полный путь ExternalDataSource.И.Table.Т.Form.Ф — как и ссылки на поля.

check-inline-drift получил проверку РАЗРЕШИМОСТИ: всё, что зовёт скопированное тело, обязано
быть определено в том же навыке. Критерий «своей» функции — имя, определённое в навыке-эталоне,
поэтому список командлетов-исключений не нужен. Проверено обратным экспериментом: гард ловит
исходный дефект с прямой формулировкой «копия неразрешима».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 17:10:43 +03:00
Nick ShirokovandClaude Opus 5 938db35c85 docs(meta-dsl-spec): полное описание DSL внешних источников данных
Раздел 18 был указателем на reference навыка — это ломало принятое разделение: спецификация
в docs описывает DSL полно (для нас), инструкция навыка — только как применять (для модели).
Указатель экономил дублирование ценой того, что полного описания не оказалось нигде.

Теперь §18 описывает источник, таблицы, поля и функции целиком: ключи, XML-теги, умолчания,
конвенции ссылок на поля, presence-aware inputByString, различение BinaryData и ValueStorage,
запрет составного типа, и границы — почему в DSL нет значения незаполненного родителя (платформа
сбрасывает его при любой загрузке XML) и почему нет кубов OLAP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 15:43:57 +03:00
Nick ShirokovandClaude Opus 5 89ce1af9fa feat(meta-validate): проверка ссылок на поля таблицы внешнего источника
Свойства таблицы ссылаются на её же поля шестичастным путём
(ExternalDataSource.И.Table.Т.Field.П). Опечатка в имени поля даёт «Неизвестный объект
метаданных» при загрузке, а глазами в таком пути её не видно. Проверка 21 разбирает форму
пути, требует, чтобы таблица в ссылке была той же самой, и чтобы поле существовало —
в сообщении перечисляются имеющиеся поля.

Отдельно: таблица без ключевых полей даёт предупреждение, а не ошибку. Загрузку XML такой
таблицы платформа принимает молча (измерено на 8.3.24.1691), Конфигуратор интерактивно
требует ключ, а рабочие конфигурации без ключей существуют — значит это повод предупредить,
а не отвергнуть.

Закрыты последние пункты плана: раздел про внешние источники в docs/meta-dsl-spec.md
(формой и связями с общими конвенциями, без дублирования таблицы свойств — она живёт
в reference навыка) и кейсы meta-validate, включая негативный.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 15:35:56 +03:00
Nick ShirokovandClaude Opus 5 24cfe6a0a6 feat(meta-decompile): разбор внешних источников данных и раундтрип
Декомпилятор собирает DSL источника целиком: читает файл источника, подтягивает файлы таблиц
из <Источник>/Tables/ и складывает поля, ключи, ссылки и функции обратно в тот же синтаксис,
который принимает meta-compile. Ссылки на поля возвращаются короткими именами, таблица без
собственных свойств — коротким массивом полей, функция с типом по умолчанию — одной строкой.

Это замыкает дешёвый контур проверки: XML → декомпиляция → компиляция → сравнение с исходником,
без 1С и без Docker. На нём и проверено: наша выгрузка, выгрузка платформы и четыре таблицы
внешних источников из чужого рабочего проекта возвращаются байт в байт. У выгрузки платформы
остаются два известных расхождения: значение незаполненного родителя (платформа сама сбрасывает
его при любой загрузке) и формы (декомпилятор их не захватывает — так задумано).

Побочно закрыт дефект, к внешним источникам не относящийся: xs:base64Binary разбирался как
ХранилищеЗначения, хотя это ДвоичныеДанные. Различать их можно по квалификаторам —
у ХранилищеЗначения платформа пишет v8:ValueStorage, а у двоичных данных есть
BinaryDataQualifiers. Компилятор научен типу BinaryData (и BinaryData(N)) — до этого он
принимал такое имя, но эмитил его как есть, то есть невалидный XML.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 14:32:43 +03:00
Nick ShirokovandClaude Opus 5 123d6e326b feat(meta-edit): таблица и функция добавляются в существующий внешний источник
Дыра в сценарии: добавить таблицу или функцию к уже существующему источнику было нечем.
Совет «пересоберите источник целиком через meta-compile» оказался вредным — повторная
компиляция заменяет файл источника, выдаёт объекту НОВЫЙ uuid и оставляет файлы не
упомянутых таблиц сиротами: в ChildObjects их уже нет, а на диске они остались.

meta-edit на файле источника принимает add.tables и add.functions. Таблица — единственная
операция навыка, создающая файл: <Источник>/Tables/<Имя>.xml плюс имя в ChildObjects.
Синтаксис тот же, что у meta-compile.

Формат файла таблицы обязан быть один и тот же, кем бы файл ни был создан, поэтому эмиттеры
скопированы из meta-compile механически и зарегистрированы в check-inline-drift.mjs — гард
теперь сверяет семь функций между двумя навыками. Чтобы копии не тянули за собой пол-navыка,
две из них развязаны от эмиттеров-специфик: Build-EdsTableXml принимает готовый XML полей,
Emit-EdsFunction — готовый XML типа. Каждый навык рендерит их своим эмиттером, вывод не изменился.

meta-compile теперь предупреждает о перезаписи существующего объекта — для всех видов, не только
внешних источников: молчаливая замена uuid ломает ссылки, а узнать об этом было неоткуда.

Раундтрип на платформе 8.3.24.1691: таблица, добавленная через meta-edit, возвращается из
выгрузки байт в байт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-06 13:55:01 +03:00
Nick ShirokovandClaude Opus 5 ed766154fe feat(meta-edit,meta-remove): точечная правка и удаление внешних источников
meta-edit добавляет поля в таблицу внешнего источника: тот же парсер реквизита, свой тег <Field>,
свой контекст (без индексов и полнотекстового поиска — чужой таблицей 1С не владеет) и три своих
свойства в хвосте. Сам источник точечно не правится: и таблица (отдельный файл), и функция (узел
с полным набором свойств) требуют эмиттера, который живёт в meta-compile, — дублировать его здесь
значило бы завести вторую реализацию одного и того же.

Заодно закрыт тихий отказ, существовавший независимо от внешних источников: проверка допустимых
детей смотрела на истинность списка, а не на наличие ключа, поэтому для вида с пустым списком
трактовалась как «ограничений нет» и чужой ребёнок молча записывался в объект. Теперь add-attribute
на таблице внешнего источника отвергается с предупреждением, а не пишет <Attribute> вместо <Field>.

meta-remove понимает две формы: ExternalDataSource.PG — источник целиком, и четырёхчастную
ExternalDataSource.PG.Table.products — одну таблицу. Таблица числится в ChildObjects файла
источника, а не конфигурации, поэтому дерегистрация разведена переменной реестра; поиск ссылок
дополнен путями вида ExternalDataSource.И.Table.Т и ExternalDataSourceTableRef.И.Т.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 21:44:23 +03:00
Nick ShirokovandClaude Opus 5 bb56a7e898 feat(meta-info): чтение внешних источников данных
На источнике и его таблице навык печатал только заголовок и строку поддержки: вида он не
знал, а универсальный блок «Реквизиты» ищет <Attribute>, тогда как у таблицы поля — <Field>.

Источник показывает режим блокировки, список таблиц и функции с выражением и типом результата.
Таблица — вид (таблица/выражение), имя или выражение в источнике, объектность, признак «только
чтение», ссылки на поля (ключ, представление, родитель, версия данных, ввод по строке) короткими
именами, и список полей. У поля отмечается имя колонки в источнике, если оно отличается от имени
поля, а также «только чтение» и допустимость NULL.

Заодно два типа перестали печататься сырыми: xs:base64Binary → ДвоичныеДанные (касается всех
объектов, не только внешних источников) и ExternalDataSourceTableRef → русское имя, как у прочих
ссылочных типов.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 21:17:36 +03:00
Nick ShirokovandClaude Opus 5 97c8478382 docs(meta-compile): уточнена формулировка про значение незаполненного родителя
Свойство теряется не «в DSL», а при любой загрузке XML — платформа сбрасывает его даже при
загрузке собственной выгрузки. Проверено изолированно: dump-1 без правок загружен и выгружен
обратно, узел UnfilledParentValue — единственное расхождение во всём файле.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 21:05:31 +03:00
Nick ShirokovandClaude Opus 5 ca64988154 docs(meta-compile): инструкция по внешним источникам — про применение, не про устройство
Перечитал reference глазами модели, которая будет им пользоваться, и убрал то, что отвечает
на вопрос «как сделано», а не «как применять»: развёртывание коротких имён полей в полный путь,
«компилятор отказывает сразу», обоснование через поведение Конфигуратора, отдельный раздел про
пустой ключ (ужат до строки в таблице свойств).

Исправлено по существу:
- `unfilledParentValue` был описан как рабочий ключ, хотя платформа сбрасывает его при загрузке
  XML — теперь сказано прямо, что через DSL его не задать;
- добавлена строка `readOnly` у таблицы с подсказкой ставить его представлениям и таблицам вида
  Expression (загрузка этого не требует — проверено, — но писать в них нельзя);
- `inputByString` документирует вывод из поля представления;
- убран дубль строки `readOnly` в таблице свойств.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 21:03:51 +03:00
Nick ShirokovandClaude Opus 5 6dde36a690 feat(meta-compile): создание внешних источников данных
Вид ExternalDataSource навыки meta-* не поддерживали вовсе: в корпусе выгрузок его нет
ни одного, формат был неизвестен. Разведан на стенде (PostgreSQL в Docker + база 8.3.24)
и по реальной выгрузке рабочего проекта.

meta-compile собирает источник целиком одним JSON: сам источник, его таблицы с полями
и функции. Единственный вид, у которого объект складывается более чем из одного XML —
файл источника плюс по файлу на таблицу в <Источник>/Tables/.

DSL без новых конвенций: tables — dict имя → массив полей ЛИБО объект (как tabularSections),
поле — обычный реквизит плюс три своих ключа (nameInDataSource/readOnly/allowNull),
ссылки на поля короткими именами (как inputByString у справочника), functions — dict
имя → строка ИЛИ объект (как urlTemplates). Параметры функций не объекты метаданных:
они живут в самом выражении как &1, &2.

Инструкция под каскадом: в SKILL.md строка индекса, вся специфика — в
reference/external-data-source.md.

meta-validate принимает ExternalDataSource и Table, знает их наборы GeneratedType,
состав ChildObjects и шестичастную ссылку на форму таблицы. Проверено на выгрузке
платформы и на чужой рабочей выгрузке из другого проекта — обе проходят чисто.

Раундтрип на платформе 8.3.24.1691: наш эмит → загрузка → выгрузка даёт файлы,
идентичные поданным (с точностью до uuid). Два поведения платформы пришлось воспроизвести
явно, оба измерены: ввод по строке выводится из поля представления, а заданное значение
незаполненного родителя платформа при загрузке XML сбрасывает в пустую строку — задать
его можно только интерактивно.

Спека: раздел «Внешние источники данных» в 1c-config-objects-spec.md, формат ссылок,
наборы GeneratedType, строка в индексе спек.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 20:53:37 +03:00
Nick ShirokovandClaude Opus 5 c1f07f96ad feat(spec): измерена позиция внешнего источника данных в порядке ChildObjects
В корпусе выгрузок внешних источников нет ни одного, поэтому позиция вида в порядке
ChildObjects была неизвестна, а check-type-maps держал ExternalDataSource исключением
с причиной «позиция в порядке не измерена».

Измерено на платформе 8.3.24.1691: в конфигурацию с объектами-маркерами семи видов
(CommonModule … Task) и настоящим сервисом интеграции загружен внешний источник,
выгрузка показала порядок Task → ExternalDataSource → IntegrationService. Независимо
подтверждено рабочей конфигурацией с внешними источниками.

Спека: ExternalDataSource = 46, IntegrationService сдвинут на 47; добавлены наборы
GeneratedType источника (3 категории) и его таблицы (8 категорий, имя элемента
трёхчастное: префикс.Источник.Таблица).

Вид внесён в 52 карты одиннадцати навыков — без этого cf-validate считал бы каталог
ExternalDataSources/ неизвестным, а порядок в ChildObjects навыки-создатели ставили бы
в конец блока, откуда платформа переставила бы группу при первой же выгрузке.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 20:21:47 +03:00
Nick ShirokovandClaude Opus 5 5592590a0e fix(meta-compile): константа без valueType получала тип значения "Constant"
Определение {"type": "Constant", "name": "X"} эмитило <v8:Type>Constant</v8:Type>:
в тип значения попадал ВИД объекта. Платформа отвергала всю конфигурацию целиком —
«Неизвестное имя типа - Constant», — то есть один минимальный объект ронял загрузку.

Причина: Emit-ConstantProperties передавал корневое определение в общий Build-TypeStr,
а тот читает valueType, иначе type. Для реквизита это верно, для корневого определения
нет: там type — вид объекта. Добавлен флаг ValueTypeOnly, которым корневые эмиттеры
отключают подхват; заодно убрано мёртвое условие в typeEmpty, пришедшее туда из
контекста реквизита.

Поведение теперь совпадает с документированным (reference/simple.md): Строка(10).
Проверено платформой 8.3.24.1691.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi
2026-09-05 20:20:22 +03:00
Nick ShirokovandClaude Opus 5 cc0b7d3b64 test(runner): expectError больше не отключает файловые ожидания
Кейс с expectError и fileContains/fileNotContains/filesEqual/preserves проходил
при любом содержимом файла — проверки лежали под !expectError вместе со снэпшотом.
Под !expectError остались только снэпшот и идемпотентность: эталон с аварийного
состояния снимать нельзя, а файлы, написанные до отказа, проверять нужно.
Заодно во втором кодовом пути раннера не было filesEqual — пути выровнены.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MQkqXxErBepYmoUycfkbn
2026-09-05 19:10:58 +03:00
Nick ShirokovandClaude Opus 5 06bdee3e25 fix(epf-build): проверка исходников не видела модулей и форм
Относительный путь при копировании содержимого объекта в конфигурацию-заглушку
считался через Substring по длине переданной строки. Каталог может прийти с
коротким именем (C:\Users\NSHIRO~1\...), а Get-ChildItem отдаёт полное — разница
в один символ, и файлы уезжали в <Объект>\а\Ext\ObjectModule.bsl. Проверка при
этом молча шла по объекту без модулей и форм и всегда была зелёной. Длина
берётся у разрешённого пути; py-порт иммунен (os.path.relpath).

Дефект нашли новые кейсы stub-db-create — конвертация внешней обработки в объект
конфигурации: корневой тег и порождаемые типы, перевыпуск GUID, копирование .bsl
байт в байт. Каждый сверен возвратом дефекта в обоих портах.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MQkqXxErBepYmoUycfkbn
2026-09-05 19:10:58 +03:00
Nick ShirokovandClaude Opus 5 368d6bb278 test(check-inline-drift): гард видит копии во всех скриптах навыка
Индекс брал первый файл навыка по алфавиту, поэтому второй скрипт был для
гарда невидим. Единственный такой навык — epf-build (epf-build + stub-db-create),
и в невидимой копии прожили три расхождения с эталонами, включая потерянную
POSIX-ветку run_v8. Теперь сверяются все копии, в сообщении указывается файл.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MQkqXxErBepYmoUycfkbn
2026-09-05 17:46:30 +03:00
Nick ShirokovandClaude Opus 5 448f05ed11 feat(epf-build): исходники проверяются платформой до сборки
Сборка .epf/.erf не компилирует модули, поэтому сломанный BSL уезжал
пользователю и падал при открытии обработки. Прямой команды «проверь
внешнюю обработку» у платформы нет, но объект конфигурации она проверяет:
обработка кладётся в конфигурацию временной базы (stub-db-create
-EmbedSourceFile) и спрашивается /CheckConfig. При находках сборка
отменяется, в выводе — сообщение платформы и путь к файлу исходника.

Ключи -Checks и -Context; выключение -Checks off или externalCheck: false
в .v8-project.json. С выключенной проверкой поведение прежнее.

У копии объекта перевыпускаются все GUID: с идентификаторами исходника
платформа путает копию в базе с загружаемой внешней обработкой и через раз
отвечает «Исключение XDTO при чтении файла» на исправном исходнике.

Ручная матрица (база есть/нет × ps1/py × исправный/битый × с модулями/без)
вскрыла ещё три вещи: отказ платформы принять исходники при подготовке
проверки выдавался за сбой базы; py-порт стаба терял причину отказа
LoadConfigFromFiles (не было /Out, в PS он есть); копии общих утилит в
stub-db-create.py разошлись с эталоном db-create — у run_v8 не было
POSIX-ветки, из-за чего на darwin/Linux временная база с пробелом в пути
молча не находилась бы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013MQkqXxErBepYmoUycfkbn
2026-09-05 17:46:30 +03:00
Nick ShirokovandClaude Opus 5 a3ea0b7be6 feat(db-*): после загрузки расширения проверяется его применимость
Платформа отчитывается успехом и о расширении, которое не применит: отказ
всплывает лениво, при первом вызове метода, записью в журнал регистрации.
Теперь db-load-xml, db-load-git, db-load-cf и db-update после успешной операции
с расширением спрашивают платформу явно и печатают предупреждение; код возврата
операции не меняется — применение неприменимого расширения не разрушительно,
платформа просто работает по оригиналу. Поднять код возврата может -StrictLog,
он для регрессов и в инструкциях не значится.

Проверка обязана быть ОТДЕЛЬНЫМ запуском платформы: в одной командной строке
DESIGNER выполняет только последнюю пакетную команду, и дописанная проверка
отменила бы саму загрузку (замер: /LoadConfigFromFiles + /CheckCanApply… → код 0,
пустой лог, расширение в базе не изменилось). Об этом сказано в теле функции,
чтобы «оптимизация» не вернула команды в одну строку.

Выключатель: ключ -NoApplyCheck и настройка проекта extensionApplyCheck (ключ
команды сильнее). В ветке ibcmd проверка идёт соседним 1cv8; если его рядом нет —
одна строка [note], а не тишина.

Общий блок (Invoke-ApplyCheck / Invoke-ApplyCheckReport / Get-ApplyCheckEnabled и
их py-двойники) объявлен семьёй в check-inline-drift.mjs: разъехавшиеся копии
означали бы, что один навык предупреждает, а соседний по той же операции молчит.
db-load-cf получил недостающие копии Find-V8Project и ключ -StrictLog.

Раннер: у фейковой платформы появился отдельный ответ на проверку (spec.check),
выбираемый по составу аргументов, — иначе сценарий «загрузка прошла, проверка
провалилась» невыразим.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-09-05 14:06:59 +03:00
Nick ShirokovandClaude Opus 5 5943828216 fix(db-*): пакетная команда в дополнительных аргументах отбивается
-AdditionalV8Arguments пропускал чужие пакетные команды платформы, а в одной
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ из них, остальные молча
отбрасывает. Проверено на 8.3.24: db-load-xml с /CheckCanApplyConfigurationExtensions
в дополнительных аргументах печатал «Load completed successfully», возвращал 0 —
и не загружал ничего, что подтвердилось выгрузкой расширения из базы.

Assert-ExtraArgs отбивал только ключи, которыми навык владеет сам (V8OwnedKeys);
рядом заведён V8BatchKeys — известные пакетные команды (/CheckConfig,
/CheckModules, /CheckCanApplyConfigurationExtensions, /DumpDBCfgList, /DeleteCfg,
/UpdateCfg, /CompareCfg, /MergeCfg, /ManageCfgSupport, /RollbackCfg,
/ConvertFiles) — со своим сообщением: такая команда подменила бы операцию навыка.
Обычные опции (/UseHwLicenses+ и прочие) проходят как раньше.

Правка внесена во все 15 копий общего блока в обоих портах; тела Assert-ExtraArgs
остались побайтно одинаковыми (у стаба epf-build сохранён его поток stderr).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-09-04 21:49:34 +03:00
Nick ShirokovandClaude Opus 5 057c1044d9 fix(db-update): -Dynamic принимает on/off — значение "-" парсер PowerShell не связывает
Документированная форма -Dynamic <+/-> работала наполовину: `-Dynamic "+"` через
powershell.exe -File связывается, а `-Dynamic "-"` парсер не связывает вовсе —
процесс выходит с кодом 2, не напечатав ни строки, и до скрипта управление не
доходит. То есть «отключить динамическое обновление» из навыка было недостижимо,
и притом молча. Склеенная форма из примера SKILL.md (-Dynamic+) — тоже ошибка
разбора. В .py-порте все формы связывались, так что расхождение было ещё и между
портами.

Каноническая форма теперь словесная: -Dynamic on / off. "+"/"-" и yes/no
принимаются для совместимости, но в инструкции не значатся. Значение
нормализуется сразу после разбора, обе ветки (1cv8 -Dynamic+/-, ibcmd
--dynamic=auto/disable) получают прежний вход.

Кейсы dynamic-on / dynamic-off фиксируют, что именно уходит платформе; на прежнем
скрипте dynamic-off падает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-09-04 19:47:25 +03:00
Nick ShirokovandClaude Opus 5 a0f2c4988a feat(db-cfe-admin): навык администрирования расширений в базе
Расширение можно было положить в базу, но нельзя было посмотреть, что там лежит,
в каком оно состоянии, применяется ли, и убрать лишнее — всё это делалось руками
через 1cv8 и ibcmd.

Четыре команды: list (состав и свойства подключения), check (применимость и
синтаксический контроль), set-properties (безопасный режим, активность, защита от
опасных действий, область действия, профиль, РИБ), delete.

Конструкция опирается на замеры платформы (8.3.24.1691 и 8.3.27.1859):

- /CheckModules не нужен: /CheckConfig с теми же контекстными флагами даёт
  дословно тот же вывод и код возврата, но умеет вдобавок конфигурационные
  проверки. Одна команда платформы вместо двух, при запросе modules+config —
  один запуск;
- обе проверки БЕЗ флагов контекста рапортуют «ошибок не обнаружено» с кодом 0
  на заведомо сломанном модуле, поэтому набор контекстов всегда явный;
- применимость и синтаксис друг друга не заменяют (первая слепа к синтаксису,
  второй — к дрейфу контроля), отсюда умолчание apply,modules;
- коды возврата разные: применимость 1, /CheckConfig 101;
- /DeleteCfg -Extension "" возвращает 0, рапортует успех и удаляет ПЕРВОЕ
  расширение из списка, поэтому пустое имя отбивается до вызова платформы, а
  отсутствие имени никогда не значит «все»;
- список и удаление делает Конфигуратор (работает всегда и на серверной базе),
  свойства — ibcmd, которого в установке платформы может не быть: тогда колонки
  помечены прочерком с названной причиной, а set-properties отказывает внятно.

Значения флагов словесные (on/off), а не +/-: значение "-" через powershell.exe
-File парсер съедает молча — проверено, у соседнего db-update -Dynamic "-" по
этой причине не работает вовсе.

delete и set-properties проверяют результат перечитыванием состояния, а не
кодом возврата платформы.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-09-04 19:01:22 +03:00
Nick ShirokovandClaude Opus 5 d4832ce363 fix(cfe-patch-method): -Check ловит расхождение списка параметров
-Check сравнивал только тело и сигнатуру не смотрел вовсе. Если поставщик добавил
методу параметр, а тело не изменил (новый параметр часто ещё не используется),
платформа перехватчик отвергает — «Список параметров метода "X" не соответствует
методу "Y"» — а -Check говорил АКТУАЛЕН, и починка не запускалась.

Замер на стенде (8.3.24, /CheckCanApplyConfigurationExtensions + рантайм): платформа
сверяет только ЧИСЛО параметров. Имена, значения по умолчанию и лишний Экспорт не
сравнивает; дефолты копии вдобавок не действуют — берутся из оригинала. Поэтому
сверяем количество и ничего сверх того, иначе получили бы ложный дрейф там, где
платформа молчит.

Число параметров считается существующим Split-TopLevel по ParamsText обеих сторон;
при расхождении метод идёт обычной веткой дрейфа со статусом ДРЕЙФ и причиной
«список параметров: в оригинале N, в перехватчике M». -Actualize починку уже умел:
он собирает строку сигнатуры заново из оригинала — не запускался только потому, что
-Check не звал.

Кейсы: расхождение числа (ДРЕЙФ), переименованные параметры с другим дефолтом
(АКТУАЛЕН, как у платформы) и -Actualize, переписывающий список параметров.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-30 19:17:18 +03:00
533dd0ded4 fix(cfe-patch-method): ключ сравнения контроля — по измеренному правилу платформы
-Check считал дрейфом расхождение по пустым строкам (шумный ложный ДРЕЙФ, из-за
которого -Actualize предлагал косметическую перезапись тела) и НЕ считал дрейфом
расхождение по пробелам между токенами (тихий ложный АКТУАЛЕН — платформа такой
метод молча не применяет, он рвётся при первом вызове).

Правило платформы измерено на стенде: 27 вариантов косметики, платформы 8.3.24 и
8.3.27, общий модуль и модуль объекта, два независимых оракула
(/CheckCanApplyConfigurationExtensions и рантайм-вызов метода) — результат везде
одинаковый. Каждая строка Trim, строки, пустые после Trim, из сравнения выброшены,
всё остальное байт в байт и с учётом регистра: значимы пробелы между токенами,
пробелы внутри строкового литерала, комментарии целиком и регистр.

Вердикт АКТУАЛЕН считается по этому правилу (Get-ControlKey / control_key); в PS
сравнение ordinal, иначе -eq игнорирует регистр и порт расходится с Python.
Эвристики слияния (якоря, поглощение, дифф) остались на прежнем мягком normalize.

Кейсы: допуски (пустые строки, отступ, хвостовые пробелы) и два негатива —
пробелы между токенами и регистр идентификатора.

Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-30 18:29:14 +03:00
Nick ShirokovandClaude Opus 5 4fa57c73b5 docs(skills): Language исключён из сортировки без замера — так и написать
В комментарии семьи is_order_sensitive_type и в гайде причина исключения Language была
подана как установленная: «порядок языков задаёт порядок <v8:item> в мультиязычных
строках по всей выгрузке». Это гипотеза, высказанная при обсуждении, и мы её не
проверяли. Основания у четырёх исключений разные, и теперь это видно: CommonAttribute —
исключение самого стандарта, Subsystem и CommandGroup — замер по корпусу (в ACC 15 живых
групп команд из 39 вне GroupsOrder), Language — осторожность без замера.

Правка задевает тело семьи, поэтому идёт по всем пяти навыкам в обоих портах.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-30 14:37:15 +03:00
Nick ShirokovandClaude Opus 5 e48599164e docs(cf-edit): убрать обоснования исключений из инструкции навыка
В справочнике операции при каждом исключённом виде стояла причина: «порядок
разделителей задаёт порядок параметров сеанса», «порядок в панели», «порядок
мультиязычных строк». Позицию вставки выбирает проект, а не модель, — в инструкции
навыка этому места нет. К тому же документальное основание есть только у
CommonAttribute (исключение самого стандарта), остальные три — наш вывод из замеров,
и подавать его как факт тем более не следовало. Осталось перечисление видов.

Заодно сняты два утверждения, которые предыдущая правка сделала ложными: «взаимный
порядок групп видов навыки не меняют вовсе» в гайде и та же фраза в докстрингах обоих
портов — рядом с реализацией, которая ровно это и делает. Формулировка про четыре вида
в гайде уточнена: не сортируются их ИМЕНА, положение самой группы канонично всегда.

Обоснования остаются в docs/v8-project-guide.md, где решение принимает человек.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-30 14:05:47 +03:00
Nick ShirokovandClaude Opus 5 989b3490bc feat(cf-edit): sort-childObjects без аргумента выравнивает и порядок групп видов
Настройка newObjectPosition не чинит накопленное, а собранная навыками конфигурация
могла держать группы видов не в каноне — первая же выгрузка платформы давала диф.
Теперь вызов без значения приводит в порядок и сами группы. Вызов с явным видом этого
не делает: назвали вид — операция остаётся точечной и трогает только имена внутри него.

Приём тот же, что уже работал для имён: переставляется содержимое существующих узлов, а
не сами узлы, поэтому отступы и структура файла не меняются, а диф остаётся перестановкой
строк. Отличие в том, что меняется и имя тега: в py это присваивание el.tag, в PS имя
элемента неизменяемо, поэтому узел заменяется через ReplaceChild — он сохраняет
окружающие пробельные узлы на месте (тот же приём, что в meta-edit.ps1).

Проверки: порты дают байт-в-байт одинаковый файл; повторный прогон пишет Modified: 0 и
не меняет ни байта; на боевой выгрузке УТ (13 979 строк) команда сообщает Modified: 0 —
там уже канон, и Bot остался между DefinedType и CommonCommand. Раундтрип через
платформу 8.3.24: отсортированная конфигурация вернулась побайтово той же, то есть наш
канон совпадает с платформенным и сортировка не создаёт работы следующей выгрузке.

Правило видов с осмысленным порядком продолжает действовать для ИМЁН: в раундтрипе языки
остались как были. Положение самой группы оно не регулирует — это порядок платформы.

Три прежних кейса фиксировали поведение «группы не трогаем» и переписаны по факту.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 22:09:38 +03:00
Nick ShirokovandClaude Opus 5 1bc943e846 feat(skills): новая группа вида встаёт в канонический порядок, а не в конец блока
Навыки-создатели дописывали первый объект нового вида перед </ChildObjects>. Собранная
ими конфигурация выходила в неканоническом порядке видов, и первая же загрузка-выгрузка
давала диф: на стенде подали Language -> Catalog -> Document -> CommandGroup ->
CommonCommand, платформа вернула Language -> CommonCommand -> CommandGroup -> Catalog ->
Document. При этом cf-edit add-childObject и cfe-borrow канонический порядок уже держали —
очередная «одна работа, разные реализации».

Логика перенесена из cf-edit в семью Register-InChildObjects и в вариант nested-parent:
запись встаёт перед первой группой вида старше по CHILD_OBJECT_TYPES. Для этого список
канонического порядка добавлен в meta-compile, role-compile, xdto-compile и
subsystem-compile (оба порта) и заведён в check-type-maps — копий стало 9 вместо 5, зато
все под гардом.

У подсистем нашлось больше, чем планировалось: вставка шла в конец ВСЕГО блока, поэтому
подсистема покидала и собственную группу, если ниже были другие виды. Одно правило
закрывает оба случая. Во вложенном Subsystem.xml порядок видов неприменим — потомок там
всегда один, поведение не изменилось.

Сдвиг эталонов широкий, но проверяемый: 42 файла в 12 навыках, 53 добавленные строки
против 53 удалённых с совпадающим мультимножеством — чистая перестановка, все изменённые
строки вида <Тег>Имя</Тег>. Из 425 эталонов с ChildObjects неканоничными осталось 15: 11
фикстур сортировки (неканоничны по замыслу) и 4 статические фикстуры на сохранение EOL и
разбор форм, которые мы намеренно не переупорядочиваем.

Версии подняты в обоих портах и за эту правку, и за предыдущую (карты типов).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 21:53:52 +03:00
Nick ShirokovandClaude Opus 5 bdd2ed63f9 fix(spec): позиция Bot в порядке типов ChildObjects, добавлен PaletteColor
Спецификация держала Bot на позиции 11 (после CommonModule), а платформа ставит его
сразу за DefinedType. Замерено на стенде: конфигурацию с намеренно дописанными в конец
Bot и PaletteColor загрузили и выгрузили обратно —

  8.5.1.1302:  DefinedType -> Bot -> PaletteColor -> CommonCommand
  8.3.27.1859: DefinedType -> Bot -> CommonCommand

то есть позиция не версионная, а взаимный порядок Bot и PaletteColor, которого не было
ни в одной выгрузке корпуса, теперь известен.

Последствия неверного эталона были не только косметические: cf-edit add-childObject уже
сегодня ставил новую группу Bot не туда, а планируемое выравнивание порядка групп по
такому списку передвинуло бы корректно лежащий Bot в конфигурации вроде УТ.

PaletteColor (8.5) в спецификации отсутствовал вовсе. Добавлен рядом с Bot; §7.4
поправлен: порядок типов между версиями не меняется, а набор растёт.

Синхронизированы все карты под гардом check-type-maps: порядок в cf-edit, cf-info,
cf-validate, cfe-validate, cfe-borrow и каталоги в тех же плюс cfe-diff. Проверка
эталона: последовательность групп в ut_8.3.27 и unf_8.5 теперь монотонна по списку —
раньше УТ давала False.

Кейс add-bot позицию не проверял: в фикстуре были только язык и бот, тест прошёл бы и с
неверным порядком. Добавлен add-childobject-bot-position с CommonModule, DefinedType и
CommandGroup, где место видно однозначно.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 21:21:46 +03:00
Nick ShirokovandClaude Opus 5 1046e6018c feat(cf-edit): виды с осмысленным порядком не сортируются автоматически
Правило «Subsystem не упорядочиваем» распространено на четыре вида и вынесено в
семью is_order_sensitive_type / Test-OrderSensitiveType: раньше литерал "Subsystem"
был размазан по четырём точкам — регистрация в Register-InChildObjects,
add-childObject, заимствование в расширение и сортировка.

CommonAttribute — исключение самого стандарта #std467: у общих реквизитов-разделителей
порядок в дереве задаёт порядок установки параметров сеанса
(https://github.com/1C-Company/v8-code-style/issues/78). Единственный из четырёх, где
сортировка ломает поведение, а не только диф.

CommandGroup и Language добавлены по замерам. В ACC 15 живых групп команд из 39 не
перечислены ни в одном GroupsOrder — как и подсистемы, они падают на порядок дерева.
Порядок языков задаёт порядок <v8:item> в мультиязычных строках по всей выгрузке.

Прежний вывод «сортировать их безопасно» опирался на стендовый прогон, где не было
ни подсистем (а значит и CommandInterface.xml), ни второго языка у объектов: ломаться
там было нечему, и отсутствие изменений ничего не доказывало.

PaletteColor сортируется — это переменные цветов.

Второе послабление стандарта (объекты «Удалить*» можно держать в конце ветки) не
реализуем: типовые им не пользуются — в ACC 360 таких объектов из 373 стоят по
алфавиту, а сортировка стандарту не противоречит. Оговорено в гайде.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 20:21:55 +03:00
Nick ShirokovandClaude Opus 5 ecde0d1c41 fix(skills): единый порядок поиска .v8-project.json для newObjectPosition
Резолвер настройки искал файл от каталога конфигурации и лишь потом от cwd —
единственное место в репозитории с таким отсчётом. Остальные ищут от рабочего
каталога: support-guard (18 навыков) с фолбэком на каталог конфигурации, группа
db-* (5 навыков) вообще без фолбэка, потому что каталога конфигурации у них нет.

Обоснование прежнего порядка («cwd почти всегда чужой проект») опиралось на нашу
стендовую раскладку — вызов из репозитория навыков по выгрузке в cfsrc. У
пользователя cwd и есть каталог проекта с .v8-project.json, а скрипт навыка
зовётся по абсолютному пути и cwd не меняет, так что оба порядка эквивалентны.

Кейс cfe-borrow/newobjectposition-byname клал настройку внутрь каталога расширения
и тем закреплял несуществующую раскладку — конфиг проекта лежит в корне проекта,
расширение внутри него. Поправлен на реальную.

Из гайда убран абзац, объяснявший особый порядок: объяснять больше нечего.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 19:14:27 +03:00
Nick ShirokovandClaude Opus 5 1ccd56606b fix(cfe-borrow): вернуть вставку заимствованных реквизитов в свой ChildObjects
При переводе сохранения объекта на семью Detect-XmlStyle/Finalize-XmlText из блока
выпал вызов Insert-IntoOwnChildObjects: заимствованные реквизиты собираются текстом
и вставляются в собственный ChildObjects объекта, а не через InnerXml (тот ломает
пространства имён). Без вызова слияние реквизитов при заимствовании по глубокому
пути молча теряло их.

Вызов стоит до Finalize-XmlText, чтобы схлопывание пустых тегов накрывало и
вставленные реквизиты — как было в исходном порядке.

Тестами не ловится: путь Merge-AttributesIntoObject не покрыт ни одним кейсом,
поэтому удаление прошло 29/29 и верификацию платформой. Проверено изолированным
прогоном связки: реквизит вставлен, самозакрытый ChildObjects раскрыт, пустые теги
плотные, XML валиден. Покрытие этого пути — отдельная задача.

Попутно: кейс type-name-russian расширен на add-childObject — русское имя вида
проверялось только в sort и remove.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 18:24:21 +03:00
c7b0dce151 feat(skills): порядок объектов метаданных в ChildObjects
Навыки-создатели дописывали новый объект в конец группы своего вида, а стандарт
требует порядка по имени (АПК:1108). Замеры на 8 боевых выгрузках: 103 385 объектов
лежат по алфавиту, нарушают его ровно дописки в хвост. Стенд подтвердил, что
беспорядок вечен: платформа нормализует порядок ВИДОВ (возвращает канонический),
но порядок имён внутри вида не трогает.

Настройка newObjectPosition в .v8-project.json (end по умолчанию | byName) —
решение проекта, а не вызова: читают её meta-compile, role-compile, xdto-compile,
cfe-borrow и cf-edit add-childObject. Компаратор при этом константа, моделирующая
дерево Конфигуратора: ключ «ранг+символ» без культурных таблиц, одинаковый в обоих
портах на любой ОС. На корпусе он даёт 4 нарушения на 125 088 пар против 2866 у
ordinal-сравнения, которым cf-edit и cfe-borrow сортировали до сих пор.

Subsystem не упорядочивается автоматически нигде: пока подсистемы не перечислены
в <SubsystemsOrder>, порядок дерева задаёт порядок разделов в панели, а платформа
этот список сама не заводит.

Настройка не чинит накопленное, поэтому cf-edit получил операцию sort-childObjects
(вся конфигурация или названные виды). Она переставляет значения узлов, а не узлы,
поэтому диф — чистая перестановка строк. Имя вида принимается в любом регистре,
во множественном числе и по-русски; неизвестный вид — отказ со списком допустимых.

Попутно:
- PS-порты создателей переведены с DOM-сериализации на текстовую вставку: на
  выгрузке не в каноне Конфигуратора они переписывали заголовок без просьбы;
- PS-сторона семьи detect_xml_style/finalize_xml_bytes закрыта в cf-edit и
  cfe-borrow — правка чужого файла наследует его BOM/EOL/заголовок;
- xdto-compile присоединён к семье Register-InChildObjects вместо инлайн-копии;
- закрыт ложный успех в py subsystem-compile: <ChildObjects /> с пробелом ET
  разбирает, а текстовые ветки не находили — файл писался без вставки;
- cfe-borrow py не импортировал json, из-за чего резолвер молча возвращал end;
- cf-edit add-childObject ставил объект в конец блока, за пределы своей группы,
  когда видов старше в файле не было.

Проверки: 906/906 на PowerShell и 903/906 на Python, 10 гардов, верификация
эталонов платформой без падений, раундтрип отсортированной конфигурации на
8.3.24 и 8.3.27, сортировка боевой выгрузки ACC (1,4 МБ) и расширения из корпуса.

Задачу принёс PR #85; часть кода взята оттуда.

Co-Authored-By: Sergei Pleshanov <72200277+Abacadabras@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QoAJmoNbgWKobA7JGgN5S3
2026-08-29 17:34:09 +03:00
Nick ShirokovandClaude Opus 5 7409bacd47 docs(db-repo): шаг 2 цитирует фактический текст предупреждения
В инструкции стояло «локальная конфигурация изменена: из хранилища
получено N», скрипт печатает «…изменена, получено объектов из
хранилища: N». Модель искала в выводе фразу, которой там нет.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:00:34 +03:00
Nick ShirokovandClaude Opus 5 a812d145bd test(db-repo): захват с получением версий и без него — две ветки закреплены
Различение было реализовано, но не покрыто: ни один фейковый лог не
содержал строки «Объект получен из хранилища», поэтому подсказка о
перевыгрузке могла начать печататься всегда — и тесты бы это пропустили.

Логи кейсов сняты с живого стенда (FINDINGS #14).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:00:34 +03:00
Nick ShirokovandClaude Opus 5 789047a975 test(integration): перечисления создаются до объектов, которые на них ссылаются
Справочник Номенклатура объявлял реквизиты типа EnumRef.* до того,
как сами перечисления попадали в конфигурацию. Валидатор, ужесточённый
в 7b40eb4c, проверяет разрешимость ссылочных типов и отвергал такое
промежуточное состояние.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:41:07 +03:00
Nick ShirokovandClaude Opus 5 3adffd1537 chore: выровнять версии PS-портов с py
Правило требует синхронного бампа обоих портов, а я поднял только py — дважды
за сегодня, в правке кавычек на POSIX и в выравнивании потоков. Версия
описывает состояние навыка, а не отдельного файла, поэтому пара обязана
двигаться вместе.

Двадцать один PS-порт выровнен по своему py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:05:02 +03:00
Nick ShirokovandClaude Opus 5 4f61ef77ca fix(py-порты): ошибки печатать в тот же поток, что и PS
Двадцать один py-порт печатал ошибки в stderr, тогда как их PS-мастера пишут
через Write-Host в stdout. Счётчики совпадали один в один (14↔14, 19↔19,
13↔13) — сообщения были те же, разъехался только поток. Это нарушало
соответствие из docs/python-porting-guide.md, где Write-Host сопоставлен
обычному print.

Это не косметика. Харнесс не чередует потоки, а группирует: сначала весь
stderr, потом весь stdout. Из-за этого в py-порте вердикт «Error dumping
configuration (code: 1)» печатался ПЕРЕД строками, которые его объясняют, а
причина из лога платформы оказывалась в самом низу — причинный порядок вывода
переворачивался. Порт, работающий на macOS, читался хуже того, что работает на
Windows.

Тесты этого не ловили по построению: текст ошибки сверяют только кейсы со
строковым expectError, а он смотрит в stderr — потому такие кейсы есть лишь у
семейства, где потоки сходятся, а в db-* их ноль.

Добавлен гард check-error-streams.mjs: нет записи в stderr в PS-порте — не
должно быть и в py, и симметрично. Он сразу нашёл пять навыков сверх тех, что
я насчитал вручную, и отсеял два ложных срабатывания (в meta-remove слово
Write-Error стоит в комментарии «почему НЕ Write-Error»).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:03:34 +03:00
Nick ShirokovandClaude Opus 5 fdeae5c87f test(db-dump-xml): не сверять текст ошибки — потоки портов различаются
Кейс проверял текст через stdoutContains и был зелёным на PowerShell, красным на
python: Write-Host уходит в stdout, а py-порт печатает ошибки в stderr. Это
общее для группы расхождение, поэтому давний error-partial-no-objects тоже
проверяет только код возврата. Новый кейс приведён к тому же виду.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:58:25 +03:00
Nick ShirokovandClaude Opus 5 989f4c5526 fix(db-dump-xml,db-load-xml): список объектов задаёт частичную операцию
Список без режима давал молчаливо неверный результат, причём разный. У выгрузки
умолчание Changes игнорировало -Objects и отдавало «изменённое с прошлой
выгрузки». У загрузки на движке 1cv8 умолчание Full игнорировало -Files и
заменяло всю конфигурацию базы, тогда как на ibcmd та же команда выполнялась
частично — одни и те же аргументы вели себя по-разному.

Теперь перечисленные объекты или файлы сами задают частичную операцию. Режим
разрешается до ветвления на движки, поэтому расхождение исчезает по построению;
проверено на стенде: обе ветки дают одинаковый результат.

Заданный вместе со списком -Mode Full или Changes не отбрасывается молча — о нём
сообщает [note]. Список с -Mode UpdateInfo отвергается: это другая операция,
обновление ConfigDumpInfo без выгрузки файлов, и вывод подменил бы её.

Инструкции намеренно не менялись: -Mode Partial остаётся каноничной формой,
а вывод режима — прощающим вводом, который мы не документируем.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:43:39 +03:00
Nick ShirokovandClaude Opus 5 7c37fe2ac2 docs(db-repo): disconnect не необратим — мы сами подключали базу обратно
В каскаде стояло «Операция необратима». Это неверно: переподключение работает,
просто требует обоих флагов connect, о чём сказано абзацем выше. Ложная
безвозвратность отпугивала бы от законного действия.

Убрано и «пропускает диалог аутентификации»: в пакетном режиме с реквизитами
диалога нет — это теория из документации, а не действие.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:16:10 +03:00
Nick ShirokovandClaude Opus 5 1eb565b1d1 fix(db-repo): новый объект конфигурации — захватывается только корень
Инструкция велела захватывать «объект и корень». Объекта ещё нет ни в базе, ни
в хранилище, поэтому список отвергается целиком — «Загруженный список объектов
пуст» — и корень тоже не достаётся: модель осталась бы вовсе без захвата.

Верный порядок такой же, как у новой формы или макета: захватываем то, что
существует (корень), частичная загрузка создаёт объект, и при помещении он
называется вместе с корнем. Пример в SKILL.md разделён на два шага.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:10:42 +03:00
Nick ShirokovandClaude Opus 5 e26f122895 docs(tests): описать fakePlatform в формате кейса
Раннер получил новое поле — формат кейса обязан его описывать, иначе следующий
автор снова напишет .cmd руками и пометит кейс osOnly.

Отдельно сказано, что таким кейсам osOnly не нужен, и почему: именно забытый
-posix двойник давал дыру, из-за которой на маке выполнялся один кейс из
двенадцати.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 16:27:43 +03:00
Nick ShirokovandClaude Opus 5 0704c0ef12 test(runner): фейковая платформа — забота раннера, а не кейса
Кейс описывал механику: сам клал fake.cmd с batch-кодом, прописывал путь к нему
и помечался osOnly win32, потому что .cmd на маке не исполнится. Тот же сценарий
на POSIX требовал второго файла с fake.sh — отсюда двойники, а забытый двойник
означал дыру: у db-repo на маке выполнялся один кейс из двенадцати, и заметили
это случайно.

Теперь кейс объявляет намерение — лог и код возврата в поле fakePlatform, — а
раннер сам кладёт .cmd или .sh под текущую ОС, пишет лог и заглушку базы и
подставляет {fakePlatform} в аргументы. Механизм включается только по объявлению
поля; остальные кейсы не затронуты.

Мигрированы db-repo (11), db-update (4), db-load-xml (4); 19 двойников удалены.
Пропусков на Windows стало 0 вместо 19 — это и были двойники.

Кейсы db-create, db-dump-cf, db-run, epf-build оставлены как есть: часть из них
однопортовая по существу (cp866 в выводе, гейт «ложного успеха» на runtimeOnly),
и каждый требует отдельного решения.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 16:19:09 +03:00
Nick ShirokovandClaude Opus 5 84e908304e fix(db-*): на POSIX снимать и кавычки склеенных ключей
Проверка прямым вызовом платформы на darwin: /ConfigurationRepositoryF"путь с
пробелом" отвергается, тот же ключ без кавычек проходит. Значит на POSIX лишние
и обрамляющие кавычки значений, и кавычки внутри склеенных ключей — они нужны
только для склейки команды на Windows.

File="…" не трогаем: там кавычки часть синтаксиса строки соединения, и с ними
на POSIX всё работает (проверено db-create).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:45:51 +03:00
Nick ShirokovandClaude Opus 5 b7837c86ce fix(db-*): на POSIX снимать обрамляющие кавычки с аргументов платформы
Воспроизведено на darwin: db-load-xml с путём, содержащим пробел, падает с
«Неопределена информационная база». На POSIX аргументы уходят списком, и
кавычки, нужные для склейки команды на Windows, становятся частью значения.
Тот же механизм ранее молча терял многословный -comment в db-repo.

Правка в общей run_v8 (семья platform: run_v8) — чинит все двенадцать
потребителей разом. Снимается ОДИН слой обрамляющих кавычек, поэтому склеенные
ключи (/N"user", File="…", /ConfigurationRepositoryF"путь") не задеты: у них
кавычки внутри токена, там их ждёт разборщик 1С.

PS1 не затронут: там команда всегда склеивается в строку. Точечный arg_value(),
добавленный в db-repo при разборе дефекта, откачен — семейная правка делает его
лишним.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 15:42:46 +03:00
Nick ShirokovandClaude Opus 5 ab5c1fd7c1 fix(db-repo): бамп версии после правки кавычек на POSIX
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:56:41 +03:00
Nick ShirokovandClaude Opus 5 5e11cf2912 fix(db-repo): на POSIX не добавлять свои кавычки к значениям аргументов
Живой прогон на darwin показал: многословный -comment теряется целиком, а
однословный доходит. На POSIX аргументы уходят списком, и наши кавычки
становятся частью значения; склейка в одну строку нужна только на Windows,
там же нужны и кавычки.

Ключи вида /F"путь" и /N"имя" не трогаем: там кавычки внутри токена требует
сам разборщик 1С, и они нужны на обеих ОС.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:55:11 +03:00
Nick ShirokovandClaude Opus 5 72a4777192 test(db-repo): posix-двойники кейсов — на маке прогонялся один из двенадцати
Кейсы используют фейковую платформу в виде .cmd и потому помечены osOnly win32.
На Mac mini из двенадцати выполнялся один, то есть py-порт там почти не
проверялся — ровно та ловушка, о которой предупреждает памятка по стенду.

Добавлены двойники на fake.sh для всех кейсов, кроме уже имевшегося.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:21:09 +03:00
Nick ShirokovandClaude Opus 5 b653ca74d9 docs(db-repo): убрать навязанный рецепт сравнения версий
В history.md лежала цепочка «выгрузить CF версии → создать временную базу →
загрузить → выгрузить XML → сравнить». Это один из способов, поданный как
единственный: сравнить можно и средствами конфигуратора, а по задаче #76
источником сравнения может быть прямо версия хранилища, без временной базы —
тогда рецепт станет ещё и неверным.

Работа навыка заканчивается на dump-cfg; что делать с полученным CF, решает
пользователь.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:16:01 +03:00
Nick ShirokovandClaude Opus 5 24579c2580 docs(db-repo): в инструкциях только применение, http отделён от tcp
Перечитал изменённые инструкции свежим взглядом и убрал всё, что описывает
поведение навыка вместо способа им пользоваться: образец вывода отчёта (модель
увидит настоящий), фразу «навык подсказывает оба флага», объяснение, почему
длинный отчёт не печатается, и заверение, что неверная пара путь+расширение
ничего не ломает. Осталось действие: как назвать, что указать, чего ждать от
кода возврата.

Столбец «Что теряется» переименован в «Почему»: у lock -All не теряется ничего,
и строка «Ничего, но…» в таком столбце читалась криво.

Подсказка о недоступном хранилище разделена по схеме адреса: tcp ведёт к
серверу хранилища и порту, http — к веб-серверу и публикации. Прежний общий
текст советовал бы проверять сервер хранилища и порт 1542 для http-адреса, где
это неверно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:14:18 +03:00
Nick ShirokovandClaude Opus 5 8edacf39c8 docs(db-list): убрать из инструкции историю проверок
В SKILL.md место только тому, как навык применять. Оговорка о том, что форма
адреса не описана в известной документации и проверена на стенде, — это история
работы, модели она ничего не даёт. Осталось применимое: вид адреса, порт по
умолчанию и то, что недоступный сервер отвечает тем же сообщением, что и
отсутствие реквизитов.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:08:30 +03:00
Nick ShirokovandClaude Opus 5 1c74843c05 feat(db-repo): сетевое хранилище проверено, различаем недоступный сервер
Установлен crserver 8.3.23, поднят на порту 1542 — сетевое хранилище перестало
быть непроверенным местом. Адрес tcp://<хост>[:<порт>]/<имя>, порт по умолчанию
1542; хранилище создаётся сервером по требованию. Весь цикл, файл списка
объектов и администрирование работают так же, как на файловом, и одинаково в
обоих портах. Отдельного поведения у сетевого хранилища не обнаружено.

Нашлась двусмысленность: недоступный сервер даёт «Соединение с хранилищем
конфигурации не установлено» — ровно то же сообщение, что и отсутствие
реквизитов. Прежняя подсказка «добавьте repository в реестр» в сетевом случае
уводила не туда. Теперь db-repo различает по схеме адреса и советует проверить
сервер и порт, а подсказка db-load-xml и db-load-git называет обе причины.

Документация больше не оговаривается «не проверено»: форма адреса подтверждена
на стенде.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 14:07:10 +03:00
Nick ShirokovandClaude Opus 5 3cdba1f70c docs: не выдавать синтаксис сетевого хранилища за документированный
Форма tcp://srv01/MyApp пришла из примера в ишью #77, а мы повторили её в
справочнике реестра и в db-list как факт. Прицельный поиск показал, что в
доступной документации её нет: глава 7.4.15 описывает только каталог хранилища,
а в обоих «Приложение 4» (8.3.24 и 8.3.27) не встречаются ни crserver, ни
tcp:// — совпадения по слову «хранилище» относятся к лицензированию.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:54:33 +03:00
Nick ShirokovandClaude Opus 5 b2ee1216bf feat(db-load-xml,db-dump-xml): каталог расширения из extensions[].src
Пункт плана, остававшийся открытым: каталог исходников расширения модель
указывала руками, хотя он объявлен в записи базы. Каталог выбирает модель, а не
скрипт, поэтому это строка в инструкции — рядом с такой же про configSrc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:50:54 +03:00
Nick ShirokovandClaude Opus 5 4f6bfd75eb fix(db-repo): длинный отчёт по версиям не печатать вовсе, а не обрезать
Печать отчёта целиком стоит модели контекста, но опаснее другое: обрезанный
отчёт по версиям читается как полный ответ. По куску легко заключить, что
объект не менялся, — в отличие от списка объектов, где неполнота очевидна.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:47:39 +03:00
Nick ShirokovandClaude Opus 5 ac20d95fea feat(db-repo): снять с модели рутину, которую видно по прогону каскада
Три места, где модель делала работу за навык.

report требовал -OutputFile, хотя отчёт всё равно печатается в вывод: путь
приходилось придумывать ради файла, который никто не читает. Теперь без
параметра отчёт уходит во временный файл, а путь называется — сохранить
осознанно по-прежнему можно.

create и connect с явным путём хранилища теперь печатают готовый блок
repository для .v8-project.json. Без записи в реестре реквизиты придётся
передавать в каждом вызове, а update откажется работать вовсе — вспомнить
об этом модель не может, а подставить готовое мы можем.

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

Проверено, что печатаемый блок — валидный JSON: строка из вывода разбирается
парсером, путь читается обратно. В PS обратный слэш в строке замены -replace
не спецсимвол, из-за чего слэши сперва удвоились дважды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:43:04 +03:00
Nick ShirokovandClaude Opus 5 8faa1f77c0 feat(db-repo): подсказки по отказам подключения, смоук каскадных операций
Административные и сервисные команды собирались по документации, а вживую
гонялись сырыми вызовами платформы — сборка аргументов навыком не проверялась.
Прогон через навык показал, что работают dump-cfg (включая -Version), set-label,
add-user, clear-cache во всех областях, optimize, copy-users, create -NoBind.

Нашлись два тупика подряд при переподключении базы: платформа сначала отвергает
непустую конфигурацию, а после -ForceReplaceCfg — то, что за пользователем
хранилища уже числится эта база. Оба раза действие не называется. Добавлены
подсказки на оба сообщения, сценарий описан в references/connect.md.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:36:42 +03:00
Nick ShirokovandClaude Opus 5 e64c48cdfd fix(db-repo): полная форма вызова в каскаде — короткая ломала переключение рантайма
Файлы references/ несли сокращённую запись «... db-repo.ps1 -Command …», и
scripts/switch.py её не видел: его регулярное выражение требует полной формы
powershell.exe -NoProfile -File <path>.ps1. В python-дистрибуции каскад остался
бы с .ps1, то есть на macOS указывал бы на неисполнимый файл, а гард
переносимости этого не ловит — он следит за плейсхолдером ${CLAUDE_SKILL_DIR},
которого в сокращённой записи тоже не было.

Проверено прогоном switch_runtime_content по всем четырём файлам: до правки
переключатель находил 0 вызовов при трёх упоминаниях .ps1, после — все
конвертируются, .ps1 не остаётся.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:33:00 +03:00
Nick ShirokovandClaude Opus 5 19d62afb29 docs(db-list): схема хранилища конфигурации в реестре баз
db-list/SKILL.md — фактическая спека .v8-project.json, которую читает модель,
и без repository она давала неполную картину: база под хранилищем не принимает
ни одной операции конфигуратора без реквизитов доступа, причём это касается не
только db-repo, но и всей группы загрузки-выгрузки.

Добавлены repository и extensions[] в пример и таблицу полей, раздел с
описанием обоих (включая то, что у расширения своё хранилище со своим путём,
а пользователь хранилища не наследуется от пользователя базы), пункт в
интерактивный сценарий добавления базы и форма ключей доступа в разделе про
строку подключения.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:29:55 +03:00
Nick ShirokovandClaude Opus 5 4a49eea422 feat(db-repo): порог на длину вывода и функциональные тесты
Операция над всей конфигурацией перечисляет тысячи объектов, и весь список
уезжал в вывод модели. Списки обрезаются на двадцати позициях, полный уходит
в файл, путь к которому назван. Сырой лог платформы при отказе показывается
хвостом в 200 строк: итог и причину платформа пишет в конце.

Тесты на фейковой платформе — 12 кейсов, разбор лога проверяется без 1С на
логах, снятых со стенда. Ключевой кейс закрывает главное правило навыка:
платформа вернула 1, часть объектов захвачена, навык возвращает 0 с поимённым
предупреждением. Обрезка списка проверена на логе из тридцати объектов —
кейс требует и строку «и ещё 10», и отсутствие двадцать первого объекта
в выводе.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:25:11 +03:00
Nick ShirokovandClaude Opus 5 67b8ad5b47 fix(db-repo): порядок вывода и формулировки подсказок
Прогон всех веток вывода подряд показал, что длинный совет про перевыгрузку
вставал МЕЖДУ фактами и вердиктом: строка «захват не выполнен» терялась в
середине. Совет теперь печатается последним во всех ветках — сначала факты,
затем вердикт.

Убрана мёртвая подсказка про -Yes: флага больше нет. Снято утверждение «по
убыванию частоты» у причин отказа commit — проверить его нечем. Сняты повтор
слова «проверьте» и склонение «объект(ов)». Заголовок «Не были захвачены»
дополнен пояснением, что снимать нечего.

Примеры в шапке скрипта приведены к именованной подкоманде.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:19:39 +03:00
Nick ShirokovandClaude Opus 5 1327b4fc5a feat(db-repo): py-порт, реестр семей копий и подсказки в db-load-git
Порт .py зеркалит .ps1 по порядку функций, именам и комментариям — расхождения
только там, где их диктует рантайм. Общие функции взяты из соседних портов
дословно.

Копии зарегистрированы в check-inline-drift.mjs: db-repo присоединён к шести
платформенным семьям, четыре функции блока реквизитов хранилища заведены новыми
семьями, разбор сообщений хранилища — семьёй с эталоном db-load-xml. Гард сразу
нашёл настоящее расхождение: копии Get-RepositoryArgs в четырёх соседях остались
со старым comma-return, эталон правился позже. Ресинхронизировано.

Разбор сообщений хранилища добавлен и в db-load-git: он тоже грузит в базу
частично и получает те же три отказа платформы.

Подкоманда стала именованной (-Command): в группе скрипты принимают только
именованные параметры, слэш-форму в них переводит модель. Позиционный параметр
у пишущего навыка запрещён гардом check-positional-binding. Mandatory при этом
не ставим — обязательный параметр PowerShell запрашивает интерактивно, а в
пакетном запуске это зависание.

Все девять гардов и функциональные тесты db-* зелёные в обоих рантаймах.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 13:08:52 +03:00
Nick ShirokovandClaude Opus 5 012f5c3c30 feat(db-repo): операции над всей конфигурацией — только по явному -All
Захват всей конфигурации на большой базе идёт долго и блокирует работу всей
команде, а получался он от одного забытого -Objects: «вся конфигурация» была
умолчанием. Теперь для lock, unlock и commit это отдельный флаг -All, который
нельзя совместить с -Objects. Захват корня с -WithChildren, означающий то же
самое, отсылает к нему же.

update без -Objects остаётся обычным вызовом: получение всех изменений — это
нормальный ежедневный сценарий, а не тяжёлая операция.

Флаг отдельный, а не -Force: у -Force уже есть платформенное значение, разное
по подкомандам, и третье значение сделало бы его нечитаемым.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:51:05 +03:00
Nick ShirokovandClaude Opus 5 33633aaf0e feat(db-repo): подменять часть объекта владельцем вместо отказа платформы
Модель может попросить захватить реквизит, табличную часть, измерение или
ресурс. Платформа на это отвечает «Загруженный список объектов пуст» — без
имени объекта и без причины, отладить такой ответ нечем.

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

Заодно убран comma-return у двух функций: в связке с @() у вызывающего он даёт
вложенный массив, из-за чего список объектов склеивался в один fullName.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:45:36 +03:00
Nick ShirokovandClaude Opus 5 3e816d3e12 feat(db-repo): правила захвата, нулевой шаг цикла и диагностика по отчёту субагента
Проверка навыка субагентом на сквозном сценарии вскрыла фактическую ошибку
в инструкции: реквизиты и табличные части перечислялись наравне с формами как
захватываемые объекты. Платформа их объектами не считает — список объектов
получается пустым, причём без секции «отсутствующие в конфигурации». Правишь
реквизит, табличную часть, измерение, ресурс или модуль — захватывай владельца;
формы, макеты и команды захватываются отдельно.

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

В цикл добавлен нулевой шаг — получение актуального состояния перед началом
работы: правки должны опираться на актуальные версии в том числе тех объектов,
которые не меняются, но используются. Загрузка и обновление БД слиты в один
шаг через -UpdateDB, поэтому цикл не удлинился.

Захват корня конфигурации с -WithChildren отклоняется: это захват всей
конфигурации, а выглядит как захват корня. Для всей конфигурации есть
однозначная форма — вызов без -Objects.

Текстовый отчёт печатается, а не только сохраняется в файл.

Схема repository и extensions[] описана в docs/v8-project-guide.md,
цикл под хранилищем — в docs/db-guide.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:40:44 +03:00
Nick ShirokovandClaude Opus 5 478e7489f9 feat(db-repo): навык работы с хранилищем конфигурации 1С
Новый навык /db-repo с подкомандами: рабочий цикл (lock, unlock, commit,
update), подключение базы (connect, disconnect), история (report, dump-cfg),
администрирование (create, add-user, copy-users) и сервис (set-label,
optimize, clear-cache). Каскад инструкций в references/.

Ядро навыка — разбор вывода платформы: код возврата о фактическом результате
не говорит. Захват и помещение не атомарны (код 1 при реально захваченном
объекте), а все no-op'ы дают код 0. Вердикт строится по строкам лога и
отражает достижение запрошенного состояния, а не факт изменения.

Захват, обновление и unlock -Force молча подтягивают свежие версии в локальную
конфигурацию. Навык называет полученные объекты и печатает готовую команду
перевыгрузки: без неё частичная загрузка старых исходников молча откатывает
чужие изменения.

Соседние навыки группы получили общий блок разрешения реквизитов хранилища из
.v8-project.json — база под хранилищем не принимает без них ни одной операции
конфигуратора. db-load-xml дополнительно подсказывает действие по трём
сообщениям платформы, db-dump-xml получил -ObjectsFile для передачи списка
объектов между навыками без перепечатывания.

Порты .py следуют отдельно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:31:39 +03:00
0442aa015a fix(tests,web-test): не отдавать rmSync/cpSync пути с не-ASCII символами
На Windows fs.rmSync и fs.cpSync молча не делают ничего, когда не-ASCII символы
есть в самом аргументе пути (nodejs/node#61067). Для аудитории проекта это боевой
сценарий: кириллическое имя пользователя даёт кириллический %TEMP%, где раннер
создаёт воркспейсы, плюс кириллические имена объектов 1С в путях внутри кейсов.

Замерено на восьми сборках. Затронуты 22.18.0–22.23.2 (последняя LTS Jod) и
24.12.0–24.14.0; исправны 22.15.1, 24.15.0+, 25.x, 26.x. Фикс приехал в fs-слой
Node 24.15 и в ветку 22.x не бэкпортирован, поэтому «обновить Node» вопрос не
закрывает. Зависимость не монотонна по версиям (24.14 чинит rmSync, но не
cpSync) — гард по номеру версии невозможен, решает только сам путь.

Симптомы: rmSync молча ничего не удаляет; cpSync с не-ASCII приёмником молча
ничего не копирует; cpSync с не-ASCII источником валит процесс нативно
(0xC0000409) мимо try/catch. Не затронуты mkdirSync, readdirSync, lstatSync,
copyFileSync (включая перезапись), unlinkSync, rmdirSync — на них стоит обход.

Что ломалось: под кириллическим %TEMP% фикстуры не доезжали до воркспейсов
(meta-info — 15 ложных падений из 26, один кейс ложно-зелёный на пустом
воркспейсе); cfe-validate/module-state-flag-without-file был красным даже при
ASCII %TEMP% (кириллица в самом deletePath); --update-snapshots молча не сносил
старый эталон, то есть портил коммитимые артефакты.

Реализация — единый fsutil в двух побайтно одинаковых копиях (tests/common/ и
внутри автономного навыка web-test), раскатанный на все 42 точки вызова в 11
файлах: tests/skills/*, tests/web-test/*, hooks/test/run.mjs, движок web-test.
Предикат судит по resolve(p), а не по строке аргумента: относительный
ASCII-аргумент при не-ASCII cwd платформа роняет так же молча. Ретраи доживают
до ручного обхода, перезапись как у cpSync с force: true, настоящие ошибки не
глотаются, выживший после удаления путь — громкая ошибка.

Гард tests/skills/check-nonascii-fs.mjs в check-all.mjs проверяет хелпер, а не
платформу (поэтому зелёный и на исправной Node), сверяет хеши обеих копий и
печатает справкой состояние текущей сборки.

Co-Authored-By: androman.pro <5669019+andromanpro@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 19:27:02 +03:00
Nick ShirokovandClaude Opus 5 36bae190b2 test(role-compile): объявлен пропуск платформенной проверки для намеренно невалидных фикстур
Фикстуры childobjects-missing и childobjects-selfclosed невалидны by design: первая вовсе без
<ChildObjects>, вторая с пустым. Кейсы проверяют реакцию навыка на такую конфигурацию, а платформа
её не принимает — «читаемое свойство не соответствует ожидаемому. Текущее Configuration, ожидаемое
ChildObjects» и «Неизвестный объект метаданных - Language.Русский» соответственно.

Причина пропуска записана по факту отказа платформы, а не по предположению.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:46:15 +03:00
Nick ShirokovandClaude Opus 5 66a11d1dba fix(tests): verify-snapshots понимает inputRaw, inputEncoding и версию формата EPF/ERF
Полный прогон верификации давал шесть падений; четыре из них — дефекты самого инструмента.

Вход кейса. Файл писался только из caseData.input и только UTF-8, поэтому кейсы, задающие вход
через inputRaw (сырая строка) и inputEncoding (UTF-16, cp1251), оставляли навык без входного
файла: meta-compile/encoding-utf16-bom и interface-edit/bad-definition-file падали на «Cannot bind
argument to parameter». Хуже того, кейс, у которого нет ни input, ни preRun, ни params, отбрасывался
в discoverCases как «ничего не подающий навыку» — inputRaw там тоже не учитывался, и такие кейсы
в верификацию не попадали вовсе (meta-compile/bad-encoding-cp1251,
subsystem-compile/bad-definition-file). Теперь оба ключа поддержаны, encodeInput перенесён копией
из runner.mjs — общего модуля у раннеров нет, и README предписывает добавлять ключ в оба.

Это ровно та «тихая дыра», о которой README предупреждает: ключ, известный лишь одному раннеру,
даёт кейс, зелёный в функциональном прогоне и не доезжающий до платформы в верификации.

Версия формата. Гейт вызывался только при наличии каталога конфигурации, поэтому кейсы
epf-init/format-221 и erf-init/format-221 доходили до сборки и падали на стенде без 8.5 — красным,
хотя это свойство стенда. Ядро гейта выделено и читает версию из шапки любого MetaDataObject,
включая исходник обработки и отчёта; в EPF-ветке проверка выполняется до epf-build.

Проверено точечно: два кейса с inputRaw проходят, два format-221 честно пропускаются по версии
платформы, два ранее невидимых кейса берутся в прогон и проходят. Функциональный раннер не задет:
role-compile 26/26, meta-compile 85/85.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:46:15 +03:00
Nick ShirokovandClaude Opus 5 9c66e89372 fix(tests): verify-snapshots грузит фикстурные кейсы и показывает долю непроверенного
configDir приходил только из скилл-уровневого setup (empty-config) и из ветки external, поэтому
у навыков с setup: none кейс на фикстуре до платформы не доезжал — и всё равно получал PASS.
Таких кейсов девять: пять support-edit, три meta-validate и один skd-decompile. В наборе по
умолчанию дефект не проявлялся: там у всех навыков setup: empty-config, и загрузка шла.

Теперь фикстура-конфигурация грузится как external-выгрузка, а путь «грузить нечего и спец-маршрут
не подошёл» вместо молчаливого PASS требует объявить skipPlatformVerify с причиной. skd-decompile
отнесён к standalone-навыкам: его выход — JSON-описание, грузить нечего.

Отдельно — видимость: «прошло» и «проверено платформой» больше не одно и то же число. В консоли и
в отчёте печатается, сколько кейсов реально обратилось к платформе, а сколько прошло мимо и почему
(external, standalone, ожидаемый отказ навыка, недоступная платформа); в таблице отчёта появилась
колонка «Платформа». Падения в эту долю не попадают: у них обращение было и не удалось.

Девяти кейсам проставлен пропуск с проверенной причиной. Для support-edit причина выяснялась
опытом: фикстуры содержат рукотворный Ext/ParentConfigurations.bin, и платформа отвечает «Ошибка
формата потока», тогда как настоящий bin из типовой грузится, а выгрузка без bin — тоже. Попытка
пересобрать фикстуры полноценными объектами вылечила XML, но не bin, и была откачена.

Документация: в README добавлена таблица маршрутов платформенной проверки и правила появления
configDir — раньше это знание жило только в коде, и «зелёный» отчёт не отличался от «проверенного».
В docs/1c-support-state-spec.md записано, что по описанной грамматике bin можно читать, но нельзя
собрать файл, который примет платформа.

Полный прогон verify-snapshots до и после правки даёт одинаковый состав: 461 passed, 6 failed,
30 skipped из 497 — новых падений нет. Тесты: 871/879, интеграционные 13/13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:04:55 +03:00
Nick ShirokovandClaude Opus 5 333bcad938 fix(meta-validate): batch выполняется в одном процессе вместо процесса на объект
py-порт в batch-режиме запускал сам себя через subprocess на каждый объект, и на разбор в единицы
миллисекунд приходилось ~146 мс старта интерпретатора с импортом lxml. На корпусном прогоне это
давало 92 мс на объект против 40 мс у PS — при том, что обычно py быстрее в разы. Причина
асимметрии: PS вызывает себя через & в том же процессе, платя свой дорогой старт один раз на батч.

Теперь py делает то же самое: файл компилируется один раз и выполняется в свежем globals() на
каждый объект. Изоляция состояния получается той же, что у & в PowerShell (script-переменные не
переживают вызов), а старт и компиляция платятся однократно.

Прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов): 274 c против ~2900 c, вывод побайтово тот же
и совпадает с PS-портом строка в строку. Батч с битым объектом в середине по-прежнему доходит до
конца и возвращает ненулевой код, -OutFile пишет те же файлы на объект.

Заодно уточнён комментарий про кэш состава конфигурации: «порождает процесс на объект» было верно
только для py, тогда как в PS кэш не переживает объект из-за новой области видимости.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 15:59:57 +03:00
Nick Shirokov 18510d65d5 merge: meta-validate — устранение ложных срабатываний на типовых конфигурациях (246 ERROR и 1662 WARN → 0 и 1) 2026-08-22 14:14:11 +03:00
Nick ShirokovandClaude Opus 5 d6f3b69810 fix(tests): причины исключений в реестре check-type-maps
Оба исключения по IntegrationService стояли с пустой причиной, а такое гард лишь помечает как долг
и продолжает. Именно так вид метаданных и выпал незамеченным: meta-validate не знал его вовсе и
отвергал штатный объект типовой конфигурации.

Причины проставлены по смыслу своих записей: meta-compile такие объекты не создаёт, meta-validate
проверяет их структурно. Долг исключений без причины закрыт.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:00:37 +03:00
Nick ShirokovandClaude Opus 5 f694c48835 fix(meta-validate): ложные срабатывания на типовых конфигурациях
Прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов) давал 246 ERROR и 1662 WARN на выгрузках,
которые платформа принимает без единой ошибки, — то есть ложным был весь вывод валидатора.
Две группы при этом роняли exit=1 на типовой конфигурации: то же, из-за чего заводились #79 и #84,
только зеркально.

Разобрано платформой на стенде 8.3.24.1691, каждый случай — копия эталона с одной мутацией:

- пустой <Type/> у реквизита — это тип «Произвольный»; загрузка проходит, round-trip сохраняет
  конструкцию без изменений. Было ERROR (245 раз), стало молчание;
- блока Type нет вовсе — загрузка тоже проходит, но платформа молча подставляет тип нового
  реквизита, Строка(10). Замысел теряется, загрузка нет: ERROR заменён на WARN;
- IntegrationService и Bot отсутствовали в списках видов метаданных (в спеке 45 типов, навык знал
  43) — штатный объект типовой давал «Unrecognized metadata type» и exit=1;
- HierarchyType платформа пишет всегда и при выключенной иерархии игнорирует: из 1254 срабатываний
  1253 приходились на её собственный дефолт. Предупреждаем только о явно заданном другом типе;
- источник подписки задают и наборами типов (<v8:TypeSet>), а состав журнала — элементами
  <xr:Item>; проверки искали только <v8:Type> и потому не видели ни одной такой подписки и ни
  одного журнала. Зато реально пустой источник и пустой состав платформа отвергает
  («Источник событий должен быть задан», «Для журнала не заданы регистрируемые документы») —
  там уровень поднят с WARN до ERROR;
- сигнатура процедуры переносится на несколько строк, и Экспорт оказывается не на строке с именем;
  регулярка требовала одну строку. Модуль поставщика в двоичном виде (Module.bin) больше не даёт
  предупреждения «файл не найден» — текста там нет by design;
- ExchangeDate — легаси-реквизит планов обмена, поддержан в meta-compile и meta-decompile:
  добавлен как допустимый, но не обязательный.

После правки корпус даёт 0 ERROR и 1 WARN — тот самый единственный недефолтный тип иерархии.
Объём проверки не потерян: объектов провалидировано 33 895 против 33 894.

Восемь новых кейсов держат обе стороны каждой правки: валидная конструкция молчит, настоящая
ошибка по-прежнему ловится.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:00:22 +03:00
Nick Shirokov 12aecbf50b merge: meta-validate #84 — состав конфигурации решает уровень ссылки; form-add — нотация и приём обиходных написаний 2026-08-22 13:03:59 +03:00
Nick ShirokovandClaude Opus 5 aafc680ad5 fix(form-add): нотация параметров в документации и приём обиходных написаний
Таблица параметров и примеры в SKILL.md описывали слэш-нотацию (--set-default, --purpose),
а скрипт принимает -SetDefault и -Purpose. Модель, читающая таблицу, получала отказ биндинга
на первом же вызове. Документация приведена к исполняемой форме — она теперь одна.

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

Порты выровнены: PS принимает это через алиас параметра, py — через перечисление опций;
проверено девятью написаниями на обоих, результат совпадает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:52:31 +03:00
Nick ShirokovandClaude Opus 5 9efa336fc1 test(meta-validate): кейсы valid-constant и valid-info-register ссылались на несуществующий справочник
Оба назывались «корректными», но объект создавался со ссылкой на CatalogRef.Валюты, которого
в конфигурации нет — платформа такую выгрузку не принимает. Пока уровень был WARN, кейсы
проходили и закрепляли неверное: «валидный» объект с висячей ссылкой.

Справочник добавлен в preRun, эталоны пересняты и проверены платформой.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:34:56 +03:00
Nick ShirokovandClaude Opus 5 7b40eb4c0b fix(meta-validate): состав конфигурации, а не наличие файла, решает уровень ссылки (#84)
Проверки 16 и 20 выбирали уровень по наличию файла, тогда как платформа судит по составу
конфигурации. Из-за этого валидатор ошибался в обе стороны: пропускал ссылку на форму
объекта, которого в конфигурации нет (WARN, exit=0 — платформа такую выгрузку не грузит),
и ложно отказывал на фрагменте частичной выгрузки, где файла формы нет намеренно.

Единое правило: ChildObjects отвечает за существование объекта, наличие файла — только за
полноту выгрузки. Нет в составе -> ERROR, в составе есть, а файла нет -> WARN. Уровень для
фрагмента остаётся мягким сознательно: один и тот же фрагмент грузится или нет в зависимости
от состояния конфигурации БД, которого валидатору не видно.

Матрица снята на платформе 8.3.24.1691 (полная и частичная загрузка, семь ситуаций).
Прогноз про загрузку убран из текста WARN — там он не гарантирован.

Состав читается поиском подстроки в границах секции ChildObjects, без разбора XML:
batch-режим порождает процесс на объект, и полная карта оплачивалась бы каждым объектом.

Корпусный прогон по acc_8.3.24 и erp_8.3.24 (33 895 объектов): расхождений до/после нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:34:45 +03:00
Nick ShirokovandClaude Opus 5 0a0322faab fix(skills): пишущие навыки — запрет позиционного связывания параметров
Значение с пробелом без кавычек разрывалось на токены, и лишние молча растекались по свободным
позиционным параметрам. На документированной форме вызова (powershell -NoProfile -File):

  form-compile.ps1 -JsonPath a.json -OutputPath b.xml -Purpose Форма списка
  → Purpose=[Форма] ObjectPath=[списка], ошибки НЕТ

Навык отрабатывал успешно с усечённым значением; реальный триггер — путь с пробелом в -EmitDsl
или -ObjectPath, результат уезжал не туда молча. У навыков с парой -DefinitionFile/-Operation
осколок уезжал в -DefinitionFile, и навык жаловался на параметр, которого в команде не было.

Расхождение портов: py на том же вызове отвечает «unrecognized arguments: списка» — там все
аргументы объявлены опциями. Правка выравнивает PS по py, python не менялся.

41 навык получает [CmdletBinding(PositionalBinding=$false)] и ни одного позиционного параметра.
Без Position=0 (в отличие от read-only навыков): «главный» параметр механически не выводится —
у meta-edit первым объявлен -DefinitionFile, а путь к объекту вторым, — а ошибка выбора тестами
не ловится, раннер передаёт только именованные флаги. Позиционной формы вызова нет ни в одной из
140 строк SKILL.md, внутренние вызовы навыков друг из друга тоже именованные.

check-positional-binding.mjs расширен на пишущие навыки статической проверкой. Поведенческая
(канареечный файл) остаётся только у read-only: у web-stop и db-run все параметры
Mandatory=$false, связывание прошло бы и тело выполнилось — гард остановил бы Apache и запустил 1С.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:24:58 +03:00
Nick ShirokovandClaude Opus 5 77ee8f1047 fix(skills): «файл не найден» на входном JSON — одинаково во всех навыках
Часть навыков проверяла путь ко входному JSON сама и отвечала внятной строкой, часть — нет и
роняла дамп: FileNotFoundError с traceback в py, MethodInvocationException с CategoryInfo в PS1.
Один и тот же промах давал разный ответ в зависимости от навыка, а у form-edit — ещё и от порта.

Проверка перенесена в тело семьи read_json_file / Read-JsonInputFile: пол одинаков везде по
построению, и новые навыки получают его вместе с функцией. Навыки со своей проверкой срабатывают
раньше и сохраняют прежний текст, поэтому существующие кейсы не двигаются.

Заодно каталог вместо файла: раньше IsADirectoryError / отказ доступа, теперь
«Expected a JSON file, got a directory».

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:46:20 +03:00
Nick ShirokovandClaude Opus 5 229e66b907 fix(skills): чтение входного JSON — кодировка из BOM, эхо только для inline-значения (#80)
Этажом ниже разбора, на чтении файла, жил тот же класс дефектов в худшей форме. Файл в cp1251
с кириллицей: PS1 `Get-Content -Encoding UTF8` менял имя на 12 символов U+FFFD, JSON после этого
разбирался УСПЕШНО, и навык создавал объект с именем из «замен» — молча. Py-порт на том же файле
падал traceback-ом. Файл в UTF-16 давал ту же пару: traceback против ложного «JSON must have
'type' field».

Новая семья read_json_file / Read-JsonInputFile: кодировка берётся из BOM (UTF-8, UTF-16 LE/BE),
без BOM — строгий UTF-8, при провале сообщение называет файл и байт. Кодовую страницу не
подбираем: угаданное имя уехало бы в метаданные так же молча.

Эхо полученного значения печатается теперь только для inline-входа. Для файла оно показывало
первые 60 символов первой строки независимо от того, что ошибка на 120-й, и спорило с позицией
от парсера.

Заодно уравнен пустой вход: PS 5.1 на пустой строке отдаёт $null, а не ошибку, и навык уходил
дальше с $null, тогда как py-порт падал.

Раннеру добавлен ключ inputEncoding (utf-16le / utf-16be / cp1251) — иначе кейс про кодировку
не выразить, writeFileSync пишет только UTF-8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 16:41:54 +03:00
Nick ShirokovandClaude Opus 5 0770889e47 fix(skills): усечение эха в сообщении о разборе JSON не маскируется под данные
Эхо полученного значения обрезалось многоточием, и обрыв читался как обрыв самих данных:
агент шёл дописывать «неполный» файл, хотя ошибка была на 57-й строке. Плюс усечение спорило
с позицией от парсера — два сигнала об одном месте, которые не сходятся.

Теперь усечение называет себя: got (first 60 chars, whitespace collapsed). Число символов
не печатаем — после схлопывания пробелов оно не сходилось со смещением из сообщения парсера.
На коротком входе, ради которого эхо и заводилось (съеденные оболочкой кавычки дают
{group:X,commands:[Y]} — 22 символа), усечения нет вовсе.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:40:40 +03:00
Nick ShirokovandClaude Opus 5 a44a29a7d8 fix(skills): внятная диагностика разбора JSON вместо стектрейса (#80)
Неверный входной JSON ронял скрипты необработанным исключением: PS1 отдавал дамп
ConvertFrom-Json с CategoryInfo, py-порт — traceback с внутренностями json/decoder.py.
Имя файла в сообщении не фигурировало, а для полиморфного -Value не было видно, какую
форму ждёт операция.

Общий хелпер ConvertFrom-JsonInput / parse_json_input в 12 навыках × 2 порта (24 места),
зарегистрирован семьёй в check-inline-drift.mjs — копии держит гард. Сообщение в одну
строку: ожидаемая форма, полученное значение, текст парсера в скобках.

Эхо полученного значения нужно потому, что съеденные оболочкой кавычки дают почти тот же
JSON ({group:X} вместо {"group":"X"}), и без него агент считает свой вызов верным. PS 5.1
печатает лишь огрызок и локализованно, Python — только номер колонки.

Возврат в PS1 через Write-Output -NoEnumerate: вынос разбора в функцию добавляет второй
анруллинг, и одноэлементный JSON-массив стал бы скаляром. Импорты внутри тела py-хелпера —
skd-decompile импортирует json локально как _json, а тело семьи обязано быть одинаковым.

В раннер добавлен inputRaw (запись входного файла дословно): через case.input битый JSON
невыразим, JSON.stringify всегда даёт валидный документ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 15:25:54 +03:00
Nick ShirokovandClaude Opus 5 53fc16d2f1 fix(form-add): вид объекта берём из структуры документа, а не поиском по имени
Вид искался как первое совпадение //md:<Вид> по всему файлу и потому зависел
от порядка перебора видов. У бизнес-процесса есть свойство <Task>, и объект
определялся как задача, после чего имя не находилось вовсе: «Не удалось
определить имя объекта из Properties/Name». Прежний упорядоченный список видов
это маскировал — в нём BusinessProcess стоял раньше Task.

Теперь вид — первый элемент-потомок MetaDataObject, и неподдерживаемый вид
отвергается по имени, а не проваливается в поиск имени объекта.

Нашлось платформенной проверкой всех видов разом: собрана конфигурация с
формой каждого поддержанного вида (15 видов, 43 формы) и загружена в 1С.

Заодно покрытие назначений доведено до полного (добавились FolderChoice, Load,
Record), а мелкие кейсы на один объект объединены: несколько назначений в одном
кейсе, эталон показывает все слоты сразу.

Гард дополнен инвариантом «ровно одна основная форма на вид» и его паритетом
между портами — именно им пользуется умолчание -Purpose.

Инструкция: в таблице параметров умолчание Purpose исправлено с Object на
основную форму вида.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:55:30 +03:00
Nick ShirokovandClaude Opus 5 0f7745af4b feat(tests): verify-snapshots --strict — пропуск считается падением
Пропуск по версии платформы гасит проверку: кривую фикстуру он тоже спрячет,
если её формат объявлен выше платформы стенда. Обычный прогон теперь прямо
говорит, что пропуски не проверены, а на стенде с нужными платформами
--strict не оставляет непроверенных кейсов.

Опечатка в версии формата при этом не маскируется в любом режиме: версии вне
лестницы в таблице соответствия нет, пропуск не срабатывает, и кейс доезжает
до платформы.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:15:55 +03:00
Nick ShirokovandClaude Opus 5 64dd135882 fix(tests): verify-snapshots — гейт по версии формата, а не только по совместимости
Пропуск кейса решался только режимом совместимости. Кейс на формате 2.21 с
совместимостью 8.3.27 на платформе 8.3.24 пропускался, а на 8.3.27 доходил до
загрузки и падал: формат 2.21 требует 8.5. На стендах с промежуточной
платформой (в том числе macOS с 8.3.27) это давало постоянный ложный красный.

Эталон соответствия «формат → платформа» — таблица «Лестница версий» из
docs/1c-configuration-spec.md, как и в check-format-versions.mjs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 12:09:52 +03:00
Nick ShirokovandClaude Opus 5 179f5334d9 test(skills): кейсы form-add по видам и назначениям форм
Десять кейсов на то, что раньше не проверялось вовсе: журнал документов
(список и отказ для формы объекта), перечисление, план видов расчёта, наборы
записей регистров, форма группы справочника, произвольная форма, формы
хранилища настроек и отказ для константы. Дыра в покрытии и позволила дефекту
дожить: журналов и регистров среди кейсов не было.

Пять чужих кейсов (form-compile-from-object, cfe-validate) звали form-add без
-Purpose для регистров и полагались на умолчание Object — назначение им
проставлено явно. Их эталоны пересняты: слот основной формы, который раньше
молча оставался пустым, теперь заполняется. Эталоны проверены загрузкой в 1С.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:51:24 +03:00
Nick ShirokovandClaude Opus 5 399e02148d fix(tests): verify-snapshots понимает шаг deletePath
Шаг preRun deletePath поддерживал только runner.mjs. В платформенной проверке
такой шаг проваливался в ветку запуска скрипта и падал на step.script.split,
то есть кейс не проверялся вовсе, а выглядел упавшим инструментом.

Шаг без script/writeFile/deletePath теперь отвергается с внятным текстом, а не
падает на undefined.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:51:07 +03:00
Nick ShirokovandClaude Opus 5 aad9956e12 test(skills): гард на таблицу назначений форм
Держит три инварианта form-add: состав видов сходится со спецификацией,
свойство «основная форма» у назначения есть у этого вида по спецификации,
таблицы PS и PY совпадают между собой.

Проверено на регрессии: если вернуть журналу DefaultListForm, гард падает
дважды — по спецификации и по паритету портов.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:51:06 +03:00
Nick ShirokovandClaude Opus 5 45353afc51 fix(form-add): таблица видов вместо четырёх разошедшихся списков
Для журнала документов навык писал в форму битый главный реквизит —
cfg:.Журнал с пустым префиксом. Платформа такую выгрузку не принимает
(«Исключение XDTO при чтении файла»), а навык рапортовал успех.

Причина не в забытой строке: знание о видах было размазано по четырём
независимым спискам (поддерживаемые типы, объектные, обработко-подобные,
карта типов реквизита) плюс switch по свойствам DefaultForm. DocumentJournal
попал в поддерживаемые, но не в карту типов — и подстановка $null дала точку
без типа. Теперь одна плоская таблица: строка на (вид, назначение), в ней тип
главного реквизита и свойство «основная форма».

Того же корня и починено заодно:
- Purpose=Object принимался для видов, у которых формы объекта не бывает
  (журнал, перечисление, регистры накопления и бухгалтерии);
- свойство «основная форма» выбиралось без учёта вида: журналу писался
  DefaultListForm, которого у него нет, и слот молча не находился;
- AccumulationRegister описывался как RecordSet, хотя у регистра такого
  главного реквизита не бывает, а слота под него нет вовсе;
- ChartOfCalculationTypes отвергался, хотя формы объекта у него обычные.

Новое: назначения Folder и FolderChoice (формы группы), RecordSet для всех
регистров, Save/Load для хранилища настроек и Custom — произвольная форма без
главного реквизита, самая частая в типовых после объектной. Умолчание Purpose
теперь основная форма вида, а не жёсткий Object: для регистра сведений это
форма записи, для журнала — форма списка.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:50:51 +03:00
Nick ShirokovandClaude Opus 5 e987eeece2 docs(form-spec): формы по видам объектов — полные таблицы
Таблица «Свойства DefaultForm по типам объектов» была неполной: в ней не было
журнала документов, перечисления, регистров накопления/бухгалтерии/расчёта,
плана видов расчёта, критерия отбора, хранилища настроек и внешних объектов.
Сверять код было не с чем, и код оказался неполон ровно так же.

Добавлена вторая таблица — главный реквизит формы по назначению. Она разводит
случаи, которые легко перепутать: форма группы это форма ОБЪЕКТА группы, а
форма выбора группы — динамический список; у формы набора записей свойства,
чтобы назначить её основной, нет вовсе; форма без главного реквизита
(«произвольная») — обычное дело, а не полуфабрикат.

Источник — выгрузки типовых: слоты и типы главного реквизита сняты по
конфигурациям acc, erp, ut, unf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 10:49:31 +03:00
Nick ShirokovandClaude Opus 5 bfef1b8a0c test(skills): кейсы на висячие ссылки на форму
form-remove: удаление заблокировано ссылками; -Force чистит слот реквизита,
ChoiceForm внутри формы и элемент начальной страницы; регресс на матч
ссылки — удаление своей формы не трогает ссылку на чужую форму с тем же
именем.

meta-remove: общая форма, на которую смотрит Configuration.xml, — ссылка
попадает в отчёт, -Force обнуляет слот.

meta-validate: незарегистрированная форма и отсутствующий файл формы дают
ошибку, GUID в слоте — нет.

Эталоны проверены платформой: verify-snapshots поднимает базу и грузит
результат.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 20:12:36 +03:00
Nick ShirokovandClaude Opus 5 da166845cb fix(tests): кейсы на fixture-фикстурах попадали мимо платформенной проверки
verify-snapshots отбирал кейсы по правилу «нет input, preRun и params →
read-only, пропускаем». Кейс на `setup: fixture:` под него подходил, хотя
навык правит скопированную фикстуру, — и молча выпадал из проверки, ради
которой инструмент и существует. Отчёт при этом оставался зелёным.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 20:11:18 +03:00
Nick ShirokovandClaude Opus 5 d791a00419 feat(meta-validate): ссылка на несуществующую форму — ошибка
Непустое Default*Form / Auxiliary*Form / ChoiceForm могло указывать на
форму, которой нет, а валидатор отвечал OK. Платформа такую выгрузку не
принимает: «Неизвестный объект метаданных».

Новая проверка разбирает обе формы записи. Для своего объекта проверяется
регистрация формы в ChildObjects — это работает и на одиночном файле, без
конфигурации; для общей формы и чужого объекта (форма журнала документов в
слоте документа) нужна конфигурация, и проверяется наличие файла формы.
Отдельно ловится случай, когда форма зарегистрирована, а файла нет.

Значением слота бывает GUID — такие пропускаются, как и в cf-validate.
Заимствованные объекты расширения пропускаются целиком: их формы живут в
основной конфигурации.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 20:11:18 +03:00
Nick ShirokovandClaude Opus 5 cb46348c8b fix(meta-remove): Configuration.xml перестал быть слепой зоной скана
Файл целиком исключался из проверки ссылок как «чистится автоматически»,
хотя автоматически в нём чистится только ChildObjects. Ссылки уровня
конфигурации (DefaultReportForm и соседи) после удаления общей формы
оставались висеть и в отчёте не упоминались.

Теперь такие слоты видны в списке ссылок, а с -Force очищаются — наравне со
слотами других объектов и элементами начальной страницы. Ссылки на типы и
вызовы в .bsl по-прежнему не трогаем: чем их заменить, неизвестно.

Два полных обхода конфигурации свёрнуты в один, Get-ChildItem -Recurse
заменён на Directory.EnumerateFiles: на большой конфигурации проход занимал
180 с против 47 с, а проходов было два.

Общие паттерны ищутся в тексте без form-слотов — иначе файл со слотом
попадал в список дважды, а пропуск файла целиком спрятал бы настоящую
ссылку рядом со слотом.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 20:11:02 +03:00
Nick ShirokovandClaude Opus 5 93d9511a46 fix(form-remove): проверка ссылок перед удалением формы
Форма могла удаляться, оставляя ссылки на себя в других объектах: платформа
затем отвергала загрузку с «Неизвестный объект метаданных». Навык правил
только файл своего объекта и про остальную конфигурацию не знал.

Теперь перед удалением конфигурация сканируется на ссылки на эту форму
(слоты Default*/Auxiliary*Form и ChoiceForm, ChoiceForm и SettingsStorage
внутри форм, элементы начальной страницы). Есть ссылки — удаление
останавливается со списком мест; с -Force форма удаляется, а ссылки
очищаются: слот пустеет, элемент начальной страницы вырезается.

Заодно починен матч ссылки: сравнение шло по хвосту «Form.<Имя>» без вида и
объекта, поэтому при удалении своей ФормаСписка обнулялась и ссылка на
DocumentJournal.Ж.Form.ФормаСписка в том же файле.

Сравнение путей переведено на длинную форму: Resolve-Path отдаёт короткое имя
8.3, перечисление файлов — длинное, из-за чего собственный файл объекта не
исключался из скана.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 20:10:50 +03:00
Nick ShirokovandClaude Opus 5 97f1a0d4e1 fix(switch): навыки без привязки к Claude — порт на другие платформы не уносит хвосты
Три файла из 77 навыков нарушали соглашение о переносимости, и сборка
Codex-порта уносила их as is (#75).

Функциональный из трёх один: json-dsl.md писал путь к скрипту литералом
.claude/skills/meta-edit/..., то есть на любой платформе кроме claude-code
команда указывала в несуществующий каталог и молча не находила скрипт.
Заменено на ${CLAUDE_SKILL_DIR}/ — дальше путь разворачивает switch.py.

Ещё два — тексты: проза с названием агента в mxl-compile (заодно избыточная:
у соседних *-compile это одна строка «принимает X → генерирует Y») и
.claude-путь в комментарии state.mjs.

От регресса — check-agent-portability.mjs: вырезает разрешённый
${CLAUDE_SKILL_DIR}/ и падает на любом оставшемся claude. Ловит и плейсхолдер
без завершающего слеша: switch.py разворачивает только форму со слешем,
остальные проехали бы насквозь. Сторонний node_modules исключён — playwright
содержит ClaudeGenerator и .claude/agents, переписывать его нельзя.

switch.py не менялся: ни одного плейсхолдера и ни одного вызова скрипта вне
*.md верхнего уровня навыка нет, рекурсия не нужна.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 16:35:07 +03:00
Nick ShirokovandClaude Opus 5 9475aa6171 fix(skd-edit): не срезать значимое локальное объявление xmlns при пересборке поля
При пересборке поля навык сохранял <valueType> через OuterXml/tostring и срезал
из фрагмента ВСЕ объявления namespace. Для префиксов, объявленных в корне схемы,
это верно — DOM переобъявляет их на фрагменте избыточно. Но объявление, которого
в корне нет, значимо: xmlns:d5p1 живёт только локально на <v8:Type> и связывает
префикс, которым квалифицировано значение узла (d5p1:CatalogRef.X). Срез оставлял
висячий префикс — XML остаётся well-formed (префикс в тексте парсер не проверяет),
а 1С отвергает тип. Радиус — modify-field.

Слепой срез заменён на Strip-InheritedXmlns / strip_inherited_xmlns: объявление
выбрасывается, только если корень объявляет тот же префикс с тем же URI. Карта
корня снимается с уже имевшегося RawRootOpening; разделитель \s+, поскольку
корневой тег бывает разложен по строкам. Префикс и URI сравниваются ординально —
в PS -eq и @{} регистронезависимы. Подставлено во все пять точек среза каждого
порта; для корневых префиксов поведение не меняется.

Кейс modify-field-ref-type: modify-field по ref-полю не был покрыт. Проверено,
что кейс краснеет до правки — и по снэпшоту, и по skd-validate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 14:27:03 +03:00
Nick ShirokovandClaude Opus 5 b6857c9406 docs(tests): check-positional-binding в таблице гардов
Гард добавлен в check-all.mjs прошлой волной, а в таблицу README не попал.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:52:40 +03:00
Nick ShirokovandClaude Opus 5 1c63d9ddcc refactor(skills): регистрация в ChildObjects — именованная функция под анти-дрейфом
Три навыка делали одну работу тремя инлайновыми копиями в main(), и одна копия
молча разъехалась: role-compile.py рапортовал об успехе, не тронув файл. Это
ровно тот класс, от которого заведён check-inline-drift.mjs, но в реестр
регистрацию было не записать — он работает по именованным функциям.

Регистрация вынесена в Register-InChildObjects / register_in_childobjects
с контрактом (родительский XML, тег родителя, тег потомка, имя) и исходом
added | already | no-childobj | no-config. Печать сообщений осталась на
вызывающей стороне: тексты у навыков разные, и сведение их меняло бы вывод.

Эталон — meta-compile, форма функции задана на .ps1. role-compile берёт его
тело копией, и гард это подтверждает. subsystem-compile идёт отдельным
вариантом с обоснованием: родителем бывает вложенный Subsystem.xml
произвольной глубины, поэтому отступ он берёт из документа, а запись
дописывает в конец блока — фиксированные три табуляции там неверны,
и группировать по типу нечего.

Вынос поведенчески нейтрален: 837 кейсов зелёные на обоих рантаймах,
снэпшоты не дрейфовали, матрица «навык × порт × изломанная форма
Configuration.xml» даёт те же исходы, а порты — байт-в-байт одинаковый файл.
Попутно из subsystem-compile.py убран мёртвый ET.SubElement перед pass:
дерево мутировалось и выбрасывалось, правка идёт по сырому тексту.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:52:40 +03:00
2b0ac3ea75 fix(role-compile): py-порт рапортовал об успешной регистрации, не тронув Configuration.xml
Configuration.xml без <ChildObjects> и с самозакрытым <ChildObjects/> — в обеих
формах py печатал «<Role>X</Role> added to ChildObjects», а файл оставался
байт-в-байт прежним. Модель считала роль включённой в состав конфигурации,
хотя её там не было. PS-порт обе формы обрабатывал верно: самозакрытый тег
раскрывал, при отсутствующем честно предупреждал.

Ветвление перенесено из meta-compile.py, где та же задача давно решена: есть
блок ChildObjects — вставляем внутрь; есть самозакрытый тег — раскрываем первой
записью; нет ни того ни другого — исход no-childobj и файл не трогаем. Ветка
вывода no-childobj в py уже была и до сих пор оставалась недостижимой.

Порядок вставки и способ правки файла не менялись: проверка на боевых выгрузках
(acc/erp, CRLF и LF) показала, что действующая реализация даёт дельту ровно
в одну вставленную строку, а платформа порядок ChildObjects не нормализует.

Кейсы на обе формы. На неисправленном коде оба падают на python и проходят
на powershell. Проверка «успех не объявлен» опирается на stdoutNotContains
и снапшот: Write-Warning в PS 5.1 идёт в stdout с локализованным префиксом,
а py пишет в stderr, поэтому портируемого stderrContains тут быть не может.

Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 11:32:46 +03:00
Nick ShirokovandClaude Opus 5 1cfc76da2e fix(tests): гард позиционного связывания не зеленеет на незапустившемся интерпретаторе
Поведенческая секция считала успехом любой ненулевой код возврата, а не
запустившийся интерпретатор даёт status: null — проверка проходила вакуумно,
ничего не проверив. Теперь spawn-ошибка сама является нарушением.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:13:24 +03:00
Nick ShirokovandClaude Opus 5 9e883d7f4f fix(skills): в read-only навыках позиционным остаётся только путь ко входу
Тот же класс дефекта, что закрыт в role-validate, был возможен во всей семье
*-info / *-validate / cfe-diff: лишний позиционный аргумент связывается со
следующим параметром по порядку объявления. Остальные скрипты спасала
случайность типов — на втором слоте оказался [int]MaxErrors/Limit или строка
с [ValidateSet], которые падают на конвертации. Любая перестановка параметров
в param() открывала дыру заново, а снапшот-тесты сверяют вывод, а не связывание.

Во всех 21 скрипте объявлен [CmdletBinding(PositionalBinding=$false)], входной
путь помечен Position=0. Инвариант: анализирующий навык не пишет в файл, который
ему не назвали по имени. Документированные вызовы в репозитории все именованные,
поведение навыков не меняется — 835 кейсов зелёные на обоих рантаймах без дрейфа
снэпшотов.

Гард check-positional-binding.mjs держит инвариант статически (объявление и
единственный Position=0; в py-порте все add_argument именованные) и поведенчески
(лишний позиционный аргумент роняет вызов, канареечный файл остаётся цел).
Семья определяется по имени навыка, поэтому новый *-info/*-validate попадает под
гард сам. На состоянии до правки гард падает на role-validate обеими проверками.

Версии обоих портов подняты синхронно; из шапок убраны чужие хвосты.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:59:52 +03:00
83d63fac47 fix(role-validate): лишний позиционный аргумент затирал файл валидационным отчётом
Вызов role-validate.ps1 с двумя позиционными аргументами — естественная ошибка
по документации role-compile/SKILL.md, где значилось
`/role-validate <RightsPath> [MetadataPath]`, хотя параметра MetadataPath у
навыка нет. Второй аргумент связывался с -OutFile, и скрипт перезаписывал
указанный файл (например, Roles/Имя.xml) текстом отчёта — исходник роли терялся.

PositionalBinding=$false оставляет позиционным только путь ко входу; -OutFile
передаётся строго по имени. Справка приведена к факту. Python-порт уже иммунен:
argparse принимает только именованные параметры.

Co-Authored-By: Sergei Pleshanov <2357qwr@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 14:59:37 +03:00
Nick ShirokovandClaude Opus 5 77cef101f7 test(cfe-borrow): причина пропуска на 8.3.27.1688 — с механизмом, а не с наблюдением
Разбор довели до механизма. Сборка не разрешает поля динамического списка заимствованной
формы в авто-режиме (ManualQuery=false + MainTable). Проверено и отброшено: порт скрипта,
ОС, имена латиницей, заимствованность таблицы, порядок загрузки, BaseForm/UseAlways/Settings,
полный эталонный вид с ListSettings и Список.Ref. Проходит только ручной запрос, что меняет
семантику списка.

Решающий факт: ту же базу, где расширение валидно загружено платформой 8.3.24, сборка 1688
выгружает с битыми путями (~Список.Склад) — то есть не разрешает их даже из собственного
хранилища. Наш эмит при этом совпадает с эталоном Конфигуратора (upload/cfe/ut/ФормаСписка2).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:22:07 +03:00
Nick ShirokovandClaude Opus 5 9dcaa9b6b1 fix(tests): пропуск платформенной верификации, привязанный к сборке платформы
Кейсы form-main-attr-dynamic-list и form-main-attr-list-query падали на маке. Диагностика:
дело не в порте (Windows+python проходит) и не в ОС (мак на 8.3.24.1691 проходит) — отвергает
конкретная сборка 8.3.27.1688. Решающий опыт: расширение, загруженное на 8.3.24 и выгруженное
самой платформой, сборка 1688 тоже отвергает с теми же «Неверный путь к данным» — то есть она
не принимает собственный артефакт Конфигуратора, а не наш эмит.

Глухой skipPlatformVerify снял бы проверку и на стендах, где она работает, поэтому он теперь
принимает объект {reason, platforms:[…]} — пропуск только на перечисленных сборках.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 23:13:44 +03:00
Nick ShirokovandClaude Opus 5 e9a40a1499 fix(tests): verify-snapshots помечает пропуски в REPORT.md, а не считает их падениями
Консольная сводка пропуски различала, а файл отчёта нет: статус считался как
`passed ? OK : FAIL`, поэтому пропущенный кейс печатался как FAIL, попадал в счётчик
падений и в раздел Findings. Отчёт читают глазами и по нему решают, есть ли проблема, —
расхождение с консолью здесь дороже всего.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:55:17 +03:00
Nick ShirokovandClaude Opus 5 eda24ae1e7 fix(tests): verify-snapshots читает оба потока при падении навыка
ps1 печатает строку ошибки в stdout, py — в stderr, а лог платформы оба кладут в stdout.
Верификатор брал `stderr || stdout`, поэтому на python-порте лог терялся целиком: падение
выглядело как «Error loading configuration (code: 1)» без причины, и распознать по нему
неподдерживаемую версию формата было нечем — кейсы format-221 на маке падали вместо пропуска.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:35:17 +03:00
Nick ShirokovandClaude Opus 5 54ee6efe93 fix(tests): каталог расширения в кейсах cfe-* — cfe вместо ext (closes #74)
Кейсы клали расширение в `ext`, а cf-init создаёт в корне конфигурации платформенный `Ext/`.
На регистронезависимой ФС (Windows, APFS) это один каталог: эталоны годами фиксировали
слипшееся дерево — в snapshots/<case>/ext/ лежали вперемешку ClientApplicationInterface.xml
конфигурации и Configuration.xml расширения, а имя каталога в коммите зависело от того, кто
создал его первым (ext у одних кейсов, Ext у других при одинаковом входе). На Linux те же
кейсы дали бы два каталога и другой снапшот.

Переименование механическое: 64 кейса пяти навыков, 349 файлов эталонов переехали без
единой правки содержимого. Заодно переписан регистр 21 пути в индексе — git с core.ignorecase
не показывал, что файл конфигурации числится под ext/, а лежит в Ext/.

Гард в runner.mjs: имя каталога расширения из params, совпавшее без учёта регистра с тем, что
уже есть в фикстуре, валит кейс с объяснением. Проверено, что старое имя теперь не проходит.

verify-snapshots.mjs чинится тем же заходом — там нашлись две дыры:
- каталог расширения был захардкожен как 'ext', поэтому кейсы с другим именем МОЛЧА теряли
  вторую половину проверки: блок загрузки расширения обходился по existsSync и в отчёте это
  выглядело успехом;
- setup брался только из _skill.json, и кейсы с гейтом по версии формата (218/220/221)
  проверялись на конфигурации 2.17 — то есть платформа не видела ровно того поведения, ради
  которого кейс написан.
Плюс два honest-skip вместо ложных падений: режим совместимости выше платформы и формат
выгрузки новее платформы (второе читаем из ответа платформы, а не дублируем лестницу версий).

Проверено: регресс 827/0/8 (ps1) и 824/0/11 (py), гарды 5/5. Платформенная верификация:
cfe-borrow 26/28 на 8.3.27, cfe-patch-method 22/23, cfe-init 6/7, все 14 кейсов format-221
на 8.5 — остальное осознанные пропуски.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 22:20:28 +03:00
Nick ShirokovandClaude Opus 5 113dc9e928 fix(cfe-validate): проверка модулей затирала имя расширения в итоговой строке
Новая проверка (сверка «файл модуля ↔ пометка») переиспользовала $objName — ту же
переменную, из которой собирается финальная строка отчёта. В расширении с заимствованным
объектом ps1 печатал «Validation OK: Extension.Цены» — имя последнего проверенного объекта
вместо имени расширения. py-порт был прав, то есть порты разошлись молча.

Расхождение видно только на непустом расширении с чистым результатом, поэтому ни один
существующий кейс его не ловил: тесты сверяют снапшот, а итоговую строку — нет. Добавлены
утверждения на неё в кейсы valid, with-borrowed-object и module-state-file-without-flag;
проверено, что на старой версии скрипта они падают.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 21:20:26 +03:00
Nick ShirokovandClaude Opus 5 8fe727e32f feat(cfe-borrow,cfe-patch-method,cfe-validate): модули заимствованных объектов и пометка расширенного свойства
Объект заимствуют, чтобы дописать в него модуль, — теперь пустой файл модуля создаётся
навыком, а не руками. Тип с единственным модулем (CommonModule, HTTPService, WebService)
получает его без указаний, остальные — по -Module; -Module None отменяет. Существующий
файл не перезаписывается никогда.

Вместе с файлом проставляется <xr:PropertyState> со State=Extended. Замер по лестнице
платформ 8.3.20…8.5.1: элемент появился в формате 2.19 (8.3.26), ниже платформа молча
выбрасывает его при загрузке — отсюда гейт по версии формата. Имя свойства равно базовому
имени файла модуля, у заимствованной формы — Form. Правило владения одно: пометку ставит
тот, кто создал файл, поэтому её ставит и cfe-patch-method.

Заодно закрыта перезапись при повторном заимствовании: прямая ветка писала XML уже
заимствованного объекта начисто, унося собственные реквизиты расширения и регистрацию
формы, — молча, с успешным отчётом. Повторный вызов теперь безопасен и служит способом
дозаимствовать модуль.

cfe-validate сверяет «файл модуля ↔ пометка» в обе стороны (предупреждение: перекос
платформа принимает, но выгрузка Конфигуратора так не выглядит).

Проверено: полный регресс 827/0/8 (ps1) и 824/0/11 (py), гарды 5/5, сквозной раундтрип
на 8.3.26 и 8.3.27 — InternalInfo совпадает с выгрузкой платформы.

Закрывает #70. Разбор и эталоны выгрузки — Romandredan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 20:59:39 +03:00
Nick ShirokovandClaude Opus 5 89f65ca6a6 fix(tests): чинит фикстуру role-info/multiple-types и иконку скипа в verify
Кейс собирал через meta-compile регистр сведений без измерений и ресурсов —
платформа такую конфигурацию не принимает («Регистр без измерений, ресурсов и
реквизитов»), и verify-snapshots падал на нём на обеих ОС. Регистру добавлены
измерение и ресурс, эталон переснят: дельта только в XML регистра, права роли
не изменились.

Заодно построчный вывод verify рисовал скипы как ✗ (иконка выбиралась по
result.passed, который скип не выставляет): десяток «✗» под «0 failed» читался
как сломанный набор.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:44:14 +03:00
Nick ShirokovandClaude Opus 5 da51d8ab7d fix(tests): verify-snapshots скипает недоступную внешнюю выгрузку, а не падает
runner.mjs при отсутствии external-пути возвращает SKIP, verify-snapshots писал
ошибку. Путь к дампу ERP/БП машинозависим: на Mac mini корпуса нет, и два кейса
role-validate краснели при полностью исправном навыке. README предупреждает ровно
об этом: раннеры читают один DSL каждый своей реализацией.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:15:26 +03:00
Nick ShirokovandClaude Opus 5 261ea8c96d test: раскрытие сервисов в role-compile, права на корень и заимствованные объекты
Кейсы: раскрытие HTTP- и веб-сервиса, раскрытие сервиса интеграции, раскрытие
заимствованного сервиса, дедупликация раскрытого листа с явным, отказ без
метаданных, отказ основной роли расширения, право на корень сервиса в
role-validate и в cfe-validate.

Фикстуры HTTP- и веб-сервиса строит preRun через meta-compile — рукописный XML
платформа отвергала (у Operation обязателен замыкающий ChildObjects, у сервиса
нужен Ext/Module.bsl). Оба кейса проходят verify-snapshots: 1С принимает
конфигурацию с раскрытыми правами при полной загрузке.

Вручную написаны только фикстуры, которые навыками не собрать, — сервис
интеграции (meta-compile его не поддерживает) и расширение с заимствованным
сервисом (выгрузка Конфигуратора); оба с skipPlatformVerify и причиной.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:54:08 +03:00
Nick ShirokovandClaude Opus 5 9702a4c485 fix(cfe-validate): основные роли не дают прав на заимствованные объекты
Платформа: «Назначение прав доступа на заимствованные объекты основными ролями
в расширениях недопустимо». Роль вне DefaultRoles так делать вправе, поэтому
проверяются только основные. Ловится статически, а по симптому — отказ загрузки
расширения — причина не читается.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:19:55 +03:00
Nick ShirokovandClaude Opus 5 4be10e3d7f fix(role-validate): право на сервис целиком — ошибка
Роль с правом на `HTTPService.Имя` формально валидна и молча не работает:
платформа проверяет вложенный объект (метод шаблона URL, операцию, канал),
маршруты отвечают 403, и по симптому причина не читается. В Конфигураторе
галки на корне сервиса нет вовсе — это недостижимое состояние, а не стилистика.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:19:55 +03:00
Nick ShirokovandClaude Opus 5 756bd857dd fix(role-compile): права на сервис — на вложенных объектах, а не на корне
Короткая запись `HTTPService.X: Use` писала узел, которого платформа не видит:
роль проходила role-validate и cfe-validate, а маршруты отвечали 403. В корпусе
(acc/erp/ut/unf, 907 записей прав на сервисы) корневого узла нет ни разу — право
живёт на методе шаблона URL, операции, канале; в Конфигураторе галки на корне нет.

Короткая запись теперь раскрывается во все вложенные объекты сервиса по его
метаданным в OutputDir. Заимствованный сервис несёт заимствованные шаблоны и
методы, поэтому раскрытие даёт ровно тот набор, на который права дать можно.
Нет метаданных или вложенных объектов — отказ с подсказкой формата.

Плюс отказ, когда основная роль расширения (из DefaultRoles) даёт права на
заимствованный сервис: платформа такое расширение не принимает.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:19:43 +03:00
Nick ShirokovandClaude Opus 5 4967b752d7 docs(spec): тело XDTO-пакета — Ext/Package.bin, а не Package.xdto
Выгрузка конфигуратора кладёт модель пакета в Ext/Package.bin
(текстовый XML в UTF-8 с BOM). Файла Package.xdto не существует.

Closes #69

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:03:37 +03:00
1745 changed files with 77299 additions and 3535 deletions
+1
View File
@@ -34,6 +34,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cf-edit.ps1" -Confi
| `modify-property` | `Ключ=Значение` (batch `;;`) | Изменить свойство |
| `add-childObject` | `Type.Name` (batch `;;`) | Зарегистрировать уже существующий файл объекта в ChildObjects. Для создания нового объекта используй `/meta-compile`, `/role-compile`, `/subsystem-compile` — они регистрируют автоматически |
| `remove-childObject` | `Type.Name` (batch `;;`) | Удалить объект из ChildObjects |
| `sort-childObjects` | вид, напр. `Catalog` (batch `;;`), либо пусто | Упорядочить ChildObjects по имени внутри вида. Без значения — все виды, кроме четырёх (см. reference) |
| `add-defaultRole` | `Role.Name` или `Name` | Добавить роль по умолчанию |
| `remove-defaultRole` | `Role.Name` или `Name` | Удалить роль по умолчанию |
| `set-defaultRoles` | Имена через `;;` | Заменить список ролей по умолчанию |
+14
View File
@@ -39,6 +39,20 @@
Batch: `"Catalog.Товары ;; Document.Заказ ;; Enum.ВидыОплат"`
## sort-childObjects
Упорядочивает объекты в `<ChildObjects>` по имени **внутри вида**. Значение — имя вида (`Catalog`, `Role`, …), batch через `;;`. Без значения обрабатываются все виды, какие есть в файле.
```
-Operation sort-childObjects — все виды, кроме перечисленных ниже
-Operation sort-childObjects -Value "Catalog" — только справочники
-Operation sort-childObjects -Value "Catalog ;; Role"
```
Не сортируются, пока вид не назван явно: `CommonAttribute`, `Subsystem`, `CommandGroup`, `Language`.
Вызов без значения дополнительно ставит группы видов в канонический порядок; вызов с явным видом трогает только имена внутри него.
## add-defaultRole / remove-defaultRole / set-defaultRoles
Имя роли: `ПолныеПрава` или `Role.ПолныеПрава` (префикс `Role.` добавляется автоматически).
+366 -43
View File
@@ -1,15 +1,80 @@
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.29 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)][Alias('Path')][string]$ConfigPath,
[string]$DefinitionFile,
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page")]
[ValidateSet("modify-property","add-childObject","remove-childObject","add-defaultRole","remove-defaultRole","set-defaultRoles","set-panels","set-home-page","sort-childObjects")]
[string]$Operation,
[string]$Value,
[switch]$NoValidate
)
$ErrorActionPreference = "Stop"
# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
try {
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
$parsed = $text | ConvertFrom-Json
} catch {
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
if ($Inline) {
$got = ($text -replace '\s+', ' ').Trim()
$label = 'got'
if (-not $got) { $got = '(empty)' }
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
$what = "${what}, ${label}: ${got}"
}
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
exit 1
}
Write-Output -NoEnumerate $parsed
}
# --- Чтение входного JSON-файла ---
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
# угаданное имя уйдёт в метаданные так же молча.
function Read-JsonInputFile([string]$path) {
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
# проверкой срабатывают раньше и сохраняют свой текст.
if (-not (Test-Path -LiteralPath $path)) {
[Console]::Error.WriteLine("[ERROR] File not found: $path")
exit 1
}
if (Test-Path -LiteralPath $path -PathType Container) {
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
exit 1
}
$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
}
try {
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
} catch {
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
exit 1
}
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Mode validation ---
@@ -212,27 +277,27 @@ foreach ($child in $script:propsEl.ChildNodes) {
}
Info "Configuration: $($script:objName)"
# --- Canonical type order for ChildObjects (44 types) ---
# --- Canonical type order for ChildObjects (46 types) ---
$script:typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
)
# --- Type → on-disk directory name (plural) ---
$script:typeToDir = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"; "CommonTemplate"="CommonTemplates"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"; "SettingsStorage"="SettingsStorages"; "FunctionalOption"="FunctionalOptions"
"FunctionalOptionsParameter"="FunctionalOptionsParameters"; "DefinedType"="DefinedTypes"; "CommonCommand"="CommonCommands"; "CommandGroup"="CommandGroups"
@@ -241,7 +306,7 @@ $script:typeToDir = @{
"Report"="Reports"; "DataProcessor"="DataProcessors"; "InformationRegister"="InformationRegisters"; "AccumulationRegister"="AccumulationRegisters"
"ChartOfCharacteristicTypes"="ChartsOfCharacteristicTypes"; "ChartOfAccounts"="ChartsOfAccounts"; "AccountingRegister"="AccountingRegisters"
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"; "CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "IntegrationService"="IntegrationServices"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
}
# --- XML manipulation helpers (from subsystem-edit pattern) ---
@@ -311,6 +376,21 @@ function Import-Fragment([string]$xmlString) {
}
# --- Parse batch value (split by ;;) ---
# Имя вида из пользовательского ввода → каноническое имя или $null.
# Ввод прощающий: регистр не важен, принимается имя каталога выгрузки (Catalogs → Catalog)
# и русское имя вида в единственном и множественном числе.
function Resolve-TypeName([string]$token) {
$key = "$token".Trim()
if (-not $key) { return $null }
foreach ($canon in $script:typeOrder) { if ($canon -eq $key) { return $canon } }
$byDir = $script:dirToType[$key.ToLowerInvariant()]
if ($byDir) { return $byDir }
$ru = $script:ruTypeMap[$key.ToLowerInvariant()]
if ($ru) { return $ru }
return $null
}
function Parse-BatchValue([string]$val) {
$items = @()
foreach ($part in $val.Split(";;")) {
@@ -376,6 +456,220 @@ function Do-ModifyProperty([string]$batchVal) {
}
# --- Operation: add-childObject ---
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
# остаётся рабочим каталогом проекта.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
# Явно названный вид сортируется в любом случае.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Test-OrderSensitiveType([string]$typeName) {
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
# Сортировка имён компаратором Compare-MetadataNames. В py-порту ту же роль играет
# functools.cmp_to_key — штатный способ отсортировать компаратором; в PS 5.1 его нет,
# поэтому слияние вручную. Порядок обоих портов задаёт один и тот же компаратор.
function Sort-MetadataNames([string[]]$names) {
# Возврат без запятой-обёртки: приёмная сторона всегда пишет @(...), и одноэлементный
# результат остаётся массивом. С `return ,@(...)` @() собрал бы ОДИН объект-массив.
if ($names.Count -le 1) { return $names }
$mid = [int]($names.Count / 2)
$left = @(Sort-MetadataNames $names[0..($mid - 1)])
$right = @(Sort-MetadataNames $names[$mid..($names.Count - 1)])
$out = New-Object System.Collections.ArrayList
$i = 0; $j = 0
while ($i -lt $left.Count -and $j -lt $right.Count) {
if ((Compare-MetadataNames $left[$i] $right[$j]) -le 0) { [void]$out.Add($left[$i]); $i++ }
else { [void]$out.Add($right[$j]); $j++ }
}
while ($i -lt $left.Count) { [void]$out.Add($left[$i]); $i++ }
while ($j -lt $right.Count) { [void]$out.Add($right[$j]); $j++ }
return $out.ToArray()
}
# Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
# Виды из Test-OrderSensitiveType по имени не сортируются, пока не названы явно.
# Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
# починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
# ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
# остаются как были, в дифе только перестановка строк.
function Do-SortChildObjects([string]$batchVal) {
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
# Ввод прощающий: регистр не важен, принимается и имя каталога (Catalogs → Catalog) —
# в дереве выгрузки виды видны именно во множественном числе.
# Без @(...) на приёме: Parse-BatchValue возвращает ,$items — обёртка, которую @()
# собрал бы как ОДИН объект-массив, и вид не нашёлся бы в $script:typeOrder.
$tokens = @()
if ("$batchVal".Trim()) { $tokens = Parse-BatchValue $batchVal }
$requested = @()
foreach ($token in $tokens) {
$canon = Resolve-TypeName $token
if (-not $canon) { Write-Error "Unknown type '$token'. Valid: $($script:typeOrder -join ', ')"; exit 1 }
$requested += $canon
}
$groups = New-Object System.Collections.Specialized.OrderedDictionary
foreach ($child in $script:childObjsEl.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$ln = $child.get_LocalName()
if (-not $groups.Contains($ln)) { $groups[$ln] = New-Object System.Collections.ArrayList }
[void]$groups[$ln].Add($child)
}
$targets = if ($requested.Count -gt 0) { $requested } else { @($groups.Keys | Where-Object { -not (Test-OrderSensitiveType $_) }) }
foreach ($typeName in $targets) {
if (-not $groups.Contains($typeName)) { continue }
$els = $groups[$typeName]
if ($els.Count -lt 2) { continue }
$names = @(foreach ($e in $els) { $e.InnerText })
$ordered = @(Sort-MetadataNames $names)
$same = $true
for ($i = 0; $i -lt $names.Count; $i++) { if ($names[$i] -cne $ordered[$i]) { $same = $false; break } }
if ($same) { continue }
for ($i = 0; $i -lt $els.Count; $i++) { $els[$i].InnerText = $ordered[$i] }
$script:modifyCount++
Info "Sorted: $typeName ($($els.Count))"
}
if ($requested.Count -gt 0) { return }
# Без аргумента приводим в порядок и сами группы видов: собранная навыками конфигурация
# может держать их не в каноне, и первая же выгрузка платформы даст диф. Переставляем
# содержимое существующих узлов, а не узлы, поэтому отступы и структура файла не меняются —
# в дифе только перестановка строк. Имя тега у XmlElement неизменяемо, поэтому там, где вид
# меняется, узел заменяется через ReplaceChild: он сохраняет окружающие пробельные узлы.
$elems = @()
foreach ($child in $script:childObjsEl.ChildNodes) {
if ($child.NodeType -eq 'Element') { $elems += $child }
}
$tags = @(); $texts = @()
foreach ($e in $elems) { $tags += $e.get_LocalName(); $texts += $e.InnerText }
$rank = @()
for ($i = 0; $i -lt $tags.Count; $i++) {
$r = $script:typeOrder.IndexOf($tags[$i])
if ($r -lt 0) { $r = $script:typeOrder.Count }
$rank += $r
}
# Порядок стабильный: вторым ключом идёт исходная позиция
$order = @(0..($tags.Count - 1) | Sort-Object @{e={$rank[$_]}}, @{e={$_}})
$same = $true
for ($i = 0; $i -lt $order.Count; $i++) { if ($order[$i] -ne $i) { $same = $false; break } }
if ($same) { return }
for ($i = 0; $i -lt $elems.Count; $i++) {
$srcIdx = $order[$i]
if ($tags[$i] -ceq $tags[$srcIdx]) {
$elems[$i].InnerText = $texts[$srcIdx]
continue
}
$newEl = $script:xmlDoc.CreateElement($tags[$srcIdx], $script:mdNs)
$newEl.InnerText = $texts[$srcIdx]
[void]$script:childObjsEl.ReplaceChild($newEl, $elems[$i])
}
$script:modifyCount++
Info "Reordered type groups: $($elems.Count) entries"
}
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
# финальный перенос. $null → файл новый (сохранить текущее поведение).
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Detect-XmlStyle([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return $null }
$raw = [System.IO.File]::ReadAllBytes($path)
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
$m = [regex]::Match($head, 'encoding="([^"]+)"')
return @{
bom = $bom
crlf = $body.Contains("`r`n")
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
finalNl = $body.EndsWith("`n")
}
}
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Finalize-XmlText([string]$text, $style) {
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
if ($style -and $style.finalNl) { $text += "`n" }
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
return $text
}
function Do-AddChildObject([string]$batchVal) {
if (-not $script:childObjsEl) { Write-Error "No <ChildObjects> element found"; exit 1 }
@@ -395,6 +689,8 @@ function Do-AddChildObject([string]$batchVal) {
exit 1
}
$typeName = $item.Substring(0, $dotIdx)
$canonType = Resolve-TypeName $typeName
if ($canonType) { $typeName = $canonType }
$objNameVal = $item.Substring($dotIdx + 1)
# Check type is valid
@@ -439,11 +735,11 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
continue
}
# Find insertion point: after last element of same type, or after last element of preceding type
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition.
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $script:configDir) -eq "byName")
$insertBefore = $null
$lastSameType = $null
$lastPrecedingType = $null
$currentTypeIdx = -1
$firstLaterType = $null
foreach ($child in $script:childObjsEl.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
@@ -451,17 +747,29 @@ To create a new $typeName, use $hintSkill (auto-registers in Configuration.xml):
if ($childTypeIdx -lt 0) { continue }
if ($child.LocalName -eq $typeName) {
# Same type — check alphabetical order
if ($child.InnerText -gt $objNameVal -and -not $insertBefore) {
# Insert before this element (alphabetical)
# Внутри вида — по newObjectPosition: end (по умолчанию) кладёт после последнего
# объекта того же вида, byName — по имени. Subsystem по имени не упорядочиваем
# никогда: порядок подсистем в дереве задаёт порядок разделов в панели.
$lastSameType = $child
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objNameVal) -gt 0) {
$insertBefore = $child
}
$lastSameType = $child
} elseif ($childTypeIdx -lt $typeIdx) {
$lastPrecedingType = $child
} elseif ($childTypeIdx -gt $typeIdx -and -not $insertBefore) {
# First element of a later type — insert before it
$insertBefore = $child
} elseif ($childTypeIdx -gt $typeIdx -and -not $firstLaterType) {
$firstLaterType = $child
}
}
if (-not $insertBefore) {
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
# то есть перед его следующим соседом. Через $firstLaterType этого не сделать:
# если видов старше в файле нет, запись уехала бы в самый конец блока,
# за пределы своей группы.
if ($lastSameType) {
$next = $lastSameType.NextSibling
while ($next -and $next.NodeType -ne 'Element') { $next = $next.NextSibling }
$insertBefore = $next
} else {
$insertBefore = $firstLaterType
}
}
@@ -493,6 +801,8 @@ function Do-RemoveChildObject([string]$batchVal) {
exit 1
}
$typeName = $item.Substring(0, $dotIdx)
$canonType = Resolve-TypeName $typeName
if ($canonType) { $typeName = $canonType }
$objNameVal = $item.Substring($dotIdx + 1)
$found = $false
@@ -639,10 +949,7 @@ function Do-SetPanels($valArg) {
# Accept string (JSON), PSCustomObject, or hashtable
$layout = $valArg
if ($layout -is [string]) {
try { $layout = $layout | ConvertFrom-Json } catch {
Write-Error "set-panels value must be valid JSON object, got: $valArg"
exit 1
}
$layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-panels'" "a JSON object with panel layout" -Inline
}
if (-not $layout) {
Write-Error "set-panels value is empty"
@@ -725,6 +1032,29 @@ $script:ruTypeMap = @{
"бот" = "Bot"
"планобмена" = "ExchangePlan"
"хранилищенастроек" = "SettingsStorage"
# Множественное число: в дереве конфигурации виды подписаны именно так.
"справочники" = "Catalog"
"документы" = "Document"
"перечисления" = "Enum"
"отчёты" = "Report"
"отчеты" = "Report"
"обработки" = "DataProcessor"
"общиеформы" = "CommonForm"
"журналыдокументов" = "DocumentJournal"
"планывидовхарактеристик" = "ChartOfCharacteristicTypes"
"планысчетов" = "ChartOfAccounts"
"планывидоврасчета" = "ChartOfCalculationTypes"
"планывидоврасчёта" = "ChartOfCalculationTypes"
"регистрысведений" = "InformationRegister"
"регистрынакопления" = "AccumulationRegister"
"регистрыбухгалтерии" = "AccountingRegister"
"регистрырасчета" = "CalculationRegister"
"регистрырасчёта" = "CalculationRegister"
"бизнеспроцессы" = "BusinessProcess"
"задачи" = "Task"
"боты" = "Bot"
"планыобмена" = "ExchangePlan"
"хранилищанастроек" = "SettingsStorage"
}
# plural folder → singular type
$script:dirToType = @{}
@@ -826,9 +1156,7 @@ $indent</Item>
function Do-SetHomePage($valArg) {
$layout = $valArg
if ($layout -is [string]) {
try { $layout = $layout | ConvertFrom-Json } catch {
Write-Error "set-home-page value must be valid JSON object"; exit 1
}
$layout = ConvertFrom-JsonInput $layout "-Value for operation 'set-home-page'" "a JSON object with home page layout" -Inline
}
if (-not $layout) { Write-Error "set-home-page value is empty"; exit 1 }
@@ -942,8 +1270,8 @@ if ($DefinitionFile) {
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
}
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
$ops = $jsonText | ConvertFrom-Json
$jsonText = Read-JsonInputFile $DefinitionFile
$ops = ConvertFrom-JsonInput $jsonText $DefinitionFile
if ($ops -is [System.Array]) {
foreach ($op in $ops) { $operations += $op }
} else {
@@ -968,11 +1296,16 @@ foreach ($op in $operations) {
"set-defaultRoles" { Do-SetDefaultRoles $opValueStr }
"set-panels" { Do-SetPanels $opValue }
"set-home-page" { Do-SetHomePage $opValue }
"sort-childObjects" { Do-SortChildObjects $opValueStr }
default { Write-Error "Unknown operation: $opName"; exit 1 }
}
}
# --- Save ---
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$xmlStyle = Detect-XmlStyle $resolvedPath
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
@@ -983,22 +1316,12 @@ $writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$script:xmlDoc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?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 $resolvedPath) -and ([System.IO.File]::ReadAllText($resolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
$text = Finalize-XmlText $text $xmlStyle
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($resolvedPath, $text, $utf8Bom)
$writeBom = ($null -eq $xmlStyle) -or $xmlStyle.bom
[System.IO.File]::WriteAllText($resolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
Info "Saved: $resolvedPath"
# --- Auto-validate ---
+299 -44
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# cf-edit v1.19 — Edit 1C configuration root (Configuration.xml)
# cf-edit v1.29 — Edit 1C configuration root (Configuration.xml)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import functools
import json
import os
import re
@@ -14,6 +15,68 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def parse_json_input(text, source, expected=None, inline=False):
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
expected заполняем только для полиморфного входа: у файла подсказка
была бы наполнителем — имя файла и текст парсера самодостаточны. inline печатает ещё и то,
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
"""
import json as _pj
import sys as _psys
try:
if not str(text).strip():
raise ValueError("input is empty")
return _pj.loads(text)
except ValueError as exc:
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
if inline:
got = " ".join(str(text).split())
label = "got"
if not got:
got = "(empty)"
elif len(got) > 60:
label = "got (first 60 chars)"
got = got[:60]
what = "%s, %s: %s" % (what, label, got)
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
_psys.exit(1)
def read_json_file(path):
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
BOM — объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
"""
import os as _pos
import sys as _psys
if not _pos.path.exists(path):
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
_psys.exit(1)
if _pos.path.isdir(path):
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
_psys.exit(1)
with open(path, "rb") as _fh:
data = _fh.read()
if data[:3] == b"\xef\xbb\xbf":
return data[3:].decode("utf-8")
if data[:2] == b"\xff\xfe":
return data[2:].decode("utf-16-le")
if data[:2] == b"\xfe\xff":
return data[2:].decode("utf-16-be")
try:
return data.decode("utf-8")
except UnicodeDecodeError as exc:
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
% (path, exc), file=_psys.stderr)
_psys.exit(1)
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
@@ -254,27 +317,27 @@ XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
V8_NS = "http://v8.1c.ru/8.1/data/core"
XS_NS = "http://www.w3.org/2001/XMLSchema"
# Canonical type order for ChildObjects (44 types)
# Canonical type order for ChildObjects (46 types)
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
]
# Type → on-disk directory name (plural)
TYPE_TO_DIR = {
"Language": "Languages", "Subsystem": "Subsystems", "StyleItem": "StyleItems", "Style": "Styles",
"CommonPicture": "CommonPictures", "SessionParameter": "SessionParameters", "Role": "Roles", "CommonTemplate": "CommonTemplates",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"FilterCriterion": "FilterCriteria", "CommonModule": "CommonModules", "Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "CommonAttribute": "CommonAttributes", "ExchangePlan": "ExchangePlans",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices", "HTTPService": "HTTPServices", "WSReference": "WSReferences",
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs", "SettingsStorage": "SettingsStorages", "FunctionalOption": "FunctionalOptions",
"FunctionalOptionsParameter": "FunctionalOptionsParameters", "DefinedType": "DefinedTypes", "CommonCommand": "CommonCommands", "CommandGroup": "CommandGroups",
@@ -283,7 +346,7 @@ TYPE_TO_DIR = {
"Report": "Reports", "DataProcessor": "DataProcessors", "InformationRegister": "InformationRegisters", "AccumulationRegister": "AccumulationRegisters",
"ChartOfCharacteristicTypes": "ChartsOfCharacteristicTypes", "ChartOfAccounts": "ChartsOfAccounts", "AccountingRegister": "AccountingRegisters",
"ChartOfCalculationTypes": "ChartsOfCalculationTypes", "CalculationRegister": "CalculationRegisters",
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "IntegrationService": "IntegrationServices",
"BusinessProcess": "BusinessProcesses", "Task": "Tasks", "ExternalDataSource": "ExternalDataSources", "IntegrationService": "IntegrationServices",
}
ML_PROPS = ["Synonym", "BriefInformation", "DetailedInformation", "Copyright", "VendorInformationAddress", "ConfigurationInformationAddress"]
@@ -291,6 +354,137 @@ SCALAR_PROPS = ["Name", "Version", "Vendor", "Comment", "NamePrefix", "UpdateCat
REF_PROPS = ["DefaultLanguage"]
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
остаётся рабочим каталогом проекта.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def is_order_sensitive_type(type_name):
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
Явно названный вид сортируется в любом случае.
Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
RU_TYPE_MAP = {
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
"плансчетов": "ChartOfAccounts",
"планвидоврасчета": "ChartOfCalculationTypes",
"планвидоврасчёта": "ChartOfCalculationTypes",
"регистрсведений": "InformationRegister",
"регистрнакопления": "AccumulationRegister",
"регистрбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
# Множественное число: в дереве конфигурации виды подписаны именно так.
"справочники": "Catalog", "документы": "Document", "перечисления": "Enum",
"отчёты": "Report", "отчеты": "Report", "обработки": "DataProcessor",
"общиеформы": "CommonForm", "журналыдокументов": "DocumentJournal",
"планывидовхарактеристик": "ChartOfCharacteristicTypes",
"планысчетов": "ChartOfAccounts",
"планывидоврасчета": "ChartOfCalculationTypes",
"планывидоврасчёта": "ChartOfCalculationTypes",
"регистрысведений": "InformationRegister",
"регистрынакопления": "AccumulationRegister",
"регистрыбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister", "регистрырасчета": "CalculationRegister",
"регистрырасчёта": "CalculationRegister",
"бизнеспроцессы": "BusinessProcess",
"боты": "Bot",
"задачи": "Task", "планыобмена": "ExchangePlan",
"хранилищанастроек": "SettingsStorage",
}
def resolve_type_name(token):
"""Имя вида из пользовательского ввода → каноническое имя или None.
Ввод прощающий: регистр не важен, принимается имя каталога выгрузки
(Catalogs → Catalog) и русское имя вида в единственном и множественном числе.
"""
key = (token or "").strip().lower()
if not key:
return None
for canon in TYPE_ORDER:
if canon.lower() == key:
return canon
for canon, dir_name in TYPE_TO_DIR.items():
if dir_name.lower() == key:
return canon
return RU_TYPE_MAP.get(key)
def localname(el):
return etree.QName(el.tag).localname
@@ -436,7 +630,7 @@ def main():
parser = argparse.ArgumentParser(description="Edit 1C configuration root (Configuration.xml)", allow_abbrev=False)
parser.add_argument("-ConfigPath", "-Path", required=True)
parser.add_argument("-DefinitionFile", default=None)
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page"])
parser.add_argument("-Operation", default=None, choices=["modify-property", "add-childObject", "remove-childObject", "add-defaultRole", "remove-defaultRole", "set-defaultRoles", "set-panels", "set-home-page", "sort-childObjects"])
parser.add_argument("-Value", default=None)
parser.add_argument("-NoValidate", action="store_true")
args = ci_parse_args(parser)
@@ -575,7 +769,7 @@ def main():
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
obj_name_val = item[dot_idx + 1:]
if type_name not in TYPE_ORDER:
@@ -612,8 +806,15 @@ def main():
warn(f"Already exists: {type_name}.{obj_name_val}")
continue
# Find insertion point
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт
# порядок разделов в панели, пока их не перечислили в <SubsystemsOrder>.
by_name = (not is_order_sensitive_type(type_name)
and get_new_object_position(config_dir) == "byName")
insert_before = None
last_same = None
first_later = None
for child in child_objs_el:
if not isinstance(child.tag, str):
continue
@@ -623,10 +824,24 @@ def main():
child_type_idx = TYPE_ORDER.index(child_type_name)
if child_type_name == type_name:
if (child.text or "") > obj_name_val and insert_before is None:
last_same = child
if (by_name and insert_before is None
and compare_metadata_names(child.text or "", obj_name_val) > 0):
insert_before = child
elif child_type_idx > type_idx and insert_before is None:
insert_before = child
elif child_type_idx > type_idx and first_later is None:
first_later = child
if insert_before is None:
# Место не выбрано именем — ставим сразу за последним объектом того же вида,
# то есть перед его следующим соседом. Через first_later этого не сделать:
# если видов старше в файле нет, запись уехала бы в самый конец блока,
# за пределы своей группы.
if last_same is not None:
siblings = [c for c in child_objs_el if isinstance(c.tag, str)]
pos = siblings.index(last_same)
insert_before = siblings[pos + 1] if pos + 1 < len(siblings) else None
else:
insert_before = first_later
new_el = etree.Element(f"{{{MD_NS}}}{type_name}")
new_el.text = obj_name_val
@@ -639,6 +854,69 @@ def main():
add_count += 1
info(f"Added: {type_name}.{obj_name_val}")
def do_sort_child_objects(batch_val):
"""Упорядочить <ChildObjects>: имена внутри вида, а без аргумента — и группы видов.
Виды из is_order_sensitive_type по имени не сортируются, пока не названы явно.
Вызов без значения дополнительно ставит группы видов в канонический порядок: платформа
починила бы его только при загрузке-выгрузке, то есть неканоничный файл даёт диф на
ровном месте. Переставляем ЗНАЧЕНИЯ узлов, а не сами узлы — отступы и структура файла
остаются как были, в дифе только перестановка строк.
"""
nonlocal modify_count
if child_objs_el is None:
print("No <ChildObjects> element found", file=sys.stderr)
sys.exit(1)
requested = []
for token in (parse_batch_value(batch_val) if str(batch_val or "").strip() else []):
canon = resolve_type_name(token)
if canon is None:
print(f"Unknown type '{token}'. Valid: {', '.join(TYPE_ORDER)}", file=sys.stderr)
sys.exit(1)
requested.append(canon)
groups = {}
for child in child_objs_el:
if not isinstance(child.tag, str):
continue
groups.setdefault(localname(child), []).append(child)
targets = requested or [t for t in groups if not is_order_sensitive_type(t)]
for type_name in targets:
els = groups.get(type_name, [])
if len(els) < 2:
continue
names = [e.text or "" for e in els]
ordered = sorted(names, key=functools.cmp_to_key(compare_metadata_names))
if names == ordered:
continue
for el, name in zip(els, ordered):
el.text = name
modify_count += 1
info(f"Sorted: {type_name} ({len(els)})")
if requested:
# Вид назван явно — точечная операция: взаимный порядок групп не трогаем.
return
# Без аргумента приводим в порядок и сами группы видов: собранная навыками
# конфигурация может держать их не в каноне, и первая же выгрузка платформы даст
# диф. Переставляем содержимое существующих узлов, а не узлы, поэтому отступы и
# структура файла не меняются — в дифе только перестановка строк.
elems = [c for c in child_objs_el if isinstance(c.tag, str)]
pairs = [(localname(c), c.text or "") for c in elems]
ranked = sorted(range(len(pairs)),
key=lambda i: (TYPE_ORDER.index(pairs[i][0]) if pairs[i][0] in TYPE_ORDER else len(TYPE_ORDER), i))
wanted = [pairs[i] for i in ranked]
if wanted == pairs:
return
for el, (tag, text) in zip(elems, wanted):
el.tag = f'{{{MD_NS}}}{tag}'
el.text = text
modify_count += 1
info(f"Reordered type groups: {len(elems)} entries")
def do_remove_child_object(batch_val):
nonlocal remove_count
if child_objs_el is None:
@@ -651,7 +929,7 @@ def main():
if dot_idx < 1:
print(f"Invalid format '{item}', expected 'Type.Name'", file=sys.stderr)
sys.exit(1)
type_name = item[:dot_idx]
type_name = resolve_type_name(item[:dot_idx]) or item[:dot_idx]
obj_name_val = item[dot_idx + 1:]
found = False
@@ -821,11 +1099,8 @@ def main():
nonlocal modify_count
layout = value
if isinstance(layout, str):
try:
layout = ci_json(json.loads(layout))
except json.JSONDecodeError:
print(f"set-panels value must be valid JSON object", file=sys.stderr)
sys.exit(1)
layout = ci_json(parse_json_input(
layout, "-Value for operation 'set-panels'", "a JSON object with panel layout", inline=True))
if not isinstance(layout, dict) or not layout:
print("set-panels value must be non-empty object", file=sys.stderr)
sys.exit(1)
@@ -874,24 +1149,6 @@ def main():
info(f"Wrote panel layout: {cai_path}")
# --- set-home-page (writes Ext/HomePageWorkArea.xml from scratch) ---
RU_TYPE_MAP = {
"справочник": "Catalog", "документ": "Document", "перечисление": "Enum",
"отчёт": "Report", "отчет": "Report", "обработка": "DataProcessor",
"общаяформа": "CommonForm", "журналдокументов": "DocumentJournal",
"планвидовхарактеристик": "ChartOfCharacteristicTypes",
"плансчетов": "ChartOfAccounts",
"планвидоврасчета": "ChartOfCalculationTypes",
"планвидоврасчёта": "ChartOfCalculationTypes",
"регистрсведений": "InformationRegister",
"регистрнакопления": "AccumulationRegister",
"регистрбухгалтерии": "AccountingRegister",
"регистррасчета": "CalculationRegister",
"регистррасчёта": "CalculationRegister",
"бизнеспроцесс": "BusinessProcess",
"бот": "Bot",
"задача": "Task", "планобмена": "ExchangePlan",
"хранилищенастроек": "SettingsStorage",
}
DIR_TO_TYPE = {v.lower(): k for k, v in TYPE_TO_DIR.items()}
UUID_RE = __import__("re").compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
@@ -976,11 +1233,8 @@ def main():
nonlocal modify_count
layout = value
if isinstance(layout, str):
try:
layout = ci_json(json.loads(layout))
except json.JSONDecodeError:
print("set-home-page value must be valid JSON object", file=sys.stderr)
sys.exit(1)
layout = ci_json(parse_json_input(
layout, "-Value for operation 'set-home-page'", "a JSON object with home page layout", inline=True))
if not isinstance(layout, dict) or not layout:
print("set-home-page value must be non-empty object", file=sys.stderr)
sys.exit(1)
@@ -1044,8 +1298,7 @@ def main():
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = ci_json(json.loads(fh.read()))
ops = ci_json(parse_json_input(read_json_file(def_file), def_file))
if isinstance(ops, list):
operations = ops
else:
@@ -1075,6 +1328,8 @@ def main():
do_set_panels(op_value)
elif op_key == "set-home-page":
do_set_home_page(op_value)
elif op_key == "sort-childobjects":
do_sort_child_objects(op_value if isinstance(op_value, str) else str(op_value))
else:
print(f"Unknown operation: {op_name}", file=sys.stderr)
sys.exit(1)
+8 -7
View File
@@ -1,7 +1,8 @@
# cf-info v1.5 — Compact summary of 1C configuration root
# cf-info v1.9 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$true)][Alias('Path')][string]$ConfigPath,
[Parameter(Mandatory=$true, Position=0)][Alias('Path')][string]$ConfigPath,
[ValidateSet("overview","brief","full")]
[string]$Mode = "overview",
[Alias('Name')]
@@ -85,20 +86,20 @@ function Get-PropML([string]$propName) {
return (Get-MLText $n)
}
# --- Type name maps (canonical order, 44 types) ---
# --- Type name maps (canonical order, 46 types) ---
$typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
)
$typeRuNames = @{
@@ -119,7 +120,7 @@ $typeRuNames = @{
"ChartOfCharacteristicTypes"="ПВХ"; "ChartOfAccounts"="Планы счетов"
"AccountingRegister"="Регистры бухгалтерии"; "ChartOfCalculationTypes"="ПВР"
"CalculationRegister"="Регистры расчёта"; "BusinessProcess"="Бизнес-процессы"
"Task"="Задачи"; "IntegrationService"="Сервисы интеграции"
"Task"="Задачи"; "ExternalDataSource"="Внешние источники данных"; "IntegrationService"="Сервисы интеграции"
}
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
+10 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-info v1.5 — Compact summary of 1C configuration root
# cf-info v1.9 — Compact summary of 1C configuration root
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -61,11 +61,11 @@ if os.path.isdir(config_path):
if os.path.isfile(candidate):
config_path = candidate
else:
print(f"[ERROR] No Configuration.xml found in directory: {config_path}", file=sys.stderr)
print(f"[ERROR] No Configuration.xml found in directory: {config_path}")
sys.exit(1)
if not os.path.isfile(config_path):
print(f"[ERROR] File not found: {config_path}", file=sys.stderr)
print(f"[ERROR] File not found: {config_path}")
sys.exit(1)
# --- Load XML ---
@@ -82,12 +82,12 @@ NS = {
md_root = xml_root # root is MetaDataObject itself
if etree.QName(md_root.tag).localname != "MetaDataObject":
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)", file=sys.stderr)
print("[ERROR] Not a valid 1C metadata XML file (no MetaDataObject root)")
sys.exit(1)
cfg_node = md_root.find("md:Configuration", NS)
if cfg_node is None:
print("[ERROR] No <Configuration> element found", file=sys.stderr)
print("[ERROR] No <Configuration> element found")
sys.exit(1)
version = md_root.get("version", "")
@@ -113,20 +113,20 @@ def get_prop_ml(prop_name):
n = props_node.find(f"md:{prop_name}", NS)
return get_ml_text(n)
# --- Type name maps (canonical order, 44 types) ---
# --- Type name maps (canonical order, 46 types) ---
type_order = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
]
type_ru_names = {
@@ -147,7 +147,7 @@ type_ru_names = {
"ChartOfCharacteristicTypes": "ПВХ", "ChartOfAccounts": "Планы счетов",
"AccountingRegister": "Регистры бухгалтерии", "ChartOfCalculationTypes": "ПВР",
"CalculationRegister": "Регистры расчёта", "BusinessProcess": "Бизнес-процессы",
"Task": "Задачи", "IntegrationService": "Сервисы интеграции",
"Task": "Задачи", "ExternalDataSource": "Внешние источники данных", "IntegrationService": "Сервисы интеграции",
}
# --- Read panel layout (Ext/ClientApplicationInterface.xml) ---
+2 -1
View File
@@ -1,5 +1,6 @@
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[string]$Name,
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-init v1.14 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# cf-init v1.15 — Create empty 1C configuration scaffold (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration."""
import sys, os, argparse, re, uuid
@@ -1,7 +1,8 @@
# cf-validate v1.7 — Validate 1C configuration root structure
# cf-validate v1.10 — Validate 1C configuration root structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Parameter(Mandatory, Position=0)]
[Alias('Path')]
[string]$ConfigPath,
@@ -121,16 +122,16 @@ $validClassIds = @(
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
)
# Type -> directory mapping
@@ -139,6 +140,7 @@ $childTypeDirMap = @{
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"Bot"="Bots"
"PaletteColor"="PaletteColors"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
@@ -155,7 +157,7 @@ $childTypeDirMap = @{
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"IntegrationService"="IntegrationServices"
"ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
}
# Valid enum values for Configuration properties
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cf-validate v1.7 — Validate 1C configuration XML structure
# cf-validate v1.10 — Validate 1C configuration XML structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates Configuration.xml: root structure, InternalInfo, properties, ChildObjects, languages."""
import sys, os, argparse, re
@@ -59,16 +59,16 @@ VALID_CLASS_IDS = [
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
'Constant', 'CommonForm', 'Catalog', 'Document',
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
'ChartOfCalculationTypes', 'CalculationRegister',
'BusinessProcess', 'Task', 'IntegrationService',
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
]
# Type -> directory mapping
@@ -76,7 +76,7 @@ CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -93,6 +93,7 @@ CHILD_TYPE_DIR_MAP = {
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
'CalculationRegister': 'CalculationRegisters',
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
'ExternalDataSource': 'ExternalDataSources',
'IntegrationService': 'IntegrationServices',
}
+8 -1
View File
@@ -31,6 +31,7 @@ allowed-tools:
| `ExtensionPath` | Путь к каталогу расширения (обязат.) |
| `ConfigPath` | Путь к конфигурации-источнику (обязат.) |
| `Object` | Что заимствовать (обязат.), batch через `;;` |
| `Module` | Создать пустые модули объекта: `ObjectModule`, `ManagerModule`, `RecordSetModule`, `ValueManagerModule` (через запятую) или `None`. У типов с единственным модулем (`CommonModule`, `HTTPService`, `WebService`) он создаётся и без параметра |
| `BorrowMainAttribute` | Заимствовать основной реквизит формы. Без параметра — не заимствует. `Form` — реквизиты, используемые на форме. `All` — все реквизиты объекта. Требует форму в -Object |
## Формат -Object
@@ -65,7 +66,7 @@ allowed-tools:
2. `/meta-edit` — добавить новый реквизит в объект расширения
3. `/form-edit` — вывести реквизит на заимствованную форму
**Защита существующих данных**: если зависимый объект уже заимствован с содержимым (реквизитами, формами) — скрипт не перезаписывает его, а добавляет только недостающее.
**Защита существующих данных**: уже заимствованный объект не перезаписывается — добавляется только недостающее. Повторный вызов безопасен: собственные реквизиты расширения, заимствованные подобъекты и код в модулях сохраняются.
## Команда
@@ -79,6 +80,12 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/cfe-borrow.ps1" -Ex
# Заимствовать один объект
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты"
# Заимствовать справочник вместе с модулями объекта и менеджера
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты" -Module ObjectModule,ManagerModule
# Общий модуль без файла модуля
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "CommonModule.РаботаСФайлами" -Module None
# Заимствовать форму (автоматически заимствует родительский объект)
... -ExtensionPath src\cfe\extname -ConfigPath src\cf -Object "Catalog.Контрагенты.Form.ФормаЭлемента"
+338 -77
View File
@@ -1,10 +1,12 @@
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.37 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)][string]$ExtensionPath,
[Parameter(Mandatory)][string]$ConfigPath,
[Parameter(Mandatory)][string]$Object,
[string]$BorrowMainAttribute
[string]$BorrowMainAttribute,
[string]$Module
)
$ErrorActionPreference = "Stop"
@@ -255,12 +257,38 @@ $childTypeDirMap = @{
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
"XDTOPackage"="XDTOPackages"; "WebService"="WebServices"
"HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "Language"="Languages"
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"; "Language"="Languages"
}
# --- 4a. Модули заимствованных объектов ---
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
$script:moduleKindsByType = @{
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
"ExchangePlan"=@("ObjectModule","ManagerModule")
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
"InformationRegister"=@("RecordSetModule","ManagerModule")
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
"AccountingRegister"=@("RecordSetModule","ManagerModule")
"CalculationRegister"=@("RecordSetModule","ManagerModule")
"Sequence"=@("RecordSetModule","ManagerModule")
"Constant"=@("ValueManagerModule","ManagerModule")
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
"FilterCriterion"=@("ManagerModule")
}
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
# Отказ — `-Module None`.
$script:autoModuleTypes = @("CommonModule", "HTTPService", "WebService")
$script:moduleKindNames = @("Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule")
# --- 4b. Russian synonym → English type ---
$synonymMap = @{
"Справочник"="Catalog"; "Документ"="Document"; "Перечисление"="Enum"
@@ -282,20 +310,20 @@ $synonymMap = @{
"HTTPСервис"="HTTPService"; "СервисИнтеграции"="IntegrationService"
}
# --- 5. Canonical type order (44 types) ---
# --- 5. Canonical type order (46 types) ---
$script:typeOrder = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
)
# --- 6. GeneratedType patterns per type ---
@@ -427,6 +455,11 @@ $script:generatedTypes = @{
"DefinedType" = @(
@{ prefix = "DefinedType"; category = "DefinedType" }
)
"ExternalDataSource" = @(
@{ prefix = "ExternalDataSourceManager"; category = "Manager" }
@{ prefix = "ExternalDataSourceTablesManager"; category = "TablesManager" }
@{ prefix = "ExternalDataSourceCubesManager"; category = "CubesManager" }
)
"Sequence" = @(
@{ prefix = "SequenceRecord"; category = "Record" }
@{ prefix = "SequenceManager"; category = "Manager" }
@@ -591,6 +624,50 @@ if ($BorrowMainAttribute) {
}
}
# --- 9c. Validate -Module ---
$script:requestedModules = @()
$script:noModule = $false
if ($Module) {
foreach ($raw in ($Module -split '[,;]')) {
$kind = $raw.Trim()
if (-not $kind) { continue }
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно (-ieq): в py-порте это отдельная ветка, и молчаливое
# расхождение портов на «none» ловится только глазами.
if ($kind -ieq "None") { $script:noModule = $true; continue }
$canon = @($script:moduleKindNames | Where-Object { $_ -ieq $kind })
if ($canon.Count -eq 0) {
Write-Error "Неизвестный вид модуля '$kind'. Допустимо: $($script:moduleKindNames -join ', '), None"
exit 1
}
$script:requestedModules += $canon[0]
}
if ($script:noModule -and $script:requestedModules.Count -gt 0) {
Write-Error "-Module None нельзя сочетать с видами модулей"
exit 1
}
}
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
function Resolve-ModuleKinds {
param([string]$typeName)
if ($script:noModule) { return @() }
$allowed = @($script:moduleKindsByType[$typeName])
if ($allowed.Count -eq 0) { return @() }
if ($script:autoModuleTypes -contains $typeName) { return @($allowed[0]) }
if ($script:requestedModules.Count -eq 0) { return @() }
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
$selected = @($allowed | Where-Object { $script:requestedModules -contains $_ })
if ($selected.Count -eq 0) {
Warn " Тип $typeName не имеет запрошенных модулей — пропущено. Допустимо: $($allowed -join ', ')"
}
return $selected
}
# --- 10. Helper: read source object XML ---
# Имена реквизитов исходного объекта → uuid. Нужны для непрозрачной формы пути в ссылках
# параметров выбора (см. Rewrite-ChoiceParameterLinks).
@@ -1275,32 +1352,22 @@ function Register-FormInObject {
}
# Save object XML
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$style2 = Detect-XmlStyle $objFile
$settings2 = New-Object System.Xml.XmlWriterSettings
$settings2.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings2.Indent = $false
$settings2.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream2 = New-Object System.IO.MemoryStream
$writer2 = [System.Xml.XmlWriter]::Create($memStream2, $settings2)
$objDoc.Save($writer2)
$writer2.Flush(); $writer2.Close()
$bytes2 = $memStream2.ToArray()
$text2 = [System.Text.Encoding]::UTF8.GetString($memStream2.ToArray())
$memStream2.Close()
$text2 = [System.Text.Encoding]::UTF8.GetString($bytes2)
if ($text2.Length -gt 0 -and $text2[0] -eq [char]0xFEFF) { $text2 = $text2.Substring(1) }
$text2 = $text2.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text2 = [regex]::Replace($text2, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom2 = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text2 = ($text2 -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objFile, $text2, $utf8Bom2)
$text2 = Finalize-XmlText $text2 $style2
$writeBom2 = ($null -eq $style2) -or $style2.bom
[System.IO.File]::WriteAllText($objFile, $text2, (New-Object System.Text.UTF8Encoding($writeBom2)))
Info " Registered form in: $objFile"
}
@@ -1313,6 +1380,81 @@ function Test-ObjectBorrowed {
return (Test-Path $objFile)
}
# --- 10f. Helper: пометка расширенного свойства (<xr:PropertyState>) ---
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке — эмитим, чтобы исходники навыка
# совпадали с эталоном. Имя свойства = базовое имя файла модуля (Module / ObjectModule / …),
# у заимствованной формы — Form. Ставит тот, кто создал файл модуля (или форму).
function Build-PropertyStateXml {
param([string]$propertyName, [string]$indent)
$sb = New-Object System.Text.StringBuilder
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
return $sb.ToString()
}
function Set-PropertyStateFlag {
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
if ((Get-FormatRank $formatVersion) -lt 219) { return }
if (-not (Test-Path $objFile)) { return }
$enc = New-Object System.Text.UTF8Encoding($true)
$text = [System.IO.File]::ReadAllText($objFile, $enc)
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
# но они лежат ниже, внутри <ChildObjects>.
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
$ind = $empty.Groups[1].Value
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
} elseif ($open.Success) {
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
$ind = $open.Groups[1].Value
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
$text = $text.Insert($closeAt, "${block}${nl}")
} else {
return
}
[System.IO.File]::WriteAllText($objFile, $text, $enc)
}
# --- 10g. Helper: пустой модуль заимствованного объекта ---
function New-BorrowedModuleFile {
param([string]$typeName, [string]$objName, [string]$moduleKind)
$dirName = $childTypeDirMap[$typeName]
$objDir = Join-Path (Join-Path $extDir $dirName) $objName
$moduleDir = Join-Path $objDir "Ext"
if (-not (Test-Path $moduleDir)) { New-Item -ItemType Directory -Path $moduleDir -Force | Out-Null }
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный код
# (то же правило, что у модуля формы).
$moduleFile = Join-Path $moduleDir "${moduleKind}.bsl"
if (Test-Path $moduleFile) {
Info " Preserved existing ${moduleKind}.bsl"
} else {
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($moduleFile, "", $enc)
Info " Created: $moduleFile"
}
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
Set-PropertyStateFlag (Join-Path (Join-Path $extDir $dirName) "${objName}.xml") $moduleKind $script:formatVersion
return $moduleFile
}
# --- 11. Helper: generate InternalInfo XML ---
function Build-InternalInfoXml {
param([string]$typeName, [string]$objName, [string]$indent)
@@ -1738,6 +1880,9 @@ function Merge-AttributesIntoObject {
}
# Save via text manipulation to avoid namespace issues with InnerXml
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$style3 = Detect-XmlStyle $objFile
$settings3 = New-Object System.Xml.XmlWriterSettings
$settings3.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings3.Indent = $false
@@ -1746,28 +1891,15 @@ function Merge-AttributesIntoObject {
$writer3 = [System.Xml.XmlWriter]::Create($memStream3, $settings3)
$objDoc.Save($writer3)
$writer3.Flush(); $writer3.Close()
$bytes3 = $memStream3.ToArray()
$text3 = [System.Text.Encoding]::UTF8.GetString($memStream3.ToArray())
$memStream3.Close()
$text3 = [System.Text.Encoding]::UTF8.GetString($bytes3)
if ($text3.Length -gt 0 -and $text3[0] -eq [char]0xFEFF) { $text3 = $text3.Substring(1) }
$text3 = $text3.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Самозакрытый элемент раскрывается текстом, а не пробельным узлом в DOM: тот давал
# лишнюю строку с табуляцией перед первым <Attribute> (у Конфигуратора пустых строк нет).
# Стоит ДО Finalize-XmlText, чтобы схлопывание пустых тегов накрыло и вставленные реквизиты.
$text3 = Insert-IntoOwnChildObjects $text3 $allAttrXml
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
# Стоит ПОСЛЕ вставки реквизитов, чтобы накрыть и их.
$text3 = [regex]::Replace($text3, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom3 = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $objFile) -and ([System.IO.File]::ReadAllText($objFile) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text3 = ($text3 -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($objFile, $text3, $utf8Bom3)
$text3 = Finalize-XmlText $text3 $style3
$writeBom3 = ($null -eq $style3) -or $style3.bom
[System.IO.File]::WriteAllText($objFile, $text3, (New-Object System.Text.UTF8Encoding($writeBom3)))
Info " Merged $added attribute(s) into: $objFile"
}
}
@@ -2083,6 +2215,127 @@ function Build-BorrowedObjectXml {
}
# --- 13. Helper: add object to extension ChildObjects ---
# Стиль существующего файла для round-trip-сохранения: BOM / EOL / регистр encoding /
# финальный перенос. $null → файл новый (сохранить текущее поведение).
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Detect-XmlStyle([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return $null }
$raw = [System.IO.File]::ReadAllBytes($path)
$bom = ($raw.Length -ge 3 -and $raw[0] -eq 0xEF -and $raw[1] -eq 0xBB -and $raw[2] -eq 0xBF)
$body = if ($bom) { [System.Text.Encoding]::UTF8.GetString($raw, 3, $raw.Length - 3) } else { [System.Text.Encoding]::UTF8.GetString($raw) }
$head = if ($body.Length -gt 200) { $body.Substring(0, 200) } else { $body }
$m = [regex]::Match($head, 'encoding="([^"]+)"')
return @{
bom = $bom
crlf = $body.Contains("`r`n")
enc = $(if ($m.Success) { $m.Groups[1].Value } else { "utf-8" })
finalNl = $body.EndsWith("`n")
}
}
# Привести текст XmlWriter к стилю оригинала; для НОВОГО файла ($null) — к канону выгрузки
# Конфигуратора: encoding="UTF-8", CRLF, без перевода строки в конце.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Finalize-XmlText([string]$text, $style) {
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$encDecl = $(if ($style) { $style.enc } else { "UTF-8" })
$text = $text.Replace('encoding="utf-8"', 'encoding="' + $encDecl + '"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$text = ($text -replace "`r`n", "`n").TrimEnd("`n")
if ($style -and $style.finalNl) { $text += "`n" }
if (-not $style -or $style.crlf) { $text = $text -replace "`n", "`r`n" }
return $text
}
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
}
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
# остаётся рабочим каталогом проекта.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
# Явно названный вид сортируется в любом случае.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Test-OrderSensitiveType([string]$typeName) {
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
function Add-ToChildObjects {
param([string]$typeName, [string]$objName)
@@ -2108,7 +2361,12 @@ function Add-ToChildObjects {
}
}
# Find insertion point: after last element of same type, or before first element of later type
# Место вставки. Вид — по $script:typeOrder; внутри вида — по newObjectPosition: end
# (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени. Так же
# заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects не отсортирован.
# Subsystem по имени не упорядочиваем никогда: порядок подсистем в дереве задаёт порядок
# разделов в панели.
$byName = (-not (Test-OrderSensitiveType $typeName) -and (Get-NewObjectPosition $extDir) -eq "byName")
$insertBefore = $null
$lastSameType = $null
@@ -2118,8 +2376,7 @@ function Add-ToChildObjects {
if ($childTypeIdx -lt 0) { continue }
if ($child.LocalName -eq $typeName) {
# Same type -- check alphabetical order
if ($child.InnerText -gt $objName -and -not $insertBefore) {
if ($byName -and -not $insertBefore -and (Compare-MetadataNames $child.InnerText $objName) -gt 0) {
$insertBefore = $child
}
$lastSameType = $child
@@ -2204,6 +2461,9 @@ foreach ($item in $items) {
$hasBMA = [bool]$BorrowMainAttribute
$formFiles = Borrow-Form $typeName $objName $formName -BorrowMainAttr:$hasBMA
$script:borrowedFiles += $formFiles
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
Set-PropertyStateFlag $formFiles[0] "Form" $script:formatVersion
$borrowedCount++
# Borrow main attribute if requested
@@ -2212,26 +2472,37 @@ foreach ($item in $items) {
}
} else {
# --- Object borrowing (existing logic) ---
Info "Borrowing ${typeName}.${objName}..."
$src = Read-SourceObject $typeName $objName
Info " Source UUID: $($src.Uuid)"
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
$targetDir = Join-Path $extDir $dirName
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
$targetFile = Join-Path $targetDir "${objName}.xml"
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
Info " Created: $targetFile"
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
# расширения, заимствованные подобъекты и состояния, которые из источника не выводятся.
# Повторный вызов — законный способ доделать модуль (-Module), а не переиздать заготовку.
if (Test-ObjectBorrowed $typeName $objName) {
Info "Already borrowed: ${typeName}.${objName} — XML сохранён без изменений"
} else {
Info "Borrowing ${typeName}.${objName}..."
$src = Read-SourceObject $typeName $objName
Info " Source UUID: $($src.Uuid)"
$borrowedXml = Build-BorrowedObjectXml $typeName $objName $src.Uuid $src.Properties
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
$enc = New-Object System.Text.UTF8Encoding($true)
[System.IO.File]::WriteAllText($targetFile, $borrowedXml, $enc)
Info " Created: $targetFile"
}
Add-ToChildObjects $typeName $objName
$script:borrowedFiles += $targetFile
foreach ($kind in (Resolve-ModuleKinds $typeName)) {
$script:borrowedFiles += (New-BorrowedModuleFile $typeName $objName $kind)
}
$borrowedCount++
}
}
@@ -2278,32 +2549,22 @@ while ($true) {
}
# --- 15. Save modified Configuration.xml ---
# Стиль исходника снимаем ДО записи: правка чужого файла наследует его BOM/EOL/заголовок
# (#44/#46/#47), новый файл получает канон выгрузки. Зеркало _detect_xml_style в py-порту.
$style = Detect-XmlStyle $extResolvedPath
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = New-Object System.Text.UTF8Encoding($true)
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$script:xmlDoc.Save($writer)
$writer.Flush(); $writer.Close()
$bytes = $memStream.ToArray()
$text = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
if ($text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) { $text = $text.Substring(1) }
$text = $text.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$text = [regex]::Replace($text, '(?s)<!\[CDATA\[.*?\]\]>|<!--.*?-->|(?<=\S) />', { param($m) if ($m.Value -eq ' />') { '/>' } else { $m.Value } })
$utf8Bom = New-Object System.Text.UTF8Encoding($true)
# Целевой перевод строки: стиль файла-назначения — правка наследует его (#44/#46/#47),
# новый файл получает канон выгрузки CRLF. Зеркало _detect_xml_style в py-порту.
$targetEol = if ((Test-Path -LiteralPath $extResolvedPath) -and ([System.IO.File]::ReadAllText($extResolvedPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$text = ($text -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($extResolvedPath, $text, $utf8Bom)
$text = Finalize-XmlText $text $style
$writeBom = ($null -eq $style) -or $style.bom
[System.IO.File]::WriteAllText($extResolvedPath, $text, (New-Object System.Text.UTF8Encoding($writeBom)))
Info "Saved: $extResolvedPath"
# --- 16. Summary ---
+271 -18
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3
# cfe-borrow v1.31 — Borrow objects from configuration into extension (CFE)
# cfe-borrow v1.37 — Borrow objects from configuration into extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
import os
import re
import sys
@@ -202,6 +203,97 @@ def decode_numeric_entities(s):
return s
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 get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
(так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
остаётся рабочим каталогом проекта.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
def is_order_sensitive_type(type_name):
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
Явно названный вид сортируется в любом случае.
Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
def localname(el):
return etree.QName(el.tag).localname
@@ -233,13 +325,39 @@ CHILD_TYPE_DIR_MAP = {
"EventSubscription": "EventSubscriptions", "ScheduledJob": "ScheduledJobs",
"SettingsStorage": "SettingsStorages", "FilterCriterion": "FilterCriteria",
"CommandGroup": "CommandGroups", "DocumentNumerator": "DocumentNumerators",
"Sequence": "Sequences", "IntegrationService": "IntegrationServices",
"Sequence": "Sequences", "ExternalDataSource": "ExternalDataSources", "IntegrationService": "IntegrationServices",
"XDTOPackage": "XDTOPackages", "WebService": "WebServices",
"HTTPService": "HTTPServices", "WSReference": "WSReferences",
"CommonAttribute": "CommonAttributes", "Style": "Styles",
"Bot": "Bots", "Language": "Languages",
"Bot": "Bots", "PaletteColor": "PaletteColors", "PaletteColor": "PaletteColors", "Language": "Languages",
}
# --- Модули заимствованных объектов ---
# Порядок внутри значения — порядок выгрузки Конфигуратора: сначала «объектный» модуль
# (ObjectModule / RecordSetModule / ValueManagerModule), затем ManagerModule.
MODULE_KINDS_BY_TYPE = {
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
"ExchangePlan": ["ObjectModule", "ManagerModule"],
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
"InformationRegister": ["RecordSetModule", "ManagerModule"],
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
"Sequence": ["RecordSetModule", "ManagerModule"],
"Constant": ["ValueManagerModule", "ManagerModule"],
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
"FilterCriterion": ["ManagerModule"],
}
# Типы с ЕДИНСТВЕННЫМ модулем: ради него объект и заимствуют, поэтому файл создаётся молча.
# Отказ — `-Module None`.
AUTO_MODULE_TYPES = ["CommonModule", "HTTPService", "WebService"]
MODULE_KIND_NAMES = ["Module", "ObjectModule", "ManagerModule", "RecordSetModule", "ValueManagerModule"]
SYNONYM_MAP = {
"\u0421\u043f\u0440\u0430\u0432\u043e\u0447\u043d\u0438\u043a": "Catalog",
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442": "Document",
@@ -281,16 +399,16 @@ SYNONYM_MAP = {
TYPE_ORDER = [
"Language", "Subsystem", "StyleItem", "Style",
"CommonPicture", "SessionParameter", "Role", "CommonTemplate",
"FilterCriterion", "CommonModule", "Bot", "CommonAttribute", "ExchangePlan",
"FilterCriterion", "CommonModule", "CommonAttribute", "ExchangePlan",
"XDTOPackage", "WebService", "HTTPService", "WSReference",
"EventSubscription", "ScheduledJob", "SettingsStorage", "FunctionalOption",
"FunctionalOptionsParameter", "DefinedType", "CommonCommand", "CommandGroup",
"FunctionalOptionsParameter", "DefinedType", "Bot", "PaletteColor", "CommonCommand", "CommandGroup",
"Constant", "CommonForm", "Catalog", "Document",
"DocumentNumerator", "Sequence", "DocumentJournal", "Enum",
"Report", "DataProcessor", "InformationRegister", "AccumulationRegister",
"ChartOfCharacteristicTypes", "ChartOfAccounts", "AccountingRegister",
"ChartOfCalculationTypes", "CalculationRegister",
"BusinessProcess", "Task", "IntegrationService",
"BusinessProcess", "Task", "ExternalDataSource", "IntegrationService",
]
GENERATED_TYPES = {
@@ -421,6 +539,11 @@ GENERATED_TYPES = {
"DefinedType": [
{"prefix": "DefinedType", "category": "DefinedType"},
],
"ExternalDataSource": [
{"prefix": "ExternalDataSourceManager", "category": "Manager"},
{"prefix": "ExternalDataSourceTablesManager", "category": "TablesManager"},
{"prefix": "ExternalDataSourceCubesManager", "category": "CubesManager"},
],
"Sequence": [
{"prefix": "SequenceRecord", "category": "Record"},
{"prefix": "SequenceManager", "category": "Manager"},
@@ -516,6 +639,55 @@ def format_rank(ver):
return int(m.group(1)) * 100 + int(m.group(2)) if m else 0
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
# Копии этих функций есть в cfe-patch-method (навыки автономны); держать их одинаковыми — сознательно.
def build_property_state_xml(property_name, indent):
return "\n".join([
f"{indent}<xr:PropertyState>",
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
f"{indent}\t<xr:State>Extended</xr:State>",
f"{indent}</xr:PropertyState>",
])
def set_property_state_flag(obj_file, property_name, format_version):
if format_rank(format_version) < 219:
return
if not os.path.isfile(obj_file):
return
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
text = fh.read()
nl = "\r\n" if "\r\n" in text else "\n"
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
# но они лежат ниже, внутри <ChildObjects>.
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
if empty and (not opened or empty.start() < opened.start()):
ind = empty.group(1)
block = build_property_state_xml(property_name, ind + "\t")
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
text = text[:empty.start()] + replacement + text[empty.end():]
elif opened:
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
return
ind = opened.group(1)
block = build_property_state_xml(property_name, ind + "\t")
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
close_at = opened.end() - len("</InternalInfo>") - len(ind)
text = text[:close_at] + block + nl + text[close_at:]
else:
return
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(text)
def apply_pal_ns(format_version):
"""2.21 (8.5) добавила в шапку пространство палитры — ради <Color> у значений перечисления.
Вставляем НА МЕСТО (после lf, перед style): платформа держит объявления по алфавиту,
@@ -655,6 +827,7 @@ def main():
parser.add_argument("-ConfigPath", required=True)
parser.add_argument("-Object", required=True)
parser.add_argument("-BorrowMainAttribute", nargs="?", const="Form", default=None)
parser.add_argument("-Module", default=None)
args = ci_parse_args(parser)
# --- 1. Resolve paths ---
@@ -860,6 +1033,25 @@ def main():
sys.exit(1)
return src_uuid
# --- Пустой модуль заимствованного объекта ---
def new_borrowed_module_file(type_name, obj_name, module_kind):
dir_name = CHILD_TYPE_DIR_MAP[type_name]
module_dir = os.path.join(ext_dir, dir_name, obj_name, "Ext")
os.makedirs(module_dir, exist_ok=True)
# NEVER overwrite an existing one: повторное заимствование не должно затирать дописанный
# код (то же правило, что у модуля формы).
module_file = os.path.join(module_dir, f"{module_kind}.bsl")
if os.path.isfile(module_file):
info(f" Preserved existing {module_kind}.bsl")
else:
write_utf8_bom(module_file, "")
info(f" Created: {module_file}")
# Флаг ставим и для уже существовавшего файла: состояние объекта должно отражать факт модуля.
set_property_state_flag(os.path.join(ext_dir, dir_name, f"{obj_name}.xml"), module_kind, format_version)
return module_file
def build_internal_info_xml(type_name, obj_name, indent):
types = GENERATED_TYPES.get(type_name)
if not types:
@@ -940,6 +1132,13 @@ def main():
warn(f"Already in ChildObjects: {type_name}.{obj_name}")
return
# Место вставки. Вид — по TYPE_ORDER; внутри вида — по newObjectPosition:
# end (по умолчанию) кладёт после последнего объекта того же вида, byName — по имени.
# Так же, как заимствует Конфигуратор: в боевых выгрузках расширений ChildObjects
# не отсортирован. Subsystem по имени не упорядочиваем никогда: порядок подсистем
# в дереве задаёт порядок разделов в панели.
by_name = (not is_order_sensitive_type(type_name)
and get_new_object_position(ext_dir) == "byName")
insert_before = None
for child in child_objs_el:
if not isinstance(child.tag, str):
@@ -950,7 +1149,8 @@ def main():
child_type_idx = TYPE_ORDER.index(child_type_name)
if child_type_name == type_name:
if (child.text or "") > obj_name and insert_before is None:
if (by_name and insert_before is None
and compare_metadata_names(child.text or "", obj_name) > 0):
insert_before = child
elif child_type_idx > type_idx and insert_before is None:
insert_before = child
@@ -2019,6 +2219,47 @@ def main():
print("-BorrowMainAttribute requires a form in -Object (e.g. 'Catalog.X.Form.Y')", file=sys.stderr)
sys.exit(1)
# --- 9c. Validate -Module ---
requested_modules = []
no_module = False
if args.Module:
for raw in re.split(r"[,;]", args.Module):
kind = raw.strip()
if not kind:
continue
# Сравнение РЕГИСТРОНЕЗАВИСИМОЕ явно: в ps1-порте `-ieq`, и молчаливое расхождение
# портов на «none» ловится только глазами.
if kind.lower() == "none":
no_module = True
continue
canon = [k for k in MODULE_KIND_NAMES if k.lower() == kind.lower()]
if not canon:
print(f"Неизвестный вид модуля '{kind}'. Допустимо: {', '.join(MODULE_KIND_NAMES)}, None", file=sys.stderr)
sys.exit(1)
requested_modules.append(canon[0])
if no_module and requested_modules:
print("-Module None нельзя сочетать с видами модулей", file=sys.stderr)
sys.exit(1)
# Какие модули создать для объекта. Тип с единственным модулем получает его всегда — уточнять
# там нечего; -Module разбирает только неоднозначные типы. Иначе батч смешанных типов
# (`CommonModule.X ;; Catalog.Y`) не выражался бы одним вызовом.
def resolve_module_kinds(type_name):
if no_module:
return []
allowed = MODULE_KINDS_BY_TYPE.get(type_name, [])
if not allowed:
return []
if type_name in AUTO_MODULE_TYPES:
return [allowed[0]]
if not requested_modules:
return []
# Порядок берём из таблицы типа, а не из порядка ключей в -Module.
selected = [k for k in allowed if k in requested_modules]
if not selected:
warn(f" Тип {type_name} не имеет запрошенных модулей — пропущено. Допустимо: {', '.join(allowed)}")
return selected
# --- 10. Process each item ---
borrowed_count = 0
@@ -2070,6 +2311,9 @@ def main():
has_bma = borrow_main_attribute_mode is not None
form_files = borrow_form(type_name, obj_name, form_name, borrow_main_attr=has_bma)
borrowed_files.extend(form_files)
# Замер на 8.3.26: платформа помечает форму расширенной сразу при заимствовании,
# даже если элементы не менялись. Флаг живёт в метаданных формы, не у владельца.
set_property_state_flag(form_files[0], "Form", format_version)
borrowed_count += 1
# Borrow main attribute if requested
@@ -2077,23 +2321,32 @@ def main():
borrow_main_attribute(type_name, obj_name, form_name, borrow_main_attribute_mode)
else:
# --- Object borrowing ---
info(f"Borrowing {type_name}.{obj_name}...")
src = read_source_object(type_name, obj_name)
info(f" Source UUID: {src['Uuid']}")
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
target_dir = os.path.join(ext_dir, dir_name)
os.makedirs(target_dir, exist_ok=True)
target_file = os.path.join(target_dir, f"{obj_name}.xml")
write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}")
# Уже заимствованный объект НЕ переписываем: в его XML лежат собственные реквизиты
# расширения, заимствованные подобъекты и состояния, которые из источника не
# выводятся. Повторный вызов — законный способ доделать модуль (-Module), а не
# переиздать заготовку.
if test_object_borrowed(type_name, obj_name):
info(f"Already borrowed: {type_name}.{obj_name} — XML сохранён без изменений")
else:
info(f"Borrowing {type_name}.{obj_name}...")
src = read_source_object(type_name, obj_name)
info(f" Source UUID: {src['Uuid']}")
borrowed_xml = build_borrowed_object_xml(type_name, obj_name, src["Uuid"], src["Properties"])
os.makedirs(target_dir, exist_ok=True)
write_xml_file(target_file, borrowed_xml)
info(f" Created: {target_file}")
add_to_child_objects(type_name, obj_name)
borrowed_files.append(target_file)
for kind in resolve_module_kinds(type_name):
borrowed_files.append(new_borrowed_module_file(type_name, obj_name, kind))
borrowed_count += 1
# --- Владельцы заимствованных справочников ---
+5 -3
View File
@@ -1,7 +1,8 @@
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Parameter(Mandatory, Position=0)]
[string]$ExtensionPath,
[Parameter(Mandatory)]
@@ -48,10 +49,11 @@ $childTypeDirMap = @{
"EventSubscription"="EventSubscriptions"; "ScheduledJob"="ScheduledJobs"
"SettingsStorage"="SettingsStorages"; "FilterCriterion"="FilterCriteria"
"CommandGroup"="CommandGroups"; "DocumentNumerator"="DocumentNumerators"
"Sequence"="Sequences"; "IntegrationService"="IntegrationServices"
"Sequence"="Sequences"; "ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
"CommonAttribute"="CommonAttributes"; "Style"="Styles"; "XDTOPackage"="XDTOPackages"
"WebService"="WebServices"; "HTTPService"="HTTPServices"; "WSReference"="WSReferences"
"Bot"="Bots"
"PaletteColor"="PaletteColors"
}
# --- Parse extension Configuration.xml ---
+3 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-diff v1.2 — Analyze and compare 1C configuration extension (CFE) (+тип Bot; cfe-diff/cfe-borrow: недостающие типы)
# cfe-diff v1.5 — Analyze and compare 1C configuration extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -80,6 +80,7 @@ CHILD_TYPE_DIR_MAP = {
"CommandGroup": "CommandGroups",
"DocumentNumerator": "DocumentNumerators",
"Sequence": "Sequences",
"ExternalDataSource": "ExternalDataSources",
"IntegrationService": "IntegrationServices",
"CommonAttribute": "CommonAttributes",
"Style": "Styles",
@@ -88,6 +89,7 @@ CHILD_TYPE_DIR_MAP = {
"HTTPService": "HTTPServices",
"WSReference": "WSReferences",
"Bot": "Bots",
"PaletteColor": "PaletteColors",
}
+2 -1
View File
@@ -1,5 +1,6 @@
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# cfe-init v1.11 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[string]$Name,
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-init v1.10 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# cfe-init v1.11 — Create 1C configuration extension scaffold (CFE) (+write_xml_file/write_utf8_bom: общий эталон записи)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Generates minimal XML source files for a 1C configuration extension."""
import sys, os, re, argparse, uuid
+3 -1
View File
@@ -88,7 +88,7 @@ allowed-tools:
Правила:
- Маркеры — на **отдельной строке с 0-й колонки** (без отступа), даже внутри отступов и текста запроса (`|…`).
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
- **Незамеченные (unmarked) строки должны совпадать с оригиналом дословно** — это и есть «контроль». Дословно — включая комментарии, регистр и пробелы внутри строки (`Х = Х + 1``Х=Х+1`); свободны только отступ и пустые строки. Если оригинал в конфигурации-источнике изменится, unmarked-контекст разойдётся → метод потребует актуализации (см. ниже; проверить пачкой — `-Check`).
- Меняешь только свои `#Вставка`/`#Удаление`; чужой оригинал не трогай.
## Актуализация
@@ -107,6 +107,8 @@ allowed-tools:
Повторный вызов `Before`/`After`/`Instead` для уже перехваченного метода дубль не создаёт (`[ПРОПУЩЕН]`).
`-Check` смотрит исходники. Вердикт платформы — уже после загрузки в базу: `/db-cfe-admin check`.
## Команда
```powershell
@@ -1,5 +1,6 @@
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[string]$ExtensionPath,
@@ -361,6 +362,22 @@ function Get-Normalized {
return (($line -replace '\s+', ' ').Trim())
}
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
function Get-ControlKey {
param($lines)
return (@($lines | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) -join "`n")
}
# Parameter count of a signature params text. The platform compares only the number of
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
function Get-ParamCount {
param([string]$paramsText)
if ([string]::IsNullOrWhiteSpace($paramsText)) { return 0 }
return @(Split-TopLevel $paramsText | Where-Object { $_.Trim() -ne '' }).Count
}
# Reconstruct v1 body and edit ops from a marked body
function Parse-MarkedBody {
param($bodyLines)
@@ -650,7 +667,14 @@ function Invoke-Resync {
$v1norm = @($v1 | ForEach-Object { Get-Normalized $_ })
$v2norm = @($v2 | ForEach-Object { Get-Normalized $_ })
if (($v1norm -join "`n") -eq ($v2norm -join "`n")) {
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
$extParamCount = Get-ParamCount $sig.ParamsText
$srcParamCount = Get-ParamCount $method.ParamsText
$paramsDrift = ($extParamCount -ne $srcParamCount)
$paramsReason = if ($paramsDrift) { "список параметров: в оригинале $srcParamCount, в перехватчике $extParamCount" } else { '' }
if (-not $paramsDrift -and [string]::Equals((Get-ControlKey $v1), (Get-ControlKey $v2), 'Ordinal')) {
return @{ Id = $methodId; Status = 'АКТУАЛЕН'; ExtBsl = $extBsl }
}
@@ -700,7 +724,9 @@ function Invoke-Resync {
if ($ReportOnly) {
$st = if ($disputed.Count -gt 0) { 'КОНФЛИКТ' } elseif ($transferred.Count -eq 0 -and $absorbed.Count -gt 0) { 'ПЕРЕНЕСЕНО В ОСНОВНУЮ' } else { 'ДРЕЙФ' }
if ($paramsDrift -and $st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { $st = 'ДРЕЙФ' }
$rsn = if ($disputed.Count -gt 0) { Get-ResyncConflictReason $disputed } elseif ($st -eq 'ПЕРЕНЕСЕНО В ОСНОВНУЮ') { 'все правки уже в основной конфигурации' } else { '' }
if ($paramsDrift) { $rsn = if ($rsn) { "$paramsReason; $rsn" } else { $paramsReason } }
return @{ Id = $methodId; Status = $st; ExtBsl = $extBsl; Transferred = $transferred.Count; Absorbed = $absorbed.Count; Disputed = $disputed.Count; Reason = $rsn; AbsorbedNotes = $absorbedNotes }
}
@@ -788,6 +814,100 @@ if (Test-Path $ExtensionPath -PathType Leaf) { $ExtensionPath = Split-Path $Exte
$cfgFile = Join-Path $ExtensionPath "Configuration.xml"
if (-not (Test-Path $cfgFile)) { Write-Error "Configuration.xml не найден в расширении: $ExtensionPath"; exit 1 }
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
function Get-FormatRank([string]$ver) {
if ($ver -match '^(\d+)\.(\d+)$') { return [int]$Matches[1] * 100 + [int]$Matches[2] }
return 0
}
function Detect-FormatVersion([string]$dir) {
$d = $dir
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"
if (Test-Path $cfgPath) {
$cfgText = [System.IO.File]::ReadAllText($cfgPath, [System.Text.Encoding]::UTF8)
# Длину среза берём по СТРОКЕ, а не по размеру файла: размер в БАЙТАХ, Substring считает
# СИМВОЛЫ, и на кириллице байт больше — короткий Configuration.xml ронял навык исключением.
$head = $cfgText.Substring(0, [Math]::Min(2000, $cfgText.Length))
if ($head -match '<MetaDataObject[^>]+version="(\d+\.\d+)"') { return $Matches[1] }
}
$parent = Split-Path $d -Parent
if ($parent -eq $d) { break }
$d = $parent
}
return "2.17"
}
function Build-PropertyStateXml {
param([string]$propertyName, [string]$indent)
$sb = New-Object System.Text.StringBuilder
$sb.AppendLine("${indent}<xr:PropertyState>") | Out-Null
$sb.AppendLine("${indent}`t<xr:Property>${propertyName}</xr:Property>") | Out-Null
$sb.AppendLine("${indent}`t<xr:State>Extended</xr:State>") | Out-Null
$sb.Append("${indent}</xr:PropertyState>") | Out-Null
return $sb.ToString()
}
function Set-PropertyStateFlag {
param([string]$objFile, [string]$propertyName, [string]$formatVersion)
if ((Get-FormatRank $formatVersion) -lt 219) { return }
if (-not (Test-Path $objFile)) { return }
$enc = New-Object System.Text.UTF8Encoding($true)
$text = [System.IO.File]::ReadAllText($objFile, $enc)
$nl = if ($text -match "`r`n") { "`r`n" } else { "`n" }
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
# но они лежат ниже, внутри <ChildObjects>.
$empty = [regex]::Match($text, '([ \t]*)<InternalInfo\s*/>')
$open = [regex]::Match($text, '(?s)([ \t]*)<InternalInfo>(.*?)</InternalInfo>')
if ($empty.Success -and (-not $open.Success -or $empty.Index -lt $open.Index)) {
$ind = $empty.Groups[1].Value
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
$replacement = "${ind}<InternalInfo>${nl}${block}${nl}${ind}</InternalInfo>"
$text = $text.Remove($empty.Index, $empty.Length).Insert($empty.Index, $replacement)
} elseif ($open.Success) {
if ($open.Groups[2].Value -match "<xr:Property>$([regex]::Escape($propertyName))</xr:Property>") { return }
$ind = $open.Groups[1].Value
$block = Build-PropertyStateXml $propertyName ($ind + "`t")
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
$closeAt = $open.Index + $open.Length - "</InternalInfo>".Length - $ind.Length
$text = $text.Insert($closeAt, "${block}${nl}")
} else {
return
}
[System.IO.File]::WriteAllText($objFile, $text, $enc)
}
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
function Get-ModuleFlagTarget {
param([string[]]$relParts, [string]$extRoot)
if ($relParts.Count -ne 4 -or $relParts[2] -ne "Ext") { return $null }
$prop = [System.IO.Path]::GetFileNameWithoutExtension($relParts[3])
return @{
File = (Join-Path (Join-Path $extRoot $relParts[0]) "$($relParts[1]).xml")
Property = $prop
}
}
# --- Read NamePrefix ---
$cfgDoc = New-Object System.Xml.XmlDocument
$cfgDoc.PreserveWhitespace = $false
@@ -1084,6 +1204,12 @@ if ($reuseRegionIdx -ge 0) {
}
}
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
$flagTarget = Get-ModuleFlagTarget $relParts $ExtensionPath
if ($flagTarget) {
Set-PropertyStateFlag $flagTarget.File $flagTarget.Property (Detect-FormatVersion $ExtensionPath)
}
Write-Host "[OK] Перехватчик &$decoratorRu(`"$MethodName`") — $placement"
Write-Host " Файл: $extBsl"
Write-Host " Процедура: $interceptorName($($method.ParamsText -replace '\s+', ' '))"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-patch-method v2.7 — Source-aware method interceptor for 1C extension (CFE) (+прощающий ввод: имя каталога наравне с именем типа)
# cfe-patch-method v2.11 — Source-aware method interceptor for 1C extension (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -75,6 +75,99 @@ CONTEXT_RE = re.compile(
)
# --- Пометка расширенного свойства (<xr:PropertyState>) ---
# Свойство появилось в формате 2.19 (8.3.26): на 2.18 и ниже платформа молча выбрасывает элемент
# при загрузке. С 2.19 Конфигуратор ставит его сам при выгрузке. Правило: флаг ставит тот, кто
# создал файл модуля, — здесь это мы. Имя свойства = базовое имя файла модуля.
# Копии этих функций есть в cfe-borrow (навыки автономны); держать их одинаковыми — сознательно.
def detect_format_version(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")
if os.path.isfile(cfg_path):
with open(cfg_path, "r", encoding="utf-8-sig") as f:
head = f.read(2000)
m = re.search(r'<MetaDataObject[^>]+version="(\d+\.\d+)"', head)
if m:
return m.group(1)
parent = os.path.dirname(d)
if parent == d:
break
d = parent
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 build_property_state_xml(property_name, indent):
return "\n".join([
f"{indent}<xr:PropertyState>",
f"{indent}\t<xr:Property>{property_name}</xr:Property>",
f"{indent}\t<xr:State>Extended</xr:State>",
f"{indent}</xr:PropertyState>",
])
def set_property_state_flag(obj_file, property_name, format_version):
if format_rank(format_version) < 219:
return
if not os.path.isfile(obj_file):
return
with open(obj_file, "r", encoding="utf-8-sig", newline="") as fh:
text = fh.read()
nl = "\r\n" if "\r\n" in text else "\n"
# ПЕРВЫЙ <InternalInfo> в файле — собственный у объекта: у реквизитов и подобъектов свои,
# но они лежат ниже, внутри <ChildObjects>.
empty = re.search(r"([ \t]*)<InternalInfo\s*/>", text)
opened = re.search(r"([ \t]*)<InternalInfo>(.*?)</InternalInfo>", text, re.S)
if empty and (not opened or empty.start() < opened.start()):
ind = empty.group(1)
block = build_property_state_xml(property_name, ind + "\t")
replacement = f"{ind}<InternalInfo>{nl}{block}{nl}{ind}</InternalInfo>"
text = text[:empty.start()] + replacement + text[empty.end():]
elif opened:
if re.search(rf"<xr:Property>{re.escape(property_name)}</xr:Property>", opened.group(2)):
return
ind = opened.group(1)
block = build_property_state_xml(property_name, ind + "\t")
# Дописываем в КОНЕЦ InternalInfo: у Конфигуратора PropertyState идёт после GeneratedType.
close_at = opened.end() - len("</InternalInfo>") - len(ind)
text = text[:close_at] + block + nl + text[close_at:]
else:
return
with open(obj_file, "w", encoding="utf-8-sig", newline="") as fh:
fh.write(text)
# Модуль формы сюда не попадает: у формы флаг называется Form и ставится при заимствовании,
# а не при появлении модуля (замер на 8.3.26 — пустой модуль формы платформа не выгружает).
def get_module_flag_target(rel_parts, ext_root):
if len(rel_parts) != 4 or rel_parts[2] != "Ext":
return None
prop = os.path.splitext(rel_parts[3])[0]
return {
"file": os.path.join(ext_root, rel_parts[0], f"{rel_parts[1]}.xml"),
"property": prop,
}
def get_module_rel_path(module_path):
parts = module_path.split(".")
if len(parts) < 2:
@@ -398,6 +491,21 @@ def normalize(line):
return re.sub(r'\s+', ' ', line).strip()
# Control comparison key, as the platform compares a &ИзменениеИКонтроль copy with the original:
# each line trimmed, blank lines dropped, everything else byte-for-byte and case-sensitive
# (inner spaces, comments and letter case are significant). Measured on 8.3.24 and 8.3.27.
def control_key(lines):
return "\n".join([k for k in (x.strip() for x in lines) if k != ""])
# Parameter count of a signature params text. The platform compares only the number of
# parameters of a &ИзменениеИКонтроль copy (names and default values it ignores).
def param_count(params_text):
if not params_text or not params_text.strip():
return 0
return len([p for p in split_top_level(params_text) if p.strip()])
def parse_marked_body(body_lines):
v1 = []
ops = []
@@ -841,6 +949,12 @@ def main():
place_new(ext_bsl, ext_lines, ext_exists, method["chain"], core)
# Модуль в расширении есть — отражаем это в метаданных объекта (формат ≥ 2.19).
flag_target = get_module_flag_target(rel_parts, extension_path)
if flag_target:
set_property_state_flag(flag_target["file"], flag_target["property"],
detect_format_version(extension_path))
# emit summary
placement = place_new.placement
print('[OK] Перехватчик &%s("%s") — %s' % (decorator_ru, method_name, placement))
@@ -1030,7 +1144,7 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
sig = read_signature(ext_lines, sig_line_idx)
if not sig:
return {"id": method_id, "status": "ОШИБКА", "ext_bsl": ext_bsl, "reason": "не разобрать сигнатуру"}
_params, sig_end = sig
ext_params_text, sig_end = sig
is_func = bool(re.match(r'^\s*(?:Асинх\s+)?Функция\b', ext_lines[sig_line_idx], re.IGNORECASE))
end_re = re.compile(r'^\s*КонецФункции\b' if is_func else r'^\s*КонецПроцедуры\b', re.IGNORECASE)
block_end = -1
@@ -1047,7 +1161,15 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
v1norm = [normalize(x) for x in v1]
v2norm = [normalize(x) for x in v2]
if "\n".join(v1norm) == "\n".join(v2norm):
# Signature: the platform rejects the interceptor when the parameter COUNT differs, so a
# body-only comparison would miss a vendor-added parameter. Names/defaults it ignores.
ext_param_count = param_count(ext_params_text)
src_param_count = param_count(method["params_text"])
params_drift = ext_param_count != src_param_count
params_reason = ("список параметров: в оригинале %d, в перехватчике %d"
% (src_param_count, ext_param_count)) if params_drift else ""
if not params_drift and control_key(v1) == control_key(v2):
return {"id": method_id, "status": "АКТУАЛЕН", "ext_bsl": ext_bsl}
insert_top = []; insert_after = {}; del_start = set(); del_end = set(); disputed = []; transferred = 0; absorbed = 0; absorbed_notes = []
@@ -1109,7 +1231,11 @@ def resync_one(ext_bsl, ext_lines, dup, method, logical_module, conflict_folder,
st = "ПЕРЕНЕСЕНО В ОСНОВНУЮ"
else:
st = "ДРЕЙФ"
if params_drift and st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ":
st = "ДРЕЙФ"
rsn = conflict_reason(disputed) if disputed else ("все правки уже в основной конфигурации" if st == "ПЕРЕНЕСЕНО В ОСНОВНУЮ" else "")
if params_drift:
rsn = ("%s; %s" % (params_reason, rsn)) if rsn else params_reason
return {"id": method_id, "status": st, "ext_bsl": ext_bsl, "transferred": transferred,
"absorbed": absorbed, "disputed": len(disputed), "reason": rsn, "absorbed_notes": absorbed_notes}
+3 -1
View File
@@ -10,7 +10,9 @@ allowed-tools:
# /cfe-validate — валидация расширения конфигурации (CFE)
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты. Аналог `/cf-validate`, но для расширений.
Проверяет структурную корректность расширения: XML-формат, свойства, состав, заимствованные объекты, права ролей. Аналог `/cf-validate`, но для расширений.
Проверяются исходники. Применимость — уже после загрузки в базу: `/db-cfe-admin check`.
## Параметры
@@ -1,7 +1,8 @@
# cfe-validate v1.10 — Validate 1C configuration extension structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# cfe-validate v1.16 — Validate 1C configuration extension structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Parameter(Mandatory, Position=0)]
[Alias('Path')]
[string]$ExtensionPath,
@@ -107,7 +108,28 @@ function Get-FormatRank([string]$ver) {
}
# --- Reference tables ---
$guidPattern = '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
$moduleKindsByType = @{
"CommonModule"=@("Module"); "HTTPService"=@("Module"); "WebService"=@("Module")
"Catalog"=@("ObjectModule","ManagerModule"); "Document"=@("ObjectModule","ManagerModule")
"Report"=@("ObjectModule","ManagerModule"); "DataProcessor"=@("ObjectModule","ManagerModule")
"ExchangePlan"=@("ObjectModule","ManagerModule")
"ChartOfCharacteristicTypes"=@("ObjectModule","ManagerModule")
"ChartOfAccounts"=@("ObjectModule","ManagerModule")
"ChartOfCalculationTypes"=@("ObjectModule","ManagerModule")
"BusinessProcess"=@("ObjectModule","ManagerModule"); "Task"=@("ObjectModule","ManagerModule")
"InformationRegister"=@("RecordSetModule","ManagerModule")
"AccumulationRegister"=@("RecordSetModule","ManagerModule")
"AccountingRegister"=@("RecordSetModule","ManagerModule")
"CalculationRegister"=@("RecordSetModule","ManagerModule")
"Sequence"=@("RecordSetModule","ManagerModule")
"Constant"=@("ValueManagerModule","ManagerModule")
"Enum"=@("ManagerModule"); "DocumentJournal"=@("ManagerModule")
"FilterCriterion"=@("ManagerModule")
}
$guidPattern ='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
$identPattern = '^[A-Za-z\u0410-\u042F\u0401\u0430-\u044F\u0451_][A-Za-z0-9\u0410-\u042F\u0401\u0430-\u044F\u0451_]*$'
# 7 fixed ClassIds for Configuration
@@ -121,25 +143,25 @@ $validClassIds = @(
"fb282519-d103-4dd3-bc12-cb271d631dfc"
)
# 44 types in canonical order
# 46 types in canonical order
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","Bot","CommonAttribute","ExchangePlan",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","CommonCommand","CommandGroup",
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","IntegrationService"
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
)
# Type -> directory mapping
$childTypeDirMap = @{
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"
"Language"="Languages"; "Subsystem"="Subsystems"; "StyleItem"="StyleItems"; "Style"="Styles"; "Bot"="Bots"; "PaletteColor"="PaletteColors"
"CommonPicture"="CommonPictures"; "SessionParameter"="SessionParameters"; "Role"="Roles"
"CommonTemplate"="CommonTemplates"; "FilterCriterion"="FilterCriteria"; "CommonModule"="CommonModules"
"CommonAttribute"="CommonAttributes"; "ExchangePlan"="ExchangePlans"; "XDTOPackage"="XDTOPackages"
@@ -158,7 +180,7 @@ $childTypeDirMap = @{
"ChartOfCalculationTypes"="ChartsOfCalculationTypes"
"CalculationRegister"="CalculationRegisters"
"BusinessProcess"="BusinessProcesses"; "Task"="Tasks"
"IntegrationService"="IntegrationServices"
"ExternalDataSource"="ExternalDataSources"; "IntegrationService"="IntegrationServices"
}
# Наборы GeneratedType по типу объекта (эталон — таблица §2.5 спецификации конфигурации).
@@ -186,6 +208,7 @@ $generatedTypeCategories = @{
"Sequence" = @("Record","Manager","RecordSet")
"FilterCriterion" = @("Manager","List")
"SettingsStorage" = @("Manager")
"ExternalDataSource" = @("Manager","TablesManager","CubesManager")
"IntegrationService" = @("Manager")
"WSReference" = @("Manager")
"DefinedType" = @("DefinedType")
@@ -1169,8 +1192,113 @@ if (-not $script:stopped -and $script:borrowedFormsWithTree.Count -gt 0) {
}
}
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
$defaultRoleNodes = @($cfgNode.SelectNodes("md:Properties/md:DefaultRoles/xr:Item", $ns))
if ($defaultRoleNodes.Count -gt 0) {
$adoptedCache = @{}
function Test-ObjectAdopted {
param([string]$typeName, [string]$objName)
$key = "$typeName.$objName"
if ($adoptedCache.ContainsKey($key)) { return $adoptedCache[$key] }
$adoptedCache[$key] = $false
if ($childTypeDirMap.ContainsKey($typeName)) {
$objPath = Join-Path (Join-Path $configDir $childTypeDirMap[$typeName]) "$objName.xml"
if (Test-Path $objPath) {
try {
$objDoc = New-Object System.Xml.XmlDocument
$objDoc.Load($objPath)
$objNs = New-Object System.Xml.XmlNamespaceManager($objDoc.NameTable)
$objNs.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$ob = $objDoc.SelectSingleNode("/md:MetaDataObject/md:$typeName/md:Properties/md:ObjectBelonging", $objNs)
if ($ob -and $ob.InnerText -eq "Adopted") { $adoptedCache[$key] = $true }
} catch {}
}
}
return $adoptedCache[$key]
}
$check15Ok = $true
$check15Count = 0
foreach ($rn in $defaultRoleNodes) {
$roleRef = $rn.InnerText
if ($roleRef -notmatch '^Role\.(.+)$') { continue }
$defRoleName = $Matches[1]
$rightsPath = Join-Path (Join-Path (Join-Path $configDir "Roles") $defRoleName) "Ext\Rights.xml"
if (-not (Test-Path $rightsPath)) { continue }
try {
$rDoc = New-Object System.Xml.XmlDocument
$rDoc.Load($rightsPath)
} catch {
continue
}
$rNs = New-Object System.Xml.XmlNamespaceManager($rDoc.NameTable)
$rNs.AddNamespace("r", "http://v8.1c.ru/8.2/roles")
foreach ($nameNode in $rDoc.SelectNodes("/r:Rights/r:object/r:name", $rNs)) {
$fullName = $nameNode.InnerText
$segs = $fullName.Split(".")
# Configuration.* — права самого расширения, не объект; заимствования там нет.
if ($segs.Count -lt 2 -or $segs[0] -eq "Configuration") { continue }
$check15Count++
if (Test-ObjectAdopted $segs[0] $segs[1]) {
Report-Error ("15. Роль '$defRoleName' входит в DefaultRoles и даёт права на заимствованный $($segs[0]).$($segs[1]) " +
"($fullName): платформа это запрещает. Вынесите такие права в отдельную роль вне DefaultRoles.")
$check15Ok = $false
}
}
}
if ($check15Ok -and $check15Count -gt 0) {
Report-OK "15. Основные роли: прав на заимствованные объекты нет ($check15Count checked)"
}
}
if ($script:stopped) { & $finalize; exit 1 }
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на стенде),
# но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
if ($versionRank -ge 219 -and $childObjNode) {
$stateIssues = @()
$stateChecked = 0
foreach ($child in $childObjNode.ChildNodes) {
if ($child.NodeType -ne 'Element') { continue }
$typeName = $child.LocalName
if (-not $moduleKindsByType.ContainsKey($typeName)) { continue }
if (-not $childTypeDirMap.ContainsKey($typeName)) { continue }
$stateObjName = $child.InnerText.Trim()
if (-not $stateObjName) { continue }
$typeDir = Join-Path $configDir $childTypeDirMap[$typeName]
$objFile = Join-Path $typeDir "$stateObjName.xml"
if (-not (Test-Path $objFile)) { continue }
$objText = [System.IO.File]::ReadAllText($objFile, [System.Text.Encoding]::UTF8)
if ($objText -notmatch '<ObjectBelonging>Adopted</ObjectBelonging>') { continue }
foreach ($kind in $moduleKindsByType[$typeName]) {
$stateChecked++
$hasFile = Test-Path (Join-Path (Join-Path (Join-Path $typeDir $stateObjName) "Ext") "$kind.bsl")
$hasFlag = $objText -match "<xr:Property>$kind</xr:Property>"
if ($hasFile -and -not $hasFlag) {
$stateIssues += "$typeName.$stateObjName — есть $kind.bsl, но нет <xr:PropertyState> для $kind"
} elseif ($hasFlag -and -not $hasFile) {
$stateIssues += "$typeName.$stateObjName — есть <xr:PropertyState> для $kind, но нет $kind.bsl"
}
}
}
if ($stateChecked -gt 0) {
if ($stateIssues.Count -eq 0) {
Report-OK "16. Модули заимствованных объектов: пометки расширенных свойств согласованы ($stateChecked)"
} else {
foreach ($issue in $stateIssues) { Report-Warn "16. $issue" }
}
}
}
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
$extRootDir = Split-Path $resolvedPath -Parent
$ctrlCount = 0
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# cfe-validate v1.10 — Validate 1C configuration extension XML structure (CFE) (полнота GeneratedType, ТЧ из AdditionalColumns, сверка путей с -ConfigPath)
# cfe-validate v1.16 — Validate 1C configuration extension XML structure (CFE)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates extension Configuration.xml: root, InternalInfo, extension properties, ChildObjects, borrowed objects."""
import sys, os, argparse, re
@@ -55,28 +55,49 @@ VALID_CLASS_IDS = [
'fb282519-d103-4dd3-bc12-cb271d631dfc',
]
# 44 types in canonical order
# 46 types in canonical order
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'Bot', 'CommonAttribute', 'ExchangePlan',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'CommonCommand', 'CommandGroup',
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
'Constant', 'CommonForm', 'Catalog', 'Document',
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
'ChartOfCalculationTypes', 'CalculationRegister',
'BusinessProcess', 'Task', 'IntegrationService',
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
]
# Модули заимствованных объектов: тип → виды модулей. Имя свойства в <xr:PropertyState>
# совпадает с базовым именем файла модуля. Копия таблицы есть в cfe-borrow (навыки автономны).
MODULE_KINDS_BY_TYPE = {
"CommonModule": ["Module"], "HTTPService": ["Module"], "WebService": ["Module"],
"Catalog": ["ObjectModule", "ManagerModule"], "Document": ["ObjectModule", "ManagerModule"],
"Report": ["ObjectModule", "ManagerModule"], "DataProcessor": ["ObjectModule", "ManagerModule"],
"ExchangePlan": ["ObjectModule", "ManagerModule"],
"ChartOfCharacteristicTypes": ["ObjectModule", "ManagerModule"],
"ChartOfAccounts": ["ObjectModule", "ManagerModule"],
"ChartOfCalculationTypes": ["ObjectModule", "ManagerModule"],
"BusinessProcess": ["ObjectModule", "ManagerModule"], "Task": ["ObjectModule", "ManagerModule"],
"InformationRegister": ["RecordSetModule", "ManagerModule"],
"AccumulationRegister": ["RecordSetModule", "ManagerModule"],
"AccountingRegister": ["RecordSetModule", "ManagerModule"],
"CalculationRegister": ["RecordSetModule", "ManagerModule"],
"Sequence": ["RecordSetModule", "ManagerModule"],
"Constant": ["ValueManagerModule", "ManagerModule"],
"Enum": ["ManagerModule"], "DocumentJournal": ["ManagerModule"],
"FilterCriterion": ["ManagerModule"],
}
# Type -> directory mapping
CHILD_TYPE_DIR_MAP = {
'Language': 'Languages', 'Subsystem': 'Subsystems', 'StyleItem': 'StyleItems', 'Style': 'Styles',
'CommonPicture': 'CommonPictures', 'SessionParameter': 'SessionParameters', 'Role': 'Roles',
'CommonTemplate': 'CommonTemplates', 'FilterCriterion': 'FilterCriteria', 'CommonModule': 'CommonModules',
'Bot': 'Bots',
'Bot': 'Bots', 'PaletteColor': 'PaletteColors',
'CommonAttribute': 'CommonAttributes', 'ExchangePlan': 'ExchangePlans', 'XDTOPackage': 'XDTOPackages',
'WebService': 'WebServices', 'HTTPService': 'HTTPServices', 'WSReference': 'WSReferences',
'EventSubscription': 'EventSubscriptions', 'ScheduledJob': 'ScheduledJobs',
@@ -93,6 +114,7 @@ CHILD_TYPE_DIR_MAP = {
'ChartOfCalculationTypes': 'ChartsOfCalculationTypes',
'CalculationRegister': 'CalculationRegisters',
'BusinessProcess': 'BusinessProcesses', 'Task': 'Tasks',
'ExternalDataSource': 'ExternalDataSources',
'IntegrationService': 'IntegrationServices',
}
@@ -121,6 +143,7 @@ GENERATED_TYPE_CATEGORIES = {
'Sequence': ['Record', 'Manager', 'RecordSet'],
'FilterCriterion': ['Manager', 'List'],
'SettingsStorage': ['Manager'],
'ExternalDataSource': ['Manager', 'TablesManager', 'CubesManager'],
'IntegrationService': ['Manager'],
'WSReference': ['Manager'],
'DefinedType': ['DefinedType'],
@@ -1141,10 +1164,108 @@ def main():
if check14_ok:
r.ok(f'14. Object paths vs source config: {path_check_count} checked')
# --- Check 15: основные роли расширения не дают прав на заимствованные объекты ---
# Платформа: «Назначение прав доступа на заимствованные объекты основными ролями в
# расширениях недопустимо». Роль вне <DefaultRoles> так делать вправе — проверяем только
# основные. Ловится статически, а по симптому (отказ загрузки) причина не читается.
default_role_nodes = cfg_node.findall('md:Properties/md:DefaultRoles/xr:Item', NS)
if default_role_nodes:
adopted_cache = {}
def is_object_adopted(type_name, obj_name):
key = f"{type_name}.{obj_name}"
if key in adopted_cache:
return adopted_cache[key]
adopted_cache[key] = False
dir_name = CHILD_TYPE_DIR_MAP.get(type_name)
if dir_name:
obj_path = os.path.join(config_dir, dir_name, obj_name + '.xml')
if os.path.isfile(obj_path):
try:
obj_root = etree.parse(obj_path).getroot()
ob = obj_root.find(f'md:{type_name}/md:Properties/md:ObjectBelonging', NS)
if ob is not None and (ob.text or '') == 'Adopted':
adopted_cache[key] = True
except Exception:
pass
return adopted_cache[key]
check15_ok = True
check15_count = 0
roles_ns = {'r': 'http://v8.1c.ru/8.2/roles'}
for rn in default_role_nodes:
m = re.match(r'^Role\.(.+)$', rn.text or '')
if not m:
continue
def_role_name = m.group(1)
rights_path = os.path.join(config_dir, 'Roles', def_role_name, 'Ext', 'Rights.xml')
if not os.path.isfile(rights_path):
continue
try:
rights_root = etree.parse(rights_path).getroot()
except Exception:
continue
for name_node in rights_root.findall('r:object/r:name', roles_ns):
full_name = name_node.text or ''
segs = full_name.split('.')
# Configuration.* — права самого расширения, не объект; заимствования там нет.
if len(segs) < 2 or segs[0] == 'Configuration':
continue
check15_count += 1
if is_object_adopted(segs[0], segs[1]):
r.error(f"15. Роль '{def_role_name}' входит в DefaultRoles и даёт права на заимствованный "
f"{segs[0]}.{segs[1]} ({full_name}): платформа это запрещает. "
"Вынесите такие права в отдельную роль вне DefaultRoles.")
check15_ok = False
if check15_ok and check15_count > 0:
r.ok(f'15. Основные роли: прав на заимствованные объекты нет ({check15_count} checked)')
if r.stopped:
r.finalize(out_file)
sys.exit(1)
# --- Check 16: модуль заимствованного объекта и пометка расширенного свойства ---
# Свойство <xr:PropertyState> появилось в формате 2.19 (8.3.26); ниже платформа его молча
# выбрасывает, поэтому там проверять нечего. С 2.19 состояние обязано соответствовать факту:
# есть файл модуля — есть пометка, и наоборот. Перекос платформа принимает (проверено на
# стенде), но выгрузка Конфигуратора так не выглядит — отсюда предупреждение, а не ошибка.
if version_rank >= 219 and child_obj_node is not None:
state_issues = []
state_checked = 0
for child in child_obj_node:
if not isinstance(child.tag, str):
continue
type_name = etree.QName(child.tag).localname
if type_name not in MODULE_KINDS_BY_TYPE or type_name not in CHILD_TYPE_DIR_MAP:
continue
obj_name_val = (child.text or '').strip()
if not obj_name_val:
continue
type_dir = os.path.join(config_dir, CHILD_TYPE_DIR_MAP[type_name])
obj_file = os.path.join(type_dir, f'{obj_name_val}.xml')
if not os.path.isfile(obj_file):
continue
with open(obj_file, 'r', encoding='utf-8-sig') as f:
obj_text = f.read()
if '<ObjectBelonging>Adopted</ObjectBelonging>' not in obj_text:
continue
for kind in MODULE_KINDS_BY_TYPE[type_name]:
state_checked += 1
has_file = os.path.isfile(os.path.join(type_dir, obj_name_val, 'Ext', f'{kind}.bsl'))
has_flag = f'<xr:Property>{kind}</xr:Property>' in obj_text
if has_file and not has_flag:
state_issues.append(f'{type_name}.{obj_name_val} — есть {kind}.bsl, но нет <xr:PropertyState> для {kind}')
elif has_flag and not has_file:
state_issues.append(f'{type_name}.{obj_name_val} — есть <xr:PropertyState> для {kind}, но нет {kind}.bsl')
if state_checked > 0:
if not state_issues:
r.ok(f'16. Модули заимствованных объектов: пометки расширенных свойств согласованы ({state_checked})')
else:
for issue in state_issues:
r.warn(f'16. {issue}')
# --- Breadcrumb: controlled methods (&ИзменениеИКонтроль) drift is not checked here ---
ctrl_count = 0
for dp, _dn, files in os.walk(config_dir):
+119
View File
@@ -0,0 +1,119 @@
---
name: db-cfe-admin
description: Управление расширениями конфигурации в информационной базе 1С. Используй когда нужно узнать какие расширения подключены к базе, выполнить проверку применимости или синтаксическую проверку, изменить безопасный режим или активность, удалить расширение из базы
argument-hint: <list|check|set-properties|delete> [database] [-Name <Имя>]
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-cfe-admin — Управление расширениями конфигурации
Расширения **на стороне базы**: состав и свойства подключения, проверки, удаление.
Про исходники расширения — другие навыки, см. «Смежное».
## Usage
```
/db-cfe-admin list [database]
/db-cfe-admin check [database] [-Name Расш1]
/db-cfe-admin set-properties [database] -Name Расш1 -SafeMode off
/db-cfe-admin delete [database] -Name Расш1
```
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` (путь к платформе) и разреши базу:
1. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name в `.v8-project.json`
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Если `v8path` не задан — скрипт сам попытается определить платформу.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-cfe-admin.ps1" -Command <команда> <параметры>
```
### Общие параметры
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-Command <команда>` | да | `list` / `check` / `set-properties` / `delete` |
| `-V8Path <путь>` | нет | Каталог bin платформы или полный путь к `1cv8.exe` / `ibcmd.exe` |
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С (для серверной базы) |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Пользователь базы |
| `-Password <пароль>` | нет | Пароль пользователя |
| `-Name <имя>` | усл. | Расширение. Обязателен для `set-properties`; в `delete` — вместо `-All`. Без него `list` и `check` работают по всем расширениям |
| `-All` | усл. | Только для `delete`: удалить все расширения. Вместо `-Name`, а не вместе с ним |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### Параметры `check`
| Параметр | Описание |
|----------|----------|
| `-Checks <список>` | `apply` — применимость расширения, `modules` — синтаксическая проверка, `config` — проверки конфигурации (целостность, ссылки, неиспользуемые процедуры и обработчики). Через запятую, по умолчанию `apply,modules` |
| `-Context <список>` | Контексты синтаксической проверки: `ThinClient`, `WebClient`, `Server`, `ExternalConnection`, `ThickClientManagedApplication`, `ThickClientOrdinaryApplication`, `MobileClient` и др. Через запятую, по умолчанию `ThinClient,Server` |
Применимость и синтаксис проверяют разное и друг друга не заменяют. Проверяется только
расширение: ошибки самой конфигурации сюда не попадают.
### Свойства `set-properties`
| Параметр | Описание |
|----------|----------|
| `-SafeMode <on/off>` | Безопасный режим |
| `-Active <on/off>` | Активность расширения |
| `-UnsafeActionProtection <on/off>` | Защита от опасных действий |
| `-UsedInDistributedInfobase <on/off>` | Использование в распределённой ИБ |
| `-Scope <область>` | `infobase` / `data-separation` |
| `-SecurityProfile <имя>` | Профиль безопасности |
Передавай только то, что меняешь: не указанное свойство остаётся как было. Расширение, впервые
попавшее в базу загрузкой, создаётся с включённым безопасным режимом, а в нём расширение модуля не
применяется.
Свойствами управляет `ibcmd` — он есть не в каждой установке платформы и работает с файловой базой;
остальные команды работают всегда.
## Смежное
| Задача | Навык |
|--------|-------|
| Создать расширение, заимствовать объекты, перехватить метод | `/cfe-init`, `/cfe-borrow`, `/cfe-patch-method`, `/cfe-validate` |
| Загрузить исходники расширения в базу | `/db-load-xml -Extension` (из коммита Git — `/db-load-git`) |
| Загрузить готовый `.cfe` | `/db-load-cf -Extension` |
| Выгрузить расширение из базы | `/db-dump-xml -Extension`, `/db-dump-cf -Extension` |
| Обновить конфигурацию базы после загрузки | `/db-update -Extension` |
| Проверить дрейф контролируемых методов по исходникам | `/cfe-patch-method -Check` |
## Примеры
```powershell
# Что подключено к базе
... -Command list -InfoBasePath "C:\Bases\MyDB"
# Проверить расширение
... -Command check -InfoBasePath "C:\Bases\MyDB" -Name "Расш1"
# Синтаксическая проверка в контексте веб-клиента
... -Command check -InfoBasePath "C:\Bases\MyDB" -Checks modules -Context WebClient,Server
# Снять безопасный режим
... -Command set-properties -InfoBasePath "C:\Bases\MyDB" -Name "Расш1" -SafeMode off
# Отключить, не удаляя
... -Command set-properties -InfoBasePath "C:\Bases\MyDB" -Name "Расш1" -Active off
# Убрать расширение из базы
... -Command delete -InfoBasePath "C:\Bases\MyDB" -Name "Расш1"
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -1,4 +1,4 @@
# db-create v1.11 — Create 1C information base
# db-create v1.14 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -46,7 +46,7 @@
.\db-create.ps1 -InfoBasePath "C:\Bases\NewDB" -UseTemplate "C:\Templates\config.cf" -AddToList -ListName "Новая база"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -88,6 +88,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -138,6 +149,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
+50 -17
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-create v1.11 — Create 1C information base
# db-create v1.14 — Create 1C information base
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -292,7 +307,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -313,7 +328,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -332,11 +347,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -401,15 +436,15 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
sys.exit(1)
# --- Validate template ---
if args.UseTemplate and not os.path.exists(args.UseTemplate):
print(f"Error: template file not found: {args.UseTemplate}", file=sys.stderr)
print(f"Error: template file not found: {args.UseTemplate}")
sys.exit(1)
# --- ibcmd branch (file infobase only) ---
@@ -436,10 +471,9 @@ def main():
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(f"Error creating information base (code: {exit_code})")
print_platform_output(result)
sys.exit(exit_code)
@@ -496,10 +530,9 @@ def main():
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(f"Error creating information base (code: {exit_code})")
if os.path.isfile(out_file):
try:
@@ -1,4 +1,4 @@
# db-dump-cf v1.13 — Dump 1C configuration to CF file
# db-dump-cf v1.16 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -49,7 +49,7 @@
.\db-dump-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "ext.cfe" -Extension "МоёРасширение"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -104,6 +104,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -154,6 +165,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
+53 -18
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-cf v1.13 — Dump 1C configuration to CF file
# db-dump-cf v1.16 — Dump 1C configuration to CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -422,10 +457,10 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
sys.exit(1)
# --- Ensure output directory exists ---
@@ -436,7 +471,7 @@ def main():
# --- 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)
print("Error: ibcmd config save does not support -AllExtensions (use -Extension)")
sys.exit(1)
arguments = ["infobase", "config", "save", f"--db-path={args.InfoBasePath}"]
if args.Extension:
@@ -459,9 +494,9 @@ def main():
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)
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
print(f"Error dumping configuration (code: {exit_code})")
sys.exit(exit_code)
# --- Temp dir ---
@@ -509,9 +544,9 @@ def main():
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)
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — configuration was not dumped")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
print(f"Error dumping configuration (code: {exit_code})")
if os.path.isfile(out_file):
try:
@@ -1,4 +1,4 @@
# db-dump-dt v1.12 — Dump 1C information base to DT file
# db-dump-dt v1.15 — 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 не исполняется.
<#
@@ -39,7 +39,7 @@
.\db-dump-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -OutputFile "backup.dt"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -88,6 +88,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -138,6 +149,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
+52 -17
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-dt v1.12 — Dump 1C information base to DT file
# db-dump-dt v1.15 — Dump 1C information base to DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -420,10 +455,10 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
sys.exit(1)
# --- Ensure output directory exists ---
@@ -452,9 +487,9 @@ def main():
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)
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped")
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
print(f"Error dumping information base (code: {exit_code})")
sys.exit(exit_code)
# --- Temp dir ---
@@ -496,9 +531,9 @@ def main():
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)
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — information base was not dumped")
else:
print(f"Error dumping information base (code: {exit_code})", file=sys.stderr)
print(f"Error dumping information base (code: {exit_code})")
if os.path.isfile(out_file):
try:
+1
View File
@@ -33,6 +33,7 @@ allowed-tools:
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог выгрузки по умолчанию.
Для `-Extension` каталог берётся из `extensions[].src` записи базы, если он там указан.
## Команда
@@ -1,4 +1,4 @@
# db-dump-xml v1.15 — Dump 1C configuration to XML files
# db-dump-xml v1.21 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -61,7 +61,7 @@
.\db-dump-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Objects "Справочник.Номенклатура,Документ.Заказ"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -85,8 +85,10 @@ param(
[string]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "Changes",
# Пустое значение = режим не задан. Прежнее умолчание Changes подставляется ниже, после
# того как станет видно, перечислены ли объекты.
[ValidateSet("", "Full", "Changes", "Partial", "UpdateInfo")]
[string]$Mode = "",
[Parameter(Mandatory=$false)]
[string]$Objects,
@@ -101,6 +103,18 @@ param(
[ValidateSet("Hierarchical", "Plain")]
[string]$Format = "Hierarchical",
[Parameter(Mandatory=$false)]
[string]$ObjectsFile,
[Parameter(Mandatory=$false)]
[string]$RepositoryPath,
[Parameter(Mandatory=$false)]
[string]$RepositoryUser,
[Parameter(Mandatory=$false)]
[string]$RepositoryPassword,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
@@ -111,6 +125,90 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
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 Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
function Resolve-RepositorySettings {
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
$dbRec = Find-ProjectDatabase
$rec = $null
if ($dbRec) {
if ($Extension) {
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
if ($dbRec.extensions) {
foreach ($ext in $dbRec.extensions) {
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
$rec = $ext.repository
break
}
}
}
} else {
$rec = $dbRec.repository
}
}
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
return @{
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
User = $user
Password = $pwd
FromRegistry = [bool]($rec -and $rec.path)
DbRecord = $dbRec
}
}
function Get-RepositoryArgs {
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
param([hashtable]$Repo)
$a = @()
if (-not $Repo -or -not $Repo.Path) { return $a }
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
return $a
}
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
@@ -127,12 +225,23 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
@@ -177,6 +286,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -415,8 +532,33 @@ if ($engine -eq "ibcmd") {
}
# --- Validate Partial mode ---
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
if ($ObjectsFile) {
if (-not (Test-Path $ObjectsFile)) {
Write-Host "Error: -ObjectsFile not found: $ObjectsFile" -ForegroundColor Red
exit 1
}
$fromFile = @([System.IO.File]::ReadAllLines($ObjectsFile, [System.Text.Encoding]::UTF8) |
ForEach-Object { $_.Trim() } | Where-Object { $_ -and -not $_.StartsWith('#') })
$Objects = (@(@($Objects -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + $fromFile) -join ',')
}
# Перечислены объекты — операция частичная. Иначе список молча игнорировался бы: умолчание
# Changes выгружает «изменённое с прошлой выгрузки», а не то, что просили.
if ($Objects) {
if ($Mode -eq "UpdateInfo") {
# Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов.
Write-Host "Error: -Mode UpdateInfo does not take an object list — it only refreshes ConfigDumpInfo.xml" -ForegroundColor Red
exit 1
}
if ($Mode -eq "Full" -or $Mode -eq "Changes") {
Write-Host "[note] перечислены объекты — выгружаются только они; -Mode $Mode не применён" -ForegroundColor Yellow
}
$Mode = "Partial"
}
if (-not $Mode) { $Mode = "Changes" }
if ($Mode -eq "Partial" -and -not $Objects) {
Write-Host "Error: -Objects required for Partial mode" -ForegroundColor Red
Write-Host "Error: -Objects or -ObjectsFile required for Partial mode" -ForegroundColor Red
exit 1
}
@@ -486,6 +628,11 @@ try {
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
$__repo = Resolve-RepositorySettings
$arguments += Get-RepositoryArgs $__repo
$arguments += "/DumpConfigToFiles", "`"$ConfigDir`""
$arguments += "-Format", $Format
@@ -530,7 +677,7 @@ try {
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
+178 -24
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-dump-xml v1.15 — Dump 1C configuration to XML files
# db-dump-xml v1.21 — Dump 1C configuration to XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,15 +67,111 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
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"]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
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 same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def resolve_repository_settings(args):
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
db_rec = find_project_database(args)
rec = None
if db_rec:
if args.Extension:
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
for ext in db_rec.get("extensions") or []:
if (ext.get("name") or "").lower() == args.Extension.lower():
rec = ext.get("repository")
break
else:
rec = db_rec.get("repository")
path = args.RepositoryPath or ((rec or {}).get("path") or None)
user = args.RepositoryUser or ((rec or {}).get("user") or None)
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
return {
"path": path.strip().strip('"') if path else None,
"user": user,
"password": pwd,
"from_registry": bool(rec and rec.get("path")),
}
def repository_args(repo):
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
a = []
if not repo or not repo.get("path"):
return a
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
if repo.get("user"):
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
if repo.get("password"):
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
return a
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
@@ -120,15 +216,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +298,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +345,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +383,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +400,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +419,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -352,7 +472,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -388,14 +508,18 @@ def main():
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-RepositoryPath", default="")
parser.add_argument("-RepositoryUser", default="")
parser.add_argument("-RepositoryPassword", default="")
parser.add_argument("-ConfigDir", required=True, help="Directory for configuration dump")
parser.add_argument(
"-Mode",
default="Changes",
choices=["Full", "Changes", "Partial", "UpdateInfo"],
default="",
choices=["", "Full", "Changes", "Partial", "UpdateInfo"],
help="Dump mode (default: Changes)",
)
parser.add_argument("-Objects", default="", help="Comma-separated metadata object names (for Partial mode)")
parser.add_argument("-ObjectsFile", default="")
parser.add_argument("-Extension", default="", help="Extension name to dump")
parser.add_argument("-AllExtensions", action="store_true", help="Dump all extensions")
parser.add_argument(
@@ -436,15 +560,40 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
sys.exit(1)
# --- Validate Partial mode ---
# Список объектов приходит либо строкой, либо файлом: файл нужен, чтобы не перепечатывать
# то, что уже напечатал другой навык (например /db-repo update со списком полученных объектов).
if args.ObjectsFile:
if not os.path.exists(args.ObjectsFile):
print("Error: -ObjectsFile not found: %s" % args.ObjectsFile)
sys.exit(1)
with open(args.ObjectsFile, encoding="utf-8-sig") as f:
from_file = [s.strip() for s in f.read().splitlines()
if s.strip() and not s.strip().startswith("#")]
inline = [s.strip() for s in args.Objects.split(",") if s.strip()]
args.Objects = ",".join(inline + from_file)
# Перечислены объекты — операция частичная. Иначе список молча игнорировался бы: умолчание
# Changes выгружает «изменённое с прошлой выгрузки», а не то, что просили.
if args.Objects:
if args.Mode == "UpdateInfo":
# Не «шире/уже», а другая операция: обновление ConfigDumpInfo без выгрузки файлов.
print("Error: -Mode UpdateInfo does not take an object list — it only refreshes "
"ConfigDumpInfo.xml")
sys.exit(1)
if args.Mode in ("Full", "Changes"):
print("[note] перечислены объекты — выгружаются только они; -Mode %s не применён"
% args.Mode)
args.Mode = "Partial"
if not args.Mode:
args.Mode = "Changes"
if args.Mode == "Partial" and not args.Objects:
print("Error: -Objects required for Partial mode", file=sys.stderr)
print("Error: -Objects or -ObjectsFile required for Partial mode")
sys.exit(1)
# --- Create output dir if needed ---
@@ -455,12 +604,12 @@ def main():
# --- 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)
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)")
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)
print("Error: ibcmd config export does not support Mode UpdateInfo; use 1cv8")
sys.exit(1)
elif args.Mode == "Partial":
obj_list = [o.strip() for o in args.Objects.split(",") if o.strip()]
@@ -490,9 +639,9 @@ def main():
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)
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not exported")
else:
print(f"Error exporting configuration (code: {exit_code})", file=sys.stderr)
print(f"Error exporting configuration (code: {exit_code})")
sys.exit(exit_code)
# --- Temp dir ---
@@ -513,6 +662,11 @@ def main():
if args.Password:
arguments.append(f'/P"{args.Password}"')
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
repo = resolve_repository_settings(args)
arguments.extend(repository_args(repo))
arguments += ["/DumpConfigToFiles", f'"{args.ConfigDir}"']
arguments += ["-Format", args.Format]
@@ -551,7 +705,7 @@ def main():
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)}")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
@@ -564,9 +718,9 @@ def main():
print("Dump completed successfully")
print(f"Configuration dumped to: {args.ConfigDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped", file=sys.stderr)
print(f"Error: exit code 0 but no files under {args.ConfigDir} — configuration was not dumped")
else:
print(f"Error dumping configuration (code: {exit_code})", file=sys.stderr)
print(f"Error dumping configuration (code: {exit_code})")
if os.path.isfile(out_file):
try:
+53 -1
View File
@@ -40,7 +40,19 @@ allowed-tools:
"password": "",
"aliases": ["dev", "разработка"],
"branches": ["dev", "develop", "feature/*"],
"configSrc": "C:\\WS\\myapp\\cfsrc"
"configSrc": "C:\\WS\\myapp\\cfsrc",
"repository": {
"path": "\\\\srv01\\repo\\MyApp",
"user": "Ivanov",
"password": ""
},
"extensions": [
{
"name": "МоёРасширение",
"src": "src\\cfe\\МоёРасширение",
"repository": { "path": "\\\\srv01\\repo\\MyApp_Ext", "user": "Ivanov", "password": "" }
}
]
},
{
"id": "test",
@@ -64,6 +76,9 @@ allowed-tools:
| `v8path` | string | Каталог bin платформы 1С. Необязательный — если не задан, автоопределение |
| `v8args` | array | Доп. аргументы запуска `1cv8.exe` для всех навыков, напр. `["/UseHwLicenses+"]` |
| `ibcmdargs` | array | Доп. аргументы `ibcmd` (форма `--ключ=значение`) |
| `newObjectPosition` | `"end"` / `"byName"` | Куда попадает новый объект в составе конфигурации: в конец своего вида (по умолчанию) или на место по имени. Переопределяется в `databases[]` |
| `extensionApplyCheck` | bool | Проверять ли применимость расширения после загрузки в базу (по умолчанию `true`). Разово отключается ключом `-NoApplyCheck` |
| `externalCheck` | bool | Проверять ли исходники внешней обработки/отчёта платформой перед сборкой (по умолчанию `true`). Разово отключается ключом `-Checks off` |
| `databases` | array | Массив баз данных |
| `default` | string | id базы по умолчанию |
@@ -82,6 +97,35 @@ allowed-tools:
| `aliases` | string[] | нет | Альтернативные имена для быстрого доступа |
| `branches` | string[] | нет | Git-ветки или glob-паттерны (`release/*`, `feature/*`), привязанные к этой базе |
| `configSrc` | string | нет | Каталог XML-выгрузки конфигурации |
| `repository` | object | нет | Хранилище конфигурации: `path`, `user`, `password` (см. ниже) |
| `extensions` | array | нет | Расширения: `name`, `src`, необязательное `repository` (см. ниже) |
### Хранилище конфигурации
База, подключённая к хранилищу конфигурации 1С, **не принимает ни одной операции конфигуратора**
без реквизитов доступа к хранилищу — это касается не только `/db-repo`, но и `/db-load-xml`,
`/db-dump-xml`, `/db-update`, `/db-load-git`. Реквизиты берутся из `repository` записи базы,
передавать их в каждом вызове не нужно.
| Поле | Тип | Обязательное | Описание |
|------|-----|:------------:|----------|
| `repository.path` | string | да | Каталог хранилища или `tcp://<хост>[:<порт>]/<имя>` |
| `repository.user` | string | нет | Пользователь **хранилища**. Не наследуется от `user` базы |
| `repository.password` | string | нет | Пароль пользователя хранилища |
У расширения **своё хранилище** со своим путём, поэтому одного `repository` мало:
| Поле | Тип | Обязательное | Описание |
|------|-----|:------------:|----------|
| `extensions[].name` | string | да | Имя расширения, как в конфигурации |
| `extensions[].src` | string | нет | Каталог XML-исходников расширения |
| `extensions[].repository` | object | нет | Хранилище расширения. Расширение без хранилища — обычный случай |
Пароль хранилища — такой же секрет, как `password` базы; `.v8-project.json` в `.gitignore`.
> **Сетевое хранилище.** Адрес — `tcp://<хост>[:<порт>]/<имя>`, порт по умолчанию 1542.
> Обслуживается сервером хранилища. Если он недоступен, платформа отвечает «Соединение с
> хранилищем конфигурации не установлено» — тем же сообщением, что и при отсутствии реквизитов.
## Алгоритм разрешения базы данных
@@ -128,6 +172,7 @@ test Тестовая server srv01/MyApp_Test
- path (для file) или server + ref (для server)
- user, password (необязательно)
- aliases, branches (необязательно)
- если база под хранилищем конфигурации — `repository`: путь, пользователь, пароль
Добавь в массив `databases`. Если это первая база — установи как `default`.
@@ -159,3 +204,10 @@ test Тестовая server srv01/MyApp_Test
```
> **Важно**: между `/N` и именем пробела нет. Между `/P` и паролем пробела нет. Если пароль пустой — опусти `/P` целиком.
**Хранилище конфигурации** (если у базы задан `repository`) — скрипты навыков подставляют
сами, сопоставляя параметры соединения с записью реестра:
```
/ConfigurationRepositoryF"<path>" /ConfigurationRepositoryN"<user>" /ConfigurationRepositoryP"<password>"
```
+1
View File
@@ -51,6 +51,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-cf.ps1" <п
| `-Password <пароль>` | нет | Пароль |
| `-InputFile <путь>` | да | Путь к CF-файлу |
| `-Extension <имя>` | нет | Загрузить как расширение |
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
| `-AllExtensions` | нет | Загрузить все расширения из архива |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
@@ -1,4 +1,4 @@
# db-load-cf v1.14 — Load 1C configuration from CF file
# db-load-cf v1.17 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -49,7 +49,7 @@
.\db-load-cf.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "ext.cfe" -Extension "МоёРасширение"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -78,6 +78,16 @@ param(
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
[Parameter(Mandatory=$false)]
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но расширение при этом неприменимо.
[switch]$StrictLog,
# Пропустить проверку применимости расширения после загрузки.
[Parameter(Mandatory=$false)]
[switch]$NoApplyCheck,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
@@ -121,6 +131,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -171,6 +192,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -384,6 +413,85 @@ function Write-PlatformOutput {
}
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
}
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
#
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
#
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
function Invoke-ApplyCheck {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$exeDir = Split-Path $Exe -Parent
$exeLeaf = Split-Path $Exe -Leaf
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
try {
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
$outFile = Join-Path $dir "check_log.txt"
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
$a += $ExtraArgs
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
$lines = @()
if (Test-Path $outFile) {
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
}
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
} finally {
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
}
}
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
# поднимать ли код возврата (строгий режим).
function Invoke-ApplyCheckReport {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
if ($ac.Skipped) {
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
return $false
}
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
return $true
}
return $false
}
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
function Get-ApplyCheckEnabled {
param([switch]$Disabled)
if ($Disabled) { return $false }
$pf = Find-V8Project (Get-Location).Path
if ($pf) {
try {
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
} catch {}
}
return $true
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
@@ -434,22 +542,32 @@ try {
} else {
Write-Host "Error loading configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
$acConn = @("/F", "`"$InfoBasePath`"")
if ($UserName) { $acConn += "/N`"$UserName`"" }
if ($Password) { $acConn += "/P`"$Password`"" }
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
$connArgs = @()
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
$connArgs += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UserName) { $connArgs += "/N`"$UserName`"" }
if ($Password) { $connArgs += "/P`"$Password`"" }
$arguments = @("DESIGNER") + $connArgs
$arguments += "/LoadCfg", "`"$InputFile`""
@@ -488,6 +606,11 @@ try {
}
Write-PlatformOutput $__v8.Output
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
}
exit $exitCode
} finally {
+169 -22
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-cf v1.14 — Load 1C configuration from CF file
# db-load-cf v1.17 — Load 1C configuration from CF file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -344,6 +379,91 @@ def print_platform_output(result):
print("--- End ---")
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 run_apply_check(exe, conn_args, extension, extra_args):
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
которое не применит отказ всплывает лениво, при первом вызове метода, записью в журнал
регистрации.
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
отбрасывает проверено на 8.3.24, /LoadConfigFromFiles вместе с
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
состоялась.
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
"""
exe_dir = os.path.dirname(exe)
leaf = os.path.basename(exe)
if leaf.lower().startswith("ibcmd"):
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
else:
v8 = exe
if not os.path.isfile(v8):
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
try:
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
if extension:
a += ["-Extension", f'"{extension}"']
out_file = os.path.join(temp_dir, "check_log.txt")
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
a += list(extra_args)
r = run_v8(v8, a)
lines = []
if os.path.isfile(out_file):
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def apply_check_report(exe, conn_args, extension, extra_args):
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
поднимать ли код возврата (строгий режим)."""
ac = run_apply_check(exe, conn_args, extension, extra_args)
if ac["skipped"]:
print(f"[note] applicability check skipped: {ac['reason']}")
return False
if ac["exit"] != 0 or ac["lines"]:
print("[warning] the extension is loaded, but the platform will not apply it:")
for line in ac["lines"]:
print(f" {line}")
return True
return False
def apply_check_enabled(disabled):
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
if disabled:
return False
pf = _sg_find_v8project(os.getcwd())
if pf:
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
if proj.get("extensionApplyCheck") is not None:
return bool(proj.get("extensionApplyCheck"))
except Exception:
pass
return True
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -352,7 +472,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -409,6 +529,12 @@ def main():
parser.add_argument("-InputFile", required=True)
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но расширение при этом неприменимо.
parser.add_argument("-StrictLog", action="store_true")
# Пропустить проверку применимости расширения после загрузки.
parser.add_argument("-NoApplyCheck", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -440,21 +566,21 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
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)
print(f"Error: input file not found: {args.InputFile}")
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)
print("Error: ibcmd config load does not support -AllExtensions (use -Extension)")
sys.exit(1)
arguments = ["infobase", "config", "load", f"--db-path={args.InfoBasePath}"]
if args.Extension:
@@ -473,7 +599,19 @@ def main():
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)
print(f"Error loading configuration (code: {result.returncode}){describe_exit(result.returncode)}")
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
exit_code = result.returncode
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
ac_conn = ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
ac_conn.append(f'/N"{args.UserName}"')
if args.Password:
ac_conn.append(f'/P"{args.Password}"')
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
exit_code = 1
sys.exit(result.returncode)
# --- Temp dir ---
@@ -482,17 +620,20 @@ def main():
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
conn_args = []
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
conn_args.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
conn_args.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
conn_args.append(f'/P"{args.Password}"')
arguments = ["DESIGNER"] + conn_args
arguments.extend(["/LoadCfg", f'"{args.InputFile}"'])
@@ -517,7 +658,7 @@ def main():
if exit_code == 0:
print(f"Configuration loaded successfully from: {args.InputFile}")
else:
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
if os.path.isfile(out_file):
try:
@@ -530,6 +671,12 @@ def main():
except Exception:
pass
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
exit_code = 1
print_platform_output(result)
sys.exit(exit_code)
@@ -1,4 +1,4 @@
# db-load-dt v1.13 — Load 1C information base from DT file
# db-load-dt v1.16 — 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 не исполняется.
<#
@@ -46,7 +46,7 @@
.\db-load-dt.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "backup.dt"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -118,6 +118,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -168,6 +179,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
+51 -16
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-dt v1.13 — Load 1C information base from DT file
# db-load-dt v1.16 — Load 1C information base from DT file
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -440,15 +475,15 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
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)
print(f"Error: input file not found: {args.InputFile}")
sys.exit(1)
# --- ibcmd branch (file infobase only) ---
@@ -470,7 +505,7 @@ def main():
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)
print(f"Error restoring information base (code: {result.returncode}){describe_exit(result.returncode)}")
sys.exit(result.returncode)
# --- Temp dir ---
@@ -512,7 +547,7 @@ def main():
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)
print(f"Error restoring information base (code: {exit_code}){describe_exit(exit_code)}")
if os.path.isfile(out_file):
try:
+1
View File
@@ -55,6 +55,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-git.ps1" <
| `-Source <источник>` | нет | `All` (по умолч.) / `Staged` / `Unstaged` / `Commit` |
| `-CommitRange <range>` | для Commit | Диапазон коммитов (напр. `HEAD~3..HEAD`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-DryRun` | нет | Только показать что будет загружено (без загрузки) |
@@ -1,4 +1,4 @@
# db-load-git v1.20 — Load Git changes into 1C database
# db-load-git v1.26 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -64,7 +64,7 @@
.\db-load-git.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -DryRun
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -116,6 +116,19 @@ param(
# но в логе есть отбраковка.
[switch]$StrictLog,
# Пропустить проверку применимости расширения после загрузки.
[Parameter(Mandatory=$false)]
[switch]$NoApplyCheck,
[Parameter(Mandatory=$false)]
[string]$RepositoryPath,
[Parameter(Mandatory=$false)]
[string]$RepositoryUser,
[Parameter(Mandatory=$false)]
[string]$RepositoryPassword,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
@@ -126,6 +139,115 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
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 Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
function Resolve-RepositorySettings {
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
$dbRec = Find-ProjectDatabase
$rec = $null
if ($dbRec) {
if ($Extension) {
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
if ($dbRec.extensions) {
foreach ($ext in $dbRec.extensions) {
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
$rec = $ext.repository
break
}
}
}
} else {
$rec = $dbRec.repository
}
}
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
return @{
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
User = $user
Password = $pwd
FromRegistry = [bool]($rec -and $rec.path)
DbRecord = $dbRec
}
}
function Get-RepositoryArgs {
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
param([hashtable]$Repo)
$a = @()
if (-not $Repo -or -not $Repo.Path) { return $a }
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
return $a
}
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
function Write-RepositoryHints {
param([string]$LogText)
if (-not $LogText) { return }
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
}
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
$obj = $m.Groups[1].Value
if ($obj -eq 'Configuration') {
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
} else {
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
}
}
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
Write-Host "[hint] соединение с хранилищем не установлено. Две причины:" -ForegroundColor Yellow
Write-Host " реквизиты неизвестны — добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list);" -ForegroundColor Yellow
Write-Host " либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт." -ForegroundColor Yellow
}
}
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
@@ -142,12 +264,23 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
@@ -192,6 +325,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -436,7 +577,74 @@ function Find-SilentRejections {
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
#
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
#
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
function Invoke-ApplyCheck {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$exeDir = Split-Path $Exe -Parent
$exeLeaf = Split-Path $Exe -Leaf
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
try {
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
$outFile = Join-Path $dir "check_log.txt"
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
$a += $ExtraArgs
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
$lines = @()
if (Test-Path $outFile) {
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
}
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
} finally {
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
}
}
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
# поднимать ли код возврата (строгий режим).
function Invoke-ApplyCheckReport {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
if ($ac.Skipped) {
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
return $false
}
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
return $true
}
return $false
}
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
function Get-ApplyCheckEnabled {
param([switch]$Disabled)
if ($Disabled) { return $false }
$pf = Find-V8Project (Get-Location).Path
if ($pf) {
try {
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
} catch {}
}
return $true
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
if ($engine -eq "ibcmd") {
if (-not $InfoBasePath) {
Write-Host "Error: ibcmd supports file infobases only (use -InfoBasePath)" -ForegroundColor Red
@@ -645,6 +853,13 @@ try {
} else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
$acConn = @("/F", "`"$InfoBasePath`"")
if ($UserName) { $acConn += "/N`"$UserName`"" }
if ($Password) { $acConn += "/P`"$Password`"" }
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
}
Write-PlatformOutput $applyOut
}
exit $exitCode
@@ -657,16 +872,24 @@ try {
[System.IO.File]::WriteAllLines($listFile, $configFiles, $utf8Bom)
# --- Build arguments ---
$arguments = @("DESIGNER")
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
$connArgs = @()
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
$connArgs += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UserName) { $connArgs += "/N`"$UserName`"" }
if ($Password) { $connArgs += "/P`"$Password`"" }
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
$__repo = Resolve-RepositorySettings
$connArgs += Get-RepositoryArgs $__repo
$arguments = @("DESIGNER") + $connArgs
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
$arguments += "-listFile", "`"$listFile`""
@@ -695,7 +918,7 @@ try {
# --- Execute ---
Write-Host ""
Write-Host "Executing partial configuration load..."
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
@@ -718,6 +941,7 @@ try {
}
}
Write-PlatformOutput $__v8.Output
Write-RepositoryHints $logContent
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
@@ -729,6 +953,11 @@ try {
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
}
exit $exitCode
} finally {
+273 -31
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-git v1.20 — Load Git changes into 1C database
# db-load-git v1.26 — Load Git changes into 1C database
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,15 +67,132 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
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"]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
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 same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def resolve_repository_settings(args):
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
db_rec = find_project_database(args)
rec = None
if db_rec:
if args.Extension:
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
for ext in db_rec.get("extensions") or []:
if (ext.get("name") or "").lower() == args.Extension.lower():
rec = ext.get("repository")
break
else:
rec = db_rec.get("repository")
path = args.RepositoryPath or ((rec or {}).get("path") or None)
user = args.RepositoryUser or ((rec or {}).get("user") or None)
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
return {
"path": path.strip().strip('"') if path else None,
"user": user,
"password": pwd,
"from_registry": bool(rec and rec.get("path")),
}
def repository_args(repo):
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
a = []
if not repo or not repo.get("path"):
return a
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
if repo.get("user"):
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
if repo.get("password"):
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
return a
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
def write_repository_hints(log_text):
if not log_text:
return
if "текущая конфигурация помещена в хранилище" in log_text:
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
obj = m.group(1)
if obj == "Configuration":
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
print(' /db-repo lock <база> -Objects "Конфигурация"')
else:
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
if "Соединение с хранилищем конфигурации не установлено" in log_text:
print("[hint] соединение с хранилищем не установлено. Две причины:")
print(' реквизиты неизвестны — добавьте "repository" в запись базы в .v8-project.json (см. /db-list);')
print(" либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт.")
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
@@ -120,15 +237,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +319,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +366,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +404,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +421,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +440,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -376,6 +517,76 @@ def find_silent_rejections(log_text):
return found
def run_apply_check(exe, conn_args, extension, extra_args):
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
которое не применит отказ всплывает лениво, при первом вызове метода, записью в журнал
регистрации.
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
отбрасывает проверено на 8.3.24, /LoadConfigFromFiles вместе с
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
состоялась.
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
"""
exe_dir = os.path.dirname(exe)
leaf = os.path.basename(exe)
if leaf.lower().startswith("ibcmd"):
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
else:
v8 = exe
if not os.path.isfile(v8):
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
try:
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
if extension:
a += ["-Extension", f'"{extension}"']
out_file = os.path.join(temp_dir, "check_log.txt")
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
a += list(extra_args)
r = run_v8(v8, a)
lines = []
if os.path.isfile(out_file):
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def apply_check_report(exe, conn_args, extension, extra_args):
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
поднимать ли код возврата (строгий режим)."""
ac = run_apply_check(exe, conn_args, extension, extra_args)
if ac["skipped"]:
print(f"[note] applicability check skipped: {ac['reason']}")
return False
if ac["exit"] != 0 or ac["lines"]:
print("[warning] the extension is loaded, but the platform will not apply it:")
for line in ac["lines"]:
print(f" {line}")
return True
return False
def apply_check_enabled(disabled):
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
if disabled:
return False
pf = _sg_find_v8project(os.getcwd())
if pf:
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
if proj.get("extensionApplyCheck") is not None:
return bool(proj.get("extensionApplyCheck"))
except Exception:
pass
return True
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -384,7 +595,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -460,6 +671,9 @@ def main():
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-RepositoryPath", default="")
parser.add_argument("-RepositoryUser", default="")
parser.add_argument("-RepositoryPassword", default="")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration (git repo)")
parser.add_argument(
"-Source",
@@ -482,6 +696,8 @@ def main():
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
parser.add_argument("-StrictLog", action="store_true")
# Пропустить проверку применимости расширения после загрузки.
parser.add_argument("-NoApplyCheck", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -506,10 +722,10 @@ def main():
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)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
sys.exit(1)
# --- Resolve additional arguments for the selected engine ---
@@ -526,19 +742,19 @@ def main():
# --- Validate config dir ---
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}")
sys.exit(1)
# --- Validate Commit mode ---
if args.Source == "Commit" and not args.CommitRange:
print("Error: -CommitRange required for Source=Commit", file=sys.stderr)
print("Error: -CommitRange required for Source=Commit")
sys.exit(1)
# --- Check git ---
try:
subprocess.run(["git", "--version"], capture_output=True, text=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Error: git not found in PATH", file=sys.stderr)
print("Error: git not found in PATH")
sys.exit(1)
# --- Get changed files from Git ---
@@ -617,10 +833,10 @@ def main():
config_files.append(rel_path)
if support_skipped:
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):", file=sys.stderr)
print("[ВНИМАНИЕ] Состояние поддержки изменено в коммите, но частично не загружается (исключено):")
for sf in support_skipped:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).", file=sys.stderr)
print(f" - {sf}")
print(" Смена состояния поддержки применяется только полной загрузкой (db-load-xml -Mode Full).")
if len(config_files) == 0:
print("No configuration files found in changes")
@@ -644,10 +860,10 @@ def main():
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)
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)")
sys.exit(1)
if args.AllExtensions:
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)", file=sys.stderr)
print("Error: ibcmd config import does not support -AllExtensions (use -Extension or 1cv8)")
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + config_files
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
@@ -664,7 +880,7 @@ def main():
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)
print(f"Error loading changes (code: {result.returncode}){describe_exit(result.returncode)}")
sys.exit(result.returncode)
print(f"Changes loaded successfully ({len(config_files)} files)")
exit_code = 0
@@ -682,8 +898,19 @@ def main():
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(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
print_platform_output(ar)
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
ac_conn = ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
ac_conn.append(f'/N"{args.UserName}"')
if args.Password:
ac_conn.append(f'/P"{args.Password}"')
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
exit_code = 1
sys.exit(exit_code)
# --- Write list file (UTF-8 with BOM) ---
@@ -692,17 +919,25 @@ def main():
f.write("\n".join(config_files))
# --- Build arguments ---
arguments = ["DESIGNER"]
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
conn_args = []
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
conn_args += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
conn_args.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
conn_args.append(f'/P"{args.Password}"')
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
repo = resolve_repository_settings(args)
conn_args.extend(repository_args(repo))
arguments = ["DESIGNER"] + conn_args
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
arguments += ["-listFile", f'"{list_file}"']
@@ -729,7 +964,7 @@ def main():
# --- Execute ---
print("")
print("Executing partial configuration load...")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName)}")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
@@ -739,7 +974,7 @@ def main():
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
log_content = ""
if os.path.isfile(out_file):
@@ -754,6 +989,7 @@ def main():
pass
print_platform_output(result)
write_repository_hints(log_content)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
@@ -769,6 +1005,12 @@ def main():
if args.StrictLog and exit_code == 0:
exit_code = 1
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
exit_code = 1
sys.exit(exit_code)
finally:
+2
View File
@@ -34,6 +34,7 @@ allowed-tools:
Если файла нет — предложи `/db-list add`.
Если использованная база не зарегистрирована — после выполнения предложи добавить через `/db-list add`.
Если в записи базы указан `configSrc` — используй как каталог загрузки по умолчанию.
Для `-Extension` каталог берётся из `extensions[].src` записи базы, если он там указан.
## Команда
@@ -57,6 +58,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-load-xml.ps1" <
| `-ListFile <путь>` | для Partial | Путь к файлу со списком (альтернатива `-Files`) |
| `-Extension <имя>` | нет | Загрузить в расширение |
| `-AllExtensions` | нет | Загрузить все расширения |
| `-NoApplyCheck` | нет | Не проверять применимость расширения после загрузки |
| `-Format <формат>` | нет | `Hierarchical` (по умолч.) / `Plain` |
| `-UpdateDB` | нет | После загрузки сразу обновить конфигурацию БД (`/UpdateDBCfg`) |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
@@ -1,4 +1,4 @@
# db-load-xml v1.21 — Load 1C configuration from XML files
# db-load-xml v1.28 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -61,7 +61,7 @@
.\db-load-xml.ps1 -InfoBasePath "C:\Bases\MyDB" -ConfigDir "C:\src" -Mode Partial -Files "Catalogs/Номенклатура.xml,Catalogs/Номенклатура/Ext/ObjectModule.bsl"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -85,8 +85,10 @@ param(
[string]$ConfigDir,
[Parameter(Mandatory=$false)]
[ValidateSet("Full", "Partial")]
[string]$Mode = "Full",
# Пустое значение = режим не задан. Прежнее умолчание Full подставляется ниже, после того
# как станет видно, перечислены ли файлы.
[ValidateSet("", "Full", "Partial")]
[string]$Mode = "",
[Parameter(Mandatory=$false)]
[string]$Files,
@@ -110,6 +112,19 @@ param(
[Parameter(Mandatory=$false)]
[switch]$StrictLog,
# Пропустить проверку применимости расширения после загрузки.
[Parameter(Mandatory=$false)]
[switch]$NoApplyCheck,
[Parameter(Mandatory=$false)]
[string]$RepositoryPath,
[Parameter(Mandatory=$false)]
[string]$RepositoryUser,
[Parameter(Mandatory=$false)]
[string]$RepositoryPassword,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
@@ -120,6 +135,115 @@ param(
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
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 Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
function Resolve-RepositorySettings {
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
$dbRec = Find-ProjectDatabase
$rec = $null
if ($dbRec) {
if ($Extension) {
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
if ($dbRec.extensions) {
foreach ($ext in $dbRec.extensions) {
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
$rec = $ext.repository
break
}
}
}
} else {
$rec = $dbRec.repository
}
}
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
return @{
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
User = $user
Password = $pwd
FromRegistry = [bool]($rec -and $rec.path)
DbRecord = $dbRec
}
}
function Get-RepositoryArgs {
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
param([hashtable]$Repo)
$a = @()
if (-not $Repo -or -not $Repo.Path) { return $a }
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
return $a
}
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
function Write-RepositoryHints {
param([string]$LogText)
if (-not $LogText) { return }
if ($LogText -match 'текущая конфигурация помещена в хранилище') {
Write-Host "[hint] полная загрузка в базу, подключённую к хранилищу, невозможна." -ForegroundColor Yellow
Write-Host " Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock" -ForegroundColor Yellow
}
foreach ($m in [regex]::Matches($LogText, 'объект метаданных ([^\s]+) не захвачен в хранилище')) {
$obj = $m.Groups[1].Value
if ($obj -eq 'Configuration') {
Write-Host "[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:" -ForegroundColor Yellow
Write-Host " /db-repo lock <база> -Objects `"Конфигурация`"" -ForegroundColor Yellow
} else {
Write-Host "[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects `"$obj`"" -ForegroundColor Yellow
}
}
if ($LogText -match 'Соединение с хранилищем конфигурации не установлено') {
Write-Host "[hint] соединение с хранилищем не установлено. Две причины:" -ForegroundColor Yellow
Write-Host " реквизиты неизвестны — добавьте `"repository`" в запись базы в .v8-project.json (см. /db-list);" -ForegroundColor Yellow
Write-Host " либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт." -ForegroundColor Yellow
}
}
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
@@ -153,12 +277,23 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
@@ -203,6 +338,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -452,6 +595,73 @@ function Find-SilentRejections {
}
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
#
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
#
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
function Invoke-ApplyCheck {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$exeDir = Split-Path $Exe -Parent
$exeLeaf = Split-Path $Exe -Leaf
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
try {
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
$outFile = Join-Path $dir "check_log.txt"
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
$a += $ExtraArgs
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
$lines = @()
if (Test-Path $outFile) {
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
}
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
} finally {
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
}
}
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
# поднимать ли код возврата (строгий режим).
function Invoke-ApplyCheckReport {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
if ($ac.Skipped) {
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
return $false
}
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
return $true
}
return $false
}
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
function Get-ApplyCheckEnabled {
param([switch]$Disabled)
if ($Disabled) { return $false }
$pf = Find-V8Project (Get-Location).Path
if ($pf) {
try {
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
} catch {}
}
return $true
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
@@ -475,6 +685,16 @@ if (-not (Test-Path $ConfigDir)) {
exit 1
}
# Перечислены файлы — загрузка частичная. Иначе список молча игнорировался бы, а умолчание Full
# заменило бы всю конфигурацию базы.
if ($Files -or $ListFile) {
if ($Mode -eq "Full") {
Write-Host "[note] перечислены файлы — загружаются только они; -Mode Full не применён" -ForegroundColor Yellow
}
$Mode = "Partial"
}
if (-not $Mode) { $Mode = "Full" }
# --- 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
@@ -494,7 +714,7 @@ try {
}
if ($AllExtensions) {
$arguments = @("infobase", "config", "import", "all-extensions", "$ConfigDir", "--db-path=$InfoBasePath")
} elseif ($Mode -eq "Partial" -or $Files -or $ListFile) {
} elseif ($Mode -eq "Partial") {
# partial: import specific files (relative to ConfigDir)
$fileList = @()
if ($ListFile) {
@@ -551,21 +771,36 @@ try {
}
Write-PlatformOutput $applyOut
}
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
$acConn = @("/F", "`"$InfoBasePath`"")
if ($UserName) { $acConn += "/N`"$UserName`"" }
if ($Password) { $acConn += "/P`"$Password`"" }
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
}
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
$connArgs = @()
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
$connArgs += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UserName) { $connArgs += "/N`"$UserName`"" }
if ($Password) { $connArgs += "/P`"$Password`"" }
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
$__repo = Resolve-RepositorySettings
$connArgs += Get-RepositoryArgs $__repo
$arguments = @("DESIGNER") + $connArgs
$arguments += "/LoadConfigFromFiles", "`"$ConfigDir`""
@@ -631,7 +866,7 @@ try {
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
@@ -660,6 +895,7 @@ try {
Write-Host "--- End ---"
}
Write-PlatformOutput $__v8.Output
Write-RepositoryHints $logContent
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
@@ -670,6 +906,11 @@ try {
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
# Расширение могло загрузиться «успешно» и при этом остаться неприменимым — спрашиваем платформу.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
}
exit $exitCode
} finally {
+286 -36
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-load-xml v1.21 — Load 1C configuration from XML files
# db-load-xml v1.28 — Load 1C configuration from XML files
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,15 +67,132 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
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"]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
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 same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def resolve_repository_settings(args):
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
db_rec = find_project_database(args)
rec = None
if db_rec:
if args.Extension:
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
for ext in db_rec.get("extensions") or []:
if (ext.get("name") or "").lower() == args.Extension.lower():
rec = ext.get("repository")
break
else:
rec = db_rec.get("repository")
path = args.RepositoryPath or ((rec or {}).get("path") or None)
user = args.RepositoryUser or ((rec or {}).get("user") or None)
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
return {
"path": path.strip().strip('"') if path else None,
"user": user,
"password": pwd,
"from_registry": bool(rec and rec.get("path")),
}
def repository_args(repo):
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
a = []
if not repo or not repo.get("path"):
return a
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
if repo.get("user"):
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
if repo.get("password"):
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
return a
# Сообщения платформы про хранилище конфигурации называют причину, но не действие. Действие
# дописываем сами: без него модель упирается в отказ и не знает, чем его лечить.
def write_repository_hints(log_text):
if not log_text:
return
if "текущая конфигурация помещена в хранилище" in log_text:
print("[hint] полная загрузка в базу, подключённую к хранилищу, невозможна.")
print(" Используйте -Mode Partial, предварительно захватив объекты: /db-repo lock")
for m in re.finditer(r"объект метаданных (\S+) не захвачен в хранилище", log_text):
obj = m.group(1)
if obj == "Configuration":
print("[hint] не захвачен корень конфигурации — он нужен, чтобы добавить или удалить объект:")
print(' /db-repo lock <база> -Objects "Конфигурация"')
else:
print('[hint] объект не захвачен в хранилище: /db-repo lock <база> -Objects "%s"' % obj)
if "Соединение с хранилищем конфигурации не установлено" in log_text:
print("[hint] соединение с хранилищем не установлено. Две причины:")
print(' реквизиты неизвестны — добавьте "repository" в запись базы в .v8-project.json (см. /db-list);')
print(" либо хранилище недоступно — для сетевого проверьте сервер хранилища и порт.")
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
@@ -120,15 +237,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +319,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +366,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +404,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +421,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +440,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -376,6 +517,76 @@ def find_silent_rejections(log_text):
return found
def run_apply_check(exe, conn_args, extension, extra_args):
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
которое не применит отказ всплывает лениво, при первом вызове метода, записью в журнал
регистрации.
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
отбрасывает проверено на 8.3.24, /LoadConfigFromFiles вместе с
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
состоялась.
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
"""
exe_dir = os.path.dirname(exe)
leaf = os.path.basename(exe)
if leaf.lower().startswith("ibcmd"):
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
else:
v8 = exe
if not os.path.isfile(v8):
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
try:
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
if extension:
a += ["-Extension", f'"{extension}"']
out_file = os.path.join(temp_dir, "check_log.txt")
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
a += list(extra_args)
r = run_v8(v8, a)
lines = []
if os.path.isfile(out_file):
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def apply_check_report(exe, conn_args, extension, extra_args):
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
поднимать ли код возврата (строгий режим)."""
ac = run_apply_check(exe, conn_args, extension, extra_args)
if ac["skipped"]:
print(f"[note] applicability check skipped: {ac['reason']}")
return False
if ac["exit"] != 0 or ac["lines"]:
print("[warning] the extension is loaded, but the platform will not apply it:")
for line in ac["lines"]:
print(f" {line}")
return True
return False
def apply_check_enabled(disabled):
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
if disabled:
return False
pf = _sg_find_v8project(os.getcwd())
if pf:
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
if proj.get("extensionApplyCheck") is not None:
return bool(proj.get("extensionApplyCheck"))
except Exception:
pass
return True
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -384,7 +595,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -438,11 +649,14 @@ def main():
parser.add_argument("-InfoBaseRef", default="", help="Infobase name on server")
parser.add_argument("-UserName", default="", help="1C user name")
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-RepositoryPath", default="")
parser.add_argument("-RepositoryUser", default="")
parser.add_argument("-RepositoryPassword", default="")
parser.add_argument("-ConfigDir", required=True, help="Directory with XML configuration sources")
parser.add_argument(
"-Mode",
default="Full",
choices=["Full", "Partial"],
default="",
choices=["", "Full", "Partial"],
help="Load mode (default: Full)",
)
parser.add_argument("-Files", default="", help="Comma-separated relative file paths (for Partial mode)")
@@ -461,6 +675,8 @@ def main():
action="store_true",
help="Treat silent rejection warnings in the log as errors (elevate exit code to 1)",
)
# Пропустить проверку применимости расширения после загрузки.
parser.add_argument("-NoApplyCheck", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -495,34 +711,42 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
sys.exit(1)
# --- Validate config dir ---
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}")
sys.exit(1)
# --- Validate Partial mode ---
# Перечислены файлы — загрузка частичная. Иначе список молча игнорировался бы, а умолчание
# Full заменило бы всю конфигурацию базы.
if args.Files or args.ListFile:
if args.Mode == "Full":
print("[note] перечислены файлы — загружаются только они; -Mode Full не применён")
args.Mode = "Partial"
if not args.Mode:
args.Mode = "Full"
if args.Mode == "Partial" and not args.Files and not args.ListFile:
print("Error: -Files or -ListFile required for Partial mode", file=sys.stderr)
print("Error: -Files or -ListFile required for Partial mode")
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)
print("Error: ibcmd config import supports hierarchical format only (use -Format Hierarchical or 1cv8)")
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:
elif args.Mode == "Partial":
# 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)
print(f"Error: list file not found: {args.ListFile}")
sys.exit(1)
with open(args.ListFile, encoding="utf-8-sig") as f:
file_list = [ln.strip() for ln in f if ln.strip()]
@@ -531,7 +755,7 @@ def main():
else:
file_list = []
if not file_list:
print("Error: -Files or -ListFile required for partial import", file=sys.stderr)
print("Error: -Files or -ListFile required for partial import")
sys.exit(1)
arguments = ["infobase", "config", "import", "files"] + file_list
arguments += [f"--base-dir={args.ConfigDir}", f"--db-path={args.InfoBasePath}"]
@@ -553,7 +777,7 @@ def main():
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)
print(f"Error loading configuration from files (code: {result.returncode}){describe_exit(result.returncode)}")
sys.exit(result.returncode)
print(f"Configuration loaded successfully from: {args.ConfigDir}")
exit_code = 0
@@ -571,8 +795,19 @@ def main():
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(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
print_platform_output(ar)
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
ac_conn = ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
ac_conn.append(f'/N"{args.UserName}"')
if args.Password:
ac_conn.append(f'/P"{args.Password}"')
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
exit_code = 1
sys.exit(exit_code)
# --- Temp dir ---
@@ -581,17 +816,25 @@ def main():
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
conn_args = []
if args.InfoBaseServer and args.InfoBaseRef:
arguments += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
conn_args += ["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"']
else:
arguments += ["/F", f'"{args.InfoBasePath}"']
conn_args += ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
conn_args.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
conn_args.append(f'/P"{args.Password}"')
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
repo = resolve_repository_settings(args)
conn_args.extend(repository_args(repo))
arguments = ["DESIGNER"] + conn_args
arguments += ["/LoadConfigFromFiles", f'"{args.ConfigDir}"']
@@ -603,7 +846,7 @@ def main():
# Build list file
if 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}")
sys.exit(1)
with open(args.ListFile, encoding="utf-8-sig") as f:
raw_list = [ln.strip() for ln in f if ln.strip()]
@@ -615,12 +858,12 @@ def main():
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)
print("[ВНИМАНИЕ] Служебные файлы состояния поддержки исключены из частичной загрузки (частично не грузятся):")
for sf in support_files:
print(f" - {sf}", file=sys.stderr)
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.", file=sys.stderr)
print(f" - {sf}")
print(" Смена состояния поддержки применяется только полной загрузкой: -Mode Full.")
if not file_list:
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.", file=sys.stderr)
print("Error: после исключения служебных файлов поддержки загружать нечего. Для смены поддержки используйте -Mode Full.")
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:
@@ -652,7 +895,7 @@ def main():
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)}")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
@@ -677,7 +920,7 @@ def main():
if exit_code == 0:
print("Load completed successfully")
else:
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}", file=sys.stderr)
print(f"Error loading configuration (code: {exit_code}){describe_exit(exit_code)}")
if log_content:
print("--- Log ---")
@@ -685,6 +928,7 @@ def main():
print("--- End ---")
print_platform_output(result)
write_repository_hints(log_content)
# Причину не называем: строки лога печатаются следом и говорят за себя, а класс проблемы
# разный — от отброшенного свойства до нерабочей на этой платформе конфигурации. Подсказку
# про -StrictLog не даём: загрузка уже выполнена, повторять её ради того же текста незачем.
@@ -701,6 +945,12 @@ def main():
if args.StrictLog and exit_code == 0:
exit_code = 1
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
exit_code = 1
sys.exit(exit_code)
finally:
+200
View File
@@ -0,0 +1,200 @@
---
name: db-repo
description: Работа с хранилищем конфигурации 1С. Используй когда нужно захватить объекты, поместить изменения в хранилище конфигурации, получить изменения из него, подключить базу к хранилищу
argument-hint: <lock|unlock|commit|update> [database] -Objects "<объекты>"
allowed-tools:
- Bash
- Read
- Glob
- AskUserQuestion
---
# /db-repo — Хранилище конфигурации 1С
Захват и помещение объектов, получение изменений, подключение базы, история версий,
администрирование хранилища.
> Хранилище конфигурации 1С, а не Git-репозиторий.
## Usage
```
/db-repo lock [database] -Objects "Справочник.Номенклатура"
/db-repo commit [database] -Objects "Справочник.Номенклатура" -Comment "Добавлен Артикул"
/db-repo unlock [database] -Objects "Справочник.Номенклатура"
/db-repo update [database]
```
## Порядок работы
В базу, подключённую к хранилищу, исходники грузятся **только частично** и **только по захваченным**
объектам. Выполняй строго по шагам:
```
0. /db-repo update <база> — начать с актуального состояния
1. /db-repo lock <база> -Objects "Справочник.Номенклатура"
2. если шаг 0 или 1 напечатал «локальная конфигурация изменена, получено объектов из хранилища: N» —
выгрузи названные объекты: /db-dump-xml <база> <каталог> -Mode Partial -ObjectsFile "<файл из вывода>"
3. правки в исходниках: /meta-edit, /form-edit, /skd-edit, /meta-compile и т. д.
4. /db-load-xml <каталог> <база> -Mode Partial -Files "Catalogs/Номенклатура.xml,…" -UpdateDB
5. /db-repo commit <база> -Objects "Справочник.Номенклатура" -Comment "…"
```
Шаг 0 стоит делать всегда, когда работа не продолжается сразу после предыдущего цикла: правки
должны опираться на актуальное состояние — в том числе тех объектов, которые ты не меняешь, но
используешь.
Шаг 2 пропускать нельзя: захват и обновление подтягивают из хранилища свежие версии, и загрузка
исходников, снятых раньше, откатит чужие изменения — молча, без ошибки.
**Что вообще захватывается.** Отдельные объекты хранилища — сам объект, а также его **формы,
макеты и команды**. Реквизиты, табличные части, измерения и ресурсы отдельными объектами **не
являются**: они правятся в составе владельца.
| Что правишь | Что захватывать |
|-------------|-----------------|
| Реквизит, табличную часть, измерение, ресурс, модуль объекта | сам объект: `Справочник.Контрагенты` |
| Существующую форму, макет, команду | её саму: `Справочник.Контрагенты.Форма.ФормаЭлемента` |
| Добавляешь новую форму, макет, команду | объект-владельца; при помещении назови и новый объект |
| Добавляешь новый объект конфигурации | только корень: `Конфигурация`. Самого объекта ещё нет — захватить его нельзя; при помещении назови и его |
Захватывай минимум того, что правишь: чем шире захват, тем больше конфликтов с коллегами.
Захват объекта его формы и макеты не захватывает — для этого есть `-WithChildren`.
## Параметры подключения
Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
1. Если пользователь указал параметры подключения — используй напрямую
2. Если указал базу по имени — ищи по id / alias / name
3. Если не указал — сопоставь текущую ветку Git с `databases[].branches`
4. Если ветка не совпала — используй `default`
Реквизиты хранилища передавать не нужно: запись базы находится по переданным параметрам
соединения (`-InfoBasePath` либо `-InfoBaseServer` + `-InfoBaseRef`), реквизиты берутся из её
`repository`. Задать их явно можно параметрами `-Repository*`.
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command <подкоманда> <параметры>
```
### Рабочий цикл
| Подкоманда | Что делает |
|------------|------------|
| `lock` | Захватить объекты |
| `unlock` | Отменить захват |
| `commit` | Поместить изменения в хранилище |
| `update` | Получить изменения из хранилища |
### Параметры
| Параметр | Обязательный | Описание |
|----------|:------------:|----------|
| `-InfoBasePath <путь>` | * | Файловая база |
| `-InfoBaseServer <сервер>` | * | Сервер 1С |
| `-InfoBaseRef <имя>` | * | Имя базы на сервере |
| `-UserName <имя>` | нет | Пользователь базы |
| `-Password <пароль>` | нет | Пароль пользователя базы |
| `-Objects <список>` | усл. | Объекты через запятую. Для `lock`, `unlock`, `commit` обязателен, если не задан `-All` |
| `-ObjectsFile <путь>` | нет | Файл со списком объектов, одно имя на строку |
| `-All` | нет | Операция над всей конфигурацией — вместо `-Objects`, а не вместе с ним |
| `-WithChildren` | нет | Вместе с подчинёнными объектами на полную глубину |
| `-Comment <текст>` | нет | Комментарий к помещению (`commit`). Многострочный — как есть, с переводами строк |
| `-KeepLocked` | нет | Оставить объекты захваченными после помещения |
| `-Revised` | нет | Получать захваченные объекты, если потребуется |
| `-Force` | нет | Разное по подкомандам — см. ниже |
| `-Extension <имя>` | нет | Работать с хранилищем расширения |
| `-RepositoryPath <путь>` | нет | Хранилище явно, вместо реестра |
| `-RepositoryUser <имя>` | нет | Пользователь хранилища явно |
| `-RepositoryPassword <пароль>` | нет | Пароль пользователя хранилища явно |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы `1cv8.exe` через запятую |
> `*` — нужен либо `-InfoBasePath`, либо пара `-InfoBaseServer` + `-InfoBaseRef`
### `-Force`
| Подкоманда | Что делает |
|------------|------------|
| `unlock` | **Теряет локальные правки**: объекты перезаписываются версией из хранилища |
| `commit` | Пытается очистить ссылки на удалённые объекты вместо ошибки |
| `update` | Подтверждает добавление и удаление объектов конфигурации |
### Имена объектов
Объект — `Справочник.Номенклатура`. Форма, макет, команда — полным путём:
`Документ.ЗаказПокупателя.Форма.ФормаДокумента`, `Справочник.Номенклатура.Макет.Печать`.
Корень конфигурации — `Конфигурация`.
Если объект «не найден», это не всегда опечатка: он мог появиться в хранилище позже, чем
обновлялась база (`/db-repo update`), либо это вовсе не объект хранилища — реквизит или
табличная часть.
## Результат
Нулевой код не означает, что что-то изменилось. Под нулём приходят «уже захвачено», «обновлять
нечего», «помещать нечего» и частичный захват — когда часть объектов занята другими, а остальное
захвачено и его можно править.
**Читай текст вывода, а не только код.** Там же приходит список полученных из хранилища объектов,
который требует перевыгрузки перед правкой.
## Требуют подтверждения пользователя
Перед этими операциями **спроси подтверждение**:
| Операция | Почему |
|----------|--------|
| `lock -All` | Захватывает **всю конфигурацию**: на большой базе идёт долго и блокирует работу всей команде |
| `unlock -Force` | Теряются локальные правки захваченных объектов |
| `disconnect` | Теряется подключение базы к хранилищу, в том числе на стороне хранилища |
| `connect -ForceReplaceCfg` | Конфигурация базы заменяется конфигурацией из хранилища |
`update` не выполнится, если у базы в реестре не объявлено `repository`, а реквизиты не заданы
явно: на неподключённой к хранилищу базе эта команда заменяет всю конфигурацию его содержимым и
рапортует успех.
## Расширения
У расширения своё хранилище со своим путём. Укажи `-Extension "<Имя>"` — реквизиты возьмутся из
`extensions[].repository` записи базы. Подкоманды работают одинаково для основной конфигурации и
для расширения.
## Остальные задачи
| Файл | Про что |
|------|---------|
| [connect.md](references/connect.md) | Подключение и отключение базы от хранилища |
| [history.md](references/history.md) | История версий, отчёт, выгрузка версии в CF |
| [admin.md](references/admin.md) | Создание хранилища, пользователи и права |
| [service.md](references/service.md) | Метки версий, оптимизация, очистка кеша |
## Примеры
```powershell
# Захватить справочник вместе с подчинёнными объектами
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -WithChildren
# Захватить корень — он нужен, чтобы добавить или удалить объект конфигурации
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация"
# Поместить новый объект: он уже существует, поэтому называется вместе с корнем
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Конфигурация,Справочник.Склады" -Comment "Добавлен справочник Склады"
# Поместить с комментарием, оставив захват
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command commit -InfoBasePath "C:\Bases\MyDB" -Objects "Справочник.Номенклатура" -Comment "Добавлен реквизит Артикул" -KeepLocked
# Получить изменения из хранилища
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command update -InfoBasePath "C:\Bases\MyDB"
# Серверная база, расширение
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command lock -InfoBaseServer "srv01" -InfoBaseRef "MyApp_Dev" -UserName "Admin" -Extension "МоёРасширение" -Objects "Справочник.Номенклатура"
```
## После выполнения
- `lock` или `update` сообщил о полученных объектах — выполни `/db-dump-xml -Mode Partial` с
указанным в выводе файлом, и только потом правь исходники
- после `lock` правки идут через `/db-load-xml -Mode Partial` и `/db-update`
- изменения готовы — предложи `/db-repo commit` с комментарием
@@ -0,0 +1,45 @@
# Администрирование хранилища
## create — создать хранилище
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command create -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "C:\Repo\MyApp" -RepositoryUser "Admin" -RepositoryPassword "…"
```
| Параметр | Описание |
|----------|----------|
| `-NoBind` | Не подключать базу к созданному хранилищу |
| `-AllowConfigurationChanges` | Включить возможность изменения, если конфигурация на поддержке без неё |
| `-ChangesAllowedRule <правило>` | Правило для объектов, изменения которых разрешены поставщиком |
| `-ChangesNotRecommendedRule <правило>` | То же для «изменения не рекомендуются» |
Правила: `ObjectNotEditable`, `ObjectIsEditableSupportEnabled`, `ObjectNotSupported`.
Без `-NoBind` база сразу подключается к созданному хранилищу. Создание — это версия 1.
Для расширения: `-Extension "<Имя>"` и отдельный путь — у расширения своё хранилище.
## add-user — создать пользователя
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command add-user -InfoBasePath "C:\Bases\MyDB" -NewUser "Ivanov" -NewUserPassword "…" -Rights LockObjects
```
| Право | Что даёт |
|-------|----------|
| `ReadOnly` | Просмотр |
| `LockObjects` | Захват объектов |
| `ManageConfigurationVersions` | Изменение состава версий |
| `Administration` | Административные функции |
`-RestoreDeletedUser` — восстановить одноимённого удалённого. Если пользователь с таким именем
существует, он **не** будет добавлен. Выполняющий должен иметь административные права.
## copy-users — скопировать пользователей из другого хранилища
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command copy-users -InfoBasePath "C:\Bases\MyDB" -SourcePath "\\srv01\repo\Other" -SourceUser "Admin" -SourcePassword "…"
```
`-SourcePath`, `-SourceUser`, `-SourcePassword` описывают хранилище-**источник**. Удалённые пользователи
не копируются; существующие не перезаписываются.
@@ -0,0 +1,39 @@
# Подключение базы к хранилищу
## connect — подключить
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command connect -InfoBasePath "C:\Bases\MyDB" -RepositoryPath "\\srv01\repo\MyApp" -RepositoryUser "Ivanov" -RepositoryPassword "…"
```
| Параметр | Описание |
|----------|----------|
| `-ForceReplaceCfg` | Конфигурация базы непустая — подтвердить замену её конфигурацией из хранилища. **Спроси подтверждение у пользователя** |
| `-ForceBindAlreadyBindedUser` | Подключить, даже если у этого пользователя уже есть конфигурация, связанная с хранилищем |
На пустой базе `-ForceReplaceCfg` не нужен.
**Переподключение** базы, которая уже была подключена, требует обоих флагов: конфигурация в базе
не пустая (`-ForceReplaceCfg`), а за пользователем хранилища всё ещё числится эта база
(`-ForceBindAlreadyBindedUser`).
После подключения добавь `repository` в запись базы в `.v8-project.json` — иначе остальные
подкоманды придётся каждый раз звать с явными реквизитами, а `update` откажется работать.
## disconnect — отключить
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command disconnect -InfoBasePath "C:\Bases\MyDB"
```
**Спроси подтверждение у пользователя.** Отключение снимает связь и на стороне самого хранилища:
запись о подключении удаляется. Подключить базу обратно можно, но это уже не рядовая операция —
понадобятся оба флага `connect` из раздела выше.
Если в базе есть захваченные и изменённые объекты, операция не выполнится. `-Force` выполняет её
всё равно, и эти изменения теряются.
## Расширения
У расширения своё хранилище: `-Extension "<Имя>"` указывай вместе с путём именно к нему, а не
к хранилищу основной конфигурации.
@@ -0,0 +1,31 @@
# История версий хранилища
## report — отчёт по версиям
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command report -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\repo.txt"
```
| Параметр | Описание |
|----------|----------|
| `-OutputFile <путь>` | Куда сохранить отчёт. Необязателен |
| `-NBegin <номер>` | С какой версии. `-1` — только последняя |
| `-NEnd <номер>` | По какую версию |
| `-DateBegin` / `-DateEnd` | Границы по датам |
| `-GroupByObject` | Группировать по объектам |
| `-GroupByComment` | Группировать по комментарию |
| `-ReportFormat <txt\|mxl>` | По умолчанию `txt` |
`txt` — с разделителем-табуляцией, разбирается построчно.
> На боевом хранилище полный отчёт строить не надо — тысячи версий. Нужна головная
> версия — `-NBegin -1`. Длинный отчёт в вывод не печатается: сузьте выборку
> параметрами ниже.
## dump-cfg — выгрузить версию в CF
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command dump-cfg -InfoBasePath "C:\Bases\MyDB" -OutputFile "C:\tmp\v120.cf" -Version 120
```
Без `-Version` (или при `-1`) выгружается последняя версия.
@@ -0,0 +1,31 @@
# Сервисные операции
## set-label — метка на версию
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command set-label -InfoBasePath "C:\Bases\MyDB" -Label "Релиз 1.2" -Version 120 -Comment "Передано в тест"
```
Без `-Version` метка ставится на последнюю версию. Несуществующая версия — ошибка.
## optimize — оптимизация хранения
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command optimize -InfoBasePath "C:\Bases\MyDB"
```
Оптимизирует хранение данных в хранилище. Операция долгая.
## clear-cache — очистка кеша
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-repo.ps1" -Command clear-cache -InfoBasePath "C:\Bases\MyDB" -CacheScope local
```
| `-CacheScope` | Что чистит |
|---------------|------------|
| `local` (по умолчанию) | Локальный кеш версий конфигурации |
| `global` | Глобальный кеш версий |
| `db` | Локальную базу данных хранилища |
Пригождается, когда хранилище ведёт себя странно после сбоя сети или отката версии.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+21 -2
View File
@@ -1,4 +1,4 @@
# db-run v1.8 — Launch 1C:Enterprise
# db-run v1.10 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -52,7 +52,7 @@
.\db-run.ps1 -InfoBasePath "C:\Bases\MyDB" -CParam "ЗапуститьОбновление"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -107,6 +107,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -157,6 +168,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
+25 -10
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-run v1.8 — Launch 1C:Enterprise
# db-run v1.10 — Launch 1C:Enterprise
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -64,6 +64,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -117,15 +128,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -193,14 +210,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -225,7 +240,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -260,14 +275,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -327,7 +342,7 @@ def main():
# --- Validate connection ---
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")
sys.exit(1)
# --- Build arguments ---
@@ -377,7 +392,7 @@ def main():
time.sleep(0.2)
rc = proc.poll()
if rc is not None:
print(f"Error: 1C:Enterprise exited immediately (code: {rc})", file=sys.stderr)
print(f"Error: 1C:Enterprise exited immediately (code: {rc})")
sys.exit(rc if rc and rc > 0 else 1)
print(f"PID: {proc.pid}")
print("1C:Enterprise launched")
+8 -11
View File
@@ -11,14 +11,16 @@ allowed-tools:
# /db-update — Обновление конфигурации БД
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`). Обязательный шаг после `/db-load-cf`, `/db-load-xml`, `/db-load-git`.
Применяет изменения основной конфигурации к конфигурации базы данных (`/UpdateDBCfg`)
отдельным шагом после загрузки. У `/db-load-xml` и `/db-load-git` то же самое делает
ключ `-UpdateDB`.
## Usage
```
/db-update [database]
/db-update dev
/db-update dev -Dynamic+
/db-update dev -Dynamic on
```
## Параметры подключения
@@ -50,7 +52,8 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
| `-Password <пароль>` | нет | Пароль |
| `-Extension <имя>` | нет | Обновить расширение |
| `-AllExtensions` | нет | Обновить все расширения |
| `-Dynamic <+/->` | нет | `+` — динамическое обновление, `-` — отключить |
| `-NoApplyCheck` | нет | Не проверять применимость расширения после обновления |
| `-Dynamic <on/off>` | нет | `on` — динамическое обновление, без монопольного доступа к базе; `off` — отключить |
| `-Server` | нет | Обновление на стороне сервера |
| `-WarningsAsErrors` | нет | Предупреждения считать ошибками |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
@@ -68,20 +71,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" <п
| `-BackgroundSuspend` | Приостановить |
| `-BackgroundResume` | Возобновить |
## Предупреждения
- Если обновление **не динамическое** — потребуется **монопольный доступ** к базе (все пользователи должны выйти)
- Для серверных баз рекомендуется `-Dynamic+` для обновления без остановки
- Если структура данных существенно изменилась (удаление реквизитов, изменение типов) — динамическое обновление может быть невозможно
## Примеры
```powershell
# Обычное обновление (файловая база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin"
# Динамическое обновление (серверная база)
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic "+"
# Динамическое обновление
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBaseServer "srv01" -InfoBaseRef "MyDB" -UserName "Admin" -Password "secret" -Dynamic on
# Обновление расширения
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/db-update.ps1" -InfoBasePath "C:\Bases\MyDB" -UserName "Admin" -Extension "МоёРасширение"
+218 -11
View File
@@ -1,4 +1,4 @@
# db-update v1.15 — Update 1C database configuration
# db-update v1.20 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -34,7 +34,7 @@
Обновить все расширения
.PARAMETER Dynamic
Динамическое обновление: "+" включить, "-" отключить
Динамическое обновление: on включить, off отключить
.PARAMETER Server
Обновление на стороне сервера
@@ -55,7 +55,7 @@
.\db-update.ps1 -InfoBasePath "C:\Bases\MyDB" -Dynamic "+" -Extension "МоёРасширение"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -81,8 +81,10 @@ param(
[Parameter(Mandatory=$false)]
[switch]$AllExtensions,
# on/off, а не +/-: значение "-" через powershell.exe -File парсер не связывает и молча
# выходит с кодом 2, без единого сообщения. "+"/"-" принимаются, но в инструкции не значатся.
[Parameter(Mandatory=$false)]
[ValidateSet("+", "-")]
[ValidateSet("on", "off", "yes", "no", "+", "-")]
[string]$Dynamic,
[Parameter(Mandatory=$false)]
@@ -97,6 +99,19 @@ param(
# но в логе есть отбраковка.
[switch]$StrictLog,
# Пропустить проверку применимости расширения после загрузки.
[Parameter(Mandatory=$false)]
[switch]$NoApplyCheck,
[Parameter(Mandatory=$false)]
[string]$RepositoryPath,
[Parameter(Mandatory=$false)]
[string]$RepositoryUser,
[Parameter(Mandatory=$false)]
[string]$RepositoryPassword,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
@@ -104,9 +119,95 @@ param(
[string[]]$AdditionalIbcmdArguments = @()
)
if ($Dynamic) { $Dynamic = if (@('on', 'yes', '+') -contains $Dynamic.ToLower()) { '+' } else { '-' } }
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.ps1 (сопоставление по configSrc).
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 Test-SamePath {
param([string]$A, [string]$B)
if (-not $A -or -not $B) { return $false }
try {
$na = [System.IO.Path]::GetFullPath($A).TrimEnd('\', '/')
$nb = [System.IO.Path]::GetFullPath($B).TrimEnd('\', '/')
return $na.Equals($nb, [System.StringComparison]::OrdinalIgnoreCase)
} catch { return $false }
}
function Find-ProjectDatabase {
# Запись базы в реестре, соответствующая переданному соединению. $null, если не найдена.
$pf = Find-V8Project (Get-Location).Path
if (-not $pf) { return $null }
try { $proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json } catch { return $null }
if (-not $proj.databases) { return $null }
foreach ($db in $proj.databases) {
if ($InfoBasePath -and $db.path -and (Test-SamePath $db.path $InfoBasePath)) { return $db }
if ($InfoBaseServer -and $InfoBaseRef -and $db.server -and $db.ref) {
if ($db.server.Equals($InfoBaseServer, [System.StringComparison]::OrdinalIgnoreCase) -and
$db.ref.Equals($InfoBaseRef, [System.StringComparison]::OrdinalIgnoreCase)) { return $db }
}
}
return $null
}
function Resolve-RepositorySettings {
# Возвращает @{ Path; User; Password; FromRegistry }. Явные -Repository* всегда сильнее реестра.
$dbRec = Find-ProjectDatabase
$rec = $null
if ($dbRec) {
if ($Extension) {
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
if ($dbRec.extensions) {
foreach ($ext in $dbRec.extensions) {
if ($ext.name -and $ext.name.Equals($Extension, [System.StringComparison]::OrdinalIgnoreCase)) {
$rec = $ext.repository
break
}
}
}
} else {
$rec = $dbRec.repository
}
}
$path = if ($RepositoryPath) { $RepositoryPath } elseif ($rec -and $rec.path) { [string]$rec.path } else { $null }
$user = if ($RepositoryUser) { $RepositoryUser } elseif ($rec -and $rec.user) { [string]$rec.user } else { $null }
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
$pwd = if ($RepositoryPassword) { $RepositoryPassword } elseif ($rec -and $rec.password) { [string]$rec.password } else { $null }
return @{
Path = if ($path) { $path.Trim().Trim('"') } else { $null }
User = $user
Password = $pwd
FromRegistry = [bool]($rec -and $rec.path)
DbRecord = $dbRec
}
}
function Get-RepositoryArgs {
# Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P.
param([hashtable]$Repo)
$a = @()
if (-not $Repo -or -not $Repo.Path) { return $a }
$a += "/ConfigurationRepositoryF`"$($Repo.Path)`""
if ($Repo.User) { $a += "/ConfigurationRepositoryN`"$($Repo.User)`"" }
if ($Repo.Password) { $a += "/ConfigurationRepositoryP`"$($Repo.Password)`"" }
return $a
}
function Protect-Secrets {
# Redact literal secret values from a display string (String.Replace is literal, not regex).
param([string]$Text, [string[]]$Secrets)
@@ -140,12 +241,23 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
'--user', '--password'
)
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP')
$script:V8SecretKeys = @('/P', '/UC', '/WSP', '/AWSP', '/ConfigurationRepositoryP')
$script:IbcmdSecretKeys = @('--password', '--token', '--db-pwd')
function Test-ArgKeyMatch {
@@ -190,6 +302,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -437,6 +557,73 @@ function Find-SilentRejections {
}
# Постусловие применимости расширения: платформа отчитывается успехом и о расширении, которое
# не применит — отказ всплывает лениво, при первом вызове метода, записью в журнал регистрации.
#
# Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной командной
# строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча отбрасывает —
# проверено на 8.3.24, /LoadConfigFromFiles вместе с /CheckCanApplyConfigurationExtensions
# завершились кодом 0 с пустым логом, и загрузка не состоялась.
#
# Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний исполняемый файл.
function Invoke-ApplyCheck {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$exeDir = Split-Path $Exe -Parent
$exeLeaf = Split-Path $Exe -Leaf
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
if (-not (Test-Path $v8)) { return @{ Skipped = $true; Reason = "1cv8 not found at $v8"; ExitCode = 0; Lines = @() } }
$dir = Join-Path $env:TEMP "apply_check_$(Get-Random)"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
try {
$a = @("DESIGNER") + $ConnArgs + @("/CheckCanApplyConfigurationExtensions")
if ($Extension) { $a += "-Extension", "`"$Extension`"" }
$outFile = Join-Path $dir "check_log.txt"
$a += "/Out", "`"$outFile`"", "/DisableStartupDialogs"
$a += $ExtraArgs
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
$lines = @()
if (Test-Path $outFile) {
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $lines = @($raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) }
}
return @{ Skipped = $false; Reason = ''; ExitCode = $res.ExitCode; Lines = $lines }
} finally {
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
}
}
# Проверить и напечатать. $true, если платформа расширение не применит — вызывающий решает,
# поднимать ли код возврата (строгий режим).
function Invoke-ApplyCheckReport {
param([string]$Exe, [string[]]$ConnArgs, [string]$Extension, [string[]]$ExtraArgs)
$ac = Invoke-ApplyCheck $Exe $ConnArgs $Extension $ExtraArgs
if ($ac.Skipped) {
Write-Host "[note] applicability check skipped: $($ac.Reason)" -ForegroundColor Yellow
return $false
}
if ($ac.ExitCode -ne 0 -or $ac.Lines.Count -gt 0) {
Write-Host "[warning] the extension is loaded, but the platform will not apply it:" -ForegroundColor Yellow
foreach ($l in $ac.Lines) { Write-Host " $l" -ForegroundColor Yellow }
return $true
}
return $false
}
# Проверять ли применимость: -NoApplyCheck сильнее настройки проекта.
function Get-ApplyCheckEnabled {
param([switch]$Disabled)
if ($Disabled) { return $false }
$pf = Find-V8Project (Get-Location).Path
if ($pf) {
try {
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($null -ne $proj.extensionApplyCheck) { return [bool]$proj.extensionApplyCheck }
} catch {}
}
return $true
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
@@ -482,22 +669,37 @@ try {
} else {
Write-Host "Error updating database configuration (code: $exitCode)$(Get-ExitAnnotation $exitCode)" -ForegroundColor Red
}
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
$acConn = @("/F", "`"$InfoBasePath`"")
if ($UserName) { $acConn += "/N`"$UserName`"" }
if ($Password) { $acConn += "/P`"$Password`"" }
if ((Invoke-ApplyCheckReport $V8Path $acConn $Extension @()) -and $StrictLog) { $exitCode = 1 }
}
Write-PlatformOutput $output
exit $exitCode
}
# --- 1cv8 branch ---
# --- Build arguments ---
$arguments = @("DESIGNER")
# Аргументы соединения собираем отдельно: тем же набором пойдёт запуск проверки применимости.
$connArgs = @()
if ($InfoBaseServer -and $InfoBaseRef) {
$arguments += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
$connArgs += "/S", "`"$InfoBaseServer/$InfoBaseRef`""
} else {
$arguments += "/F", "`"$InfoBasePath`""
$connArgs += "/F", "`"$InfoBasePath`""
}
if ($UserName) { $arguments += "/N`"$UserName`"" }
if ($Password) { $arguments += "/P`"$Password`"" }
if ($UserName) { $connArgs += "/N`"$UserName`"" }
if ($Password) { $connArgs += "/P`"$Password`"" }
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
$__repo = Resolve-RepositorySettings
$connArgs += Get-RepositoryArgs $__repo
$arguments = @("DESIGNER") + $connArgs
$arguments += "/UpdateDBCfg"
@@ -526,7 +728,7 @@ try {
$arguments += $extraArgs
# --- Execute ---
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName))"
Write-Host "Running: 1cv8.exe $(Protect-Secrets ((Format-ArgsForDisplay $arguments $engine) -join ' ') @($Password, $UserName, $__repo.Password))"
$__v8 = Invoke-PlatformProcess $V8Path $arguments -PreQuoted
$exitCode = $__v8.ExitCode
@@ -558,6 +760,11 @@ try {
if ($StrictLog -and $exitCode -eq 0) { $exitCode = 1 }
}
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if ($exitCode -eq 0 -and ($Extension -or $AllExtensions) -and (Get-ApplyCheckEnabled -Disabled:$NoApplyCheck)) {
if ((Invoke-ApplyCheckReport $V8Path $connArgs $Extension $extraArgs) -and $StrictLog) { $exitCode = 1 }
}
exit $exitCode
} finally {
+250 -24
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# db-update v1.15 — Update 1C database configuration
# db-update v1.20 — Update 1C database configuration
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,15 +67,111 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
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"]
V8_SECRET_KEYS = ["/P", "/UC", "/WSP", "/AWSP", "/ConfigurationRepositoryP"]
IBCMD_SECRET_KEYS = ["--password", "--token", "--db-pwd"]
# --- Реквизиты хранилища из .v8-project.json ---
# Модель их не передаёт: скрипт сопоставляет параметры соединения с записью в databases[]
# и берёт repository оттуда. Тот же приём, что в cf-edit.py (сопоставление по configSrc).
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 same_path(a, b):
if not a or not b:
return False
try:
return os.path.abspath(a).rstrip("\\/").lower() == os.path.abspath(b).rstrip("\\/").lower()
except Exception:
return False
def find_project_database(args):
"""Запись базы в реестре, соответствующая переданному соединению. None, если не найдена."""
pf = _sg_find_v8project(os.getcwd())
if not pf:
return None
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
except Exception:
return None
for db in proj.get("databases") or []:
if args.InfoBasePath and db.get("path") and same_path(db["path"], args.InfoBasePath):
return db
if args.InfoBaseServer and args.InfoBaseRef and db.get("server") and db.get("ref"):
if (db["server"].lower() == args.InfoBaseServer.lower()
and db["ref"].lower() == args.InfoBaseRef.lower()):
return db
return None
def resolve_repository_settings(args):
"""Возвращает dict path/user/password/from_registry. Явные -Repository* сильнее реестра."""
db_rec = find_project_database(args)
rec = None
if db_rec:
if args.Extension:
# У расширения СВОЁ хранилище со своим путём (проверено): выбирается парой
# /ConfigurationRepositoryF"<путь расширения>" + -Extension "<Имя>".
for ext in db_rec.get("extensions") or []:
if (ext.get("name") or "").lower() == args.Extension.lower():
rec = ext.get("repository")
break
else:
rec = db_rec.get("repository")
path = args.RepositoryPath or ((rec or {}).get("path") or None)
user = args.RepositoryUser or ((rec or {}).get("user") or None)
# Пустой пароль = отсутствующий: 1С требует опускать ключ целиком, а не передавать пустое значение.
pwd = args.RepositoryPassword or ((rec or {}).get("password") or None)
return {
"path": path.strip().strip('"') if path else None,
"user": user,
"password": pwd,
"from_registry": bool(rec and rec.get("path")),
}
def repository_args(repo):
"""Ключи доступа к хранилищу. Форма — кавычки ВНУТРИ токена, как у /N и /P."""
a = []
if not repo or not repo.get("path"):
return a
a.append('/ConfigurationRepositoryF"%s"' % repo["path"])
if repo.get("user"):
a.append('/ConfigurationRepositoryN"%s"' % repo["user"])
if repo.get("password"):
a.append('/ConfigurationRepositoryP"%s"' % repo["password"])
return a
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
@@ -120,15 +216,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +298,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +345,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +383,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +400,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +419,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -376,6 +496,76 @@ def find_silent_rejections(log_text):
return found
def run_apply_check(exe, conn_args, extension, extra_args):
"""Постусловие применимости расширения: платформа отчитывается успехом и о расширении,
которое не применит отказ всплывает лениво, при первом вызове метода, записью в журнал
регистрации.
Запуск ОБЯЗАТЕЛЬНО отдельный. Дописать эту команду в строку операции нельзя: в одной
командной строке DESIGNER выполняет только ПОСЛЕДНЮЮ пакетную команду, остальные молча
отбрасывает проверено на 8.3.24, /LoadConfigFromFiles вместе с
/CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка не
состоялась.
Проверку умеет только 1cv8; если навык работал через ibcmd, берём соседний файл.
"""
exe_dir = os.path.dirname(exe)
leaf = os.path.basename(exe)
if leaf.lower().startswith("ibcmd"):
v8 = os.path.join(exe_dir, "1cv8" + os.path.splitext(leaf)[1])
else:
v8 = exe
if not os.path.isfile(v8):
return {"skipped": True, "reason": f"1cv8 not found at {v8}", "exit": 0, "lines": []}
temp_dir = tempfile.mkdtemp(prefix="apply_check_")
try:
a = ["DESIGNER"] + list(conn_args) + ["/CheckCanApplyConfigurationExtensions"]
if extension:
a += ["-Extension", f'"{extension}"']
out_file = os.path.join(temp_dir, "check_log.txt")
a += ["/Out", f'"{out_file}"', "/DisableStartupDialogs"]
a += list(extra_args)
r = run_v8(v8, a)
lines = []
if os.path.isfile(out_file):
with open(out_file, encoding="utf-8-sig", errors="replace") as f:
lines = [x.strip() for x in f.read().splitlines() if x.strip()]
return {"skipped": False, "reason": "", "exit": r.returncode, "lines": lines}
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def apply_check_report(exe, conn_args, extension, extra_args):
"""Проверить и напечатать. True, если платформа расширение не применит — вызывающий решает,
поднимать ли код возврата (строгий режим)."""
ac = run_apply_check(exe, conn_args, extension, extra_args)
if ac["skipped"]:
print(f"[note] applicability check skipped: {ac['reason']}")
return False
if ac["exit"] != 0 or ac["lines"]:
print("[warning] the extension is loaded, but the platform will not apply it:")
for line in ac["lines"]:
print(f" {line}")
return True
return False
def apply_check_enabled(disabled):
"""Проверять ли применимость: -NoApplyCheck сильнее настройки проекта."""
if disabled:
return False
pf = _sg_find_v8project(os.getcwd())
if pf:
try:
with open(pf, encoding="utf-8-sig") as f:
proj = json.load(f)
if proj.get("extensionApplyCheck") is not None:
return bool(proj.get("extensionApplyCheck"))
except Exception:
pass
return True
def run_ibcmd(cmd, has_username=False, warn_no_user=True):
"""Run an ibcmd command non-interactively.
@@ -384,7 +574,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -438,15 +628,22 @@ def main():
parser.add_argument("-InfoBaseRef", default="")
parser.add_argument("-UserName", default="")
parser.add_argument("-Password", default="")
parser.add_argument("-RepositoryPath", default="")
parser.add_argument("-RepositoryUser", default="")
parser.add_argument("-RepositoryPassword", default="")
parser.add_argument("-Extension", default="")
parser.add_argument("-AllExtensions", action="store_true")
parser.add_argument("-Dynamic", default="", choices=["", "+", "-"])
# on/off, а не +/-: значение "-" через powershell.exe -File парсер PS не связывает и молча
# выходит с кодом 2. "+"/"-" принимаются, но в инструкции не значатся.
parser.add_argument("-Dynamic", default="", choices=["", "on", "off", "yes", "no", "+", "-"])
parser.add_argument("-Server", action="store_true")
parser.add_argument("-WarningsAsErrors", action="store_true")
# Ключ для регрессов и верификации снапшотов, не для повседневного вызова: в SKILL.md
# намеренно не выносится. Поднимает код возврата, если платформа отчиталась об успехе,
# но в логе есть отбраковка.
parser.add_argument("-StrictLog", action="store_true")
# Пропустить проверку применимости расширения после загрузки.
parser.add_argument("-NoApplyCheck", action="store_true")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -455,6 +652,9 @@ def main():
argv, v8_extra, ibcmd_extra = extract_extra_args(sys.argv[1:], known_opts)
args = ci_parse_args(parser, argv)
if args.Dynamic:
args.Dynamic = "+" if args.Dynamic.lower() in ("on", "yes", "+") else "-"
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.InfoBasePath = clean_path(args.InfoBasePath, "-InfoBasePath")
assert_infobase_exists(args.InfoBasePath)
@@ -478,16 +678,16 @@ def main():
# --- Validate connection ---
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
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")
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)
print("Error: ibcmd config apply does not support -AllExtensions (use -Extension)")
sys.exit(1)
arguments = ["infobase", "config", "apply", f"--db-path={args.InfoBasePath}", "--force"]
if args.Dynamic == "+":
@@ -509,7 +709,19 @@ def main():
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)
print(f"Error updating database configuration (code: {result.returncode}){describe_exit(result.returncode)}")
# Проверку применимости умеет только 1cv8 — соединение для неё собираем в его форме.
exit_code = result.returncode
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
ac_conn = ["/F", f'"{args.InfoBasePath}"']
if args.UserName:
ac_conn.append(f'/N"{args.UserName}"')
if args.Password:
ac_conn.append(f'/P"{args.Password}"')
if apply_check_report(v8path, ac_conn, args.Extension, []) and args.StrictLog:
exit_code = 1
sys.exit(result.returncode)
# --- Temp dir ---
@@ -518,17 +730,25 @@ def main():
try:
# --- Build arguments ---
arguments = ["DESIGNER"]
# Аргументы соединения собираем отдельно: тем же набором пойдёт проверка применимости.
conn_args = []
if args.InfoBaseServer and args.InfoBaseRef:
arguments.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
conn_args.extend(["/S", f'"{args.InfoBaseServer}/{args.InfoBaseRef}"'])
else:
arguments.extend(["/F", f'"{args.InfoBasePath}"'])
conn_args.extend(["/F", f'"{args.InfoBasePath}"'])
if args.UserName:
arguments.append(f'/N"{args.UserName}"')
conn_args.append(f'/N"{args.UserName}"')
if args.Password:
arguments.append(f'/P"{args.Password}"')
conn_args.append(f'/P"{args.Password}"')
# База под хранилищем не примет НИ ОДНОЙ операции конфигуратора без этих реквизитов, а для
# базы вне хранилища они безвредны — поэтому подставляем всегда, когда они известны.
repo = resolve_repository_settings(args)
conn_args.extend(repository_args(repo))
arguments = ["DESIGNER"] + conn_args
arguments.append("/UpdateDBCfg")
@@ -553,7 +773,7 @@ def main():
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)}")
print(f"Running: 1cv8.exe {_redact(' '.join(format_args_for_display(arguments, engine)), args.Password, args.UserName, repo['password'])}")
result = run_v8(v8path, arguments)
exit_code = result.returncode
@@ -561,7 +781,7 @@ def main():
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(f"Error updating database configuration (code: {exit_code}){describe_exit(exit_code)}")
log_content = ""
if os.path.isfile(out_file):
@@ -591,6 +811,12 @@ def main():
if args.StrictLog and exit_code == 0:
exit_code = 1
# Расширение могло загрузиться «успешно» и остаться неприменимым — спрашиваем платформу.
if (exit_code == 0 and (args.Extension or args.AllExtensions)
and apply_check_enabled(args.NoApplyCheck)):
if apply_check_report(v8path, conn_args, args.Extension, extra_args) and args.StrictLog:
exit_code = 1
sys.exit(exit_code)
finally:
+13 -1
View File
@@ -25,7 +25,8 @@ allowed-tools:
## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
под проверку исходников, если она не отключена.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
@@ -55,11 +56,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/epf-build.ps1" <п
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному EPF/ERF-файлу |
| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` |
| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Проверка перед сборкой
Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм.
Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение
платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off`
или ключом `"externalCheck": false` в `.v8-project.json`.
Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает.
## Примеры
```powershell
+212 -17
View File
@@ -1,4 +1,4 @@
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -46,7 +46,7 @@
.\epf-build.ps1 -InfoBasePath "C:\Bases\MyDB" -SourceFile "src\МойОтчёт.xml" -OutputFile "build\МойОтчёт.erf"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -72,6 +72,15 @@ param(
[Parameter(Mandatory=$true)]
[string]$OutputFile,
# Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers,
# unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers.
[Parameter(Mandatory=$false)]
[string]$Checks,
# Контексты синтаксической проверки. По умолчанию ThinClient,Server.
[Parameter(Mandatory=$false)]
[string]$Context,
[Parameter(Mandatory=$false)]
[string[]]$AdditionalV8Arguments = @(),
@@ -98,6 +107,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -148,6 +168,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -369,6 +397,122 @@ function Test-OutputNonEmpty {
return (Test-Path $Path -PathType Leaf) -and ((Get-Item $Path -ErrorAction SilentlyContinue).Length -gt 0)
}
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
}
# --- Проверка исходников платформой ---
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она
# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create
# -EmbedSourceFile) и спрашивается штатной /CheckConfig.
#
# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке
# DESIGNER выполняет лишь последнюю пакетную команду.
function Get-CheckFlags {
param([string[]]$Checks, [string[]]$Contexts)
$flags = @()
if ($Checks -contains 'modules') { foreach ($c in $Contexts) { $flags += "-$c" } }
if ($Checks -contains 'handlers') { $flags += '-HandlersExistence' }
if ($Checks -contains 'unreferenced') { $flags += '-UnreferenceProcedures' }
if ($Checks -contains 'empty-handlers') { $flags += '-EmptyHandlers' }
if ($Checks -contains 'config') { $flags += '-ConfigLogIntegrity', '-IncorrectReferences' }
return $flags
}
# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику.
# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл:
# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет.
function Resolve-SourcePath {
param([string]$Line, [string]$SourceDir)
$candidate = $null
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(?:Форма|Form)\.([^.]+)\.')
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value (Join-Path "Forms" (Join-Path $m.Groups[2].Value "Ext\Form\Module.bsl")))) }
if (-not $candidate) {
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульОбъекта|ObjectModule)')
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ObjectModule.bsl")) }
}
if (-not $candidate) {
$m = [regex]::Match($Line, '(?:Обработка|Отчет|DataProcessor|Report)\.([^.]+)\.(МодульМенеджера|ManagerModule)')
if ($m.Success) { $candidate = (Join-Path $SourceDir (Join-Path $m.Groups[1].Value "Ext\ManagerModule.bsl")) }
}
if ($candidate -and (Test-Path $candidate -PathType Leaf)) { return $candidate }
return $null
}
# $true, если платформа нашла проблемы — вызывающий не собирает артефакт.
function Invoke-SourceCheck {
param([string]$Exe, [string]$BasePath, [string[]]$Flags, [string]$SourceDir, [string[]]$ExtraArgs)
$exeDir = Split-Path $Exe -Parent
$exeLeaf = Split-Path $Exe -Leaf
$v8 = if ($exeLeaf -match '^ibcmd') { Join-Path $exeDir ("1cv8" + [System.IO.Path]::GetExtension($Exe)) } else { $Exe }
if (-not (Test-Path $v8)) {
Write-Host "[note] source check skipped: 1cv8 not found at $v8" -ForegroundColor Yellow
return $false
}
$dir = Join-Path $env:TEMP "epf_check_$(Get-Random)"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
try {
$outFile = Join-Path $dir "check_log.txt"
$a = @("DESIGNER", "/F", "`"$BasePath`"", "/CheckConfig") + $Flags + @("/Out", "`"$outFile`"", "/DisableStartupDialogs") + $ExtraArgs
Write-Host "Running: 1cv8.exe $($a -join ' ')"
$res = Invoke-PlatformProcess $v8 $a -PreQuoted
$lines = @()
if (Test-Path $outFile) {
$raw = Get-Content $outFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $lines = @($raw -split "`r?`n" | Where-Object { $_.Trim() -ne '' }) }
}
# Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0.
if ($res.ExitCode -eq 0) { return $false }
Write-Host "Error: платформа нашла проблемы в исходниках — сборка отменена" -ForegroundColor Red
# Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя.
if ($lines.Count -eq 0) { Write-Host " платформа вернула код $($res.ExitCode) без сообщений" -ForegroundColor Red }
foreach ($l in $lines) {
Write-Host " $($l.TrimEnd())" -ForegroundColor Red
$srcPath = Resolve-SourcePath $l $SourceDir
if ($srcPath) { Write-Host " -> $srcPath" -ForegroundColor Red }
}
return $true
} finally {
if (Test-Path $dir) { Remove-Item -Path $dir -Recurse -Force -ErrorAction SilentlyContinue }
}
}
# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck.
function Get-SourceCheckList {
param([string]$Checks)
$known = @('modules', 'handlers', 'unreferenced', 'empty-handlers', 'config')
if ($Checks) {
$list = @($Checks -split ',' | ForEach-Object { $_.Trim().ToLower() } | Where-Object { $_ })
if ($list -contains 'off') { return @() }
foreach ($c in $list) {
if ($known -notcontains $c) {
Write-Host "Error: unknown check '$c' (expected: $($known -join ', ') or off)" -ForegroundColor Red
exit 1
}
}
return $list
}
$pf = Find-V8Project (Get-Location).Path
if ($pf) {
try {
$proj = Get-Content $pf -Raw -Encoding UTF8 | ConvertFrom-Json
if ($null -ne $proj.externalCheck -and -not [bool]$proj.externalCheck) { return @() }
} catch {}
}
return @('modules', 'handlers')
}
$engine = if ((Split-Path $V8Path -Leaf) -match '^ibcmd') { "ibcmd" } else { "1cv8" }
# --- Resolve additional arguments for the selected engine ---
@@ -379,33 +523,69 @@ if ($engine -eq "ibcmd" -and $InfoBaseServer -and $InfoBaseRef) {
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)"
# --- Что проверяем в исходниках перед сборкой ---
$checkList = @(Get-SourceCheckList $Checks)
$contextList = @()
if ($Context) { $contextList = @($Context -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
if ($contextList.Count -eq 0) { $contextList = @('ThinClient', 'Server') }
elseif ($checkList.Count -gt 0 -and $checkList -notcontains 'modules') {
Write-Host "Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней" -ForegroundColor Red
exit 1
}
$sourceDir = Split-Path $SourceFile -Parent
function New-StubBase {
# Стаб запускает свои процессы платформы (CREATEINFOBASE, LoadConfigFromFiles, UpdateDBCfg) —
# им нужны те же дополнительные аргументы, что и сборке. Передаются только явные: файл проекта
# стаб читает сам. Вызов через -Command, не -File: -File берёт хвост буквально, и массивный
# параметр пришёл бы одним склеенным токеном.
param([string]$BasePath, [switch]$Embed)
$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)"
$stubCmd = "& $(& $q $stubScript) -SourceDir $(& $q $sourceDir) -V8Path $(& $q $V8Path) -TempBasePath $(& $q $BasePath)"
if ($Embed) { $stubCmd += " -EmbedSourceFile $(& $q $SourceFile)" }
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
$p = Start-Process -FilePath "powershell.exe" -ArgumentList "-NoProfile -Command `"$stubCmd`"" -NoNewWindow -Wait -PassThru
return $p.ExitCode
}
# --- Auto-create stub database if no connection specified ---
$autoCreatedBase = $null
$checkBase = $null
$checkBasePath = $null
if (-not $InfoBasePath -and (-not $InfoBaseServer -or -not $InfoBaseRef)) {
$autoBasePath = Join-Path $env:TEMP "epf_stub_db_$(Get-Random)"
Write-Host "No database specified. Creating temporary stub database..."
if ((New-StubBase $autoBasePath -Embed:($checkList.Count -gt 0)) -ne 0) {
# С внедрённой обработкой база падает прежде всего из-за самих исходников
# (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда.
if ($checkList.Count -gt 0) {
Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red
Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red
} else {
Write-Host "Error: failed to create stub database" -ForegroundColor Red
}
exit 1
}
$InfoBasePath = $autoBasePath
$autoCreatedBase = $autoBasePath
if ($checkList.Count -gt 0) { $checkBasePath = $autoBasePath }
} elseif ($checkList.Count -gt 0) {
# Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под
# проверку поднимается своя временная база, а сборка идёт на указанной.
$checkBase = Join-Path $env:TEMP "epf_check_db_$(Get-Random)"
Write-Host "Creating temporary database for the source check..."
if ((New-StubBase $checkBase -Embed) -ne 0) {
Write-Host "Error: платформа не приняла исходники при подготовке проверки — сборка отменена" -ForegroundColor Red
Write-Host " сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт" -ForegroundColor Red
exit 1
}
$checkBasePath = $checkBase
}
# --- Validate source file ---
@@ -414,6 +594,18 @@ if (-not (Test-Path $SourceFile)) {
exit 1
}
# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен ---
if ($checkList.Count -gt 0 -and $checkBasePath) {
# Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём.
$checkExtra = if ($engine -eq "ibcmd") { @() } else { $extraArgs }
$found = Invoke-SourceCheck $V8Path $checkBasePath (Get-CheckFlags $checkList $contextList) $sourceDir $checkExtra
if ($found) {
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) { Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue }
if ($checkBase -and (Test-Path $checkBase)) { Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue }
exit 1
}
}
# --- Ensure output directory exists ---
$outDir = Split-Path $OutputFile -Parent
if ($outDir -and -not (Test-Path $outDir)) {
@@ -507,4 +699,7 @@ try {
if ($autoCreatedBase -and (Test-Path $autoCreatedBase)) {
Remove-Item -Path $autoCreatedBase -Recurse -Force -ErrorAction SilentlyContinue
}
if ($checkBase -and (Test-Path $checkBase)) {
Remove-Item -Path $checkBase -Recurse -Force -ErrorAction SilentlyContinue
}
}
+248 -29
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env python3
# epf-build v1.13 — Build external data processor or report (EPF/ERF) from XML sources
# epf-build v1.17 — Build external data processor or report (EPF/ERF) from XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import atexit
import glob
import io
import json
import os
import random
@@ -67,6 +68,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +132,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +214,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +261,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +299,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +316,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +335,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -352,7 +388,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -375,6 +411,136 @@ def _redact(text, *secrets):
return text
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
# --- Проверка исходников платформой ---
# Сборка .epf/.erf ничего не проверяет: /LoadExternalDataProcessorOrReportFromFiles упаковывает XML
# и модули не компилирует, поэтому сломанный модуль доезжает до пользователя и падает при открытии
# обработки. Прямой команды «проверь внешнюю обработку» у платформы нет, но объект КОНФИГУРАЦИИ она
# проверяет — поэтому обработка кладётся в конфигурацию временной базы (stub-db-create
# -EmbedSourceFile) и спрашивается штатной /CheckConfig.
#
# Запуск отдельный и только через 1cv8: у ibcmd такой команды нет, а в одной командной строке
# DESIGNER выполняет лишь последнюю пакетную команду.
def get_check_flags(checks, contexts):
flags = []
if 'modules' in checks:
for c in contexts:
flags.append('-' + c)
if 'handlers' in checks:
flags.append('-HandlersExistence')
if 'unreferenced' in checks:
flags.append('-UnreferenceProcedures')
if 'empty-handlers' in checks:
flags.append('-EmptyHandlers')
if 'config' in checks:
flags += ['-ConfigLogIntegrity', '-IncorrectReferences']
return flags
# Платформа называет объект своим именем внутри конфигурации; модели нужен путь к исходнику.
# Путь СКЛЕИВАЕТСЯ по конвенции выгрузки, поэтому возвращается только существующий файл:
# выдуманный путь хуже отсутствующего — модель пойдёт открывать файл, которого нет.
def resolve_source_path(line, source_dir):
candidate = None
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u0424\u043e\u0440\u043c\u0430|Form)\.([^.]+)\.', line)
if m:
candidate = os.path.join(source_dir, m.group(1), 'Forms', m.group(2), 'Ext', 'Form', 'Module.bsl')
if candidate is None:
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041e\u0431\u044a\u0435\u043a\u0442\u0430|ObjectModule)', line)
if m:
candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ObjectModule.bsl')
if candidate is None:
m = re.search(r'(?:\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430|\u041e\u0442\u0447\u0435\u0442|DataProcessor|Report)\.([^.]+)\.(?:\u041c\u043e\u0434\u0443\u043b\u044c\u041c\u0435\u043d\u0435\u0434\u0436\u0435\u0440\u0430|ManagerModule)', line)
if m:
candidate = os.path.join(source_dir, m.group(1), 'Ext', 'ManagerModule.bsl')
if candidate and os.path.isfile(candidate):
return candidate
return None
# True, если платформа нашла проблемы — вызывающий не собирает артефакт.
def invoke_source_check(exe, base_path, flags, source_dir, extra_args):
exe_dir = os.path.dirname(exe)
exe_leaf = os.path.basename(exe)
if exe_leaf.lower().startswith('ibcmd'):
v8 = os.path.join(exe_dir, '1cv8' + os.path.splitext(exe)[1])
else:
v8 = exe
if not os.path.exists(v8):
print(f'[note] source check skipped: 1cv8 not found at {v8}')
return False
d = os.path.join(tempfile.gettempdir(), f'epf_check_{random.randint(0, 999999)}')
os.makedirs(d, exist_ok=True)
try:
out_file = os.path.join(d, 'check_log.txt')
a = (['DESIGNER', '/F', f'"{base_path}"', '/CheckConfig'] + flags
+ ['/Out', f'"{out_file}"', '/DisableStartupDialogs']
+ [quote_if_needed(x) for x in extra_args])
print(f'Running: 1cv8.exe {" ".join(a)}')
result = run_v8(v8, a)
lines = []
if os.path.isfile(out_file):
try:
with io.open(out_file, encoding='utf-8-sig', errors='replace') as fh:
raw = fh.read()
lines = [l for l in raw.splitlines() if l.strip()]
except Exception:
lines = []
# Платформа отвечает 101 на найденные проблемы; «Ошибок не обнаружено» приходит с кодом 0.
if result.returncode == 0:
return False
print('Error: \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430 \u043d\u0430\u0448\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u0432 \u0438\u0441\u0445\u043e\u0434\u043d\u0438\u043a\u0430\u0445 \u2014 \u0441\u0431\u043e\u0440\u043a\u0430 \u043e\u0442\u043c\u0435\u043d\u0435\u043d\u0430')
# Пустой лог при ненулевом коде — отказ не по находкам (база занята, нет лицензии); молчать нельзя.
if not lines:
print(f' платформа вернула код {result.returncode} без сообщений')
for l in lines:
print(f' {l.rstrip()}')
src_path = resolve_source_path(l, source_dir)
if src_path:
print(f' -> {src_path}')
return True
finally:
shutil.rmtree(d, ignore_errors=True)
# Проверять ли исходники: -Checks off сильнее настройки проекта externalCheck.
def get_source_check_list(checks):
known = ['modules', 'handlers', 'unreferenced', 'empty-handlers', 'config']
if checks:
lst = [c.strip().lower() for c in checks.split(',') if c.strip()]
if 'off' in lst:
return []
for c in lst:
if c not in known:
print(f'Error: unknown check \'{c}\' (expected: {", ".join(known)} or off)')
sys.exit(1)
return lst
pf = _sg_find_v8project(os.getcwd())
if pf:
try:
with open(pf, encoding='utf-8-sig') as fh:
proj = json.load(fh)
if 'externalCheck' in proj and not proj['externalCheck']:
return []
except Exception:
pass
return ['modules', 'handlers']
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -390,6 +556,11 @@ def main():
parser.add_argument("-Password", default="", help="1C user password")
parser.add_argument("-SourceFile", required=True, help="Path to root XML source file")
parser.add_argument("-OutputFile", required=True, help="Path to output EPF/ERF file")
# Что проверить в исходниках перед сборкой: modules (синтаксис в контекстах), handlers,
# unreferenced, empty-handlers, config; off — не проверять. По умолчанию modules,handlers.
parser.add_argument("-Checks", default="")
# Контексты синтаксической проверки. По умолчанию ThinClient,Server.
parser.add_argument("-Context", default="")
parser.add_argument("-AdditionalV8Arguments", nargs="*", default=[],
help="Extra 1cv8 arguments, e.g. /UseHwLicenses+")
parser.add_argument("-AdditionalIbcmdArguments", nargs="*", default=[],
@@ -420,37 +591,83 @@ def main():
}
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)
print("Error: ibcmd supports file infobases only (use -InfoBasePath or omit for stub)")
sys.exit(1)
# --- Auto-create stub database if no connection specified ---
auto_created_base = None
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
print("No database specified. Creating temporary stub database...")
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
"-TempBasePath", auto_base_path]
# --- Что проверяем в исходниках перед сборкой ---
check_list = get_source_check_list(args.Checks)
context_list = [c.strip() for c in args.Context.split(',') if c.strip()] if args.Context else []
if not context_list:
context_list = ['ThinClient', 'Server']
elif check_list and 'modules' not in check_list:
print('Error: -Context задан, но в -Checks нет modules — контексты относятся только к ней')
sys.exit(1)
source_dir = os.path.dirname(os.path.abspath(args.SourceFile))
def new_stub_base(base_path, embed):
# 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.
stub_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "stub-db-create.py")
stub_cmd = [sys.executable, stub_script, "-SourceDir", source_dir, "-V8Path", v8path,
"-TempBasePath", base_path]
if embed:
stub_cmd += ["-EmbedSourceFile", args.SourceFile]
if v8_extra:
stub_cmd += ["-AdditionalV8Arguments"] + list(v8_extra)
if ibcmd_extra:
stub_cmd += ["-AdditionalIbcmdArguments"] + list(ibcmd_extra)
result = subprocess.run(stub_cmd, capture_output=False)
if result.returncode != 0:
print("Error: failed to create stub database", file=sys.stderr)
return subprocess.run(stub_cmd, capture_output=False).returncode
# --- Auto-create stub database if no connection specified ---
auto_created_base = None
check_base = None
check_base_path = None
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
auto_base_path = os.path.join(tempfile.gettempdir(), f"epf_stub_db_{random.randint(0, 999999)}")
print("No database specified. Creating temporary stub database...")
if new_stub_base(auto_base_path, bool(check_list)) != 0:
# С внедрённой обработкой база падает прежде всего из-за самих исходников
# (пример: DefaultForm на несуществующую форму) — говорить про базу значит увести не туда.
if check_list:
print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена')
print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт')
else:
print("Error: failed to create stub database")
sys.exit(1)
args.InfoBasePath = auto_base_path
auto_created_base = auto_base_path
if check_list:
check_base_path = auto_base_path
elif check_list:
# Базу указали снаружи: класть проверяемую обработку в чужую конфигурацию нельзя, поэтому под
# проверку поднимается своя временная база, а сборка идёт на указанной.
check_base = os.path.join(tempfile.gettempdir(), f"epf_check_db_{random.randint(0, 999999)}")
print("Creating temporary database for the source check...")
if new_stub_base(check_base, True) != 0:
print('Error: платформа не приняла исходники при подготовке проверки — сборка отменена')
print(' сообщение платформы выше; имя объекта в нём конфигурационное: DataProcessor/Report = проверяемая внешняя обработка/отчёт')
sys.exit(1)
check_base_path = check_base
# --- Validate source file ---
if not os.path.isfile(args.SourceFile):
print(f"Error: source file not found: {args.SourceFile}", file=sys.stderr)
print(f"Error: source file not found: {args.SourceFile}")
sys.exit(1)
# --- Проверка исходников платформой: сломанный .epf до пользователя доезжать не должен ---
if check_list and check_base_path:
# Проверку ведёт 1cv8, поэтому ibcmd-шные дополнительные аргументы ей не отдаём.
check_extra = [] if engine == "ibcmd" else extra_args
found = invoke_source_check(v8path, check_base_path,
get_check_flags(check_list, context_list), source_dir, check_extra)
if found:
if auto_created_base and os.path.exists(auto_created_base):
shutil.rmtree(auto_created_base, ignore_errors=True)
if check_base and os.path.exists(check_base):
shutil.rmtree(check_base, ignore_errors=True)
sys.exit(1)
# --- Ensure output directory exists ---
out_dir = os.path.dirname(args.OutputFile)
if out_dir and not os.path.exists(out_dir):
@@ -482,9 +699,9 @@ def main():
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)
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output")
else:
print(f"Error building external data processor/report (code: {exit_code})", file=sys.stderr)
print(f"Error building external data processor/report (code: {exit_code})")
sys.exit(exit_code)
# --- Build arguments ---
@@ -521,9 +738,9 @@ def main():
if exit_code == 0:
print(f"Build completed successfully: {args.OutputFile}")
elif out_missing:
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output", file=sys.stderr)
print(f"Error: exit code 0 but no non-empty file at {args.OutputFile} — build produced no output")
else:
print(f"Error building (code: {exit_code})", file=sys.stderr)
print(f"Error building (code: {exit_code})")
if os.path.isfile(out_file):
try:
@@ -544,6 +761,8 @@ def main():
shutil.rmtree(temp_dir, ignore_errors=True)
if auto_created_base and os.path.exists(auto_created_base):
shutil.rmtree(auto_created_base, ignore_errors=True)
if check_base and os.path.exists(check_base):
shutil.rmtree(check_base, ignore_errors=True)
if __name__ == "__main__":
@@ -1,4 +1,4 @@
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -9,6 +9,10 @@ param(
[string]$TempBasePath,
# XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа
# смогла проверить его штатными проверками. Без параметра стаб работает как раньше.
[string]$EmbedSourceFile,
[string[]]$AdditionalV8Arguments = @(),
[string[]]$AdditionalIbcmdArguments = @()
@@ -49,6 +53,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -99,6 +114,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
@@ -331,6 +354,9 @@ foreach ($f in $xmlFiles) {
}
$hasRefTypes = $typeMap.Count -gt 0
# Конфигурация нужна и тогда, когда ссылочных типов нет: в неё кладётся сам объект.
$embedRequested = -not [string]::IsNullOrWhiteSpace($EmbedSourceFile)
$needCfg = $hasRefTypes -or $embedRequested
# --- 2. Determine TempBasePath ---
if (-not $TempBasePath) {
@@ -351,13 +377,110 @@ if ($needsRegistrator) {
$typeMap["Document"]["ЗаглушкаРегистратора"] = $true
}
# --- Внедрение проверяемого объекта в конфигурацию-заглушку ---
# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles
# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а
# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем
# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим
# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и
# макеты, а формат может расти без правок здесь.
#
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
# такой же текст был бы кодом, и трогать его нельзя.
function Add-SourceObjectToConfig {
param([string]$SourceXml, [string]$CfgDir)
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
# XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные
# между её файлами (ссылки внутри объекта идут по идентификатору).
$guidMap = @{}
$reGuid = [regex]'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
$reissue = {
param($m)
$k = $m.Value.ToLower()
if (-not $guidMap.ContainsKey($k)) { $guidMap[$k] = [guid]::NewGuid().ToString() }
$guidMap[$k]
}
$text = [IO.File]::ReadAllText($SourceXml, [Text.Encoding]::UTF8)
if ($text -match '<ExternalDataProcessor[\s>]') {
$extTag = 'ExternalDataProcessor'; $cfgTag = 'DataProcessor'; $folder = 'DataProcessors'
} elseif ($text -match '<ExternalReport[\s>]') {
$extTag = 'ExternalReport'; $cfgTag = 'Report'; $folder = 'Reports'
} else {
return $null
}
$name = if ($text -match '<Name>([^<]+)</Name>') { $Matches[1] } else { [IO.Path]::GetFileNameWithoutExtension($SourceXml) }
$conv = $reGuid.Replace($text, $reissue)
$conv = $conv.Replace("<$extTag ", "<$cfgTag ").Replace("<$extTag>", "<$cfgTag>").Replace("</$extTag>", "</$cfgTag>")
$conv = $conv.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
# без него платформа отвечает «отсутствует один или более типов объекта».
$mgr = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Manager.$name`" category=`"Manager`">`r`n" +
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
"`t`t`t</xr:GeneratedType>`r`n"
if ($conv -match '</InternalInfo>') {
$conv = [regex]::Replace($conv, '(\s*)</InternalInfo>', ("`r`n" + $mgr + "`t`t</InternalInfo>"), 1)
} else {
$objType = "`t`t`t<xr:GeneratedType name=`"${cfgTag}Object.$name`" category=`"Object`">`r`n" +
"`t`t`t`t<xr:TypeId>$([guid]::NewGuid().ToString())</xr:TypeId>`r`n" +
"`t`t`t`t<xr:ValueId>$([guid]::NewGuid().ToString())</xr:ValueId>`r`n" +
"`t`t`t</xr:GeneratedType>`r`n"
$conv = [regex]::Replace($conv, "(<$cfgTag[^>]*>)", ("`$1`r`n`t`t<InternalInfo>`r`n" + $objType + $mgr + "`t`t</InternalInfo>"), 1)
}
$objDir = Join-Path $CfgDir $folder
New-Item -ItemType Directory -Path $objDir -Force | Out-Null
$encBom = New-Object System.Text.UTF8Encoding($true)
[IO.File]::WriteAllText((Join-Path $objDir "$name.xml"), $conv, $encBom)
# Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт.
$srcContent = Join-Path (Split-Path $SourceXml -Parent) $name
if (Test-Path $srcContent) {
# Длину префикса берём у РАЗРЕШЁННОГО пути, а не у переданной строки: путь может
# прийти с коротким именем (C:\Users\NSHIRO~1\…), а Get-ChildItem отдаёт полное — тогда
# отрезание по длине исходной строки оставляет в относительном пути чужие символы.
$srcRoot = (Get-Item -LiteralPath $srcContent).FullName.TrimEnd('\', '/')
$dstContent = Join-Path $objDir $name
foreach ($f in (Get-ChildItem -LiteralPath $srcRoot -Recurse -File)) {
$rel = $f.FullName.Substring($srcRoot.Length).TrimStart('\', '/')
$dst = Join-Path $dstContent $rel
New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null
if ($f.Extension -ieq '.xml') {
$t = [IO.File]::ReadAllText($f.FullName, [Text.Encoding]::UTF8)
$t = $reGuid.Replace($t, $reissue)
$t = $t.Replace("${extTag}Object.", "${cfgTag}Object.").Replace("$extTag.", "$cfgTag.")
[IO.File]::WriteAllText($dst, $t, $encBom)
} else {
Copy-Item -Path $f.FullName -Destination $dst -Force
}
}
}
return @{ Tag = $cfgTag; Name = $name }
}
# --- 4. Generate configuration XML ---
if ($hasRefTypes) {
if ($needCfg) {
$enc = New-Object System.Text.UTF8Encoding($true)
$cfgDir = Join-Path $TempBasePath "cfg"
New-Item -ItemType Directory -Path $cfgDir -Force | Out-Null
$embedded = $null
if ($embedRequested) {
$embedded = Add-SourceObjectToConfig $EmbedSourceFile $cfgDir
if (-not $embedded) {
Write-Host "Error: $EmbedSourceFile is neither ExternalDataProcessor nor ExternalReport" -ForegroundColor Red
exit 1
}
}
# Заглушке нужна САМАЯ НИЗКАЯ работающая версия, а не версия исходников: ограничение платформы
# одностороннее — она читает формат не новее себя. Отсюда min(версия исходников, 2.17): на 2.17+
# заглушка остаётся 2.17 (как было), а под исходники 2.13-2.16 опускается до их версии, иначе
@@ -547,6 +670,7 @@ if ($hasRefTypes) {
$childXml += "`r`n`t`t`t<$tag>$name</$tag>"
}
}
if ($embedded) { $childXml += "`r`n`t`t`t<$($embedded.Tag)>$($embedded.Name)</$($embedded.Tag)>" }
$cfgXml = @"
<?xml version="1.0" encoding="UTF-8"?>
@@ -1529,7 +1653,7 @@ if ($stubEngine -eq "ibcmd") {
$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" }
if ($needCfg) { $ibArgs += "--import=$(Join-Path $TempBasePath 'cfg')", "--apply", "--force" }
$ibArgs += "--data=$ibData"
$ibArgs += $extraArgs
$__ib = Invoke-PlatformProcess $V8Path $ibArgs
@@ -1541,7 +1665,7 @@ if ($stubEngine -eq "ibcmd") {
Write-Error "Failed to create stub infobase (code: $ibRc)"
exit 1
}
if ($hasRefTypes) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
if ($needCfg) { Remove-Item -Path (Join-Path $TempBasePath "cfg") -Recurse -Force -ErrorAction SilentlyContinue }
Write-Host "[OK] Stub database created: $TempBasePath"
Write-Host $TempBasePath
exit 0
@@ -1557,8 +1681,8 @@ if ($proc.ExitCode -ne 0) {
exit 1
}
# --- 6. Load config and update DB if ref types exist ---
if ($hasRefTypes) {
# --- 6. Load config and update DB if there is one ---
if ($needCfg) {
$cfgDir = Join-Path $TempBasePath "cfg"
# LoadConfigFromFiles
Write-Host "Loading configuration from files..."
@@ -1,9 +1,11 @@
#!/usr/bin/env python3
# stub-db-create v1.8 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# stub-db-create v1.10 — Create temp 1C infobase with metadata stubs for EPF/ERF build
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import io
import os
import shutil
import random
import re
import subprocess
@@ -64,11 +66,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -114,6 +136,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -167,15 +200,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -243,14 +282,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -1083,6 +1120,94 @@ def write_bom(path, content):
f.write(content)
# --- Внедрение проверяемого объекта в конфигурацию-заглушку ---
# Платформа не умеет проверять внешнюю обработку: /LoadExternalDataProcessorOrReportFromFiles
# только упаковывает XML и модули не компилирует. Зато она проверяет объект КОНФИГУРАЦИИ, а
# внешняя обработка отличается от него немногим (замер 8.3.24): корневым тегом, именем
# порождаемого объектного типа и отсутствием типа менеджера. Правим ровно эти точки и переносим
# остальное как есть — под проверку попадает всё, что написал автор, включая реквизиты, формы и
# макеты, а формат может расти без правок здесь.
#
# Подстановка типа делается ТОЛЬКО в .xml (это DefaultForm и основной реквизит формы); в .bsl
# такой же текст был бы кодом, и трогать его нельзя.
GUID_RE = re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}')
def add_source_object_to_config(source_xml, cfg_dir):
# Копия объекта живёт в конфигурации базы, а следом в ту же базу грузится исходник как ВНЕШНЯЯ
# обработка. С одинаковыми идентификаторами платформа путает их и через раз отвечает «Исключение
# XDTO при чтении файла» на исправном исходнике — поэтому у копии все GUID свои, но согласованные
# между её файлами (ссылки внутри объекта идут по идентификатору).
guid_map = {}
def reissue(text):
def sub(m):
k = m.group(0).lower()
if k not in guid_map:
guid_map[k] = new_uuid()
return guid_map[k]
return GUID_RE.sub(sub, text)
with io.open(source_xml, encoding='utf-8-sig') as fh:
text = fh.read()
if re.search(r'<ExternalDataProcessor[\s>]', text):
ext_tag, cfg_tag, folder = 'ExternalDataProcessor', 'DataProcessor', 'DataProcessors'
elif re.search(r'<ExternalReport[\s>]', text):
ext_tag, cfg_tag, folder = 'ExternalReport', 'Report', 'Reports'
else:
return None
m = re.search(r'<Name>([^<]+)</Name>', text)
name = m.group(1) if m else os.path.splitext(os.path.basename(source_xml))[0]
conv = reissue(text)
conv = conv.replace('<%s ' % ext_tag, '<%s ' % cfg_tag).replace('<%s>' % ext_tag, '<%s>' % cfg_tag)
conv = conv.replace('</%s>' % ext_tag, '</%s>' % cfg_tag)
conv = conv.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
# Тип менеджера у внешней обработки не объявлен, а объекту конфигурации он обязателен:
# без него платформа отвечает «отсутствует один или более типов объекта».
mgr = ('\t\t\t<xr:GeneratedType name="%sManager.%s" category="Manager">\r\n' % (cfg_tag, name) +
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
'\t\t\t</xr:GeneratedType>\r\n')
if '</InternalInfo>' in conv:
conv = re.sub(r'\s*</InternalInfo>', lambda _m: '\r\n' + mgr + '\t\t</InternalInfo>', conv, count=1)
else:
obj_type = ('\t\t\t<xr:GeneratedType name="%sObject.%s" category="Object">\r\n' % (cfg_tag, name) +
'\t\t\t\t<xr:TypeId>%s</xr:TypeId>\r\n' % new_uuid() +
'\t\t\t\t<xr:ValueId>%s</xr:ValueId>\r\n' % new_uuid() +
'\t\t\t</xr:GeneratedType>\r\n')
conv = re.sub('(<%s[^>]*>)' % cfg_tag,
lambda m2: m2.group(1) + '\r\n\t\t<InternalInfo>\r\n' + obj_type + mgr + '\t\t</InternalInfo>',
conv, count=1)
obj_dir = os.path.join(cfg_dir, folder)
os.makedirs(obj_dir, exist_ok=True)
write_bom(os.path.join(obj_dir, '%s.xml' % name), conv)
# Содержимое объекта — как есть; в XML та же подстановка типа, .bsl копируются байт в байт.
src_content = os.path.join(os.path.dirname(source_xml), name)
if os.path.isdir(src_content):
dst_content = os.path.join(obj_dir, name)
for root, _dirs, files in os.walk(src_content):
for fname in files:
full = os.path.join(root, fname)
rel = os.path.relpath(full, src_content)
dst = os.path.join(dst_content, rel)
os.makedirs(os.path.dirname(dst), exist_ok=True)
if os.path.splitext(fname)[1].lower() == '.xml':
with io.open(full, encoding='utf-8-sig') as fh:
t = fh.read()
t = reissue(t)
t = t.replace('%sObject.' % ext_tag, '%sObject.' % cfg_tag).replace('%s.' % ext_tag, '%s.' % cfg_tag)
write_bom(dst, t)
else:
shutil.copyfile(full, dst)
return {'tag': cfg_tag, 'name': name}
def main():
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
@@ -1091,6 +1216,9 @@ def main():
parser.add_argument('-SourceDir', required=True)
parser.add_argument('-V8Path', required=True)
parser.add_argument('-TempBasePath', default='')
# XML проверяемой обработки/отчёта: объект кладётся в конфигурацию-заглушку, чтобы платформа
# смогла проверить его штатными проверками. Без параметра стаб работает как раньше.
parser.add_argument('-EmbedSourceFile', default='')
parser.add_argument('-AdditionalV8Arguments', nargs='*', default=[],
help='Extra 1cv8 arguments, e.g. /UseHwLicenses+')
parser.add_argument('-AdditionalIbcmdArguments', nargs='*', default=[],
@@ -1102,10 +1230,13 @@ def main():
args.SourceDir = clean_path(args.SourceDir, "-SourceDir")
args.V8Path = clean_path(args.V8Path, "-V8Path")
args.TempBasePath = clean_path(args.TempBasePath, "-TempBasePath")
args.EmbedSourceFile = clean_path(args.EmbedSourceFile, "-EmbedSourceFile")
type_map = scan_ref_types(args.SourceDir)
register_columns = scan_register_columns(args.SourceDir)
has_ref_types = len(type_map) > 0
embed_requested = bool(args.EmbedSourceFile and args.EmbedSourceFile.strip())
need_cfg = has_ref_types or embed_requested
stub_format_version = detect_stub_format_version(args.SourceDir)
stub_compat = stub_compatibility_mode(stub_format_version)
ns_decl = f'{NS} version="{stub_format_version}"'
@@ -1118,10 +1249,18 @@ def main():
if needs_registrator:
type_map.setdefault('Document', {})['\u0417\u0430\u0433\u043b\u0443\u0448\u043a\u0430\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430'] = True # ЗаглушкаРегистратора
if has_ref_types:
if need_cfg:
cfg_dir = os.path.join(temp_base, 'cfg')
os.makedirs(cfg_dir, exist_ok=True)
embedded = None
if embed_requested:
embedded = add_source_object_to_config(args.EmbedSourceFile, cfg_dir)
if not embedded:
print('Error: %s is neither ExternalDataProcessor nor ExternalReport' % args.EmbedSourceFile,
file=sys.stderr)
sys.exit(1)
# Configuration.xml
uuid_cfg = new_uuid()
uuid_lang = new_uuid()
@@ -1138,6 +1277,8 @@ def main():
tag = META_INFO[meta_type][0]
for name in names:
child_xml += f'\n\t\t\t<{tag}>{name}</{tag}>'
if embedded:
child_xml += '\n\t\t\t<%s>%s</%s>' % (embedded['tag'], embedded['name'], embedded['tag'])
cfg_xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<MetaDataObject {ns_decl}>
@@ -1368,7 +1509,7 @@ def main():
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:
if need_cfg:
ib_args += [f'--import={os.path.join(temp_base, "cfg")}', '--apply', '--force']
ib_args.append(f'--data={ib_data}')
ib_args.extend(extra_args)
@@ -1381,7 +1522,7 @@ def main():
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:
if need_cfg:
import shutil
shutil.rmtree(os.path.join(temp_base, 'cfg'), ignore_errors=True)
print(f'[OK] Stub database created: {temp_base}')
@@ -1397,13 +1538,24 @@ def main():
print(f'Failed to create infobase (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
if has_ref_types:
if need_cfg:
cfg_dir = os.path.join(temp_base, 'cfg')
# LoadConfigFromFiles
print('Loading configuration from files...')
load_log = os.path.join(tempfile.gettempdir(), 'stub_load_log.txt')
result = run_v8(args.V8Path, ['DESIGNER', f'/F"{temp_base}"', '/LoadConfigFromFiles', f'"{cfg_dir}"',
'/Out', f'"{load_log}"',
'/DisableStartupDialogs'] + [quote_if_needed(a) for a in extra_args])
if result.returncode != 0:
# Причина отказа живёт только в /Out: в консоль пакетный 1cv8 не пишет ничего.
if os.path.isfile(load_log):
try:
with io.open(load_log, encoding='utf-8-sig', errors='replace') as fh:
text = fh.read().strip()
if text:
print(text)
except Exception:
pass
print_platform_output(result)
print(f'Failed to load config (code: {result.returncode})', file=sys.stderr)
sys.exit(1)
+21 -2
View File
@@ -1,4 +1,4 @@
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# NB: *nix-раскладку платформы (/opt/1cv8/<ver>/1cv8, без .exe) знает только .py-порт — PS на *nix не исполняется.
<#
@@ -49,7 +49,7 @@
.\epf-dump.ps1 -InfoBasePath "C:\Bases\MyDB" -InputFile "build\МойОтчёт.erf" -OutputDir "src"
#>
[CmdletBinding()]
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$false)]
[string]$V8Path,
@@ -98,6 +98,17 @@ $script:V8OwnedKeys = @(
'/DumpConfigToFiles', '/LoadConfigFromFiles', '/UpdateDBCfg',
'/DumpExternalDataProcessorOrReportToFiles', '/LoadExternalDataProcessorOrReportFromFiles'
)
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
$script:V8BatchKeys = @(
'/CheckConfig', '/CheckModules', '/CheckCanApplyConfigurationExtensions',
'/DumpDBCfgList', '/DeleteCfg', '/UpdateCfg', '/CompareCfg', '/MergeCfg',
'/ManageCfgSupport', '/RollbackCfg', '/ConvertFiles'
)
$script:IbcmdOwnedKeys = @(
'--db-path', '--data', '--out', '--file', '--load', '--restore',
'--import', '--export', '--apply', '--force', '--create-database',
@@ -148,6 +159,14 @@ function Assert-ExtraArgs {
Write-Host "Error: '$tok' is a positional token — pass values as --key=value ($paramName cannot extend the ibcmd command)" -ForegroundColor Red
exit 1
}
if ($Engine -ne 'ibcmd') {
foreach ($b in $script:V8BatchKeys) {
if (Test-ArgKeyMatch $tok $b) {
Write-Host "Error: $b is a batch command; passed via $paramName it would replace the skill's own operation (a command line runs only its last batch command)" -ForegroundColor Red
exit 1
}
}
}
foreach ($k in $owned) {
if (Test-ArgKeyMatch $tok $k) {
$hint = ''
+54 -19
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-dump v1.12 — Dump external data processor or report (EPF/ERF) to XML sources
# epf-dump v1.15 — Dump external data processor or report (EPF/ERF) to XML sources
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -67,6 +67,17 @@ V8_OWNED_KEYS = [
"/DumpConfigToFiles", "/LoadConfigFromFiles", "/UpdateDBCfg",
"/DumpExternalDataProcessorOrReportToFiles", "/LoadExternalDataProcessorOrReportFromFiles",
]
# Пакетные команды платформы. В одной командной строке DESIGNER выполняет ТОЛЬКО ПОСЛЕДНЮЮ,
# остальные молча отбрасывает (проверено на 8.3.24: /LoadConfigFromFiles вместе с
# /CheckCanApplyConfigurationExtensions завершились кодом 0 с пустым логом, и загрузка НЕ
# состоялась). Такая команда в дополнительных аргументах подменяет собой операцию навыка, а навык
# отчитывается успехом. Дополнительные аргументы — это опции, а не режимы.
V8_BATCH_KEYS = [
"/CheckConfig", "/CheckModules", "/CheckCanApplyConfigurationExtensions",
"/DumpDBCfgList", "/DeleteCfg", "/UpdateCfg", "/CompareCfg", "/MergeCfg",
"/ManageCfgSupport", "/RollbackCfg", "/ConvertFiles",
]
IBCMD_OWNED_KEYS = [
"--db-path", "--data", "--out", "--file", "--load", "--restore",
"--import", "--export", "--apply", "--force", "--create-database",
@@ -120,15 +131,21 @@ def assert_extra_args(extra, engine, hints):
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)
if engine != "ibcmd":
for b in V8_BATCH_KEYS:
if arg_key_match(tok, b):
print(
f"Error: {b} is a batch command; passed via {param} it would replace "
f"the skill's own operation (a command line runs only its last batch command)",
)
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)
@@ -196,14 +213,12 @@ def resolve_extra_args(engine, v8_extra, ibcmd_extra, hints):
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":
@@ -245,14 +260,14 @@ def resolve_v8path(v8path):
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)
print("Error: 1C executable not found. Specify -V8Path")
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)
print(f"Error: 1C executable not found at {v8path}")
sys.exit(1)
return v8path
@@ -283,7 +298,7 @@ def assert_infobase_exists(path):
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)
print(f"Error: information base not found at {path} (no 1Cv8.1CD)")
sys.exit(1)
@@ -300,7 +315,7 @@ def clean_path(value, param=""):
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)
print(f"Error: {param or 'path'} contains a quote character: {value}")
sys.exit(1)
return v
@@ -319,11 +334,31 @@ def run_v8(v8path, arguments):
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.
На POSIX аргументы уходят СПИСКОМ, и кавычки, нужные для склейки на Windows, стали бы
частью значения: путь с пробелом платформа не находит («Неопределена информационная
база»), многословный -comment теряет молча. Поэтому здесь снимается ОДИН слой
обрамляющих кавычек. Склеенные ключи (/N"user", /ConfigurationRepositoryF"путь",
File="") не задеты: у них кавычки внутри токена, а не по краям.
"""
if os.name == "nt":
cmd = '"' + v8path + '" ' + " ".join(arguments)
else:
cmd = [v8path] + arguments
def strip_framing_quotes(a):
# Кавычки, которыми мы обрамляем значения ради склейки на Windows, на POSIX
# становятся ЧАСТЬЮ значения. Проверено на darwin: путь с пробелом отдельным
# токеном даёт «Неопределена информационная база», а склеенный
# /ConfigurationRepositoryF"путь с пробелом" — «завершилось с ошибкой»;
# без кавычек обе формы работают.
if len(a) > 1 and a[0] == '"' and a[-1] == '"':
return a[1:-1] # "значение" отдельным токеном
if a[0:1] == "/" and a[-1:] == '"' and '"' in a[:-1]:
i = a.index('"')
return a[:i] + a[i + 1:-1] # /N"имя" -> /Nимя
return a # File="…" не трогаем: там кавычки —
# часть синтаксиса строки соединения,
# и с ними на POSIX всё работает
cmd = [v8path] + [strip_framing_quotes(a) for a in arguments]
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
r.stderr = decode_platform_bytes(r.stderr)
@@ -352,7 +387,7 @@ def run_ibcmd(cmd, has_username=False, warn_no_user=True):
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.stdout.write(IBCMD_NOUSER_HINT)
sys.stderr.flush()
r = subprocess.run(cmd, input=b"", capture_output=True)
r.stdout = decode_platform_bytes(r.stdout)
@@ -428,20 +463,20 @@ def main():
# --- Validate database connection ---
if not args.InfoBasePath and (not args.InfoBaseServer or not args.InfoBaseRef):
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef", file=sys.stderr)
print("Error: database connection required. Specify -InfoBasePath or -InfoBaseServer/-InfoBaseRef")
print("Dump in an empty database loses reference types (CatalogRef, DocumentRef, etc.) irreversibly.")
sys.exit(1)
if engine == "ibcmd":
if not args.InfoBasePath:
print("Error: ibcmd supports file infobases only (use -InfoBasePath)", file=sys.stderr)
print("Error: ibcmd supports file infobases only (use -InfoBasePath)")
sys.exit(1)
if args.Format == "Plain":
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)", file=sys.stderr)
print("Error: ibcmd config export supports hierarchical format only (use -Format Hierarchical or 1cv8)")
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)
print(f"Error: input file not found: {args.InputFile}")
sys.exit(1)
# --- Ensure output directory exists ---
@@ -473,9 +508,9 @@ def main():
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)
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output")
else:
print(f"Error dumping external data processor/report (code: {exit_code})", file=sys.stderr)
print(f"Error dumping external data processor/report (code: {exit_code})")
sys.exit(exit_code)
# --- Build arguments ---
@@ -513,9 +548,9 @@ def main():
if exit_code == 0:
print(f"Dump completed successfully to: {args.OutputDir}")
elif out_missing:
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output", file=sys.stderr)
print(f"Error: exit code 0 but no files under {args.OutputDir} — dump produced no output")
else:
print(f"Error dumping (code: {exit_code})", file=sys.stderr)
print(f"Error dumping (code: {exit_code})")
if os.path.isfile(out_file):
try:
+2
View File
@@ -12,6 +12,8 @@ allowed-tools:
Проверяет структурную корректность XML-исходников внешней обработки: корневую структуру, InternalInfo, свойства, ChildObjects, реквизиты, табличные части, уникальность имён, наличие файлов форм и макетов. Также работает для внешних отчётов (ERF).
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
@@ -1,8 +1,9 @@
# epf-validate v1.5 — Validate 1C external data processor / report structure
# epf-validate v1.6 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Parameter(Mandatory, Position=0)]
[Alias('Path')]
[string]$ObjectPath,
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# epf-validate v1.5 — Validate 1C external data processor / report structure
# epf-validate v1.6 — Validate 1C external data processor / report structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# Works for both EPF (ExternalDataProcessor) and ERF (ExternalReport) — auto-detects
+13 -1
View File
@@ -25,7 +25,8 @@ allowed-tools:
## Параметры подключения (опционально)
Предпочтительно использовать конкретную базу — это надёжнее и не требует создания временной базы.
Предпочтительно использовать конкретную базу — это надёжнее. Временная база всё равно поднимается
под проверку исходников, если она не отключена.
1. Прочитай `.v8-project.json` из корня проекта. Возьми `v8path` и разреши базу:
2. Если пользователь указал параметры подключения (путь, сервер) — используй напрямую
@@ -57,11 +58,22 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/../epf-build/scripts/epf-bu
| `-Password <пароль>` | нет | Пароль |
| `-SourceFile <путь>` | да | Путь к корневому XML-файлу исходников |
| `-OutputFile <путь>` | да | Путь к выходному ERF-файлу |
| `-Checks <список>` | нет | Что проверить перед сборкой: `modules`, `handlers`, `unreferenced`, `empty-handlers`, `config`; `off` — не проверять. По умолчанию `modules,handlers` |
| `-Context <список>` | нет | Контексты проверки `modules`: `ThinClient`, `Server`, `ExternalConnection`, `ThickClientOrdinaryApplication`. По умолчанию `ThinClient,Server` |
| `-AdditionalV8Arguments <список>` | нет | Доп. аргументы запуска `1cv8.exe` через запятую, напр. `/UseHwLicenses+` |
| `-AdditionalIbcmdArguments <список>` | нет | Доп. аргументы `ibcmd` через запятую, в форме `--ключ=значение` |
> `*` — опционально. Если не указано — автоматически создаётся временная база со заглушками метаданных
## Проверка перед сборкой
Перед сборкой исходники проверяет платформа — синтаксис модулей и наличие обработчиков форм.
Если она нашла проблемы, сборка отменяется и файл не создаётся; в выводе — сообщение
платформы со строкой и колонкой и путь к файлу исходника. Отключается `-Checks off`
или ключом `"externalCheck": false` в `.v8-project.json`.
Проверка идёт на отдельной временной базе, даже если база указана: чужую конфигурацию навык не трогает.
## Примеры
```powershell
+2
View File
@@ -14,6 +14,8 @@ allowed-tools:
Использует тот же скрипт, что и `/epf-validate` — автоопределение по типу элемента (ExternalReport).
Проверяется XML. Синтаксис модулей проверяет сборка: `/epf-build`, `/erf-build`.
## Параметры
| Параметр | Обяз. | Умолч. | Описание |
+42 -15
View File
@@ -1,7 +1,7 @@
---
name: form-add
description: Добавить пустую управляемую форму к объекту 1С. Используй когда нужно создать у объекта новую форму
argument-hint: <ObjectPath> <FormName> [Purpose] [--set-default]
argument-hint: <ObjectPath> <FormName> [-Purpose <Purpose>] [-Synonym <Synonym>] [-SetDefault]
allowed-tools:
- Bash
- Read
@@ -18,16 +18,16 @@ allowed-tools:
## Usage
```
/form-add <ObjectPath> <FormName> [Purpose] [Synonym] [--set-default]
/form-add <ObjectPath> <FormName> [-Purpose <Purpose>] [-Synonym <Synonym>] [-SetDefault]
```
| Параметр | Обязательный | По умолчанию | Описание |
|-------------|:------------:|--------------|----------------------------------------------|
| ObjectPath | да | — | Путь к XML-файлу объекта (Documents/Док.xml) |
| FormName | да | — | Имя формы (ФормаДокумента) |
| Purpose | нет | Object | Назначение: Object, List, Choice, Record |
| Purpose | нет | основная форма вида | Назначение формы — см. таблицу ниже: у справочника это форма объекта, у регистра сведений — форма записи, у журнала — форма списка |
| Synonym | нет | = FormName | Синоним формы |
| --set-default | нет | авто | Установить как форму по умолчанию |
| -SetDefault | нет | авто | Сделать основной. Без флага основной становится первая форма каждого назначения |
## Команда
@@ -37,30 +37,57 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/form-add.ps1" -Obje
## Purpose — назначение формы
| Purpose | Допустимые типы объектов | Основной реквизит | DefaultForm-свойство |
|---------|-------------------------|-------------------|---------------------|
| Object | Document, Catalog, DataProcessor, Report, ExternalDataProcessor, ExternalReport, ChartOf*, ExchangePlan, BusinessProcess, Task | Объект (тип: *Object.Имя) | DefaultObjectForm (DefaultForm для DataProcessor/Report/ExternalDataProcessor/ExternalReport) |
| List | Все кроме DataProcessor | Список (DynamicList) | DefaultListForm |
| Choice | Document, Catalog, ChartOf*, ExchangePlan, BusinessProcess, Task | Список (DynamicList) | DefaultChoiceForm |
| Record | InformationRegister | Запись (InformationRegisterRecordManager) | DefaultRecordForm |
| Purpose | Какая форма | Становится основной |
|---------|-------------|---------------------|
| Object | форма объекта (элемента, документа, обработки) | да |
| List | форма списка | да |
| Choice | форма выбора | да |
| Folder | форма группы | да |
| FolderChoice | форма выбора группы | да |
| Record | форма записи | да |
| RecordSet | форма набора записей | нет — в платформе нет такого свойства |
| Save | форма сохранения настроек | да |
| Load | форма загрузки настроек | да |
| Custom | произвольная форма, без привязки к объекту | нет |
### Что доступно типу объекта
| Тип объекта | Назначения |
|-------------|------------|
| Catalog, ChartOfCharacteristicTypes | Object, Folder, List, Choice, FolderChoice, Custom |
| Document, ChartOfAccounts, ChartOfCalculationTypes, ExchangePlan, BusinessProcess, Task | Object, List, Choice, Custom |
| DataProcessor, Report, ExternalDataProcessor, ExternalReport | Object, Custom |
| InformationRegister | Record, List, RecordSet, Custom |
| AccumulationRegister, AccountingRegister, CalculationRegister | List, RecordSet, Custom |
| DocumentJournal, FilterCriterion | List, Custom |
| Enum | List, Choice, Custom |
| SettingsStorage | Save, Load, Custom |
| Table (таблица внешнего источника) | `tableDataType=ObjectData` — Object, List, Choice, Custom; `NonobjectData` — Record, List, Choice, Custom |
Недопустимое сочетание отклоняется со списком доступных для этого типа. У константы собственных
форм нет — для неё используется общая форма (`CommonForm`).
Таблица внешнего источника адресуется файлом таблицы:
`ExternalDataSources/<Источник>/Tables/<Таблица>.xml`. Ссылки в такой форме трёхчастные;
без `-Purpose` берётся форма объекта, а у таблицы с составным ключом — форма записи.
## Примеры
```
# Форма документа
/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 -SetDefault
```
## Workflow
+302 -150
View File
@@ -1,5 +1,6 @@
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
# form-add v1.29 — Add managed form to 1C config object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[string]$ObjectPath,
@@ -9,8 +10,15 @@ param(
[string]$Synonym = $FormName,
[string]$Purpose = "Object",
# Пусто = основная форма вида (Primary в таблице): у справочника это форма объекта,
# у регистра сведений — форма записи, у журнала — форма списка. Жёсткое "Object"
# по умолчанию было бы неверным для видов, у которых формы объекта не бывает.
[string]$Purpose = "",
# Алиас с дефисом внутри имени: вызов вида --set-default PowerShell разбирает как имя
# параметра "set-default" и без алиаса отвечает отказом биндинга. Написания -SetDefault,
# --SetDefault и --setdefault совпадают с именем параметра и так.
[Alias('set-default')]
[switch]$SetDefault
)
@@ -241,26 +249,175 @@ if (-not $metaDataObject) {
$metaDataObject = $xmlDoc.DocumentElement
}
$supportedTypes = @(
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal"
)
# --- Таблица видов: вид → допустимые назначения ---
#
# Одна запись на вид вместо разрозненных списков «поддерживаемые типы», «объектные типы»,
# «обработко-подобные» и «карта типов реквизита». Раньше они расходились молча: DocumentJournal
# был среди поддерживаемых, но не в карте типов, и в форму уходило `cfg:.Журнал` — платформа
# такую выгрузку не принимает, а навык рапортовал успех.
#
# MainAttr — тип главного реквизита; `{0}` подставляется именем объекта:
# "DynamicList" — динамический список (добавляется Settings/MainTable);
# $null — произвольная форма, блока Attributes нет вовсе.
# Slot — свойство объекта под «основную форму»; $null — такого свойства у вида нет.
# Эталон таблицы — docs/1c-form-spec.md, сверяется гардом check-form-purposes.mjs.
$formKinds = @{
"Catalog" = @{
"Object" = @{ MainAttr = "CatalogObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"Folder" = @{ MainAttr = "CatalogObject.{1}"; AttrName = "Объект"; Slot = "DefaultFolderForm"; SavedData = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"FolderChoice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultFolderChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"ChartOfCharacteristicTypes" = @{
"Object" = @{ MainAttr = "ChartOfCharacteristicTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"Folder" = @{ MainAttr = "ChartOfCharacteristicTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultFolderForm"; SavedData = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"FolderChoice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultFolderChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"Document" = @{
"Object" = @{ MainAttr = "DocumentObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"ChartOfAccounts" = @{
"Object" = @{ MainAttr = "ChartOfAccountsObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"ChartOfCalculationTypes" = @{
"Object" = @{ MainAttr = "ChartOfCalculationTypesObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"ExchangePlan" = @{
"Object" = @{ MainAttr = "ExchangePlanObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"BusinessProcess" = @{
"Object" = @{ MainAttr = "BusinessProcessObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"Task" = @{
"Object" = @{ MainAttr = "TaskObject.{1}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"DataProcessor" = @{
"Object" = @{ MainAttr = "DataProcessorObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"Report" = @{
"Object" = @{ MainAttr = "ReportObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"ExternalDataProcessor" = @{
"Object" = @{ MainAttr = "ExternalDataProcessorObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"ExternalReport" = @{
"Object" = @{ MainAttr = "ExternalReportObject.{1}"; AttrName = "Объект"; Slot = "DefaultForm"; Primary = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"InformationRegister" = @{
"Record" = @{ MainAttr = "InformationRegisterRecordManager.{1}"; AttrName = "Запись"; Slot = "DefaultRecordForm"; SavedData = $true; Primary = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"RecordSet" = @{ MainAttr = "InformationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"AccumulationRegister" = @{
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
"RecordSet" = @{ MainAttr = "AccumulationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"AccountingRegister" = @{
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
"RecordSet" = @{ MainAttr = "AccountingRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"CalculationRegister" = @{
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
"RecordSet" = @{ MainAttr = "CalculationRegisterRecordSet.{1}"; AttrName = "Набор"; Slot = $null; SavedData = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"DocumentJournal" = @{
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultForm"; Primary = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"FilterCriterion" = @{
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultForm"; Primary = $true }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"Enum" = @{
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm"; Primary = $true }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
"SettingsStorage" = @{
"Save" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultSaveForm"; Primary = $true }
"Load" = @{ MainAttr = $null; AttrName = $null; Slot = "DefaultLoadForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
# Таблица внешнего источника — единственный вид, чьё имя в ссылках трёхчастное
# (Источник.Таблица): подставляется {2}, а не {1}.
"Table" = @{
"Object" = @{ MainAttr = "ExternalDataSourceTableObject.{2}"; AttrName = "Объект"; Slot = "DefaultObjectForm"; SavedData = $true; Primary = $true }
"Record" = @{ MainAttr = "ExternalDataSourceTableRecordManager.{2}"; AttrName = "Запись"; Slot = "DefaultRecordForm"; SavedData = $true }
"List" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultListForm" }
"Choice" = @{ MainAttr = "DynamicList"; AttrName = "Список"; Slot = "DefaultChoiceForm" }
"Custom" = @{ MainAttr = $null; AttrName = $null; Slot = $null }
}
}
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает — отказ с причиной,
# а не «тип не поддерживается».
$noOwnForms = @{
"Constant" = "у константы нет собственных форм — используйте общую форму (CommonForm)"
}
$supportedTypes = @($formKinds.Keys) + @($noOwnForms.Keys)
# Отдельный факт, не выводимый из таблицы назначений: у форм обработок и отчётов в метаданных
# формы есть <ExtendedPresentation>.
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
# Вид объекта — первый элемент-потомок MetaDataObject, а не первое совпавшее по всему документу
# имя. Поиск по документу зависел от порядка перебора видов: у бизнес-процесса есть свойство
# <Task>, и он определялся как задача, после чего имя объекта не находилось вовсе.
$objectType = $null
$objectNode = $null
foreach ($t in $supportedTypes) {
$node = $xmlDoc.SelectSingleNode("//md:$t", $nsMgr)
if ($node) {
$objectType = $t
$objectNode = $node
foreach ($child in $metaDataObject.ChildNodes) {
if ($child.NodeType -eq [System.Xml.XmlNodeType]::Element) {
$objectType = $child.LocalName
$objectNode = $child
break
}
}
if ($objectType -and -not ($formKinds.ContainsKey($objectType) -or $noOwnForms.ContainsKey($objectType))) {
Write-Error "Тип объекта '$objectType' не поддерживается. Поддерживаемые типы: $(($formKinds.Keys | Sort-Object) -join ', ')"
exit 1
}
if (-not $objectType) {
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $($supportedTypes -join ', ')"
Write-Error "Не удалось определить тип объекта. Поддерживаемые типы: $(($formKinds.Keys | Sort-Object) -join ', ')"
exit 1
}
if ($noOwnForms.ContainsKey($objectType)) {
Write-Error "$objectType не поддерживается: $($noOwnForms[$objectType])"
exit 1
}
@@ -274,48 +431,101 @@ if (-not $objectName) {
Write-Host ""
Write-Host "=== form-add ==="
Write-Host ""
# Ссылка на объект и имя для типов формы. У всех видов это "Вид.Имя", и только
# у таблицы внешнего источника — "ExternalDataSource.<Источник>.Table.<Таблица>", а в именах
# типов — "<Источник>.<Таблица>". Имя источника в самом файле таблицы не хранится —
# единственное место, где навык смотрит на путь: ExternalDataSources/<Источник>/Tables/<Таблица>.xml
$objectQualifiedName = $objectName
$objectRef = "$objectType.$objectName"
if ($objectType -eq "Table") {
$tablesDir = Split-Path -Parent $objectXmlFull.Path
$edsSource = Split-Path -Leaf (Split-Path -Parent $tablesDir)
if (-not $edsSource -or (Split-Path -Leaf $tablesDir) -ne "Tables") {
Write-Error "Таблица внешнего источника ожидается по пути ExternalDataSources/<Источник>/Tables/<Таблица>.xml, а не '$($objectXmlFull.Path)'"
exit 1
}
$objectQualifiedName = "$edsSource.$objectName"
$objectRef = "ExternalDataSource.$edsSource.Table.$objectName"
$tdtNode = $xmlDoc.SelectSingleNode("//md:Table/md:Properties/md:TableDataType", $nsMgr)
$tableDataType = if ($tdtNode) { $tdtNode.InnerText.Trim() } else { "ObjectData" }
}
Write-Host "Object: $objectType.$objectName"
# --- Фаза 2: Валидация Purpose ---
$Purpose = $Purpose.Substring(0,1).ToUpper() + $Purpose.Substring(1).ToLower()
# Нормализация
switch ($Purpose) {
"Object" { }
"List" { }
"Choice" { }
"Record" { }
default {
Write-Error "Недопустимое назначение: $Purpose. Допустимые: Object, List, Choice, Record"
# Назначение ищем в таблице регистронезависимо — как принимает PowerShell (в py-порту .lower()).
$kindPurposes = $formKinds[$objectType]
# Обиходные написания назначения приводим к канону молча: русское название вида формы и
# английское с суффиксом Form. Ключ нормализуем — регистр, пробелы и разделители не значимы.
# Канон в документации один; здесь только приём ошибочного ввода, чтобы вызов не падал на форме
# записи вместо назначения. Применимость назначения к виду объекта проверяется ниже как обычно.
$purposeSynonyms = @{
"формаобъекта"="Object"; "формаэлемента"="Object"; "формадокумента"="Object"
"объект"="Object"; "элемент"="Object"; "документ"="Object"; "objectform"="Object"
"формасписка"="List"; "список"="List"; "listform"="List"
"формавыбора"="Choice"; "выбор"="Choice"; "choiceform"="Choice"
"формагруппы"="Folder"; "группа"="Folder"; "folderform"="Folder"
"формавыборагруппы"="FolderChoice"; "выборгруппы"="FolderChoice"; "folderchoiceform"="FolderChoice"
"формазаписи"="Record"; "запись"="Record"; "recordform"="Record"
"форманаборазаписей"="RecordSet"; "наборзаписей"="RecordSet"; "recordsetform"="RecordSet"
"формасохранения"="Save"; "формасохранениянастроек"="Save"; "сохранение"="Save"; "saveform"="Save"
"формазагрузки"="Load"; "формазагрузкинастроек"="Load"; "загрузка"="Load"; "loadform"="Load"
"произвольная"="Custom"; "произвольнаяформа"="Custom"; "customform"="Custom"
}
if ($Purpose) {
$purposeProbe = ($Purpose -replace '[\s_-]', '').ToLowerInvariant()
$isKnownPurpose = $false
foreach ($p in $kindPurposes.Keys) {
if ($p.ToLowerInvariant() -eq $Purpose.ToLowerInvariant()) { $isKnownPurpose = $true; break }
}
if (-not $isKnownPurpose -and $purposeSynonyms.ContainsKey($purposeProbe)) {
$Purpose = $purposeSynonyms[$purposeProbe]
}
}
if (-not $Purpose) {
if ($objectType -eq "Table" -and $tableDataType -eq "NonobjectData") {
# Пометка Primary в таблице видов одна на вид, а у таблицы с составным ключом
# формы объекта не бывает — основной становится форма записи.
$Purpose = "Record"
} else {
foreach ($p in $kindPurposes.Keys) {
if ($kindPurposes[$p].Primary) { $Purpose = $p; break }
}
}
}
$purposeKey = $null
foreach ($p in $kindPurposes.Keys) {
if ($p.ToLowerInvariant() -eq $Purpose.ToLowerInvariant()) { $purposeKey = $p; break }
}
if (-not $purposeKey) {
Write-Error "Назначение '$Purpose' недопустимо для $objectType. Допустимые: $(($kindPurposes.Keys | Sort-Object) -join ', ')"
exit 1
}
$Purpose = $purposeKey
$purposeRule = $kindPurposes[$Purpose]
# У таблицы внешнего источника набор назначений зависит от вида данных (замерено на
# 8.3.24.1691): ObjectData — Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
# не отвергается схемой формы, а валит загрузку всей конфигурации «Исключением XDTO» без причины.
if ($objectType -eq "Table") {
if ($tableDataType -eq "NonobjectData" -and $Purpose -eq "Object") {
Write-Error "Таблица '$objectName' с составным ключом (TableDataType=NonobjectData): формы объекта у неё нет — используйте -Purpose Record."
exit 1
}
if ($tableDataType -ne "NonobjectData" -and $Purpose -eq "Record") {
Write-Error "Таблица '$objectName' с ключом из одного поля (TableDataType=ObjectData): формы записи у неё нет — используйте -Purpose Object."
exit 1
}
}
$objectLikeTypes = @("Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes", "ExchangePlan", "BusinessProcess", "Task")
$processorLikeTypes = @("DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport")
switch ($Purpose) {
"Object" {
# допустимо для всех типов
}
"List" {
if ($objectType -eq "DataProcessor") {
Write-Error "Purpose=List недопустим для DataProcessor"
exit 1
}
}
"Choice" {
if ($objectType -in $processorLikeTypes -or $objectType -eq "InformationRegister") {
Write-Error "Purpose=Choice недопустим для $objectType"
exit 1
}
}
"Record" {
if ($objectType -ne "InformationRegister") {
Write-Error "Purpose=Record допустим только для InformationRegister"
exit 1
}
}
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой MainAttr — это
# произвольная форма (законное состояние), а вот наполовину заполненная запись означала бы, что
# таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
if ($purposeRule.MainAttr -and -not $purposeRule.AttrName) {
Write-Error "Внутренняя ошибка таблицы видов: у $objectType/$Purpose задан MainAttr без AttrName"
exit 1
}
# --- Фаза 3: Создание файлов ---
@@ -395,102 +605,47 @@ Write-XmlFile $formMetaPath $formMetaXml $encBom
$formXmlPath = Join-Path $formExtDir "Form.xml"
if ($Purpose -eq "List" -or $Purpose -eq "Choice") {
# Динамический список
# MainTable: тип.имя
$mainTable = "$objectType.$objectName"
# Одна ветка вместо трёх: что писать, решает запись таблицы видов. Раньше тип главного
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
$attributesBlock = ""
if ($purposeRule.MainAttr) {
$mainAttrType = $purposeRule.MainAttr -f $objectType, $objectName, $objectQualifiedName
$mainAttrName = $purposeRule.AttrName
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="Список" id="1">
<Type>
<v8:Type>cfg:DynamicList</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<Settings xsi:type="DynamicList">
<MainTable>$mainTable</MainTable>
</Settings>
</Attribute>
</Attributes>
</Form>
"@
} elseif ($Purpose -eq "Record") {
# Запись регистра сведений
$mainAttrName = "Запись"
$mainAttrType = "InformationRegisterRecordManager.$objectName"
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
$tailLines = ""
if ($mainAttrType -eq "DynamicList") {
$mainTable = $objectRef
$tailLines = "`n`t`t`t<Settings xsi:type=""DynamicList"">`n`t`t`t`t<MainTable>$mainTable</MainTable>`n`t`t`t</Settings>"
} elseif ($purposeRule.SavedData) {
$tailLines = "`n`t`t`t<SavedData>true</SavedData>"
}
$attributesBlock = @"
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
<Type>
<v8:Type>cfg:$mainAttrType</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>
<SavedData>true</SavedData>
<MainAttribute>true</MainAttribute>$tailLines
</Attribute>
</Attributes>
</Form>
"@
} else {
# Object — форма объекта
$mainAttrName = "Объект"
# Маппинг типа объекта на тип реквизита
$attrTypeMap = @{
"Document" = "DocumentObject"
"Catalog" = "CatalogObject"
"DataProcessor" = "DataProcessorObject"
"Report" = "ReportObject"
"ExternalDataProcessor" = "ExternalDataProcessorObject"
"ExternalReport" = "ExternalReportObject"
"ChartOfAccounts" = "ChartOfAccountsObject"
"ChartOfCharacteristicTypes" = "ChartOfCharacteristicTypesObject"
"ExchangePlan" = "ExchangePlanObject"
"BusinessProcess" = "BusinessProcessObject"
"Task" = "TaskObject"
"InformationRegister" = "InformationRegisterRecordManager"
"AccumulationRegister" = "AccumulationRegisterRecordSet"
}
$mainAttrType = "$($attrTypeMap[$objectType]).$objectName"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
$savedDataLine = ""
if ($objectType -notin $processorLikeTypes) {
$savedDataLine = "`n`t`t`t<SavedData>true</SavedData>"
}
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>
<Attributes>
<Attribute name="$mainAttrName" id="1">
<Type>
<v8:Type>cfg:$mainAttrType</v8:Type>
</Type>
<MainAttribute>true</MainAttribute>$savedDataLine
</Attribute>
</Attributes>
</Form>
"@
}
# Произвольная форма (MainAttr = $null) — без блока Attributes вовсе. В типовых это самая
# частая форма после объектной: 907 у справочников, 941 у документов, 3482 у отчётов.
$formXml = @"
<?xml version="1.0" encoding="UTF-8"?>
<Form $($script:formNsDecl) version="$($script:formatVersion)">
<AutoCommandBar name="ФормаКоманднаяПанель" id="-1">
<Autofill>true</Autofill>
</AutoCommandBar>
<ChildItems/>$attributesBlock
</Form>
"@
if (Test-Path $formXmlPath) {
Write-Host "[SKIP] Form.xml already exists: $formXmlPath — not overwriting"
} else {
@@ -606,26 +761,19 @@ if ($insertBefore) {
$existingForms = $childObjects.SelectNodes("md:Form", $nsMgr)
$isFirstFormForPurpose = $false
$defaultPropName = $null
$defaultValue = "$objectType.$objectName.Form.$FormName"
$defaultValue = "$objectRef.Form.$FormName"
# Определяем имя свойства для DefaultForm
switch ($Purpose) {
"Object" {
if ($objectType -in $processorLikeTypes) {
$defaultPropName = "DefaultForm"
} else {
$defaultPropName = "DefaultObjectForm"
}
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному Purpose без учёта
# вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не находился, навык
# молча ничего не делал.
$defaultPropName = $purposeRule.Slot
$defaultNode = $null
if ($defaultPropName) {
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
if ($defaultNode) {
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
}
"List" { $defaultPropName = "DefaultListForm" }
"Choice" { $defaultPropName = "DefaultChoiceForm" }
"Record" { $defaultPropName = "DefaultRecordForm" }
}
# Проверяем, установлено ли уже значение
$defaultNode = $xmlDoc.SelectSingleNode("//md:${objectType}/md:Properties/md:$defaultPropName", $nsMgr)
if ($defaultNode) {
$isFirstFormForPurpose = [string]::IsNullOrWhiteSpace($defaultNode.InnerText)
}
$defaultUpdated = $false
@@ -687,5 +835,9 @@ if ($alreadyRegistered) {
}
if ($defaultUpdated) {
Write-Host "${defaultPropName}: $defaultValue"
} elseif (-not $defaultPropName) {
# Молчать здесь нельзя: пользователь ждёт, что форма станет основной, а свойства под неё
# у платформы нет (форма набора записей, произвольная форма).
Write-Host "Основной не назначена: у $objectType нет свойства для формы с назначением $Purpose"
}
Write-Host ""
+317 -136
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-add v1.25 — Add managed form to 1C config object (+write_xml_file/write_utf8_bom: общий эталон записи)
# form-add v1.29 — Add managed form to 1C config object (Python port)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -323,8 +323,13 @@ def main():
parser.add_argument("-ObjectPath", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-Synonym", default=None)
parser.add_argument("-Purpose", default="Object")
parser.add_argument("-SetDefault", action="store_true")
# Пусто = основная форма вида (primary в таблице): у справочника это форма объекта,
# у регистра сведений — форма записи, у журнала — форма списка.
parser.add_argument("-Purpose", default="")
# Написания с дефисом внутри имени и с двойным дефисом: в PS-порте их принимает алиас
# set-default, здесь — перечисление опций, чтобы порты принимали ровно одно и то же.
parser.add_argument("-SetDefault", "--SetDefault", "--set-default", "-set-default",
dest="SetDefault", action="store_true")
args = ci_parse_args(parser)
object_path = args.ObjectPath
@@ -415,24 +420,192 @@ def main():
tree = etree.parse(object_xml_full, parser_xml)
root = tree.getroot()
supported_types = [
"Document", "Catalog", "DataProcessor", "Report",
"ExternalDataProcessor", "ExternalReport",
"InformationRegister", "AccumulationRegister", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task", "DocumentJournal",
]
# --- Таблица видов: вид -> допустимые назначения ---
#
# Зеркало $formKinds из PS-порта. Одна запись на вид вместо разрозненных списков
# «поддерживаемые типы», «объектные типы», «обработко-подобные» и «карта типов реквизита»:
# раньше они расходились молча, и для DocumentJournal в форму уходило `cfg:.Журнал`.
#
# main_attr — тип главного реквизита, {0} = вид, {1} = имя объекта;
# "DynamicList" — динамический список (добавляется Settings/MainTable);
# None — произвольная форма, блока Attributes нет вовсе.
# slot — свойство объекта под «основную форму»; None — такого свойства у вида нет.
# Эталон таблицы — docs/1c-form-spec.md, сверяется гардом check-form-purposes.mjs.
form_kinds = {
"Catalog": {
"Object": {"main_attr": "CatalogObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"Folder": {"main_attr": "CatalogObject.{1}", "attr_name": "Объект",
"slot": "DefaultFolderForm", "saved_data": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"FolderChoice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultFolderChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"ChartOfCharacteristicTypes": {
"Object": {"main_attr": "ChartOfCharacteristicTypesObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"Folder": {"main_attr": "ChartOfCharacteristicTypesObject.{1}", "attr_name": "Объект",
"slot": "DefaultFolderForm", "saved_data": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"FolderChoice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultFolderChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"Document": {
"Object": {"main_attr": "DocumentObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"ChartOfAccounts": {
"Object": {"main_attr": "ChartOfAccountsObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"ChartOfCalculationTypes": {
"Object": {"main_attr": "ChartOfCalculationTypesObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"ExchangePlan": {
"Object": {"main_attr": "ExchangePlanObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"BusinessProcess": {
"Object": {"main_attr": "BusinessProcessObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"Task": {
"Object": {"main_attr": "TaskObject.{1}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"DataProcessor": {
"Object": {"main_attr": "DataProcessorObject.{1}", "attr_name": "Объект",
"slot": "DefaultForm", "primary": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"Report": {
"Object": {"main_attr": "ReportObject.{1}", "attr_name": "Объект",
"slot": "DefaultForm", "primary": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"ExternalDataProcessor": {
"Object": {"main_attr": "ExternalDataProcessorObject.{1}", "attr_name": "Объект",
"slot": "DefaultForm", "primary": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"ExternalReport": {
"Object": {"main_attr": "ExternalReportObject.{1}", "attr_name": "Объект",
"slot": "DefaultForm", "primary": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"InformationRegister": {
"Record": {"main_attr": "InformationRegisterRecordManager.{1}", "attr_name": "Запись",
"slot": "DefaultRecordForm", "saved_data": True, "primary": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"RecordSet": {"main_attr": "InformationRegisterRecordSet.{1}", "attr_name": "Набор",
"slot": None, "saved_data": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"AccumulationRegister": {
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
"RecordSet": {"main_attr": "AccumulationRegisterRecordSet.{1}", "attr_name": "Набор",
"slot": None, "saved_data": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"AccountingRegister": {
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
"RecordSet": {"main_attr": "AccountingRegisterRecordSet.{1}", "attr_name": "Набор",
"slot": None, "saved_data": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"CalculationRegister": {
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
"RecordSet": {"main_attr": "CalculationRegisterRecordSet.{1}", "attr_name": "Набор",
"slot": None, "saved_data": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"DocumentJournal": {
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultForm", "primary": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"FilterCriterion": {
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultForm", "primary": True},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"Enum": {
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm", "primary": True},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
"SettingsStorage": {
"Save": {"main_attr": None, "attr_name": None, "slot": "DefaultSaveForm", "primary": True},
"Load": {"main_attr": None, "attr_name": None, "slot": "DefaultLoadForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
# Таблица внешнего источника — единственный вид, чьё имя в ссылках трёхчастное
# (Источник.Таблица): подставляется {2}, а не {1}.
"Table": {
"Object": {"main_attr": "ExternalDataSourceTableObject.{2}", "attr_name": "Объект",
"slot": "DefaultObjectForm", "saved_data": True, "primary": True},
"Record": {"main_attr": "ExternalDataSourceTableRecordManager.{2}", "attr_name": "Запись",
"slot": "DefaultRecordForm", "saved_data": True},
"List": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultListForm"},
"Choice": {"main_attr": "DynamicList", "attr_name": "Список", "slot": "DefaultChoiceForm"},
"Custom": {"main_attr": None, "attr_name": None, "slot": None},
},
}
# Виды, у которых свойство DefaultForm есть, но собственных форм не бывает.
no_own_forms = {
"Constant": "у константы нет собственных форм — используйте общую форму (CommonForm)",
}
supported_types = list(form_kinds) + list(no_own_forms)
# Отдельный факт, не выводимый из таблицы назначений: у форм обработок и отчётов в
# метаданных формы есть <ExtendedPresentation>.
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
# Вид объекта — первый элемент-потомок MetaDataObject, а не первое совпавшее по всему
# документу имя. Поиск по документу зависел от порядка перебора видов: у бизнес-процесса
# есть свойство <Task>, и он определялся как задача, после чего имя объекта не находилось.
object_type = None
object_node = None
for t in supported_types:
node = root.find(f".//md:{t}", NSMAP)
if node is not None:
object_type = t
object_node = node
for child in root:
if isinstance(child.tag, str):
object_type = etree.QName(child).localname
object_node = child
break
if object_type is not None and object_type not in form_kinds and object_type not in no_own_forms:
print(f"Тип объекта '{object_type}' не поддерживается. "
f"Поддерживаемые типы: {', '.join(sorted(form_kinds))}", file=sys.stderr)
sys.exit(1)
if object_type is None:
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(supported_types)}", file=sys.stderr)
print(f"Не удалось определить тип объекта. Поддерживаемые типы: {', '.join(sorted(form_kinds))}",
file=sys.stderr)
sys.exit(1)
if object_type in no_own_forms:
print(f"{object_type} не поддерживается: {no_own_forms[object_type]}", file=sys.stderr)
sys.exit(1)
# Object name from Properties/Name
@@ -442,6 +615,26 @@ def main():
sys.exit(1)
object_name = name_node.text
# Ссылка на объект и имя для типов формы. У всех видов это "Вид.Имя", и только
# у таблицы внешнего источника — "ExternalDataSource.<Источник>.Table.<Таблица>", а в именах
# типов — "<Источник>.<Таблица>". Имя источника в самом файле таблицы не хранится —
# единственное место, где навык смотрит на путь: ExternalDataSources/<Источник>/Tables/<Таблица>.xml
object_qualified_name = object_name
object_ref = f"{object_type}.{object_name}"
table_data_type = "ObjectData"
if object_type == "Table":
tables_dir = os.path.dirname(object_xml_full)
eds_source = os.path.basename(os.path.dirname(tables_dir))
if not eds_source or os.path.basename(tables_dir) != "Tables":
print("Таблица внешнего источника ожидается по пути "
f"ExternalDataSources/<Источник>/Tables/<Таблица>.xml, а не '{object_xml_full}'", file=sys.stderr)
sys.exit(1)
object_qualified_name = f"{eds_source}.{object_name}"
object_ref = f"ExternalDataSource.{eds_source}.Table.{object_name}"
tdt_node = root.find(".//md:Table/md:Properties/md:TableDataType", NSMAP)
if tdt_node is not None and tdt_node.text:
table_data_type = tdt_node.text.strip()
print()
print("=== form-add ===")
print()
@@ -449,32 +642,77 @@ def main():
# --- Phase 2: Validate Purpose ---
# Normalize: capitalize first letter, lowercase rest
purpose = purpose[0].upper() + purpose[1:].lower()
# Назначение ищем в таблице регистронезависимо — как принимает PowerShell.
kind_purposes = form_kinds[object_type]
valid_purposes = ["Object", "List", "Choice", "Record"]
if purpose not in valid_purposes:
print(f"Недопустимое назначение: {purpose}. Допустимые: Object, List, Choice, Record", file=sys.stderr)
# Обиходные написания назначения приводим к канону молча: русское название вида формы и
# английское с суффиксом Form. Ключ нормализуем — регистр, пробелы и разделители не значимы.
# Канон в документации один; здесь только приём ошибочного ввода, чтобы вызов не падал на форме
# записи вместо назначения. Применимость назначения к виду объекта проверяется ниже как обычно.
purpose_synonyms = {
"формаобъекта": "Object", "формаэлемента": "Object", "формадокумента": "Object",
"объект": "Object", "элемент": "Object", "документ": "Object", "objectform": "Object",
"формасписка": "List", "список": "List", "listform": "List",
"формавыбора": "Choice", "выбор": "Choice", "choiceform": "Choice",
"формагруппы": "Folder", "группа": "Folder", "folderform": "Folder",
"формавыборагруппы": "FolderChoice", "выборгруппы": "FolderChoice",
"folderchoiceform": "FolderChoice",
"формазаписи": "Record", "запись": "Record", "recordform": "Record",
"форманаборазаписей": "RecordSet", "наборзаписей": "RecordSet", "recordsetform": "RecordSet",
"формасохранения": "Save", "формасохранениянастроек": "Save", "сохранение": "Save",
"saveform": "Save",
"формазагрузки": "Load", "формазагрузкинастроек": "Load", "загрузка": "Load",
"loadform": "Load",
"произвольная": "Custom", "произвольнаяформа": "Custom", "customform": "Custom",
}
if purpose:
purpose_probe = re.sub(r"[\s_-]", "", purpose).lower()
is_known_purpose = any(k.lower() == purpose.lower() for k in kind_purposes)
if not is_known_purpose and purpose_probe in purpose_synonyms:
purpose = purpose_synonyms[purpose_probe]
if not purpose:
if object_type == "Table" and table_data_type == "NonobjectData":
# Пометка primary в таблице видов одна на вид, а у таблицы с составным ключом
# формы объекта не бывает — основной становится форма записи.
purpose = "Record"
else:
for k, rule in kind_purposes.items():
if rule.get("primary"):
purpose = k
break
purpose_key = None
for k in kind_purposes:
if k.lower() == purpose.lower():
purpose_key = k
break
if purpose_key is None:
print(f"Назначение '{purpose}' недопустимо для {object_type}. "
f"Допустимые: {', '.join(sorted(kind_purposes))}", file=sys.stderr)
sys.exit(1)
purpose = purpose_key
purpose_rule = kind_purposes[purpose]
object_like_types = ["Document", "Catalog", "ChartOfAccounts", "ChartOfCharacteristicTypes",
"ExchangePlan", "BusinessProcess", "Task"]
processor_like_types = ["DataProcessor", "Report", "ExternalDataProcessor", "ExternalReport"]
if purpose == "List":
if object_type == "DataProcessor":
print("Purpose=List недопустим для DataProcessor", file=sys.stderr)
# У таблицы внешнего источника набор назначений зависит от вида данных (замерено на
# 8.3.24.1691): ObjectData — Object/List/Choice, NonobjectData — Record/List/Choice. Неверная пара
# не отвергается схемой формы, а валит загрузку всей конфигурации «Исключением XDTO» без причины.
if object_type == "Table":
if table_data_type == "NonobjectData" and purpose == "Object":
print(f"Таблица '{object_name}' с составным ключом (TableDataType=NonobjectData): "
"формы объекта у неё нет — используйте -Purpose Record.", file=sys.stderr)
sys.exit(1)
if table_data_type != "NonobjectData" and purpose == "Record":
print(f"Таблица '{object_name}' с ключом из одного поля (TableDataType=ObjectData): "
"формы записи у неё нет — используйте -Purpose Object.", file=sys.stderr)
sys.exit(1)
elif purpose == "Choice":
if object_type in processor_like_types or object_type == "InformationRegister":
print(f"Purpose=Choice недопустим для {object_type}", file=sys.stderr)
sys.exit(1)
elif purpose == "Record":
if object_type != "InformationRegister":
print("Purpose=Record допустим только для InformationRegister", file=sys.stderr)
sys.exit(1)
# Гард от повторения дефекта: запись таблицы обязана быть заполненной. Пустой main_attr —
# это произвольная форма (законное состояние), а наполовину заполненная запись означала бы,
# что таблицу правили невнимательно, и в XML уйдёт мусор вроде `cfg:.Журнал`.
if purpose_rule.get("main_attr") and not purpose_rule.get("attr_name"):
print(f"Внутренняя ошибка таблицы видов: у {object_type}/{purpose} задан main_attr без attr_name",
file=sys.stderr)
sys.exit(1)
# --- Phase 3: Create files ---
@@ -531,100 +769,47 @@ def main():
form_xml_path = os.path.join(form_ext_dir, "Form.xml")
if purpose in ("List", "Choice"):
# Dynamic list
main_table = f"{object_type}.{object_name}"
# Одна ветка вместо трёх: что писать, решает запись таблицы видов. Раньше тип главного
# реквизита брался из отдельной карты, и отсутствие вида в ней давало `cfg:.Имя` — молча.
attributes_block = ''
if purpose_rule.get("main_attr"):
main_attr_type = purpose_rule["main_attr"].format(object_type, object_name, object_qualified_name)
main_attr_name = purpose_rule["attr_name"]
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
'\t\t<Attribute name="\u0421\u043f\u0438\u0441\u043e\u043a" id="1">\n'
'\t\t\t<Type>\n'
'\t\t\t\t<v8:Type>cfg:DynamicList</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<Settings xsi:type="DynamicList">\n'
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
'\t\t\t</Settings>\n'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
# Динамический список несёт MainTable, остальные типы — SavedData по записи таблицы.
tail_lines = ''
if main_attr_type == "DynamicList":
main_table = object_ref
tail_lines = ('\t\t\t<Settings xsi:type="DynamicList">\n'
f'\t\t\t\t<MainTable>{main_table}</MainTable>\n'
'\t\t\t</Settings>\n')
elif purpose_rule.get("saved_data"):
tail_lines = '\t\t\t<SavedData>true</SavedData>\n'
elif purpose == "Record":
# Information register record
main_attr_name = "\u0417\u0430\u043f\u0438\u0441\u044c"
main_attr_type = f"InformationRegisterRecordManager.{object_name}"
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
attributes_block = (
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
'\t\t\t<SavedData>true</SavedData>\n'
f'{tail_lines}'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
else:
# Object — object form
main_attr_name = "\u041e\u0431\u044a\u0435\u043a\u0442"
attr_type_map = {
"Document": "DocumentObject",
"Catalog": "CatalogObject",
"DataProcessor": "DataProcessorObject",
"Report": "ReportObject",
"ExternalDataProcessor": "ExternalDataProcessorObject",
"ExternalReport": "ExternalReportObject",
"ChartOfAccounts": "ChartOfAccountsObject",
"ChartOfCharacteristicTypes": "ChartOfCharacteristicTypesObject",
"ExchangePlan": "ExchangePlanObject",
"BusinessProcess": "BusinessProcessObject",
"Task": "TaskObject",
"InformationRegister": "InformationRegisterRecordManager",
"AccumulationRegister": "AccumulationRegisterRecordSet",
}
main_attr_type = f"{attr_type_map[object_type]}.{object_name}"
# SavedData: standard for Catalog/Document/etc, but not for processor-like (DataProcessor/Report/External*)
saved_data_line = ''
if object_type not in processor_like_types:
saved_data_line = '\t\t\t<SavedData>true</SavedData>\n'
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
'\t<Attributes>\n'
f'\t\t<Attribute name="{main_attr_name}" id="1">\n'
'\t\t\t<Type>\n'
f'\t\t\t\t<v8:Type>cfg:{main_attr_type}</v8:Type>\n'
'\t\t\t</Type>\n'
'\t\t\t<MainAttribute>true</MainAttribute>\n'
f'{saved_data_line}'
'\t\t</Attribute>\n'
'\t</Attributes>\n'
'</Form>'
)
# Произвольная форма (main_attr=None) — без блока Attributes вовсе. В типовых это самая
# частая форма после объектной: 907 у справочников, 941 у документов, 3482 у отчётов.
form_xml = (
f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<Form {form_ns_decl} version="{format_version}">\n'
'\t<AutoCommandBar name="\u0424\u043e\u0440\u043c\u0430\u041a\u043e\u043c\u0430\u043d\u0434\u043d\u0430\u044f\u041f\u0430\u043d\u0435\u043b\u044c" id="-1">\n'
'\t\t<Autofill>true</Autofill>\n'
'\t</AutoCommandBar>\n'
'\t<ChildItems/>\n'
f'{attributes_block}'
'</Form>'
)
if os.path.exists(form_xml_path):
print(f"[SKIP] Form.xml already exists: {form_xml_path} — not overwriting")
@@ -721,26 +906,18 @@ def main():
# --- SetDefault ---
is_first_form_for_purpose = False
default_prop_name = None
default_value = f"{object_type}.{object_name}.Form.{form_name}"
default_value = f"{object_ref}.Form.{form_name}"
# Determine property name for DefaultForm
if purpose == "Object":
if object_type in processor_like_types:
default_prop_name = "DefaultForm"
else:
default_prop_name = "DefaultObjectForm"
elif purpose == "List":
default_prop_name = "DefaultListForm"
elif purpose == "Choice":
default_prop_name = "DefaultChoiceForm"
elif purpose == "Record":
default_prop_name = "DefaultRecordForm"
# Свойство «основная форма» — из записи таблицы. Раньше выбиралось по одному purpose без
# учёта вида, и для журнала писалось DefaultListForm, которого у журнала нет: слот не
# находился, навык молча ничего не делал.
default_prop_name = purpose_rule.get("slot")
# Check if value is already set
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
if default_node is not None:
is_first_form_for_purpose = default_node.text is None or default_node.text.strip() == ""
default_node = None
if default_prop_name:
default_node = root.find(f".//md:{object_type}/md:Properties/md:{default_prop_name}", NSMAP)
if default_node is not None:
is_first_form_for_purpose = not (default_node.text or "").strip()
default_updated = False
if set_default or is_first_form_for_purpose:
@@ -767,6 +944,10 @@ def main():
print(f"Registered: <Form>{form_name}</Form> in ChildObjects")
if default_updated:
print(f"{default_prop_name}: {default_value}")
elif not default_prop_name:
# Молчать здесь нельзя: пользователь ждёт, что форма станет основной, а свойства под неё
# у платформы нет (форма набора записей, произвольная форма).
print(f"Основной не назначена: у {object_type} нет свойства для формы с назначением {purpose}")
print()
@@ -1,5 +1,6 @@
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# form-compile v1.196 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[string]$JsonPath,
@@ -14,6 +15,70 @@ param(
)
$ErrorActionPreference = "Stop"
# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
try {
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
$parsed = $text | ConvertFrom-Json
} catch {
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
if ($Inline) {
$got = ($text -replace '\s+', ' ').Trim()
$label = 'got'
if (-not $got) { $got = '(empty)' }
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
$what = "${what}, ${label}: ${got}"
}
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
exit 1
}
Write-Output -NoEnumerate $parsed
}
# --- Чтение входного JSON-файла ---
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
# угаданное имя уйдёт в метаданные так же молча.
function Read-JsonInputFile([string]$path) {
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
# проверкой срабатывают раньше и сохраняют свой текст.
if (-not (Test-Path -LiteralPath $path)) {
[Console]::Error.WriteLine("[ERROR] File not found: $path")
exit 1
}
if (Test-Path -LiteralPath $path -PathType Container) {
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
exit 1
}
$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
}
try {
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
} catch {
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
exit 1
}
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# ═══════════════════════════════════════════════════════════════════════════
@@ -300,7 +365,7 @@ function Load-Preset([string]$PresetName, [string]$ScriptDir) {
$presetDir = Join-Path (Split-Path $ScriptDir -Parent) "presets"
$builtInPath = Join-Path $presetDir "$PresetName.json"
if (Test-Path $builtInPath) {
$presetJson = Get-Content -Raw -Encoding UTF8 $builtInPath | ConvertFrom-Json
$presetJson = ConvertFrom-JsonInput (Read-JsonInputFile $builtInPath) $builtInPath
# Convert PSCustomObject to hashtable recursively
$toHash = {
param($obj)
@@ -327,7 +392,7 @@ function Load-Preset([string]$PresetName, [string]$ScriptDir) {
while ($scanDir) {
$projPreset = Join-Path (Join-Path (Join-Path (Join-Path $scanDir "presets") "skills") "form") "$PresetName.json"
if (Test-Path $projPreset) {
$projJson = Get-Content -Raw -Encoding UTF8 $projPreset | ConvertFrom-Json
$projJson = ConvertFrom-JsonInput (Read-JsonInputFile $projPreset) $projPreset
$projHash = & $toHash $projJson
foreach ($k in @($projHash.Keys)) {
$defaults[$k] = & $deepMerge $defaults[$k] $projHash[$k]
@@ -1655,8 +1720,8 @@ if ($FromObject) {
exit 1
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
$json = Read-JsonInputFile $JsonPath
$def = ConvertFrom-JsonInput $json $JsonPath
}
# Базовая директория для @file-ссылок в query динсписка (зеркало skd-compile)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-compile v1.192 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# form-compile v1.196 — Compile 1C managed form from JSON or object metadata (гвард на группу additionalColumns без ключа columns)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import copy
@@ -15,6 +15,68 @@ from lxml import etree
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def parse_json_input(text, source, expected=None, inline=False):
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
expected заполняем только для полиморфного входа: у файла подсказка
была бы наполнителем имя файла и текст парсера самодостаточны. inline печатает ещё и то,
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
"""
import json as _pj
import sys as _psys
try:
if not str(text).strip():
raise ValueError("input is empty")
return _pj.loads(text)
except ValueError as exc:
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
if inline:
got = " ".join(str(text).split())
label = "got"
if not got:
got = "(empty)"
elif len(got) > 60:
label = "got (first 60 chars)"
got = got[:60]
what = "%s, %s: %s" % (what, label, got)
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
_psys.exit(1)
def read_json_file(path):
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
BOM объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
"""
import os as _pos
import sys as _psys
if not _pos.path.exists(path):
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
_psys.exit(1)
if _pos.path.isdir(path):
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
_psys.exit(1)
with open(path, "rb") as _fh:
data = _fh.read()
if data[:3] == b"\xef\xbb\xbf":
return data[3:].decode("utf-8")
if data[:2] == b"\xff\xfe":
return data[2:].decode("utf-16-le")
if data[:2] == b"\xfe\xff":
return data[2:].decode("utf-16-be")
try:
return data.decode("utf-8")
except UnicodeDecodeError as exc:
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
% (path, exc), file=_psys.stderr)
_psys.exit(1)
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
@@ -542,8 +604,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
preset_dir = os.path.join(os.path.dirname(script_dir), 'presets')
built_in_path = os.path.join(preset_dir, f'{preset_name}.json')
if os.path.isfile(built_in_path):
with open(built_in_path, 'r', encoding='utf-8-sig') as f:
preset_data = ci_json(json.load(f))
preset_data = ci_json(parse_json_input(read_json_file(built_in_path), built_in_path))
for k in list(preset_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), preset_data[k])
@@ -552,8 +613,7 @@ def load_preset(preset_name, script_dir, out_path_resolved):
while scan_dir:
proj_preset = os.path.join(scan_dir, 'presets', 'skills', 'form', f'{preset_name}.json')
if os.path.isfile(proj_preset):
with open(proj_preset, 'r', encoding='utf-8-sig') as f:
proj_data = json.load(f)
proj_data = parse_json_input(read_json_file(proj_preset), proj_preset)
for k in list(proj_data.keys()):
defaults[k] = _deep_merge(defaults.get(k), proj_data[k])
break
@@ -6455,8 +6515,7 @@ def main():
print(f"File not found: {json_path}", file=sys.stderr)
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
defn = ci_json(json.load(f))
defn = ci_json(parse_json_input(read_json_file(json_path), json_path))
global QUERY_BASE_DIR
QUERY_BASE_DIR = os.path.dirname(os.path.abspath(json_path))
@@ -1,6 +1,7 @@
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.150 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Alias('Path')]
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-decompile v0.149 — Decompile 1C managed Form.xml to JSON DSL (draft)
# form-decompile v0.150 — Decompile 1C managed Form.xml to JSON DSL (draft)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
# ВНИМАНИЕ: раундтрип не гарантируется. Навык исключён из авто-использования моделью.
#
+67 -2
View File
@@ -1,5 +1,6 @@
# form-edit v1.14 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# form-edit v1.18 — Edit 1C managed form elements (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Alias('Path')]
@@ -10,6 +11,70 @@ param(
)
$ErrorActionPreference = "Stop"
# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
try {
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
$parsed = $text | ConvertFrom-Json
} catch {
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
if ($Inline) {
$got = ($text -replace '\s+', ' ').Trim()
$label = 'got'
if (-not $got) { $got = '(empty)' }
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
$what = "${what}, ${label}: ${got}"
}
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
exit 1
}
Write-Output -NoEnumerate $parsed
}
# --- Чтение входного JSON-файла ---
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
# угаданное имя уйдёт в метаданные так же молча.
function Read-JsonInputFile([string]$path) {
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
# проверкой срабатывают раньше и сохраняют свой текст.
if (-not (Test-Path -LiteralPath $path)) {
[Console]::Error.WriteLine("[ERROR] File not found: $path")
exit 1
}
if (Test-Path -LiteralPath $path -PathType Container) {
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
exit 1
}
$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
}
try {
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
} catch {
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
exit 1
}
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- Support guard (Ext/ParentConfigurations.bin) ---
@@ -175,7 +240,7 @@ $root = $xmlDoc.DocumentElement
# === 2. Load JSON ===
$def = Get-Content -Raw -Encoding UTF8 $JsonPath | ConvertFrom-Json
$def = ConvertFrom-JsonInput (Read-JsonInputFile $JsonPath) $JsonPath
# === 3. Form name + header ===
+64 -3
View File
@@ -1,4 +1,4 @@
# form-edit v1.14 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# form-edit v1.18 — Edit 1C managed form elements (Python port) (+esc_xml/esc_xml_text: разное экранирование атрибута и текста)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -13,6 +13,68 @@ sys.stderr.reconfigure(encoding="utf-8")
# Регистронезависимый ввод — паритет с PS1: в PowerShell имена параметров и [ValidateSet]
# регистр не различают, в argparse совпадение точное.
def parse_json_input(text, source, expected=None, inline=False):
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
expected заполняем только для полиморфного входа: у файла подсказка
была бы наполнителем имя файла и текст парсера самодостаточны. inline печатает ещё и то,
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
"""
import json as _pj
import sys as _psys
try:
if not str(text).strip():
raise ValueError("input is empty")
return _pj.loads(text)
except ValueError as exc:
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
if inline:
got = " ".join(str(text).split())
label = "got"
if not got:
got = "(empty)"
elif len(got) > 60:
label = "got (first 60 chars)"
got = got[:60]
what = "%s, %s: %s" % (what, label, got)
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
_psys.exit(1)
def read_json_file(path):
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
BOM объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
"""
import os as _pos
import sys as _psys
if not _pos.path.exists(path):
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
_psys.exit(1)
if _pos.path.isdir(path):
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
_psys.exit(1)
with open(path, "rb") as _fh:
data = _fh.read()
if data[:3] == b"\xef\xbb\xbf":
return data[3:].decode("utf-8")
if data[:2] == b"\xff\xfe":
return data[2:].decode("utf-16-le")
if data[:2] == b"\xfe\xff":
return data[2:].decode("utf-16-be")
try:
return data.decode("utf-8")
except UnicodeDecodeError as exc:
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
% (path, exc), file=_psys.stderr)
_psys.exit(1)
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
@@ -317,8 +379,7 @@ root = tree.getroot()
# ── 2. Load JSON ────────────────────────────────────────────
with open(json_path, "r", encoding="utf-8-sig") as f:
defn = ci_json(json.load(f))
defn = ci_json(parse_json_input(read_json_file(json_path), json_path))
# ── 3. Form name + header ───────────────────────────────────
@@ -1,7 +1,8 @@
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
# form-info v1.8 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory=$true)]
[Parameter(Mandatory=$true, Position=0)]
[Alias('Path')]
[string]$FormPath,
[int]$Limit = 150,
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-info v1.7 — Analyze 1C managed form structure (+единое имя хелпера состояния поддержки)
# form-info v1.8 — Analyze 1C managed form structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
+3 -2
View File
@@ -27,11 +27,12 @@ allowed-tools:
| ObjectName | да | — | Имя объекта |
| FormName | да | — | Имя формы для удаления |
| SrcDir | нет | `src` | Каталог исходников |
| Force | нет | — | Удалить, даже если на форму ссылаются, и очистить ссылки |
## Команда
```powershell
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"]
powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -ObjectName "<ObjectName>" -FormName "<FormName>" [-SrcDir "<SrcDir>"] [-Force]
```
## Что удаляется
@@ -44,4 +45,4 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/remove-form.ps1" -O
## Что модифицируется
- `<SrcDir>/<ObjectName>.xml` — убирается `<Form>` из `ChildObjects`
- Если удаляемая форма была DefaultForm — очищается значение DefaultForm
- Свойства объекта, указывавшие на удалённую форму — очищаются
@@ -1,4 +1,4 @@
# form-remove v1.9 — Remove form from 1C object
# form-remove v1.10 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[Parameter(Mandatory)]
@@ -8,7 +8,9 @@ param(
[Parameter(Mandatory)]
[string]$FormName,
[string]$SrcDir = "src"
[string]$SrcDir = "src",
[switch]$Force
)
$ErrorActionPreference = "Stop"
@@ -33,6 +35,180 @@ if (-not (Test-Path $formMetaPath)) {
exit 1
}
# --- Загрузка корневого XML: вид и имя объекта ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$typeNode = $null
foreach ($c in $xmlDoc.DocumentElement.ChildNodes) {
if ($c.NodeType -eq [System.Xml.XmlNodeType]::Element) { $typeNode = $c; break }
}
if (-not $typeNode) {
Write-Error "Не удалось определить вид объекта в $rootXmlPath"
exit 1
}
$mdType = $typeNode.LocalName
$nameNode = $typeNode.SelectSingleNode("md:Properties/md:Name", $nsMgr)
$objMetaName = if ($nameNode -and $nameNode.InnerText.Trim()) { $nameNode.InnerText.Trim() } else { [System.IO.Path]::GetFileNameWithoutExtension($rootXmlPath) }
# Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при
# удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка.
$formRef = "$mdType.$objMetaName.Form.$FormName"
# --- Чистка ссылок и сохранение в стиле файла-источника ---
# Каноничное «не задано» зависит от файла: в корневом XML объекта и в Configuration.xml
# пустой слот штатен (164 508 пустых на корпус), а внутри Ext/Form.xml пустых <ChoiceForm/>
# и <SettingsStorage/> нет ни одного — там свойство просто отсутствует.
function Clear-FormRefs {
param([System.Xml.XmlDocument]$doc, [string]$ref)
$isFormFile = $doc.DocumentElement -and $doc.DocumentElement.LocalName -eq "Form"
$touched = @()
foreach ($node in @($doc.SelectNodes("//*"))) {
if ($node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
if ($node.SelectNodes("*").Count -gt 0) { continue } # только листья
# Сравнение регистронезависимое — как у платформы (в py-порту .lower()).
if ($node.InnerText.Trim() -ne $ref) { continue }
$ln = $node.LocalName
$parent = $node.ParentNode
if ($ln -eq "Form" -and $parent -and $parent.LocalName -eq "Item") {
$touched += "$($parent.LocalName)/$ln"
Remove-NodeWithIndent $parent
} elseif ($isFormFile) {
$touched += $ln
Remove-NodeWithIndent $node
} else {
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
$touched += $ln
$node.IsEmpty = $true
}
}
return $touched
}
function Remove-NodeWithIndent {
param([System.Xml.XmlNode]$node)
$parent = $node.ParentNode
if (-not $parent) { return }
$prev = $node.PreviousSibling
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$parent.RemoveChild($prev) | Out-Null
}
$parent.RemoveChild($node) | Out-Null
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
}
function Save-XmlPreservingStyle {
param([System.Xml.XmlDocument]$doc, [string]$path)
$encBom = New-Object System.Text.UTF8Encoding($true)
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$doc.Save($writer)
$writer.Flush(); $writer.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 $path) -and ([System.IO.File]::ReadAllText($path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($path, $xmlText, $encBom)
}
# --- Поиск ссылок на форму по всей конфигурации ---
# Get-Item, а не Resolve-Path: последний оставляет короткое имя 8.3 (NSHIRO~1), а
# Get-ChildItem отдаёт длинное (nshirokov) — сравнение путей молча не совпадало.
function Get-LongPath {
param([string]$path)
if (-not (Test-Path -LiteralPath $path)) { return "" }
return (Get-Item -LiteralPath $path -Force).FullName
}
# Корень конфигурации: обычно это сам SrcDir, но объект могут передать и из глубины.
$configDir = $null
$probe = Get-LongPath $SrcDir
for ($depth = 0; $depth -lt 4; $depth++) {
if (-not $probe) { break }
if (Test-Path (Join-Path $probe "Configuration.xml")) { $configDir = $probe; break }
$probe = Split-Path $probe
}
$rootXmlLong = Get-LongPath $rootXmlFull.Path
$formMetaFull = Get-LongPath $formMetaPath
$formDirFull = Get-LongPath $formDir
$references = @()
if ($configDir) {
# Полный обход, как в meta-remove: ссылки лежат и внутри Ext/Form.xml (ChoiceForm,
# SettingsStorage), узкий скан по корневым XML их не видит.
# EnumerateFiles, а не Get-ChildItem -Recurse: на ERP (73 904 XML) обход обёртками
# занимает 180 с против 47 с — чтение файлов не узкое место, узкое место перечисление.
$refPattern = '<([A-Za-z0-9_.]+)>' + [regex]::Escape($formRef) + '</'
$scanSw = [System.Diagnostics.Stopwatch]::StartNew()
$scanned = 0
foreach ($fp in [System.IO.Directory]::EnumerateFiles($configDir, "*.xml", [System.IO.SearchOption]::AllDirectories)) {
if ($fp -eq $rootXmlLong) { continue } # свой файл чистится всегда
if ($fp -eq $formMetaFull) { continue } # файлы удаляемой формы
if ($formDirFull -and $fp.StartsWith($formDirFull)) { continue }
$scanned++
$content = [System.IO.File]::ReadAllText($fp, [System.Text.Encoding]::UTF8)
if (-not $content.Contains($formRef)) { continue }
foreach ($m in [regex]::Matches($content, $refPattern)) {
$references += @{ Path = $fp; Rel = $fp.Substring($configDir.Length + 1); Tag = $m.Groups[1].Value }
}
}
$scanSw.Stop()
if ($scanSw.Elapsed.TotalSeconds -ge 5) {
Write-Host "[INFO] Проверено ссылок в $scanned файлах за $([math]::Round($scanSw.Elapsed.TotalSeconds, 1)) c"
}
}
if ($references.Count -gt 0) {
Write-Host "[WARN] На форму $formRef ссылаются $($references.Count) раз(а):"
foreach ($grp in ($references | Group-Object { "$($_.Rel)|$($_.Tag)" } | Sort-Object Name)) {
$parts = $grp.Name.Split("|")
$suffix = if ($grp.Count -gt 1) { " x$($grp.Count)" } else { "" }
Write-Host " $($parts[0]) — <$($parts[1])>$suffix"
}
Write-Host ""
if (-not $Force) {
Write-Host "[ERROR] Удаление остановлено: форма используется."
Write-Host " Решает пользователь: убрать ссылки, отказаться от удаления или"
Write-Host " повторить с -Force — тогда ссылки будут очищены."
exit 1
}
Write-Host "[WARN] -Force: ссылки будут очищены"
Write-Host ""
} elseif (-not $configDir) {
Write-Host "[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены"
}
# --- Удаление файлов ---
if (Test-Path $formDir) {
@@ -45,70 +221,35 @@ Write-Host "[OK] Удалён файл: $formMetaPath"
# --- Модификация корневого XML ---
$rootXmlFull = Resolve-Path $rootXmlPath
$xmlDoc = New-Object System.Xml.XmlDocument
$xmlDoc.PreserveWhitespace = $true
$xmlDoc.Load($rootXmlFull.Path)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($xmlDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
# Удалить <Form>FormName</Form> из ChildObjects
$formNodes = $xmlDoc.SelectNodes("//md:ChildObjects/md:Form", $nsMgr)
foreach ($node in $formNodes) {
if ($node.InnerText -eq $FormName) {
$parent = $node.ParentNode
# Удалить предшествующий whitespace
$prev = $node.PreviousSibling
if ($prev -and $prev.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$parent.RemoveChild($prev) | Out-Null
}
$parent.RemoveChild($node) | Out-Null
# Опустевший контейнер: остаётся отступ-whitespace, и XmlWriter пишет пару
# <ChildObjects>\n\t\t</ChildObjects>. Платформа пишет только <ChildObjects/>
# (1394 самозакрывающихся на acc+erp, пустых пар ни в одной форме — 0).
if ($parent.SelectNodes("*").Count -eq 0) { $parent.IsEmpty = $true }
Remove-NodeWithIndent $node
break
}
}
# Очистить любые Default*/Auxiliary* form-слоты, указывавшие на удалённую форму
# (form-add пишет свойство по назначению: DefaultObjectForm/DefaultListForm/
# DefaultChoiceForm/DefaultRecordForm/DefaultForm — не только generic DefaultForm).
$formRefRe = "Form\.$([regex]::Escape($FormName))$"
foreach ($node in $xmlDoc.SelectNodes("//md:*", $nsMgr)) {
if ($node.LocalName -like "*Form" -and $node.InnerText -and $node.InnerText -match $formRefRe) {
# IsEmpty, а не InnerText="": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
$node.IsEmpty = $true
}
}
# Очистить слоты своего объекта, указывавшие на удалённую форму: Default*/Auxiliary*Form
# (form-add пишет свойство по назначению) и ChoiceForm у реквизитов.
Clear-FormRefs $xmlDoc $formRef | Out-Null
# Сохранить с BOM
$encBom = New-Object System.Text.UTF8Encoding($true)
$settings = New-Object System.Xml.XmlWriterSettings
$settings.Encoding = $encBom
$settings.Indent = $false
$settings.NewLineHandling = [System.Xml.NewLineHandling]::None
# Через MemoryStream, а не прямо в файл: нужен шаг пост-обработки строки.
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $settings)
$xmlDoc.Save($writer)
$writer.Flush(); $writer.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 $rootXmlFull.Path) -and ([System.IO.File]::ReadAllText($rootXmlFull.Path) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$xmlText = ($xmlText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($rootXmlFull.Path, $xmlText, $encBom)
Save-XmlPreservingStyle $xmlDoc $rootXmlFull.Path
Write-Host "[OK] Форма $FormName удалена из $rootXmlPath"
# --- Чистка ссылок в других файлах (только с -Force) ---
if ($references.Count -gt 0) {
foreach ($grp in ($references | Group-Object { $_.Path } | Sort-Object Name)) {
$path = $grp.Name
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($path)
$touched = @(Clear-FormRefs $doc $formRef)
if ($touched.Count -eq 0) { continue }
Save-XmlPreservingStyle $doc $path
$rel = $path.Substring($configDir.Length + 1)
Write-Host "[OK] Очищена ссылка в $rel$(($touched | Sort-Object -Unique) -join ', ')"
}
}
+165 -32
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-remove v1.9 — Remove form from 1C object
# form-remove v1.10 — Remove form from 1C object
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -83,6 +83,69 @@ def save_xml_with_bom(tree, path):
f.write(xml_bytes)
def long_path(path):
"""Полный путь в длинной форме. Зеркало Get-LongPath в PS: там Resolve-Path оставляет
короткое имя 8.3 (NSHIRO~1), а перечисление отдаёт длинное сравнение молча не совпадало."""
if not os.path.exists(path):
return ""
return os.path.realpath(path)
def remove_node_with_indent(node):
"""Удалить элемент вместе с предшествующим whitespace; опустевший контейнер сделать
самозакрывающимся. Зеркало Remove-NodeWithIndent в PS."""
parent = node.getparent()
if parent is None:
return
# В DOM (PS) whitespace — отдельные узлы: удаляются предшествующий и сам элемент, а
# whitespace ПОСЛЕ элемента остаётся. В lxml он лежит в node.tail и ушёл бы вместе с
# узлом, поэтому его надо передать предшественнику — иначе `</Attributes></Form>`.
prev = node.getprevious()
if prev is not None:
prev.tail = node.tail
else:
parent.text = node.tail
parent.remove(node)
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
if len(parent) == 0 and not (parent.text or "").strip():
parent.text = None
def clear_form_refs(tree, ref):
"""Очистить ссылки на форму. Каноничное «не задано» зависит от файла: в корневом XML
объекта и в Configuration.xml пустой слот штатен (164 508 пустых на корпус), а внутри
Ext/Form.xml пустых <ChoiceForm/> и <SettingsStorage/> нет ни одного там свойство
просто отсутствует. Зеркало Clear-FormRefs в PS."""
root = tree.getroot()
is_form_file = etree.QName(root).localname == "Form"
touched = []
ref_lc = ref.lower()
for el in list(root.iter()):
if not isinstance(el.tag, str):
continue
if len(el) > 0: # только листья
continue
# Сравнение регистронезависимое — как у платформы (в PS -eq регистр не различает).
if (el.text or "").strip().lower() != ref_lc:
continue
ln = etree.QName(el).localname
parent = el.getparent()
if ln == "Form" and parent is not None and etree.QName(parent).localname == "Item":
touched.append(f"{etree.QName(parent).localname}/{ln}")
remove_node_with_indent(parent)
elif is_form_file:
touched.append(ln)
remove_node_with_indent(el)
else:
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
touched.append(ln)
el.text = None
return touched
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
@@ -90,11 +153,13 @@ def main():
parser.add_argument("-ObjectName", "-ProcessorName", required=True)
parser.add_argument("-FormName", required=True)
parser.add_argument("-SrcDir", default="src")
parser.add_argument("-Force", action="store_true")
args = ci_parse_args(parser)
object_name = args.ObjectName
form_name = args.FormName
src_dir = args.SrcDir
force = args.Force
# --- Checks ---
@@ -112,6 +177,91 @@ def main():
print(f"Метаданные формы не найдены: {form_meta_path}", file=sys.stderr)
sys.exit(1)
# --- Load root XML: kind and object name ---
root_xml_full = long_path(root_xml_path) or os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
type_node = None
for c in root:
if isinstance(c.tag, str):
type_node = c
break
if type_node is None:
print(f"Не удалось определить вид объекта в {root_xml_path}", file=sys.stderr)
sys.exit(1)
md_type = etree.QName(type_node).localname
name_node = type_node.find("md:Properties/md:Name", NSMAP)
obj_meta_name = (name_node.text or "").strip() if name_node is not None else ""
if not obj_meta_name:
obj_meta_name = os.path.splitext(os.path.basename(root_xml_path))[0]
# Полная ссылка на форму. Матч по ней целиком, а не по хвосту "Form.<Имя>": иначе при
# удалении своей ФормаСписка обнулялась бы и ссылка на DocumentJournal.Ж.Form.ФормаСписка.
form_ref = f"{md_type}.{obj_meta_name}.Form.{form_name}"
# --- Find references across the configuration ---
config_dir = None
probe = long_path(src_dir) or os.path.abspath(src_dir)
for _ in range(4):
if not probe:
break
if os.path.exists(os.path.join(probe, "Configuration.xml")):
config_dir = probe
break
parent_probe = os.path.dirname(probe)
if parent_probe == probe:
break
probe = parent_probe
form_meta_full = long_path(form_meta_path)
form_dir_full = long_path(form_dir)
references = []
if config_dir:
ref_pattern = re.compile(r"<([A-Za-z0-9_.]+)>" + re.escape(form_ref) + r"</")
for dirpath, _dirnames, filenames in os.walk(config_dir):
for fn in filenames:
if not fn.lower().endswith(".xml"):
continue
fp = os.path.join(dirpath, fn)
if fp == root_xml_full or fp == form_meta_full:
continue # свой файл и файлы удаляемой формы
if form_dir_full and fp.startswith(form_dir_full):
continue
try:
with open(fp, "r", encoding="utf-8-sig") as f:
content = f.read()
except OSError:
continue
if form_ref not in content:
continue
for m in ref_pattern.finditer(content):
references.append({"path": fp, "rel": os.path.relpath(fp, config_dir),
"tag": m.group(1)})
if references:
print(f"[WARN] На форму {form_ref} ссылаются {len(references)} раз(а):")
grouped = {}
for r in references:
grouped[(r["rel"], r["tag"])] = grouped.get((r["rel"], r["tag"]), 0) + 1
for (rel, tag) in sorted(grouped):
suffix = f" x{grouped[(rel, tag)]}" if grouped[(rel, tag)] > 1 else ""
print(f" {rel} — <{tag}>{suffix}")
print()
if not force:
print("[ERROR] Удаление остановлено: форма используется.")
print(" Решает пользователь: убрать ссылки, отказаться от удаления или")
print(" повторить с -Force — тогда ссылки будут очищены.")
sys.exit(1)
print("[WARN] -Force: ссылки будут очищены")
print()
elif not config_dir:
print("[WARN] Корень конфигурации не найден — ссылки в других объектах не проверены")
# --- Delete files ---
if os.path.isdir(form_dir):
@@ -123,48 +273,31 @@ def main():
# --- Modify root XML ---
root_xml_full = os.path.abspath(root_xml_path)
parser_xml = etree.XMLParser(remove_blank_text=False)
tree = etree.parse(root_xml_full, parser_xml)
root = tree.getroot()
# Remove <Form>FormName</Form> from ChildObjects
for node in root.findall(".//md:ChildObjects/md:Form", NSMAP):
if node.text and node.text.strip() == form_name:
parent = node.getparent()
prev = node.getprevious()
if prev is not None:
# Whitespace is in prev.tail
if prev.tail and prev.tail.strip() == "":
prev.tail = ""
else:
# First child — whitespace is in parent.text
if parent.text and parent.text.strip() == "":
parent.text = ""
parent.remove(node)
# Опустевший контейнер: text="" сериализуется парой <ChildObjects></ChildObjects>,
# а нужен <ChildObjects/> — PS-порт через DOM даёт именно его.
if len(parent) == 0 and not (parent.text or "").strip():
parent.text = None
remove_node_with_indent(node)
break
# Clear any Default*/Auxiliary* form slot that pointed to the removed form
# (form-add writes the purpose-specific property: DefaultObjectForm / DefaultListForm /
# DefaultChoiceForm / DefaultRecordForm / DefaultForm — not just generic DefaultForm).
ref_re = re.compile(rf"Form\.{re.escape(form_name)}$")
for el in root.iter():
if not isinstance(el.tag, str):
continue
if etree.QName(el).localname.endswith("Form") and el.text and ref_re.search(el.text):
# text=None, а не "": пустая строка сериализуется парой <Tag></Tag>, а
# Конфигуратор пустых пар не пишет (0 на 476 942 XML корпуса) — нужен <Tag/>.
el.text = None
# Очистить слоты своего объекта: Default*/Auxiliary*Form и ChoiceForm у реквизитов.
clear_form_refs(tree, form_ref)
# Save with BOM
save_xml_with_bom(tree, root_xml_full)
print(f"[OK] Форма {form_name} удалена из {root_xml_path}")
# --- Clean references in other files (only with -Force) ---
for fp in sorted({r["path"] for r in references}):
other_tree = etree.parse(fp, parser_xml)
touched = clear_form_refs(other_tree, form_ref)
if not touched:
continue
save_xml_with_bom(other_tree, fp)
rel = os.path.relpath(fp, config_dir)
print(f"[OK] Очищена ссылка в {rel}{', '.join(sorted(set(touched)))}")
if __name__ == "__main__":
main()
@@ -1,7 +1,8 @@
# form-validate v1.17 — Validate 1C managed form
# form-validate v1.19 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Parameter(Mandatory, Position=0)]
[Alias('Path')]
[string]$FormPath,
@@ -927,6 +928,8 @@ $validCfgPrefixes = @(
"ConstantsSet","DataProcessorObject","DocumentObject","DocumentRef"
"DynamicList","EnumRef","ExchangePlanObject","ExchangePlanRef"
"ExternalDataProcessorObject","ExternalReportObject"
"ExternalDataSourceTableObject","ExternalDataSourceTableRecordManager"
"ExternalDataSourceTableRef"
"InformationRegisterRecordManager","InformationRegisterRecordSet"
"ReportObject","TaskObject","TaskRef"
)
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# form-validate v1.17 — Validate 1C managed form
# form-validate v1.19 — Validate 1C managed form
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -66,6 +66,8 @@ VALID_CFG_PREFIXES = {
'ConstantsSet', 'DataProcessorObject', 'DocumentObject', 'DocumentRef',
'DynamicList', 'EnumRef', 'ExchangePlanObject', 'ExchangePlanRef',
'ExternalDataProcessorObject', 'ExternalReportObject',
'ExternalDataSourceTableObject', 'ExternalDataSourceTableRecordManager',
'ExternalDataSourceTableRef',
'InformationRegisterRecordManager', 'InformationRegisterRecordSet',
'ReportObject', 'TaskObject', 'TaskRef',
}
@@ -1,5 +1,6 @@
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# interface-edit v1.22 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[string]$DefinitionFile,
@@ -17,6 +18,70 @@ $ErrorActionPreference = "Stop"
if ($DefinitionFile -and $Operation) { Write-Error "Cannot use both -DefinitionFile and -Operation"; exit 1 }
if (-not $DefinitionFile -and -not $Operation) { Write-Error "Either -DefinitionFile or -Operation is required"; exit 1 }
# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
try {
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
$parsed = $text | ConvertFrom-Json
} catch {
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
if ($Inline) {
$got = ($text -replace '\s+', ' ').Trim()
$label = 'got'
if (-not $got) { $got = '(empty)' }
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
$what = "${what}, ${label}: ${got}"
}
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
exit 1
}
Write-Output -NoEnumerate $parsed
}
# --- Чтение входного JSON-файла ---
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
# угаданное имя уйдёт в метаданные так же молча.
function Read-JsonInputFile([string]$path) {
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
# проверкой срабатывают раньше и сохраняют свой текст.
if (-not (Test-Path -LiteralPath $path)) {
[Console]::Error.WriteLine("[ERROR] File not found: $path")
exit 1
}
if (Test-Path -LiteralPath $path -PathType Container) {
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
exit 1
}
$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
}
try {
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
} catch {
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
exit 1
}
}
# --- Resolve path ---
if (-not [System.IO.Path]::IsPathRooted($CIPath)) {
$CIPath = Join-Path (Get-Location).Path $CIPath
@@ -351,10 +416,10 @@ function Ensure-Section([string]$sectionName) {
}
# --- Parse value: string or JSON array ---
function Parse-ValueList([string]$val) {
function Parse-ValueList([string]$val, [string]$opName) {
$val = $val.Trim()
if ($val.StartsWith("[")) {
$arr = $val | ConvertFrom-Json
$arr = ConvertFrom-JsonInput $val "-Value for operation '$opName'" "a JSON array of command names" -Inline
$result = @(); foreach ($item in $arr) { $result += "$item" }
return ,$result
}
@@ -519,7 +584,7 @@ function Do-Show([string[]]$commands) {
}
function Do-Place([string]$jsonVal) {
$def = $jsonVal | ConvertFrom-Json
$def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'place'" "a JSON object {command, group}" -Inline
$cmdName = Normalize-CmdName "$($def.command)"
$groupName = "$($def.group)"
if (-not $cmdName -or -not $groupName) { Write-Error "place requires {command, group}"; exit 1 }
@@ -552,7 +617,7 @@ function Do-Place([string]$jsonVal) {
}
function Do-Order([string]$jsonVal) {
$def = $jsonVal | ConvertFrom-Json
$def = ConvertFrom-JsonInput $jsonVal "-Value for operation 'order'" "a JSON object {group, commands:[...]}" -Inline
$groupName = "$($def.group)"
$commands = @($def.commands | ForEach-Object { Normalize-CmdName "$_" })
if (-not $groupName -or $commands.Count -eq 0) { Write-Error "order requires {group, commands:[...]}"; exit 1 }
@@ -590,7 +655,7 @@ function Do-Order([string]$jsonVal) {
}
function Do-SubsystemOrder([string]$jsonVal) {
$parsed = $jsonVal | ConvertFrom-Json
$parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'subsystem-order'" "a JSON array of subsystem paths" -Inline
$subsystems = @(); foreach ($s in $parsed) { $subsystems += "$s" }
if ($subsystems.Count -eq 0) { Write-Error "subsystem-order requires array of subsystem paths"; exit 1 }
@@ -618,7 +683,7 @@ function Do-SubsystemOrder([string]$jsonVal) {
}
function Do-GroupOrder([string]$jsonVal) {
$parsed = $jsonVal | ConvertFrom-Json
$parsed = ConvertFrom-JsonInput $jsonVal "-Value for operation 'group-order'" "a JSON array of group names" -Inline
$groups = @(); foreach ($g in $parsed) { $groups += "$g" }
if ($groups.Count -eq 0) { Write-Error "group-order requires array of group names"; exit 1 }
@@ -651,8 +716,8 @@ if ($DefinitionFile) {
if (-not [System.IO.Path]::IsPathRooted($DefinitionFile)) {
$DefinitionFile = Join-Path (Get-Location).Path $DefinitionFile
}
$jsonText = Get-Content -Raw -Encoding UTF8 $DefinitionFile
$ops = $jsonText | ConvertFrom-Json
$jsonText = Read-JsonInputFile $DefinitionFile
$ops = ConvertFrom-JsonInput $jsonText $DefinitionFile
if ($ops -is [System.Array]) {
foreach ($op in $ops) { $operations += $op }
} else {
@@ -669,8 +734,8 @@ foreach ($op in $operations) {
$opValue = if ($opValueRaw -is [string]) { $opValueRaw } else { $opValueRaw | ConvertTo-Json -Compress }
switch ($opName) {
"hide" { Do-Hide (Parse-ValueList $opValue) }
"show" { Do-Show (Parse-ValueList $opValue) }
"hide" { Do-Hide (Parse-ValueList $opValue $opName) }
"show" { Do-Show (Parse-ValueList $opValue $opName) }
"place" { Do-Place $opValue }
"order" { Do-Order $opValue }
"subsystem-order" { Do-SubsystemOrder $opValue }
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# interface-edit v1.18 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# interface-edit v1.22 — Edit 1C CommandInterface.xml (+русские алиасы типов: формы с ё и без)
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -348,10 +348,71 @@ def import_ci_fragment(xml_string):
return nodes
def parse_value_list(val):
def parse_json_input(text, source, expected=None, inline=False):
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
expected заполняем только для полиморфного входа: у файла подсказка
была бы наполнителем имя файла и текст парсера самодостаточны. inline печатает ещё и то,
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
"""
import json as _pj
import sys as _psys
try:
if not str(text).strip():
raise ValueError("input is empty")
return _pj.loads(text)
except ValueError as exc:
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
if inline:
got = " ".join(str(text).split())
label = "got"
if not got:
got = "(empty)"
elif len(got) > 60:
label = "got (first 60 chars)"
got = got[:60]
what = "%s, %s: %s" % (what, label, got)
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
_psys.exit(1)
def read_json_file(path):
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
BOM объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
"""
import os as _pos
import sys as _psys
if not _pos.path.exists(path):
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
_psys.exit(1)
if _pos.path.isdir(path):
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
_psys.exit(1)
with open(path, "rb") as _fh:
data = _fh.read()
if data[:3] == b"\xef\xbb\xbf":
return data[3:].decode("utf-8")
if data[:2] == b"\xff\xfe":
return data[2:].decode("utf-16-le")
if data[:2] == b"\xfe\xff":
return data[2:].decode("utf-16-be")
try:
return data.decode("utf-8")
except UnicodeDecodeError as exc:
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
% (path, exc), file=_psys.stderr)
_psys.exit(1)
def parse_value_list(val, op_name):
val = val.strip()
if val.startswith("["):
arr = ci_json(json.loads(val))
arr = ci_json(parse_json_input(val, "-Value for operation '%s'" % op_name, "a JSON array of command names", inline=True))
return [str(item) for item in arr]
return [val]
@@ -647,7 +708,8 @@ def main():
def do_place(json_val):
nonlocal add_count, modify_count
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
defn = ci_json(json_val if isinstance(json_val, dict) else parse_json_input(
json_val, "-Value for operation 'place'", "a JSON object {command, group}", inline=True))
cmd_name = normalize_cmd_name(str(defn["command"]))
group_name = str(defn["group"])
if not cmd_name or not group_name:
@@ -675,7 +737,8 @@ def main():
def do_order(json_val):
nonlocal add_count, remove_count
defn = ci_json(json_val if isinstance(json_val, dict) else json.loads(json_val))
defn = ci_json(json_val if isinstance(json_val, dict) else parse_json_input(
json_val, "-Value for operation 'order'", "a JSON object {group, commands:[...]}", inline=True))
group_name = str(defn["group"])
commands = [normalize_cmd_name(str(c)) for c in defn["commands"]]
if not group_name or not commands:
@@ -709,7 +772,8 @@ def main():
def do_subsystem_order(json_val):
nonlocal add_count, remove_count
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
parsed = ci_json(json_val if isinstance(json_val, list) else parse_json_input(
json_val, "-Value for operation 'subsystem-order'", "a JSON array of subsystem paths", inline=True))
subsystems = [str(s) for s in parsed]
if not subsystems:
print("subsystem-order requires array of subsystem paths", file=sys.stderr)
@@ -734,7 +798,8 @@ def main():
def do_group_order(json_val):
nonlocal add_count, remove_count
parsed = ci_json(json_val if isinstance(json_val, list) else json.loads(json_val))
parsed = ci_json(json_val if isinstance(json_val, list) else parse_json_input(
json_val, "-Value for operation 'group-order'", "a JSON array of group names", inline=True))
groups = [str(g) for g in parsed]
if not groups:
print("group-order requires array of group names", file=sys.stderr)
@@ -763,8 +828,7 @@ def main():
def_file = args.DefinitionFile
if not os.path.isabs(def_file):
def_file = os.path.join(os.getcwd(), def_file)
with open(def_file, "r", encoding="utf-8-sig") as fh:
ops = ci_json(json.loads(fh.read()))
ops = ci_json(parse_json_input(read_json_file(def_file), def_file))
if isinstance(ops, list):
operations = ops
else:
@@ -779,9 +843,9 @@ def main():
op_value = op.get("value", args.Value or "")
if op_key == "hide":
do_hide(parse_value_list(op_value))
do_hide(parse_value_list(op_value, op_name))
elif op_key == "show":
do_show(parse_value_list(op_value))
do_show(parse_value_list(op_value, op_name))
elif op_key == "place":
do_place(op_value)
elif op_key == "order":
@@ -1,7 +1,8 @@
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
# interface-validate v1.4 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)][Alias('Path')][string]$CIPath,
[Parameter(Mandatory, Position=0)][Alias('Path')][string]$CIPath,
[switch]$Detailed,
[int]$MaxErrors = 30,
[string]$OutFile
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# interface-validate v1.3 — Validate 1C CommandInterface.xml structure (+Report-*: общий эталон вывода валидаторов)
# interface-validate v1.4 — Validate 1C CommandInterface.xml structure
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Validates CommandInterface.xml sections, command references, order, duplicates."""
import sys, os, argparse, re
+1
View File
@@ -98,6 +98,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-compile.ps1" -
| CommonModule, ScheduledJob, EventSubscription | `reference/code.md` |
| HTTPService, WebService | `reference/web.md` |
| Enum, Constant, DefinedType | `reference/simple.md` |
| ExternalDataSource (внешний источник данных) | `reference/external-data-source.md` |
| FunctionalOption, FilterCriterion, SettingsStorage, CommonForm, CommonPicture, CommonTemplate, служебные | `reference/other-types.md` |
Кросс-типовые детали:
@@ -0,0 +1,147 @@
# ExternalDataSource (внешний источник данных)
Источник описывается **одним JSON целиком**: сам источник, его таблицы с полями и функции.
Результат — `ExternalDataSources/<Имя>.xml` плюс по файлу на таблицу в `<Имя>/Tables/`.
Строку соединения, пользователя, пароль и тип СУБД задавать не нужно: в конфигурации их нет,
они настраиваются в режиме «Предприятие» и хранятся в базе.
```json
{
"type": "ExternalDataSource",
"name": "PG",
"tables": {
"prices": ["product_id: Number(10,0)", "period: Date", "price: Number(15,2)"],
"products": {
"nameInDataSource": "eds.public.products",
"tableDataType": "ObjectData",
"keyFields": ["id"],
"presentationField": "name",
"fields": [
"id: Number(10,0)",
"name: String(150)",
"article: String(50) | nullable",
"parent_id: ExternalDataSourceTableRef.PG.products | nullable"
]
}
},
"functions": {
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }
}
}
```
## Свойства источника
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
| `tables` | `{}` | таблицы (см. ниже) |
| `functions` | `{}` | функции (см. ниже) |
При `AutomaticAndManaged` режим блокировок решает каждая таблица сама; при конкретном значении
одноимённое свойство таблицы игнорируется платформой.
## Таблицы
Ключ — имя таблицы в конфигурации. Значение — **массив полей** либо **объект** со свойствами
и ключом `fields` (та же двойственность, что у `tabularSections` справочника).
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `nameInDataSource` | = имя таблицы | имя физической таблицы; для реляционной СУБД обычно `<база>.<схема>.<таблица>` |
| `tableType` | `Table` | `Table` — таблица или представление; `Expression` — табличная функция |
| `readOnly` | `false` | запрет записи; ставь `true` для представлений и таблиц вида `Expression` — писать в них нельзя |
| `expressionInDataSource` | пусто | выражение для `Expression`, напр. `public.f_by_parent(&1)`; имя базы не указывается |
| `tableDataType` | `NonobjectData` | `ObjectData` (запись определяется одним полем) / `NonobjectData` |
| `keyFields` | `[]` | имена ключевых полей; без них таблица собирается, но недоступны форма записи и набор записей |
| `presentationField` | пусто | имя поля представления (только `ObjectData`) |
| `parentField` | пусто | имя поля родителя; его тип должен быть ссылкой на эту же таблицу |
| `inputByString` | `[]` | имена полей ввода по строке; ключа нет → берётся `presentationField` |
| `dataVersionField` | пусто | имя поля версии данных |
| `dataLockFields` | `[]` | имена полей блокировки |
| `transactionsIsolationLevel` | `Auto` | `Auto` / `ReadUncommitted` / `ReadCommitted` / `RepeatableRead` / `Serializable` |
| `dataLockControlMode` | `Automatic` | `Automatic` / `Managed` / `AutomaticAndManaged` |
| `basedOn` | `[]` | ввод на основании, ссылки вида `Catalog.Контрагенты` |
| `useStandardCommands` | `true` | bool |
| `quickChoice` | `false` | bool |
| `editType` | `InDialog` | `InDialog` / `InList` |
| `fields` | `[]` | поля (см. ниже); синоним — `columns` |
Ссылки на поля (`keyFields`, `presentationField`, `parentField`, `dataVersionField`,
`inputByString`, `dataLockFields`) задаются **короткими именами полей** этой же таблицы.
Прочие свойства — представления (`objectPresentation`, `listPresentation`, …), формы по умолчанию
(`defaultObjectForm`, `defaultListForm`, …), `characteristics`, `explanation`,
`includeHelpInContents` — как у справочника.
**Значение незаполненного родителя загрузкой XML не задаётся** — платформа сбрасывает его в пустое
при любой загрузке, включая загрузку собственной выгрузки. Ставится только в Конфигураторе вручную.
## Поля
Строковая и объектная форма — те же, что у реквизитов (см. `attributes.md`). Своих ключа три:
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `nameInDataSource` | = имя поля | имя колонки; в одинарных кавычках уходит в SQL как есть |
| `readOnly` | `false` | поле не записывается (вычисляемые, автоинкрементные) |
| `allowNull` | `false` | допускает `NULL` |
Флаги строковой формы: `readonly`, `nullable`.
```json
"fields": [
"id: Number(10,0) | readonly",
{ "name": "article", "type": "String(50)", "nameInDataSource": "art_code", "allowNull": true }
]
```
Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу
внешнего источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`.
Двоичные данные: `BinaryData` — безлимит (так их пишет платформа при импорте из СУБД),
`BinaryData(N)` — переменной длины, `BinaryData(N,fixed)` — фиксированной.
**Составной тип у поля недопустим** — платформа такую конфигурацию не загружает.
## Функции
Ключ — имя функции. Значение — строка (интерпретируется как `expression`) либо объект.
| Ключ | Умолчание | Значения |
|------|-----------|----------|
| `expression` | — | выражение в источнике, обязательный |
| `returns` | `String` | тип возвращаемого значения |
| `returnValue` | `true` | `false` — процедура, тип не пишется |
Параметры описываются прямо в выражении как `&1`, `&2` — отдельного ключа для них нет.
Необязательные — в фигурных скобках `f(&1{, &2})`, переменное число — `&n[]` (только последним).
```json
"functions": {
"nextKey": "NEXT VALUE FOR dbo.SimpleSequence",
"total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }
}
```
## Добавить в существующий источник
`meta-compile` описывает источник **целиком**: повторный запуск заменяет его файл и выдаёт новый
uuid, а таблицы, которых нет в описании, останутся на диске сиротами. Чтобы дописать таблицу или
функцию в уже существующий источник, есть `meta-edit`:
```json
{ "add": {
"tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } },
"functions": { "nextKey": "NEXT VALUE FOR public.seq_key" }
} }
```
Удалить таблицу — `meta-remove ExternalDataSource.<Источник>.Table.<Таблица>`.
## Не поддерживается
- **Кубы OLAP** (`Cube`, `DimensionTable`, `Dimension`, `Resource`).
- **Формы и модули** таблиц — форму добавляет навык `form-add`, содержимое собирает `form-compile`.
Набор назначений зависит от `tableDataType`: `ObjectData` — Object/List/Choice, `NonobjectData` — Record/List/Choice.
@@ -1,5 +1,6 @@
# meta-compile v1.94 — Compile 1C metadata object from JSON
# meta-compile v1.109 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[string]$JsonPath,
@@ -9,6 +10,70 @@ param(
)
$ErrorActionPreference = "Stop"
# --- Разбор пользовательского JSON ---
# Одна строка в stderr вместо дампа исключения ConvertFrom-Json (issue #80): агент по стектрейсу
# идёт чинить скрипт, а не свой вызов. $source — файл или параметр. $expected заполняем только
# для полиморфного входа: у файла подсказка была бы наполнителем. -Inline печатает ещё и то,
# что доехало: у файла такого вопроса нет — путь назван, позицию дал парсер, файл на диске.
# Возврат через -NoEnumerate: без него одноэлементный
# JSON-массив разворачивался бы в скаляр вторым анруллингом.
function ConvertFrom-JsonInput([string]$text, [string]$source, [string]$expected, [switch]$Inline) {
try {
# PS 5.1 на пустой строке отдаёт $null, а не ошибку — навык уходил дальше с $null,
# тогда как py-порт падал. Проверяем сами, чтобы порты вели себя одинаково.
if ([string]::IsNullOrWhiteSpace($text)) { throw 'input is empty' }
$parsed = $text | ConvertFrom-Json
} catch {
$what = if ($expected) { "$source expects $expected" } else { "Invalid JSON in $source" }
if ($Inline) {
$got = ($text -replace '\s+', ' ').Trim()
$label = 'got'
if (-not $got) { $got = '(empty)' }
elseif ($got.Length -gt 60) { $label = 'got (first 60 chars)'; $got = $got.Substring(0, 60) }
$what = "${what}, ${label}: ${got}"
}
[Console]::Error.WriteLine("[ERROR] ${what} ($($_.Exception.Message))")
exit 1
}
Write-Output -NoEnumerate $parsed
}
# --- Чтение входного JSON-файла ---
# Кодировку берём из BOM — это объявление самого файла, а не догадка. Без BOM ждём строгий UTF-8:
# Get-Content -Encoding UTF8 на файле в cp1251 тихо меняет кириллицу на U+FFFD, JSON после этого
# разбирается успешно, и в конфигурацию уезжает имя из «замен». Кодовую страницу не подбираем:
# угаданное имя уйдёт в метаданные так же молча.
function Read-JsonInputFile([string]$path) {
# Проверка здесь, а не по навыкам: часть навыков проверяла путь сама, часть — нет, и один и тот
# же промах давал то внятную строку, то дамп MethodInvocationException. Навыки со своей
# проверкой срабатывают раньше и сохраняют свой текст.
if (-not (Test-Path -LiteralPath $path)) {
[Console]::Error.WriteLine("[ERROR] File not found: $path")
exit 1
}
if (Test-Path -LiteralPath $path -PathType Container) {
[Console]::Error.WriteLine("[ERROR] Expected a JSON file, got a directory: $path")
exit 1
}
$bytes = [System.IO.File]::ReadAllBytes($path)
if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
return [System.Text.Encoding]::UTF8.GetString($bytes, 3, $bytes.Length - 3)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
return [System.Text.Encoding]::Unicode.GetString($bytes, 2, $bytes.Length - 2)
}
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFE -and $bytes[1] -eq 0xFF) {
return [System.Text.Encoding]::BigEndianUnicode.GetString($bytes, 2, $bytes.Length - 2)
}
try {
return (New-Object System.Text.UTF8Encoding($false, $true)).GetString($bytes)
} catch {
$detail = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { $_.Exception.Message }
[Console]::Error.WriteLine("[ERROR] ${path} is not valid UTF-8: ${detail} - save the file as UTF-8, or add a BOM if it is UTF-16")
exit 1
}
}
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# --- 1. Load and validate JSON ---
@@ -18,8 +83,8 @@ if (-not (Test-Path $JsonPath)) {
exit 1
}
$json = Get-Content -Raw -Encoding UTF8 $JsonPath
$def = $json | ConvertFrom-Json
$json = Read-JsonInputFile $JsonPath
$def = ConvertFrom-JsonInput $json $JsonPath
# --- Support guard (Ext/ParentConfigurations.bin) ---
# See docs/1c-support-state-spec.md. Blocks edits of vendor objects "на замке" /
@@ -214,6 +279,7 @@ $script:objectTypeSynonyms = @{
"ВебСервис" = "WebService"
"ОпределяемыйТип" = "DefinedType"
"ФункциональнаяОпция" = "FunctionalOption"
"ВнешнийИсточникДанных" = "ExternalDataSource"
}
# Enum property value synonyms — model often gets these slightly wrong
@@ -255,7 +321,9 @@ $script:validEnumValues = @{
"WriteMode" = @("Independent","RecorderSubordinate")
"InformationRegisterPeriodicity" = @("Nonperiodical","Second","Day","Month","Quarter","Year","RecorderPosition")
"DependenceOnCalculationTypes" = @("DontUse","OnActionPeriod")
"DataLockControlMode" = @("Automatic","Managed")
# AutomaticAndManaged — только у внешнего источника данных и его таблиц: там режим может
# решаться на уровне таблицы, у прочих объектов такого значения нет.
"DataLockControlMode" = @("Automatic","Managed","AutomaticAndManaged")
"FullTextSearch" = @("Use","DontUse")
"DataHistory" = @("Use","DontUse")
"DefaultPresentation" = @("AsDescription","AsCode")
@@ -407,7 +475,7 @@ $validTypes = @("Catalog","Document","Enum","Constant","InformationRegister","Ac
"HTTPService","WebService","DefinedType","FunctionalOption",
"Sequence","FilterCriterion","DocumentNumerator","SettingsStorage","CommonForm",
"SessionParameter","CommonCommand","CommandGroup","CommonAttribute","FunctionalOptionsParameter","WSReference",
"CommonPicture","CommonTemplate")
"CommonPicture","CommonTemplate","ExternalDataSource")
# -notin регистронезависим, поэтому "catalog" проходил проверку и дальше шёл в ИМЯ ТЕГА и в
# Configuration.xml как есть — выгрузка получалась с <catalog>, которую платформа не принимает.
# Прощаем регистр, но приводим к канону списка.
@@ -536,6 +604,10 @@ $script:typeSynonyms["bool"] = "Boolean"
# ValueStorage / UUID — прощающий ввод (модель может написать base64Binary / рус. форму → канон).
$script:typeSynonyms["valuestorage"] = "ValueStorage"
$script:typeSynonyms["base64binary"] = "ValueStorage"
# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
$script:typeSynonyms["binarydata"] = "BinaryData"
$script:typeSynonyms["двоичныеданные"] = "BinaryData"
$script:typeSynonyms["хранилищезначений"] = "ValueStorage"
$script:typeSynonyms["хранилищезначения"] = "ValueStorage"
$script:typeSynonyms["uuid"] = "UUID"
@@ -573,6 +645,7 @@ $script:typeSynonyms["планвидовхарактеристикссылка"]
$script:typeSynonyms["планвидоврасчётассылка"] = "ChartOfCalculationTypesRef"
$script:typeSynonyms["планвидоврасчетассылка"] = "ChartOfCalculationTypesRef"
$script:typeSynonyms["планобменассылка"] = "ExchangePlanRef"
$script:typeSynonyms["внешнийисточникданныхтаблицассылка"] = "ExternalDataSourceTableRef"
$script:typeSynonyms["бизнеспроцессссылка"] = "BusinessProcessRef"
$script:typeSynonyms["задачассылка"] = "TaskRef"
$script:typeSynonyms["определяемыйтип"] = "DefinedType"
@@ -738,6 +811,25 @@ function Emit-TypeContent {
}
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба).
# ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
if ($typeStr -match '^BinaryData(\(|$)') {
# BinaryData — безлимит (так платформа пишет поле внешнего источника: 4294967292/Fixed).
# BinaryData(N) — переменной длины, BinaryData(N,fixed) — фиксированной.
$bm = [regex]::Match($typeStr, '^BinaryData(\((\d+)(,\s*(fixed|variable))?\))?$', 'IgnoreCase')
if (-not $bm.Success) {
Write-Error "Неверный тип '$typeStr': ждётся BinaryData, BinaryData(Длина) или BinaryData(Длина,fixed|variable)."
exit 1
}
$blen = if ($bm.Groups[2].Success) { $bm.Groups[2].Value } else { "4294967292" }
$ballowed = if ($bm.Groups[4].Success) { if ($bm.Groups[4].Value.ToLowerInvariant() -eq "fixed") { "Fixed" } else { "Variable" } }
elseif ($bm.Groups[2].Success) { "Variable" } else { "Fixed" }
X "$indent<v8:Type>xs:base64Binary</v8:Type>"
X "$indent<v8:BinaryDataQualifiers>"
X "$indent`t<v8:Length>$blen</v8:Length>"
X "$indent`t<v8:AllowedLength>$ballowed</v8:AllowedLength>"
X "$indent</v8:BinaryDataQualifiers>"
return
}
if ($typeStr -eq "ValueStorage") {
X "$indent<v8:Type>v8:ValueStorage</v8:Type>"
return
@@ -791,7 +883,9 @@ function Emit-TypeContent {
# $script:cfgPrefix = $null означает «пишем файл, корень которого cfg НЕ объявляет»
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$') {
# ExternalDataSourceTableRef — единственный ссылочный тип с ДВУМЯ частями после префикса
# (Источник.Таблица), поэтому `(.+)$` здесь существенно.
if ($typeStr -match '^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef|ExternalDataSourceTableRef)\.(.+)$') {
if ($script:cfgPrefix) {
X "$indent<v8:Type>$($script:cfgPrefix):$typeStr</v8:Type>"
} else {
@@ -1015,8 +1109,10 @@ function Emit-FillValue {
# --- 5. Attribute shorthand parser ---
function Build-TypeStr {
param($obj)
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif ($obj.type) { "$($obj.type)" } else { "" }
# ValueTypeOnly — для корневого определения объекта: там ключ type означает ВИД объекта
# (Constant, Catalog, …), а не тип значения, и подхватывать его нельзя.
param($obj, [switch]$ValueTypeOnly)
$t = if ($obj.valueType) { "$($obj.valueType)" } elseif (-not $ValueTypeOnly -and $obj.type) { "$($obj.type)" } else { "" }
if ($t -and -not $t.Contains('(')) {
if ($t -eq "String" -and $obj.length) {
$t = "String($($obj.length))"
@@ -1119,6 +1215,10 @@ function Parse-AttributeShorthand {
# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
typeReductionMode = $val.typeReductionMode
# Поле внешнего источника данных (контекст eds-field).
nameInDataSource = if ($val.nameInDataSource) { "$($val.nameInDataSource)" } else { "" }
readOnly = if ($val.readOnly -eq $true) { $true } else { $false }
allowNull = if ($val.allowNull -eq $true) { $true } else { $false }
}
}
@@ -1259,6 +1359,11 @@ $script:generatedTypes = @{
"DefinedType" = @(
@{ prefix = "DefinedType"; category = "DefinedType" }
)
"ExternalDataSource" = @(
@{ prefix = "ExternalDataSourceManager"; category = "Manager" }
@{ prefix = "ExternalDataSourceTablesManager"; category = "TablesManager" }
@{ prefix = "ExternalDataSourceCubesManager"; category = "CubesManager" }
)
"DocumentJournal" = @(
@{ prefix = "DocumentJournalSelection"; category = "Selection" }
@{ prefix = "DocumentJournalList"; category = "List" }
@@ -1984,7 +2089,7 @@ function Emit-Attribute {
exit 1
}
}
} elseif ($context -notin @("tabular", "processor-tabular") -and
} elseif ($context -notin @("tabular", "processor-tabular", "eds-field") -and
($script:reservedAttrNames.ContainsKey($attrName) -or $script:reservedAttrNames.ContainsValue($attrName))) {
Write-Warning "Attribute '$attrName' conflicts with a standard attribute name. This may cause errors when loading into 1C."
}
@@ -2048,18 +2153,34 @@ function Emit-Attribute {
if ($parsed.fillChecking) { $fillChecking = $parsed.fillChecking }
X "$indent`t`t<FillChecking>$fillChecking</FillChecking>"
X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
# у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы.
if ($context -ne "eds-field") {
X "$indent`t`t<ChoiceFoldersAndItems>$(if ($parsed.choiceFoldersAndItems) { "$($parsed.choiceFoldersAndItems)" } else { 'Items' })</ChoiceFoldersAndItems>"
}
Emit-ChoiceParameterLinks "$indent`t`t" $parsed.choiceParameterLinks
Emit-ChoiceParameters "$indent`t`t" $parsed.choiceParameters
$qc = if ($parsed.quickChoice) { $parsed.quickChoice } else { "Auto" }
X "$indent`t`t<QuickChoice>$qc</QuickChoice>"
$coi = if ($parsed.createOnInput) { $parsed.createOnInput } else { "Auto" }
X "$indent`t`t<CreateOnInput>$coi</CreateOnInput>"
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
Emit-LinkByType "$indent`t`t" $parsed.linkByType
if ($context -ne "eds-field") {
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
Emit-LinkByType "$indent`t`t" $parsed.linkByType
}
$chi = if ($parsed.choiceHistoryOnInput) { $parsed.choiceHistoryOnInput } else { "Auto" }
X "$indent`t`t<ChoiceHistoryOnInput>$chi</ChoiceHistoryOnInput>"
if ($context -eq "eds-field") {
if ($parsed.choiceForm) { X "$indent`t`t<ChoiceForm>$(Esc-XmlText "$($parsed.choiceForm)")</ChoiceForm>" } else { X "$indent`t`t<ChoiceForm/>" }
$nids = if ($parsed.nameInDataSource) { "$($parsed.nameInDataSource)" } else { $parsed.name }
X "$indent`t`t<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>"
$ro = if ($parsed.readOnly -eq $true -or $parsed.flags -contains "readonly") { "true" } else { "false" }
X "$indent`t`t<ReadOnly>$ro</ReadOnly>"
$an = if ($parsed.allowNull -eq $true -or $parsed.flags -contains "nullable") { "true" } else { "false" }
X "$indent`t`t<AllowNull>$an</AllowNull>"
}
# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
if ($elemTag -eq "Dimension" -and $context -eq "register-info") {
$master = if ($parsed.master -eq $true -or $parsed.flags -contains "master") { "true" } else { "false" }
@@ -2113,7 +2234,8 @@ function Emit-Attribute {
}
# Indexing/FullTextSearch/DataHistory — not for non-stored objects (processor, processor-tabular)
if ($context -notin @("processor", "processor-tabular")) {
# и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет.
if ($context -notin @("processor", "processor-tabular", "eds-field")) {
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
if ($context -ne "account-flag") {
# Ресурс регистра накопления/расчёта/бухгалтерии НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
@@ -2706,8 +2828,8 @@ function Emit-ConstantProperties {
if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
# Type — valueType (пустой явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
$valueType = Build-TypeStr $def
$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '') -or ($null -ne $def.type -and "$($def.type)".Trim() -eq '')
$valueType = Build-TypeStr $def -ValueTypeOnly
$typeEmpty = ($null -ne $def.valueType -and "$($def.valueType)".Trim() -eq '')
if ($typeEmpty) { X "$i<Type/>" }
else { if (-not $valueType) { $valueType = "String" }; Emit-ValueType $i $valueType }
@@ -4249,6 +4371,222 @@ function Emit-AddressingAttribute {
Emit-Attribute $indent $parsed "task-addressing" "AddressingAttribute"
}
# --- 13h. Внешние источники данных ---
# Источник пишется в один файл, каждая его таблица — в свой. Функции живут ВНУТРИ файла источника
# полными узлами, наравне со списком имён таблиц: так их выгружает платформа.
# Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns.
# Нормализуем в [ordered]@{ имя = @{ props; fields } } — форма зеркальна tabularSections.
function Get-EdsTables {
param($val)
$tables = [ordered]@{}
if (-not $val) { return $tables }
function New-EdsTableEntry { param($v)
if ($v -is [array] -or $v.GetType().Name -eq 'Object[]') {
return @{ props = $null; fields = @($v) }
}
$f = if ($null -ne $v.fields) { @($v.fields) } elseif ($null -ne $v.columns) { @($v.columns) } else { @() }
return @{ props = $v; fields = $f }
}
if ($val -is [array] -or $val.GetType().Name -eq 'Object[]') {
foreach ($t in $val) { $tables["$($t.name)"] = New-EdsTableEntry $t }
} else {
$val.PSObject.Properties | ForEach-Object { $tables[$_.Name] = New-EdsTableEntry $_.Value }
}
return $tables
}
# Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь.
function Get-EdsFieldRef {
param([string]$srcName, [string]$tableName, [string]$fieldName)
if (-not $fieldName) { return "" }
if ($fieldName -like "ExternalDataSource.*") { return $fieldName }
return "ExternalDataSource.$srcName.Table.$tableName.Field.$fieldName"
}
function Emit-EdsFieldRefList {
param([string]$indent, [string]$tag, $names, [string]$srcName, [string]$tableName)
$list = @($names | Where-Object { $_ })
if ($list.Count -eq 0) { X "$indent<$tag/>"; return }
X "$indent<$tag>"
foreach ($n in $list) {
X "$indent`t<xr:Field>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$n"))</xr:Field>"
}
X "$indent</$tag>"
}
function Emit-EdsFieldRefScalar {
param([string]$indent, [string]$tag, $name, [string]$srcName, [string]$tableName)
if (-not $name) { X "$indent<$tag/>"; return }
X "$indent<$tag>$(Esc-XmlText (Get-EdsFieldRef $srcName $tableName "$name"))</$tag>"
}
function Emit-ExternalDataSourceProperties {
param([string]$indent)
$i = $indent
X "$i<Name>$(Esc-XmlText $objName)</Name>"
Emit-MLText $i "Synonym" $synonym
if ($def.comment) { X "$i<Comment>$(Esc-XmlText $def.comment)</Comment>" } else { X "$i<Comment/>" }
$dlcm = Get-EnumProp "DataLockControlMode" "dataLockControlMode" "Automatic"
X "$i<DataLockControlMode>$dlcm</DataLockControlMode>"
}
# Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
# в выражении как &1, &2 (см. reference/external-data-source.md).
function Emit-EdsFunction {
# $typeXml — уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
# своим эмиттером типов. Так тело функции не зависит от того, какой это навык.
param([string]$indent, [string]$fnName, $val, [string]$typeXml)
$expr = ""
$returns = ""
$returnValue = $true
$fnSynonym = $null
$fnComment = ""
if ($val -is [string]) {
$expr = "$val"
} else {
$expr = if ($val.expression) { "$($val.expression)" } elseif ($val.expressionInDataSource) { "$($val.expressionInDataSource)" } else { "" }
$returns = if ($val.returns) { "$($val.returns)" } elseif ($val.returnType) { "$($val.returnType)" } else { "" }
if ($null -ne $val.returnValue) { $returnValue = ($val.returnValue -eq $true) }
$fnSynonym = $val.synonym
$fnComment = if ($val.comment) { "$($val.comment)" } else { "" }
}
if (-not $expr) {
Write-Error "Функция '$fnName' внешнего источника данных: не задано выражение (ключ expression)."
exit 1
}
$uuid = New-Guid-String
X "$indent<Function uuid=`"$uuid`">"
X "$indent`t<Properties>"
X "$indent`t`t<Name>$(Esc-XmlText $fnName)</Name>"
Emit-MLText "$indent`t`t" "Synonym" $fnSynonym
if ($fnComment) { X "$indent`t`t<Comment>$(Esc-XmlText $fnComment)</Comment>" } else { X "$indent`t`t<Comment/>" }
X "$indent`t`t<ReturnValue>$(if ($returnValue) { 'true' } else { 'false' })</ReturnValue>"
if ($returnValue -and $typeXml) {
X $typeXml.TrimEnd("`r", "`n")
} else {
X "$indent`t`t<Type/>"
}
X "$indent`t`t<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>"
X "$indent`t</Properties>"
X "$indent</Function>"
}
# Свойства таблицы: 38 узлов в порядке выгрузки платформы.
function Emit-EdsTableProperties {
# $charXml и $defaultFormsXml — уже собранные блоки <Characteristics> и четыре слота
# <Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
# от хелперов конкретного навыка и годится для копирования (check-inline-drift).
param([string]$indent, [string]$srcName, [string]$tableName, $t, [string]$charXml, [string]$defaultFormsXml)
$i = $indent
$tblSynonym = if ($t -and $null -ne $t.synonym) { $t.synonym } else { Split-CamelCase $tableName }
X "$i<Name>$(Esc-XmlText $tableName)</Name>"
Emit-MLText $i "Synonym" $tblSynonym
if ($t -and $t.comment) { X "$i<Comment>$(Esc-XmlText "$($t.comment)")</Comment>" } else { X "$i<Comment/>" }
$tableType = if ($t -and $t.tableType) { "$($t.tableType)" } else { "Table" }
X "$i<TableType>$tableType</TableType>"
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
$nids = if ($t -and $t.nameInDataSource) { "$($t.nameInDataSource)" } elseif ($tableType -eq "Expression") { "" } else { $tableName }
if ($nids) { X "$i<NameInDataSource>$(Esc-XmlText $nids)</NameInDataSource>" } else { X "$i<NameInDataSource/>" }
$expr = if ($t -and $t.expressionInDataSource) { "$($t.expressionInDataSource)" } elseif ($t -and $t.expression) { "$($t.expression)" } else { "" }
if ($expr) { X "$i<ExpressionInDataSource>$(Esc-XmlText $expr)</ExpressionInDataSource>" } else { X "$i<ExpressionInDataSource/>" }
$dataType = if ($t -and $t.tableDataType) { "$($t.tableDataType)" } else { "NonobjectData" }
X "$i<TableDataType>$dataType</TableDataType>"
Emit-EdsFieldRefList $i "KeyFields" $(if ($t) { $t.keyFields } else { $null }) $srcName $tableName
Emit-EdsFieldRefScalar $i "PresentationField" $(if ($t) { $t.presentationField } else { $null }) $srcName $tableName
Emit-EdsFieldRefScalar $i "ParentField" $(if ($t) { $t.parentField } else { $null }) $srcName $tableName
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
# различаются формой самого значения (xsi:nil против типизированного).
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
if ($t -and $t.parentField) { X "$i<UnfilledParentValue xsi:type=`"xs:string`"/>" }
else { X "$i<UnfilledParentValue xsi:nil=`"true`"/>" }
if ($charXml) { X $charXml.TrimEnd("`r", "`n") } else { X "$i<Characteristics/>" }
X "$i<UseStandardCommands>$(if ($t -and $t.useStandardCommands -eq $false) { 'false' } else { 'true' })</UseStandardCommands>"
X "$i<QuickChoice>$(if ($t -and $t.quickChoice -eq $true) { 'true' } else { 'false' })</QuickChoice>"
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
$ibsGiven = ($t -and $t.PSObject -and $t.PSObject.Properties -and ($t.PSObject.Properties.Name -contains 'inputByString'))
$ibs = if ($ibsGiven) { $t.inputByString } elseif ($t -and $t.presentationField) { @($t.presentationField) } else { $null }
Emit-EdsFieldRefList $i "InputByString" $ibs $srcName $tableName
X "$i<CreateOnInput>$(if ($t -and $t.createOnInput) { "$($t.createOnInput)" } else { 'Auto' })</CreateOnInput>"
X "$i<SearchStringModeOnInputByString>$(if ($t -and $t.searchStringModeOnInputByString) { "$($t.searchStringModeOnInputByString)" } else { 'Begin' })</SearchStringModeOnInputByString>"
X "$i<ChoiceDataGetModeOnInputByString>$(if ($t -and $t.choiceDataGetModeOnInputByString) { "$($t.choiceDataGetModeOnInputByString)" } else { 'Directly' })</ChoiceDataGetModeOnInputByString>"
X "$i<ChoiceHistoryOnInput>$(if ($t -and $t.choiceHistoryOnInput) { "$($t.choiceHistoryOnInput)" } else { 'Auto' })</ChoiceHistoryOnInput>"
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
if ($defaultFormsXml) { X $defaultFormsXml.TrimEnd("`r", "`n") }
else { foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) { X "$i<$formTag/>" } }
foreach ($presTag in @("ObjectPresentation","ExtendedObjectPresentation","RecordPresentation",
"ExtendedRecordPresentation","ListPresentation","ExtendedListPresentation","Explanation")) {
$key = $presTag.Substring(0,1).ToLower() + $presTag.Substring(1)
Emit-MLText $i $presTag $(if ($t) { $t.$key } else { $null })
}
X "$i<IncludeHelpInContents>$(if ($t -and $t.includeHelpInContents -eq $true) { 'true' } else { 'false' })</IncludeHelpInContents>"
X "$i<ReadOnly>$(if ($t -and $t.readOnly -eq $true) { 'true' } else { 'false' })</ReadOnly>"
X "$i<TransactionsIsolationLevel>$(if ($t -and $t.transactionsIsolationLevel) { "$($t.transactionsIsolationLevel)" } else { 'Auto' })</TransactionsIsolationLevel>"
Emit-EdsFieldRefScalar $i "DataVersionField" $(if ($t) { $t.dataVersionField } else { $null }) $srcName $tableName
X "$i<EditType>$(if ($t -and $t.editType) { "$($t.editType)" } else { 'InDialog' })</EditType>"
Emit-MDRefList $i "BasedOn" $(if ($t) { $t.basedOn } else { $null })
Emit-EdsFieldRefList $i "DataLockFields" $(if ($t) { $t.dataLockFields } else { $null }) $srcName $tableName
X "$i<DataLockControlMode>$(if ($t -and $t.dataLockControlMode) { "$($t.dataLockControlMode)" } else { 'Automatic' })</DataLockControlMode>"
}
# Отдельный XML-документ таблицы. Возвращает строку: X пишет в общий StringBuilder,
# поэтому «перехват» — запомнить длину, отдать эмиттерам, вырезать добавленное
# (тот же приём, что у составного типа).
function Build-EdsTableXml {
# $fieldsXml, $charXml, $defaultFormsXml — уже собранные узлы: их рендерит вызывающий навык
# своими эмиттерами. Так тело функции не зависит от того, какой это навык.
param([string]$srcName, [string]$tableName, $entry, [string]$fieldsXml, [string]$charXml, [string]$defaultFormsXml)
$before = $script:xml.Length
$tableUuid = New-Guid-String
X '<?xml version="1.0" encoding="UTF-8"?>'
X "<MetaDataObject $($script:xmlnsDecl) version=`"$($script:formatVersion)`">"
X "`t<Table uuid=`"$tableUuid`">"
# InternalInfo у таблицы эмитится здесь, а не через $script:generatedTypes: имя элемента
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
X "`t`t<InternalInfo>"
foreach ($pair in @(
@("ExternalDataSourceTableManager", "Manager"),
@("ExternalDataSourceTableObject", "Object"),
@("ExternalDataSourceTableRef", "Ref"),
@("ExternalDataSourceTableList", "List"),
@("ExternalDataSourceTableRecord", "Record"),
@("ExternalDataSourceTableRecordSet", "RecordSet"),
@("ExternalDataSourceTableRecordKey", "RecordKey"),
@("ExternalDataSourceTableRecordManager", "RecordManager"))) {
X "`t`t`t<xr:GeneratedType name=`"$($pair[0]).$srcName.$tableName`" category=`"$($pair[1])`">"
X "`t`t`t`t<xr:TypeId>$(New-Guid-String)</xr:TypeId>"
X "`t`t`t`t<xr:ValueId>$(New-Guid-String)</xr:ValueId>"
X "`t`t`t</xr:GeneratedType>"
}
X "`t`t</InternalInfo>"
X "`t`t<Properties>"
Emit-EdsTableProperties "`t`t`t" $srcName $tableName $entry.props $charXml $defaultFormsXml
X "`t`t</Properties>"
if ($fieldsXml) {
X "`t`t<ChildObjects>"
X $fieldsXml.TrimEnd("`r", "`n")
X "`t`t</ChildObjects>"
} else {
X "`t`t<ChildObjects/>"
}
X "`t</Table>"
X "</MetaDataObject>"
$chunk = $script:xml.ToString($before, $script:xml.Length - $before)
[void]$script:xml.Remove($before, $script:xml.Length - $before)
return $chunk
}
# --- 14. Namespaces ---
$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"'
@@ -4392,6 +4730,7 @@ switch ($objType) {
"Task" { Emit-TaskProperties "`t`t`t" }
"HTTPService" { Emit-HTTPServiceProperties "`t`t`t" }
"WebService" { Emit-WebServiceProperties "`t`t`t" }
"ExternalDataSource" { Emit-ExternalDataSourceProperties "`t`t`t" }
}
X "`t`t</Properties>"
@@ -4689,6 +5028,41 @@ if ($objType -eq "WebService") {
}
}
# --- ExternalDataSource: Tables (именами) + Functions (полными узлами) ---
$script:edsTables = [ordered]@{}
if ($objType -eq "ExternalDataSource") {
$script:edsTables = Get-EdsTables $def.tables
$functions = [ordered]@{}
if ($def.functions) {
$def.functions.PSObject.Properties | ForEach-Object { $functions[$_.Name] = $_.Value }
}
if ($script:edsTables.Count -gt 0 -or $functions.Count -gt 0) {
$hasChildren = $true
X "`t`t<ChildObjects>"
foreach ($tblName in $script:edsTables.Keys) {
X "`t`t`t<Table>$(Esc-XmlText $tblName)</Table>"
}
foreach ($fnName in $functions.Keys) {
$fnVal = $functions[$fnName]
$fnReturns = if ($fnVal -is [string]) { "String" }
elseif ($fnVal.returns) { "$($fnVal.returns)" }
elseif ($fnVal.returnType) { "$($fnVal.returnType)" } else { "String" }
$fnNoValue = (-not ($fnVal -is [string])) -and ($null -ne $fnVal.returnValue) -and ($fnVal.returnValue -ne $true)
$fnTypeXml = ""
if (-not $fnNoValue) {
$typeBefore = $script:xml.Length
Emit-ValueType "`t`t`t`t`t" $fnReturns
$fnTypeXml = $script:xml.ToString($typeBefore, $script:xml.Length - $typeBefore)
[void]$script:xml.Remove($typeBefore, $script:xml.Length - $typeBefore)
}
Emit-EdsFunction "`t`t`t" $fnName $fnVal $fnTypeXml
}
X "`t`t</ChildObjects>"
} else {
X "`t`t<ChildObjects/>"
}
}
# --- CommonModule: no ChildObjects ---
X "`t</$objType>"
@@ -4737,6 +5111,7 @@ $script:typePluralMap = @{
"WSReference" = "WSReferences"
"CommonPicture" = "CommonPictures"
"CommonTemplate" = "CommonTemplates"
"ExternalDataSource" = "ExternalDataSources"
}
$typePlural = $script:typePluralMap[$objType]
@@ -4969,8 +5344,54 @@ function Write-XmlFileKeepEol([string]$path, [string]$text, $encoding) {
[System.IO.File]::WriteAllText($path, $text.TrimEnd("`r", "`n"), $encoding)
}
# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid —
# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими.
# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя.
if (Test-Path $mainXmlPath) {
Write-Warning "$objType '$objName' уже существует ($typePlural/$objName.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit."
}
Write-XmlFileKeepEol $mainXmlPath $metadataXml $enc
# Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/.
# Единственный вид, у которого объект складывается более чем из одного XML.
$edsTablesCreated = @()
if ($objType -eq "ExternalDataSource" -and $script:edsTables.Count -gt 0) {
$tablesDir = Join-Path $objSubDir "Tables"
if (-not (Test-Path $tablesDir)) { New-Item -ItemType Directory -Path $tablesDir -Force | Out-Null }
foreach ($tblName in $script:edsTables.Keys) {
$entry = $script:edsTables[$tblName]
$fieldsBefore = $script:xml.Length
foreach ($f in @($entry.fields)) {
Emit-Attribute "`t`t`t" (Parse-AttributeShorthand $f) "eds-field" "Field"
}
$fieldsXml = $script:xml.ToString($fieldsBefore, $script:xml.Length - $fieldsBefore)
[void]$script:xml.Remove($fieldsBefore, $script:xml.Length - $fieldsBefore)
$tp = $entry.props
$charBefore = $script:xml.Length
Emit-Characteristics "`t`t`t" $(if ($tp) { $tp.characteristics } else { $null })
$charXml = $script:xml.ToString($charBefore, $script:xml.Length - $charBefore)
[void]$script:xml.Remove($charBefore, $script:xml.Length - $charBefore)
# Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника —
# голое имя платформа отвергает («Неизвестный объект метаданных»).
$formsBefore = $script:xml.Length
foreach ($formTag in @("DefaultObjectForm","DefaultRecordForm","DefaultListForm","DefaultChoiceForm")) {
$key = $formTag.Substring(0,1).ToLower() + $formTag.Substring(1)
$formVal = if ($tp) { $tp.$key } else { $null }
if ($formVal -and "$formVal" -notmatch '\.') { $formVal = "ExternalDataSource.$objName.Table.$tblName.Form.$formVal" }
Emit-FormRef "`t`t`t" $formTag $formVal
}
$defaultFormsXml = $script:xml.ToString($formsBefore, $script:xml.Length - $formsBefore)
[void]$script:xml.Remove($formsBefore, $script:xml.Length - $formsBefore)
$tableXml = Build-EdsTableXml $objName $tblName $entry $fieldsXml $charXml $defaultFormsXml
$tablePath = Join-Path $tablesDir "$tblName.xml"
Write-XmlFileKeepEol $tablePath $tableXml $enc
$edsTablesCreated += $tablePath
}
}
# Module files
$modulesCreated = @()
@@ -5169,91 +5590,195 @@ if ($commands -and $commands.Count -gt 0) {
# --- 17. Register in Configuration.xml ---
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
$regResult = $null
# Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
# databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
# иначе корневое поле, иначе end. Значения: end — после последнего объекта того же вида
# (так дописывает Конфигуратор); byName — по имени среди объектов того же вида.
# Файл ищем от рабочего каталога вверх, каталог конфигурации — запасной путь: так же
# его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
# остаётся рабочим каталогом проекта.
# configSrc считается от каталога .v8-project.json, как задокументировано в
# docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Get-NewObjectPosition([string]$cfgDir) {
try {
if (-not $cfgDir) { $cfgDir = "." }
$pj = Find-V8Project (Get-Location).Path
if (-not $pj) { $pj = Find-V8Project ([System.IO.Path]::GetFullPath($cfgDir)) }
if (-not $pj) { return "end" }
$proj = Get-Content -Raw $pj | ConvertFrom-Json
$projDir = [System.IO.Path]::GetDirectoryName($pj)
$cfgFull = [System.IO.Path]::GetFullPath($cfgDir).TrimEnd('\', '/')
if ($proj.databases) {
foreach ($db in $proj.databases) {
if ($db.configSrc -and $db.newObjectPosition) {
$src = [System.IO.Path]::GetFullPath([System.IO.Path]::Combine($projDir, $db.configSrc)).TrimEnd('\', '/')
if ($cfgFull -eq $src -or $cfgFull.StartsWith($src + [System.IO.Path]::DirectorySeparatorChar)) {
if ("$($db.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
}
}
}
}
if ("$($proj.newObjectPosition)" -eq "byName") { return "byName" }
return "end"
} catch { return "end" }
}
# Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
# CommonAttribute — исключение самого стандарта (#std467): у общих реквизитов-разделителей
# порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
# пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
# порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
# (в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
# без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
# Явно названный вид сортируется в любом случае.
# Реестр семьи: tests/skills/check-inline-drift.mjs.
function Test-OrderSensitiveType([string]$typeName) {
return @("CommonAttribute", "Subsystem", "CommandGroup", "Language") -ccontains $typeName
}
# Порядок имён объектов метаданных, как в дереве Конфигуратора.
# Ключ — пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
# букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
# используются — они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
# одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
# Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
function Compare-MetadataNames([string]$a, [string]$b) {
$keys = @("", "")
$names = @($a, $b)
for ($i = 0; $i -lt 2; $i++) {
$sb = New-Object System.Text.StringBuilder
foreach ($ch in $names[$i].ToLowerInvariant().ToCharArray()) {
if ($ch -eq [char]0x0451) { $ch = [char]0x0435 }
if ([char]::IsDigit($ch)) { [void]$sb.Append('1') }
elseif ([char]::IsLetter($ch)) { [void]$sb.Append('2') }
else { [void]$sb.Append('0') }
[void]$sb.Append($ch)
}
$keys[$i] = $sb.ToString()
}
$r = [string]::CompareOrdinal($keys[0], $keys[1])
if ($r -eq 0) { $r = [string]::CompareOrdinal($a, $b) }
if ($r -lt 0) { return -1 }
if ($r -gt 0) { return 1 }
return 0
}
# Регистрация объекта в <ChildObjects> родительского XML. Общая реализация: эталон —
# meta-compile, копии — role-compile, xdto-compile. Реестр семьи: tests/skills/check-inline-drift.mjs.
# Возвращает исход: added | already | no-childobj | no-config.
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
# Реестр карт: tests/skills/check-type-maps.mjs.
$childObjectTypes = @(
"Language","Subsystem","StyleItem","Style",
"CommonPicture","SessionParameter","Role","CommonTemplate",
"FilterCriterion","CommonModule","CommonAttribute","ExchangePlan",
"XDTOPackage","WebService","HTTPService","WSReference",
"EventSubscription","ScheduledJob","SettingsStorage","FunctionalOption",
"FunctionalOptionsParameter","DefinedType","Bot","PaletteColor","CommonCommand","CommandGroup",
"Constant","CommonForm","Catalog","Document",
"DocumentNumerator","Sequence","DocumentJournal","Enum",
"Report","DataProcessor","InformationRegister","AccumulationRegister",
"ChartOfCharacteristicTypes","ChartOfAccounts","AccountingRegister",
"ChartOfCalculationTypes","CalculationRegister",
"BusinessProcess","Task","ExternalDataSource","IntegrationService"
)
function Register-InChildObjects([string]$ParentXmlPath, [string]$ParentTag, [string]$ChildTag, [string]$ChildName) {
if (-not (Test-Path $ParentXmlPath)) { return "no-config" }
$doc = New-Object System.Xml.XmlDocument
$doc.PreserveWhitespace = $true
$doc.Load($ParentXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($doc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $doc.SelectSingleNode("//md:$ParentTag/md:ChildObjects", $nsMgr)
if (-not $childObjects) { return "no-childobj" }
$existing = $childObjects.SelectNodes("md:$ChildTag", $nsMgr)
foreach ($e in $existing) {
if ($e.InnerText -eq $ChildName) { return "already" }
}
# Правка по сырому тексту, зеркально py-порту: сериализация DOM переписала бы файл целиком
# (регистр encoding, `<a />` вместо `<a/>`), а текстовая вставка хранит его байт-в-байт —
# дельта ровно в одну строку. Правим чужой файл, значит наследуем его стиль (#44/#46/#47).
# DOM выше — только на чтение: найти ChildObjects и отсечь дубликат.
$configContent = [System.IO.File]::ReadAllText($ParentXmlPath, (New-Object System.Text.UTF8Encoding($false)))
$eol = if ($configContent.Contains("`r`n")) { "`r`n" } else { "`n" }
$entry = "<$ChildTag>$(Esc-XmlText $ChildName)</$ChildTag>"
$enc = New-Object System.Text.UTF8Encoding($true)
$block = [regex]::Match($configContent, '(?s)<ChildObjects\s*>.*?</ChildObjects>')
if (-not $block.Success) {
# Самозакрытый <ChildObjects/> раскрываем первой записью
$empty = [regex]::Match($configContent, '<ChildObjects\s*/>')
if (-not $empty.Success) { return "no-childobj" }
$replacement = "<ChildObjects>$eol`t`t`t$entry$eol`t`t</ChildObjects>"
$newContent = $configContent.Substring(0, $empty.Index) + $replacement + $configContent.Substring($empty.Index + $empty.Length)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
# byName: перед первым объектом того же вида, чьё имя больше нового.
# Виды с осмысленным порядком в дереве пропускаем — см. Test-OrderSensitiveType.
if (-not (Test-OrderSensitiveType $ChildTag) -and (Get-NewObjectPosition ([System.IO.Path]::GetDirectoryName([System.IO.Path]::GetFullPath($ParentXmlPath)))) -eq "byName") {
$lineRx = [regex]"(?m)^([ \t]*)<$ChildTag>([^<]*)</$ChildTag>"
$m = $lineRx.Match($configContent, $block.Index, $block.Length)
while ($m.Success) {
if ((Compare-MetadataNames $m.Groups[2].Value $ChildName) -gt 0) {
$newContent = $configContent.Substring(0, $m.Index) + $m.Groups[1].Value + $entry + $eol + $configContent.Substring($m.Index)
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
$m = $m.NextMatch()
}
}
$closeSame = "</$ChildTag>"
$blockEnd = $block.Index + $block.Length
$lastSame = $configContent.LastIndexOf($closeSame, $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
if ($lastSame -ge 0) {
# После последнего объекта того же вида (группы по видам сохраняются)
$insertAt = $lastSame + $closeSame.Length
$newContent = $configContent.Substring(0, $insertAt) + "$eol`t`t`t$entry" + $configContent.Substring($insertAt)
} else {
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
# группой вида старше по $childObjectTypes. Дописать в конец блока нельзя: платформа
# переставит группу при первой же выгрузке и даст диф на ровном месте.
$ownIdx = $childObjectTypes.IndexOf($ChildTag)
$anchor = $null
if ($ownIdx -ge 0) {
$typeRx = [regex]"(?m)^([ \t]*)<(\w+)>[^<]*</\2>"
$tm = $typeRx.Match($configContent, $block.Index, $block.Length)
while ($tm.Success) {
$otherIdx = $childObjectTypes.IndexOf($tm.Groups[2].Value)
if ($otherIdx -gt $ownIdx) { $anchor = $tm; break }
$tm = $tm.NextMatch()
}
}
if ($anchor) {
$newContent = $configContent.Substring(0, $anchor.Index) + $anchor.Groups[1].Value + $entry + $eol + $configContent.Substring($anchor.Index)
} else {
# Видов старше в файле нет — новая строка перед </ChildObjects>,
# отступ закрывающего тега переиспользуется
$closeAt = $configContent.LastIndexOf("</ChildObjects>", $blockEnd - 1, $block.Length, [System.StringComparison]::Ordinal)
$newContent = $configContent.Substring(0, $closeAt) + "`t$entry$eol`t`t" + $configContent.Substring($closeAt)
}
}
[System.IO.File]::WriteAllText($ParentXmlPath, $newContent, $enc)
return "added"
}
# XML tag name for Configuration.xml ChildObjects
$childTag = $objType
if (Test-Path $configXmlPath) {
$configDoc = New-Object System.Xml.XmlDocument
$configDoc.PreserveWhitespace = $true
$configDoc.Load($configXmlPath)
$nsMgr = New-Object System.Xml.XmlNamespaceManager($configDoc.NameTable)
$nsMgr.AddNamespace("md", "http://v8.1c.ru/8.3/MDClasses")
$childObjects = $configDoc.SelectSingleNode("//md:Configuration/md:ChildObjects", $nsMgr)
if ($childObjects) {
$existing = $childObjects.SelectNodes("md:$childTag", $nsMgr)
$alreadyExists = $false
foreach ($e in $existing) {
if ($e.InnerText -eq $objName) {
$alreadyExists = $true
break
}
}
if ($alreadyExists) {
$regResult = "already"
} else {
$newElem = $configDoc.CreateElement($childTag, "http://v8.1c.ru/8.3/MDClasses")
$newElem.InnerText = $objName
if ($existing.Count -gt 0) {
# Insert after last existing element of same type
$lastElem = $existing[$existing.Count - 1]
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertAfter($newWs, $lastElem) | Out-Null
$childObjects.InsertAfter($newElem, $newWs) | Out-Null
} else {
# No existing elements of this type — insert before closing whitespace
$lastChild = $childObjects.LastChild
if ($lastChild.NodeType -eq [System.Xml.XmlNodeType]::Whitespace) {
$newWs = $configDoc.CreateWhitespace("`n`t`t`t")
$childObjects.InsertBefore($newWs, $lastChild) | Out-Null
$childObjects.InsertBefore($newElem, $lastChild) | Out-Null
} else {
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t`t")) | Out-Null
$childObjects.AppendChild($newElem) | Out-Null
$childObjects.AppendChild($configDoc.CreateWhitespace("`n`t`t")) | Out-Null
}
}
# Save. Пишем через MemoryStream, а не прямо в файл: нужен шаг пост-обработки
# строки — XmlWriter отдаёт `encoding="utf-8"` и `<a />`, Конфигуратор пишет
# `encoding="UTF-8"` и `<a/>`.
$cfgSettings = New-Object System.Xml.XmlWriterSettings
$cfgSettings.Encoding = New-Object System.Text.UTF8Encoding($true)
$cfgSettings.Indent = $false
$cfgSettings.NewLineHandling = [System.Xml.NewLineHandling]::None
$memStream = New-Object System.IO.MemoryStream
$writer = [System.Xml.XmlWriter]::Create($memStream, $cfgSettings)
$configDoc.Save($writer)
$writer.Flush(); $writer.Close()
$cfgText = [System.Text.Encoding]::UTF8.GetString($memStream.ToArray())
$memStream.Close()
if ($cfgText.Length -gt 0 -and $cfgText[0] -eq [char]0xFEFF) { $cfgText = $cfgText.Substring(1) }
$cfgText = $cfgText.Replace('encoding="utf-8"', 'encoding="UTF-8"')
# Пустой элемент: XmlWriter отдаёт `<a />`, Конфигуратор пишет `<a/>`. Внутри
# CDATA/комментария ` />` может быть содержимым (там `>` не экранируется),
# поэтому они идут первыми ветками альтернации и возвращаются как есть.
$cfgText = [regex]::Replace($cfgText, '(?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 $configXmlPath) -and ([System.IO.File]::ReadAllText($configXmlPath) -notmatch "`r`n")) { "`n" } else { "`r`n" }
$cfgText = ($cfgText -replace "`r`n", "`n") -replace "`n", $targetEol
[System.IO.File]::WriteAllText($configXmlPath, $cfgText, (New-Object System.Text.UTF8Encoding($true)))
$regResult = "added"
}
} else {
$regResult = "no-childobj"
}
} else {
$regResult = "no-config"
}
$configXmlPath = Join-Path $OutputDir "Configuration.xml"
$regResult = Register-InChildObjects $configXmlPath "Configuration" $childTag $objName
# --- 18. Summary ---
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-compile v1.94 — Compile 1C metadata object from JSON
# meta-compile v1.109 — Compile 1C metadata object from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
@@ -23,6 +23,68 @@ sys.stderr.reconfigure(encoding="utf-8")
# молча терял свойства DSL, написанные в другом регистре. Обёртки ниже выравнивают поведение.
# ============================================================
def parse_json_input(text, source, expected=None, inline=False):
"""Разбор пользовательского JSON: одна строка в stderr вместо traceback (issue #80).
expected заполняем только для полиморфного входа: у файла подсказка
была бы наполнителем имя файла и текст парсера самодостаточны. inline печатает ещё и то,
что доехало: у файла такого вопроса нет, он лежит на диске и его видно целиком.
Импорты внутри тела: копия функции живёт в навыках с разными именами модулей
(skd-decompile импортирует json локально как _json), а тело обязано быть одинаковым.
"""
import json as _pj
import sys as _psys
try:
if not str(text).strip():
raise ValueError("input is empty")
return _pj.loads(text)
except ValueError as exc:
what = "%s expects %s" % (source, expected) if expected else "Invalid JSON in %s" % source
if inline:
got = " ".join(str(text).split())
label = "got"
if not got:
got = "(empty)"
elif len(got) > 60:
label = "got (first 60 chars)"
got = got[:60]
what = "%s, %s: %s" % (what, label, got)
print("[ERROR] %s (%s)" % (what, exc), file=_psys.stderr)
_psys.exit(1)
def read_json_file(path):
"""Чтение входного JSON-файла с кодировкой из BOM (issue #80).
BOM объявление самого файла, поэтому ему верим; без BOM ждём строгий UTF-8. Кодовую
страницу не подбираем: угаданное имя уехало бы в метаданные молча.
"""
import os as _pos
import sys as _psys
if not _pos.path.exists(path):
print("[ERROR] File not found: %s" % path, file=_psys.stderr)
_psys.exit(1)
if _pos.path.isdir(path):
print("[ERROR] Expected a JSON file, got a directory: %s" % path, file=_psys.stderr)
_psys.exit(1)
with open(path, "rb") as _fh:
data = _fh.read()
if data[:3] == b"\xef\xbb\xbf":
return data[3:].decode("utf-8")
if data[:2] == b"\xff\xfe":
return data[2:].decode("utf-16-le")
if data[:2] == b"\xfe\xff":
return data[2:].decode("utf-16-be")
try:
return data.decode("utf-8")
except UnicodeDecodeError as exc:
print("[ERROR] %s is not valid UTF-8: %s - save the file as UTF-8, or add a BOM if it is UTF-16"
% (path, exc), file=_psys.stderr)
_psys.exit(1)
class CIDict(dict):
# Ключи храним КАК ЕСТЬ: часть из них — имена объектов (табличные части, стандартные
# реквизиты), они попадают в XML. Регистронезависим только поиск. Порядок вставки
@@ -374,10 +436,9 @@ if not os.path.isfile(json_path):
print(f'File not found: {json_path}', file=sys.stderr)
sys.exit(1)
with open(json_path, 'r', encoding='utf-8-sig') as f:
json_text = f.read()
json_text = read_json_file(json_path)
defn = ci_json(json.loads(json_text))
defn = ci_json(parse_json_input(json_text, json_path))
assert_edit_allowed(output_dir, "editable")
@@ -436,6 +497,7 @@ object_type_synonyms = {
'ВебСервис': 'WebService',
'ОпределяемыйТип': 'DefinedType',
'ФункциональнаяОпция': 'FunctionalOption',
'ВнешнийИсточникДанных': 'ExternalDataSource',
}
# Enum property value synonyms — model often gets these slightly wrong
@@ -481,7 +543,9 @@ valid_enum_values = {
'WriteMode': ['Independent', 'RecorderSubordinate'],
'InformationRegisterPeriodicity': ['Nonperiodical', 'Second', 'Day', 'Month', 'Quarter', 'Year', 'RecorderPosition'],
'DependenceOnCalculationTypes': ['DontUse', 'OnActionPeriod'],
'DataLockControlMode': ['Automatic', 'Managed'],
# AutomaticAndManaged — только у внешнего источника данных и его таблиц: там режим может
# решаться на уровне таблицы, у прочих объектов такого значения нет.
'DataLockControlMode': ['Automatic', 'Managed', 'AutomaticAndManaged'],
'FullTextSearch': ['Use', 'DontUse'],
'DataHistory': ['Use', 'DontUse'],
'DefaultPresentation': ['AsDescription', 'AsCode'],
@@ -631,7 +695,7 @@ valid_types = [
'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage',
'CommonForm',
'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference',
'CommonPicture', 'CommonTemplate',
'CommonPicture', 'CommonTemplate', 'ExternalDataSource',
]
# Регистр имени вида — как в PS (-contains регистронезависим): приводим к канону списка
obj_type = next((t for t in valid_types if t.lower() == obj_type.lower()), obj_type)
@@ -671,6 +735,10 @@ type_synonyms = {
# ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон).
'valuestorage': 'ValueStorage',
'base64binary': 'ValueStorage',
# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как
# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных.
'binarydata': 'BinaryData',
'двоичныеданные': 'BinaryData',
'хранилищезначений': 'ValueStorage',
'хранилищезначения': 'ValueStorage',
'uuid': 'UUID',
@@ -684,6 +752,7 @@ type_synonyms = {
'планвидоврасчётассылка': 'ChartOfCalculationTypesRef',
'планвидоврасчетассылка': 'ChartOfCalculationTypesRef',
'планобменассылка': 'ExchangePlanRef',
'внешнийисточникданныхтаблицассылка': 'ExternalDataSourceTableRef',
'бизнеспроцессссылка': 'BusinessProcessRef',
'задачассылка': 'TaskRef',
'определяемыйтип': 'DefinedType',
@@ -862,6 +931,25 @@ def emit_type_content(indent, type_str):
if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$', type_str):
X(f'{indent}<v8:TypeSet>cfg:{type_str}</v8:TypeSet>')
return
# ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников).
if re.match(r'^BinaryData(\(|$)', type_str, re.I):
# BinaryData — безлимит (так платформа пишет поле внешнего источника: 4294967292/Fixed).
# BinaryData(N) — переменной длины, BinaryData(N,fixed) — фиксированной.
m_bin = re.match(r'^BinaryData(?:\((\d+)(?:,\s*(fixed|variable))?\))?$', type_str, re.I)
if not m_bin:
print(f"Неверный тип '{type_str}': ждётся BinaryData, BinaryData(Длина) или BinaryData(Длина,fixed|variable).", file=sys.stderr)
sys.exit(1)
blen = m_bin.group(1) or '4294967292'
if m_bin.group(2):
ballowed = 'Fixed' if m_bin.group(2).lower() == 'fixed' else 'Variable'
else:
ballowed = 'Variable' if m_bin.group(1) else 'Fixed'
X(f'{indent}<v8:Type>xs:base64Binary</v8:Type>')
X(f'{indent}<v8:BinaryDataQualifiers>')
X(f'{indent}\t<v8:Length>{blen}</v8:Length>')
X(f'{indent}\t<v8:AllowedLength>{ballowed}</v8:AllowedLength>')
X(f'{indent}</v8:BinaryDataQualifiers>')
return
# ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary).
if type_str == 'ValueStorage':
X(f'{indent}<v8:Type>v8:ValueStorage</v8:Type>')
@@ -908,7 +996,9 @@ def emit_type_content(indent, type_str):
# cfg_prefix = None означает «пишем файл, корень которого cfg НЕ объявляет»
# (Ext/Predefined.xml — его шапка это predef/v8/xr/xs/xsi). Там платформа сама уходит
# на локальную форму: в корпусе `<v8:Type xmlns:d6p1="…current-config">d6p1:CatalogRef.Валюты`.
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef)\.(.+)$', type_str)
# ExternalDataSourceTableRef — единственный ссылочный тип с ДВУМЯ частями после префикса
# (Источник.Таблица), поэтому `(.+)$` здесь существенно.
m = re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|BusinessProcessRoutePointRef|TaskRef|ExternalDataSourceTableRef)\.(.+)$', type_str)
if m:
if cfg_prefix:
X(f'{indent}<v8:Type>{cfg_prefix}:{type_str}</v8:Type>')
@@ -1133,8 +1223,10 @@ def emit_fill_value(indent, type_str, spec, has_spec, type_empty=False):
# 5. Attribute shorthand parser
# ---------------------------------------------------------------------------
def build_type_str(obj):
t = str(obj.get('valueType') or obj.get('type') or '')
def build_type_str(obj, value_type_only=False):
# value_type_only — для корневого определения объекта: там ключ type означает ВИД объекта
# (Constant, Catalog, …), а не тип значения, и подхватывать его нельзя.
t = str(obj.get('valueType') or ('' if value_type_only else obj.get('type')) or '')
if t and '(' not in t:
if t == 'String' and obj.get('length'):
t = f"String({obj['length']})"
@@ -1230,6 +1322,10 @@ def parse_attribute_shorthand(val):
# Режим приведения типов измерения РС (формат 2.18). Ключ обязан доехать до эмиттера:
# без него не-дефолтное значение (Deny / DeleteData) молча заменялось на TransformValues.
'typeReductionMode': val.get('typeReductionMode'),
# Поле внешнего источника данных (контекст eds-field).
'nameInDataSource': str(val['nameInDataSource']) if val.get('nameInDataSource') else '',
'readOnly': val.get('readOnly') is True,
'allowNull': val.get('allowNull') is True,
}
def parse_enum_value_shorthand(val):
@@ -1366,6 +1462,11 @@ generated_types = {
'DefinedType': [
{'prefix': 'DefinedType', 'category': 'DefinedType'},
],
'ExternalDataSource': [
{'prefix': 'ExternalDataSourceManager', 'category': 'Manager'},
{'prefix': 'ExternalDataSourceTablesManager', 'category': 'TablesManager'},
{'prefix': 'ExternalDataSourceCubesManager', 'category': 'CubesManager'},
],
'DocumentJournal': [
{'prefix': 'DocumentJournalSelection', 'category': 'Selection'},
{'prefix': 'DocumentJournalList', 'category': 'List'},
@@ -2217,15 +2318,28 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
if parsed.get('fillChecking'):
fill_checking = parsed['fillChecking']
X(f'{indent}\t\t<FillChecking>{fill_checking}</FillChecking>')
X(f'{indent}\t\t<ChoiceFoldersAndItems>{parsed.get("choiceFoldersAndItems") or "Items"}</ChoiceFoldersAndItems>')
# Поле внешнего источника (eds-field) не имеет ChoiceFoldersAndItems и LinkByType, а ChoiceForm
# у него стоит ПОСЛЕ ChoiceHistoryOnInput, а не перед — порядок снят с выгрузки платформы.
if context != 'eds-field':
X(f'{indent}\t\t<ChoiceFoldersAndItems>{parsed.get("choiceFoldersAndItems") or "Items"}</ChoiceFoldersAndItems>')
emit_choice_parameter_links(f'{indent}\t\t', parsed.get('choiceParameterLinks'))
emit_choice_parameters(f'{indent}\t\t', parsed.get('choiceParameters'))
X(f'{indent}\t\t<QuickChoice>{parsed.get("quickChoice") or "Auto"}</QuickChoice>')
X(f'{indent}\t\t<CreateOnInput>{parsed.get("createOnInput") or "Auto"}</CreateOnInput>')
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
if context != 'eds-field':
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
emit_link_by_type(f'{indent}\t\t', parsed.get('linkByType'))
chi = parsed.get('choiceHistoryOnInput') or 'Auto'
X(f'{indent}\t\t<ChoiceHistoryOnInput>{chi}</ChoiceHistoryOnInput>')
if context == 'eds-field':
X(f'{indent}\t\t<ChoiceForm>{esc_xml_text(str(parsed["choiceForm"]))}</ChoiceForm>' if parsed.get('choiceForm') else f'{indent}\t\t<ChoiceForm/>')
nids = parsed.get('nameInDataSource') or parsed['name']
X(f'{indent}\t\t<NameInDataSource>{esc_xml_text(str(nids))}</NameInDataSource>')
ro = 'true' if (parsed.get('readOnly') is True or 'readonly' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<ReadOnly>{ro}</ReadOnly>')
an = 'true' if (parsed.get('allowNull') is True or 'nullable' in parsed.get('flags', [])) else 'false'
X(f'{indent}\t\t<AllowNull>{an}</AllowNull>')
# Измерение регистра сведений: Master/MainFilter/DenyIncompleteValues (между ChoiceHistoryOnInput и Indexing).
if elem_tag == 'Dimension' and context == 'register-info':
master = 'true' if (parsed.get('master') is True or 'master' in parsed.get('flags', [])) else 'false'
@@ -2273,7 +2387,8 @@ def emit_attribute(indent, parsed, context, elem_tag='Attribute'):
use_value = parsed.get("use") or "ForItem"
if context == 'catalog':
X(f'{indent}\t\t<Use>{use_value}</Use>')
if context not in ('processor', 'processor-tabular'):
# и не для полей внешнего источника: индексами и полнотекстовым поиском чужой таблицы 1С не владеет.
if context not in ('processor', 'processor-tabular', 'eds-field'):
# Признаки учёта ПС (account-flag) не имеют <Indexing>/<FullTextSearch>, но имеют <DataHistory>.
if context != 'account-flag':
# Ресурс регистра накопления НЕ имеет <Indexing> (только <FullTextSearch>); измерение/реквизит — имеют.
@@ -2808,9 +2923,8 @@ def emit_constant_properties(indent):
else:
X(f'{i}<Comment/>')
# Type — valueType (явный '' → <Type/>, реквизит без типа; отсутствие → String дефолт).
value_type = build_type_str(defn)
type_empty = (defn.get('valueType') is not None and str(defn.get('valueType')).strip() == '') or \
(defn.get('type') is not None and str(defn.get('type')).strip() == '')
value_type = build_type_str(defn, value_type_only=True)
type_empty = defn.get('valueType') is not None and str(defn.get('valueType')).strip() == ''
if type_empty:
X(f'{i}<Type/>')
else:
@@ -4246,6 +4360,248 @@ def emit_addressing_attribute(indent, addr_def):
parsed = parse_attribute_shorthand(addr_def)
emit_attribute(indent, parsed, 'task-addressing', 'AddressingAttribute')
# ---------------------------------------------------------------------------
# 13h. Внешние источники данных
# ---------------------------------------------------------------------------
# Источник пишется в один файл, каждая его таблица — в свой. Функции живут ВНУТРИ файла источника
# полными узлами, наравне со списком имён таблиц: так их выгружает платформа.
def get_eds_tables(val):
"""Таблицы: dict имя → массив полей ЛИБО объект со свойствами и ключом fields/columns."""
tables = {}
if not val:
return tables
def entry(v):
if isinstance(v, list):
return {'props': None, 'fields': list(v)}
f = v.get('fields') if v.get('fields') is not None else v.get('columns')
return {'props': v, 'fields': list(f) if f else []}
if isinstance(val, list):
for t in val:
tables[str(t.get('name'))] = entry(t)
else:
for k, v in val.items():
tables[k] = entry(v)
return tables
def get_eds_field_ref(src_name, table_name, field_name):
"""Ссылка на поле таблицы: в DSL короткое имя, в XML — полный путь."""
if not field_name:
return ''
if str(field_name).startswith('ExternalDataSource.'):
return str(field_name)
return f'ExternalDataSource.{src_name}.Table.{table_name}.Field.{field_name}'
def emit_eds_field_ref_list(indent, tag, names, src_name, table_name):
items = [n for n in (names or []) if n]
if not items:
X(f'{indent}<{tag}/>')
return
X(f'{indent}<{tag}>')
for n in items:
X(f'{indent}\t<xr:Field>{esc_xml_text(get_eds_field_ref(src_name, table_name, n))}</xr:Field>')
X(f'{indent}</{tag}>')
def emit_eds_field_ref_scalar(indent, tag, name, src_name, table_name):
if not name:
X(f'{indent}<{tag}/>')
return
X(f'{indent}<{tag}>{esc_xml_text(get_eds_field_ref(src_name, table_name, name))}</{tag}>')
def emit_external_data_source_properties(indent):
i = indent
X(f'{i}<Name>{esc_xml_text(obj_name)}</Name>')
emit_mltext(i, 'Synonym', synonym)
if defn.get('comment'):
X(f'{i}<Comment>{esc_xml_text(str(defn["comment"]))}</Comment>')
else:
X(f'{i}<Comment/>')
dlcm = get_enum_prop('DataLockControlMode', 'dataLockControlMode', 'Automatic')
X(f'{i}<DataLockControlMode>{dlcm}</DataLockControlMode>')
def emit_eds_function(indent, fn_name, val, type_xml):
"""Функция внешнего источника. Параметров как объектов метаданных нет: они записаны прямо
в выражении как &1, &2 (см. reference/external-data-source.md).
type_xml уже собранный узел <Type> возвращаемого значения: его рендерит вызывающий навык
своим эмиттером типов. Так тело функции не зависит от того, какой это навык."""
fn_synonym = None
fn_comment = ''
returns = ''
return_value = True
if isinstance(val, str):
expr = val
else:
expr = str(val.get('expression') or val.get('expressionInDataSource') or '')
returns = str(val.get('returns') or val.get('returnType') or '')
if val.get('returnValue') is not None:
return_value = val.get('returnValue') is True
fn_synonym = val.get('synonym')
fn_comment = str(val['comment']) if val.get('comment') else ''
if not expr:
print(f"ERROR: Функция '{fn_name}' внешнего источника данных: не задано выражение (ключ expression).",
file=sys.stderr)
sys.exit(1)
X(f'{indent}<Function uuid="{new_uuid()}">')
X(f'{indent}\t<Properties>')
X(f'{indent}\t\t<Name>{esc_xml_text(fn_name)}</Name>')
emit_mltext(f'{indent}\t\t', 'Synonym', fn_synonym)
if fn_comment:
X(f'{indent}\t\t<Comment>{esc_xml_text(fn_comment)}</Comment>')
else:
X(f'{indent}\t\t<Comment/>')
X(f'{indent}\t\t<ReturnValue>{"true" if return_value else "false"}</ReturnValue>')
if return_value and type_xml:
X(type_xml.rstrip('\r\n'))
else:
X(f'{indent}\t\t<Type/>')
X(f'{indent}\t\t<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>')
X(f'{indent}\t</Properties>')
X(f'{indent}</Function>')
def emit_eds_table_properties(indent, src_name, table_name, t, char_xml, default_forms_xml):
"""Свойства таблицы: 38 узлов в порядке выгрузки платформы.
char_xml и default_forms_xml уже собранные блоки <Characteristics> и четыре слота
<Default*Form>: их рендерит вызывающий навык своим эмиттером. Так тело не зависит
от хелперов конкретного навыка и годится для копирования (check-inline-drift)."""
i = indent
t = t or {}
tbl_synonym = t['synonym'] if t.get('synonym') is not None else split_camel_case(table_name)
X(f'{i}<Name>{esc_xml_text(table_name)}</Name>')
emit_mltext(i, 'Synonym', tbl_synonym)
if t.get('comment'):
X(f'{i}<Comment>{esc_xml_text(str(t["comment"]))}</Comment>')
else:
X(f'{i}<Comment/>')
table_type = str(t.get('tableType') or 'Table')
X(f'{i}<TableType>{table_type}</TableType>')
# Имя в источнике по умолчанию равно имени объекта — так поступает и платформа.
if t.get('nameInDataSource'):
nids = str(t['nameInDataSource'])
elif table_type == 'Expression':
nids = ''
else:
nids = table_name
X(f'{i}<NameInDataSource>{esc_xml_text(nids)}</NameInDataSource>' if nids else f'{i}<NameInDataSource/>')
expr = str(t.get('expressionInDataSource') or t.get('expression') or '')
X(f'{i}<ExpressionInDataSource>{esc_xml_text(expr)}</ExpressionInDataSource>' if expr else f'{i}<ExpressionInDataSource/>')
X(f'{i}<TableDataType>{t.get("tableDataType") or "NonobjectData"}</TableDataType>')
emit_eds_field_ref_list(i, 'KeyFields', t.get('keyFields'), src_name, table_name)
emit_eds_field_ref_scalar(i, 'PresentationField', t.get('presentationField'), src_name, table_name)
emit_eds_field_ref_scalar(i, 'ParentField', t.get('parentField'), src_name, table_name)
# Признака незаполненного родителя отдельным узлом нет: NULL против «Заданного значения»
# различаются формой самого значения (xsi:nil против типизированного).
# ВАЖНО: платформа при загрузке XML сбрасывает заданное значение в пустую строку — проверено
# на её собственной выгрузке. Задать его можно только интерактивно, поэтому дефолт у таблицы
# с полем родителя — пустая строка (как после загрузки), а без него — nil.
if t.get('parentField'):
X(f'{i}<UnfilledParentValue xsi:type="xs:string"/>')
else:
X(f'{i}<UnfilledParentValue xsi:nil="true"/>')
if char_xml:
X(char_xml.rstrip('\r\n'))
else:
X(f'{i}<Characteristics/>')
X(f'{i}<UseStandardCommands>{"false" if t.get("useStandardCommands") is False else "true"}</UseStandardCommands>')
X(f'{i}<QuickChoice>{"true" if t.get("quickChoice") is True else "false"}</QuickChoice>')
# Ввод по строке: ключа нет → выводим из поля представления (так делает платформа при загрузке).
# Явный список, в том числе пустой, уважаем как есть — отсюда presence-aware проверка.
if 'inputByString' in t:
ibs = t.get('inputByString')
elif t.get('presentationField'):
ibs = [t['presentationField']]
else:
ibs = None
emit_eds_field_ref_list(i, 'InputByString', ibs, src_name, table_name)
X(f'{i}<CreateOnInput>{t.get("createOnInput") or "Auto"}</CreateOnInput>')
X(f'{i}<SearchStringModeOnInputByString>{t.get("searchStringModeOnInputByString") or "Begin"}</SearchStringModeOnInputByString>')
X(f'{i}<ChoiceDataGetModeOnInputByString>{t.get("choiceDataGetModeOnInputByString") or "Directly"}</ChoiceDataGetModeOnInputByString>')
X(f'{i}<ChoiceHistoryOnInput>{t.get("choiceHistoryOnInput") or "Auto"}</ChoiceHistoryOnInput>')
# Пустая строка — четыре слота всё равно обязаны быть: в свойствах таблицы их ровно 38.
if default_forms_xml:
X(default_forms_xml.rstrip('\r\n'))
else:
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
X(f'{i}<{form_tag}/>')
for pres_tag in ('ObjectPresentation', 'ExtendedObjectPresentation', 'RecordPresentation',
'ExtendedRecordPresentation', 'ListPresentation', 'ExtendedListPresentation', 'Explanation'):
key = pres_tag[0].lower() + pres_tag[1:]
emit_mltext(i, pres_tag, t.get(key))
X(f'{i}<IncludeHelpInContents>{"true" if t.get("includeHelpInContents") is True else "false"}</IncludeHelpInContents>')
X(f'{i}<ReadOnly>{"true" if t.get("readOnly") is True else "false"}</ReadOnly>')
X(f'{i}<TransactionsIsolationLevel>{t.get("transactionsIsolationLevel") or "Auto"}</TransactionsIsolationLevel>')
emit_eds_field_ref_scalar(i, 'DataVersionField', t.get('dataVersionField'), src_name, table_name)
X(f'{i}<EditType>{t.get("editType") or "InDialog"}</EditType>')
emit_md_ref_list(i, 'BasedOn', t.get('basedOn'))
emit_eds_field_ref_list(i, 'DataLockFields', t.get('dataLockFields'), src_name, table_name)
X(f'{i}<DataLockControlMode>{t.get("dataLockControlMode") or "Automatic"}</DataLockControlMode>')
EDS_TABLE_GENERATED_TYPES = (
('ExternalDataSourceTableManager', 'Manager'),
('ExternalDataSourceTableObject', 'Object'),
('ExternalDataSourceTableRef', 'Ref'),
('ExternalDataSourceTableList', 'List'),
('ExternalDataSourceTableRecord', 'Record'),
('ExternalDataSourceTableRecordSet', 'RecordSet'),
('ExternalDataSourceTableRecordKey', 'RecordKey'),
('ExternalDataSourceTableRecordManager', 'RecordManager'),
)
def build_eds_table_xml(src_name, table_name, entry, fields_xml, char_xml, default_forms_xml):
"""Отдельный XML-документ таблицы. fields_xml, char_xml, default_forms_xml — уже собранные
узлы: их рендерит вызывающий навык своими эмиттерами, поэтому тело не зависит от того,
какой это навык.
Возвращает строку: X пишет в общий список строк,
поэтому «перехват» запомнить длину, отдать эмиттерам, срезать добавленное
(в ps1-порте тот же приём выражен через StringBuilder различие рантаймов, не логики)."""
before = len(lines)
X('<?xml version="1.0" encoding="UTF-8"?>')
X(f'<MetaDataObject {xmlns_decl} version="{format_version}">')
X(f'\t<Table uuid="{new_uuid()}">')
# InternalInfo у таблицы эмитится здесь, а не через generated_types: имя элемента
# трёхчастное (Префикс.Источник.Таблица), общая карта такой формы не знает.
X('\t\t<InternalInfo>')
for prefix, category in EDS_TABLE_GENERATED_TYPES:
X(f'\t\t\t<xr:GeneratedType name="{prefix}.{src_name}.{table_name}" category="{category}">')
X(f'\t\t\t\t<xr:TypeId>{new_uuid()}</xr:TypeId>')
X(f'\t\t\t\t<xr:ValueId>{new_uuid()}</xr:ValueId>')
X('\t\t\t</xr:GeneratedType>')
X('\t\t</InternalInfo>')
X('\t\t<Properties>')
emit_eds_table_properties('\t\t\t', src_name, table_name, entry['props'], char_xml, default_forms_xml)
X('\t\t</Properties>')
if fields_xml:
X('\t\t<ChildObjects>')
X(fields_xml.rstrip('\r\n'))
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
X('\t</Table>')
X('</MetaDataObject>')
chunk = '\r\n'.join(lines[before:])
del lines[before:]
return chunk
# ---------------------------------------------------------------------------
# 14. Namespaces
# ---------------------------------------------------------------------------
@@ -4386,6 +4742,7 @@ property_emitters = {
'Task': emit_task_properties,
'HTTPService': emit_http_service_properties,
'WebService': emit_web_service_properties,
'ExternalDataSource': emit_external_data_source_properties,
}
property_emitters[obj_type]('\t\t\t')
@@ -4659,6 +5016,36 @@ if obj_type == 'WebService':
else:
X('\t\t<ChildObjects/>')
# --- ExternalDataSource: Tables (именами) + Functions (полными узлами) ---
eds_tables = {}
if obj_type == 'ExternalDataSource':
eds_tables = get_eds_tables(defn.get('tables'))
functions = {}
if defn.get('functions'):
for k, v in defn['functions'].items():
functions[k] = v
if eds_tables or functions:
has_children = True
X('\t\t<ChildObjects>')
for tbl_name in eds_tables:
X(f'\t\t\t<Table>{esc_xml_text(tbl_name)}</Table>')
for fn_name, fn_val in functions.items():
if isinstance(fn_val, str):
fn_returns, fn_no_value = 'String', False
else:
fn_returns = str(fn_val.get('returns') or fn_val.get('returnType') or 'String')
fn_no_value = fn_val.get('returnValue') is not None and fn_val.get('returnValue') is not True
fn_type_xml = ''
if not fn_no_value:
type_before = len(lines)
emit_value_type('\t\t\t\t\t', fn_returns)
fn_type_xml = '\r\n'.join(lines[type_before:])
del lines[type_before:]
emit_eds_function('\t\t\t', fn_name, fn_val, fn_type_xml)
X('\t\t</ChildObjects>')
else:
X('\t\t<ChildObjects/>')
# --- CommonModule: no ChildObjects ---
X(f'\t</{obj_type}>')
@@ -4708,6 +5095,7 @@ type_plural_map = {
'WSReference': 'WSReferences',
'CommonPicture': 'CommonPictures',
'CommonTemplate': 'CommonTemplates',
'ExternalDataSource': 'ExternalDataSources',
}
type_plural = type_plural_map[obj_type]
@@ -4726,8 +5114,50 @@ os.makedirs(type_dir, exist_ok=True)
if obj_type not in types_no_sub_dir:
os.makedirs(obj_sub_dir, exist_ok=True)
# Объект с таким именем уже есть: компиляция заменит его файл ЦЕЛИКОМ и выдаст новый uuid —
# ссылки на прежний объект (из кода, состава подсистем, типов реквизитов) станут висячими.
# Для доработки существующего объекта есть meta-edit; молчать об этом нельзя.
if os.path.exists(main_xml_path):
print(f"WARNING: {obj_type} '{obj_name}' уже существует ({type_plural}/{obj_name}.xml) — файл будет перезаписан, объект получит НОВЫЙ uuid, ссылки на прежний сломаются. Для правки существующего объекта используйте meta-edit.", file=sys.stderr)
write_xml_file_keep_eol(main_xml_path, metadata_xml)
# Таблицы внешнего источника — отдельными файлами в <Источник>/Tables/.
# Единственный вид, у которого объект складывается более чем из одного XML.
eds_tables_created = []
if obj_type == 'ExternalDataSource' and eds_tables:
tables_dir = os.path.join(obj_sub_dir, 'Tables')
os.makedirs(tables_dir, exist_ok=True)
for tbl_name in eds_tables:
entry = eds_tables[tbl_name]
fields_before = len(lines)
for f in entry['fields']:
emit_attribute('\t\t\t', parse_attribute_shorthand(f), 'eds-field', 'Field')
fields_xml = '\r\n'.join(lines[fields_before:])
del lines[fields_before:]
tp = entry['props'] or {}
char_before = len(lines)
emit_characteristics('\t\t\t', tp.get('characteristics'))
char_xml = '\r\n'.join(lines[char_before:])
del lines[char_before:]
# Слот формы: короткое имя разворачивается в полный путь таблицы внешнего источника —
# голое имя платформа отвергает («Неизвестный объект метаданных»).
forms_before = len(lines)
for form_tag in ('DefaultObjectForm', 'DefaultRecordForm', 'DefaultListForm', 'DefaultChoiceForm'):
key = form_tag[0].lower() + form_tag[1:]
form_val = tp.get(key)
if form_val and '.' not in str(form_val):
form_val = f'ExternalDataSource.{obj_name}.Table.{tbl_name}.Form.{form_val}'
emit_form_ref('\t\t\t', form_tag, form_val)
default_forms_xml = '\r\n'.join(lines[forms_before:])
del lines[forms_before:]
table_xml = build_eds_table_xml(obj_name, tbl_name, entry, fields_xml, char_xml, default_forms_xml)
table_path = os.path.join(tables_dir, f'{tbl_name}.xml')
write_xml_file_keep_eol(table_path, table_xml)
eds_tables_created.append(table_path)
# Module files
modules_created = []
@@ -5148,78 +5578,208 @@ if commands:
# 17. Register in Configuration.xml
# ---------------------------------------------------------------------------
config_xml_path = os.path.join(output_dir, 'Configuration.xml')
reg_result = None
def get_new_object_position(cfg_dir):
"""Куда навык ставит новую запись в <ChildObjects> — настройка newObjectPosition.
child_tag = obj_type
databases[].newObjectPosition базы, чей configSrc охватывает каталог родительского XML,
иначе корневое поле, иначе end. Значения: end после последнего объекта того же вида
(так дописывает Конфигуратор); byName по имени среди объектов того же вида.
Файл ищем от рабочего каталога вверх, каталог конфигурации запасной путь: так же
его ищут support-guard и группа db-*, а скрипт навыка зовут по абсолютному пути, и cwd
остаётся рабочим каталогом проекта.
configSrc считается от каталога .v8-project.json, как задокументировано в
docs/v8-project-guide.md. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
try:
pj = _sg_find_v8project(os.getcwd()) or _sg_find_v8project(os.path.abspath(cfg_dir or "."))
if not pj:
return "end"
proj = json.loads(open(pj, encoding="utf-8-sig").read())
proj_dir = os.path.dirname(pj)
cfg_full = os.path.normcase(os.path.abspath(cfg_dir or ".")).rstrip("\\/")
for db in proj.get("databases", []):
src = db.get("configSrc")
if src and db.get("newObjectPosition"):
src_full = os.path.normcase(os.path.abspath(os.path.join(proj_dir, src))).rstrip("\\/")
if cfg_full == src_full or cfg_full.startswith(src_full + os.sep):
return "byName" if str(db["newObjectPosition"]).lower() == "byname" else "end"
if str(proj.get("newObjectPosition") or "").lower() == "byname":
return "byName"
return "end"
except Exception:
return "end"
if os.path.isfile(config_xml_path):
# Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation).
with open(config_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
def is_order_sensitive_type(type_name):
"""Виды, у которых порядок в дереве несёт смысл: автоматически их не упорядочиваем.
CommonAttribute исключение самого стандарта (#std467): у общих реквизитов-разделителей
порядок в дереве задаёт порядок установки параметров сеанса. Subsystem и CommandGroup:
пока они не перечислены в <SubsystemsOrder> / <GroupsOrder> файла Ext/CommandInterface.xml,
порядок дерева задаёт порядок в интерфейсе, а платформа эти списки сама не заводит
(в выгрузке ACC вне GroupsOrder 15 живых групп из 39). Language исключён из осторожности,
без замера: языков обычно один-два, и в типовых их порядок не алфавитный.
Явно названный вид сортируется в любом случае.
Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
return type_name in ("CommonAttribute", "Subsystem", "CommandGroup", "Language")
def compare_metadata_names(a, b):
"""Порядок имён объектов метаданных, как в дереве Конфигуратора.
Ключ пары «ранг+символ»: регистр не учитывается, подчёркивание раньше цифр, цифры раньше
букв, буквы по кодам (латиница раньше кириллицы), ё на месте е. Культурные таблицы не
используются они разные на разных ОС и в разных рантаймах, а так оба порта сравнивают
одинаково везде. Равные ключи разводит ordinal-сравнение исходных строк.
Возвращает -1 | 0 | 1. Реестр семьи: tests/skills/check-inline-drift.mjs.
"""
keys = []
for name in (a, b):
parts = []
for ch in name.lower():
if ch == "ё":
ch = "е"
if ch.isdigit():
parts.append("1" + ch)
elif ch.isalpha():
parts.append("2" + ch)
else:
parts.append("0" + ch)
keys.append("".join(parts))
if keys[0] != keys[1]:
return -1 if keys[0] < keys[1] else 1
if a != b:
return -1 if a < b else 1
return 0
# Канонический порядок видов в <ChildObjects> — эталон в docs/1c-configuration-spec.md,
# таблица «Порядок типов в ChildObjects». Нужен, чтобы новая группа вида вставала на своё
# место: иначе платформа переставит её при первой же выгрузке и даст диф на ровном месте.
# Реестр карт: tests/skills/check-type-maps.mjs.
CHILD_OBJECT_TYPES = [
'Language', 'Subsystem', 'StyleItem', 'Style',
'CommonPicture', 'SessionParameter', 'Role', 'CommonTemplate',
'FilterCriterion', 'CommonModule', 'CommonAttribute', 'ExchangePlan',
'XDTOPackage', 'WebService', 'HTTPService', 'WSReference',
'EventSubscription', 'ScheduledJob', 'SettingsStorage', 'FunctionalOption',
'FunctionalOptionsParameter', 'DefinedType', 'Bot', 'PaletteColor', 'CommonCommand', 'CommandGroup',
'Constant', 'CommonForm', 'Catalog', 'Document',
'DocumentNumerator', 'Sequence', 'DocumentJournal', 'Enum',
'Report', 'DataProcessor', 'InformationRegister', 'AccumulationRegister',
'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'AccountingRegister',
'ChartOfCalculationTypes', 'CalculationRegister',
'BusinessProcess', 'Task', 'ExternalDataSource', 'IntegrationService',
]
def register_in_childobjects(parent_xml_path, parent_tag, child_tag, child_name):
"""Регистрация объекта в <ChildObjects> родительского XML.
Общая реализация: эталон meta-compile, копии role-compile, xdto-compile.
Реестр семьи: tests/skills/check-inline-drift.mjs.
Возвращает исход: added | already | no-childobj | no-config.
"""
if not os.path.isfile(parent_xml_path):
return 'no-config'
# Read raw content, preserving BOM/EOL byte-for-byte (newline='' => no translation)
with open(parent_xml_path, 'r', encoding='utf-8-sig', newline='') as f:
config_content = f.read()
ns = 'http://v8.1c.ru/8.3/MDClasses'
# ET is used ONLY read-only here: to locate ChildObjects and detect a duplicate.
# We deliberately do NOT re-serialize Configuration.xml with ElementTree.write():
# it drops every xmlns declaration used only inside attribute VALUES (e.g.
# xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees those
# xsi:type="app:ApplicationUsePurpose" in UsePurposes) because ET never sees such
# prefixes in element/attribute names. The dropped declaration makes XDTO read the
# value as anyType and Designer refuses to load the file (issue #38). Registration is
# therefore done by raw-text insertion, preserving BOM, EOL and all namespaces
# byte-for-byte (same approach as subsystem-compile).
tree = ET.parse(config_xml_path)
tree = ET.parse(parent_xml_path)
root = tree.getroot()
child_objects = root.find(f'{{{ns}}}Configuration/{{{ns}}}ChildObjects')
child_objects = root.find(f'{{{ns}}}{parent_tag}/{{{ns}}}ChildObjects')
if child_objects is None:
# Try direct path
config_elem = root.find(f'{{{ns}}}Configuration')
if config_elem is not None:
child_objects = config_elem.find(f'{{{ns}}}ChildObjects')
parent_elem = root.find(f'{{{ns}}}{parent_tag}')
if parent_elem is not None:
child_objects = parent_elem.find(f'{{{ns}}}ChildObjects')
if child_objects is None:
reg_result = 'no-childobj'
return 'no-childobj'
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
if any((e.text or '').strip() == child_name for e in existing):
return 'already'
eol = '\r\n' if '\r\n' in config_content else '\n'
entry = f'<{child_tag}>{esc_xml_text(child_name)}</{child_tag}>'
block = re.search(r'<ChildObjects\s*>.*?</ChildObjects>', config_content, re.S)
if block is None:
# Empty self-closing <ChildObjects/> => open it with the first entry.
empty = re.search(r'<ChildObjects\s*/>', config_content)
if empty is None:
return 'no-childobj'
replacement = f'<ChildObjects>{eol}\t\t\t{entry}{eol}\t\t</ChildObjects>'
new_content = config_content[:empty.start()] + replacement + config_content[empty.end():]
write_utf8_bom(parent_xml_path, new_content)
return 'added'
# byName: перед первым объектом того же вида, чьё имя больше нового.
# Виды с осмысленным порядком в дереве пропускаем — см. is_order_sensitive_type.
if (not is_order_sensitive_type(child_tag)
and get_new_object_position(os.path.dirname(os.path.abspath(parent_xml_path))) == 'byName'):
line_rx = re.compile(rf'(?m)^([ \t]*)<{child_tag}>([^<]*)</{child_tag}>')
for m in line_rx.finditer(config_content, block.start(), block.end()):
if compare_metadata_names(m.group(2), child_name) > 0:
new_content = (config_content[:m.start()]
+ f'{m.group(1)}{entry}{eol}'
+ config_content[m.start():])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
close_same = f'</{child_tag}>'
last_same = config_content.rfind(close_same, block.start(), block.end())
if last_same != -1:
# After the last element of the same type (keeps them grouped).
insert_at = last_same + len(close_same)
new_content = (config_content[:insert_at]
+ f'{eol}\t\t\t{entry}'
+ config_content[insert_at:])
else:
existing = child_objects.findall(f'{{{ns}}}{child_tag}')
already_exists = any((e.text or '').strip() == obj_name for e in existing)
if already_exists:
reg_result = 'already'
# Группы своего вида ещё нет: ставим её в канонический порядок видов — перед первой
# группой вида старше по CHILD_OBJECT_TYPES. Дописать в конец блока нельзя: платформа
# переставит группу при первой же выгрузке и даст диф на ровном месте.
anchor = None
if child_tag in CHILD_OBJECT_TYPES:
own_idx = CHILD_OBJECT_TYPES.index(child_tag)
type_rx = re.compile(r'(?m)^([ \t]*)<(\w+)>[^<]*</\2>')
for m in type_rx.finditer(config_content, block.start(), block.end()):
other = m.group(2)
if other in CHILD_OBJECT_TYPES and CHILD_OBJECT_TYPES.index(other) > own_idx:
anchor = m
break
if anchor is not None:
new_content = (config_content[:anchor.start()]
+ f'{anchor.group(1)}{entry}{eol}'
+ config_content[anchor.start():])
else:
eol = '\r\n' if '\r\n' in config_content else '\n'
entry = f'<{child_tag}>{esc_xml_text(obj_name)}</{child_tag}>'
# Видов старше в файле нет — новая строка перед </ChildObjects>,
# отступ закрывающего тега переиспользуется.
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
new_content = (config_content[:close_at]
+ f'\t{entry}{eol}\t\t'
+ config_content[close_at:])
write_utf8_bom(parent_xml_path, new_content)
return 'added'
block = re.search(r'<ChildObjects\s*>.*?</ChildObjects>', config_content, re.S)
if block is None:
# Empty self-closing <ChildObjects/> => open it with the first entry.
empty = re.search(r'<ChildObjects\s*/>', config_content)
if empty is None:
reg_result = 'no-childobj'
else:
replacement = f'<ChildObjects>{eol}\t\t\t{entry}{eol}\t\t</ChildObjects>'
new_content = config_content[:empty.start()] + replacement + config_content[empty.end():]
write_utf8_bom(config_xml_path, new_content)
reg_result = 'added'
else:
close_same = f'</{child_tag}>'
last_same = config_content.rfind(close_same, block.start(), block.end())
if last_same != -1:
# After the last element of the same type (keeps them grouped).
insert_at = last_same + len(close_same)
new_content = (config_content[:insert_at]
+ f'{eol}\t\t\t{entry}'
+ config_content[insert_at:])
else:
# No element of this type yet: new line before </ChildObjects>,
# reusing the block's existing closing indent for </ChildObjects>.
close_at = config_content.rfind('</ChildObjects>', block.start(), block.end())
new_content = (config_content[:close_at]
+ f'\t{entry}{eol}\t\t'
+ config_content[close_at:])
write_utf8_bom(config_xml_path, new_content)
reg_result = 'added'
else:
reg_result = 'no-config'
child_tag = obj_type
config_xml_path = os.path.join(output_dir, 'Configuration.xml')
reg_result = register_in_childobjects(config_xml_path, 'Configuration', child_tag, obj_name)
# ---------------------------------------------------------------------------
# 18. Summary
@@ -1,10 +1,11 @@
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# meta-decompile v0.69 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document,
# InformationRegister, AccumulationRegister, AccountingRegister, CalculationRegister, BusinessProcess, Task, Enum. Инверс meta-compile (omit-on-default: ключ эмитим только
# когда значение в XML отличается от умолчания компилятора). Неподдерживаемый тип / не-MetaDataObject
# root → exit 3 (ring3, как form-decompile).
[CmdletBinding(PositionalBinding=$false)]
param(
[Parameter(Mandatory)]
[Alias('Path')]
@@ -92,7 +93,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode =
if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 }
$objType = $objNode.LocalName
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService')) {
if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService', 'ExternalDataSource')) {
[Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3
}
@@ -205,7 +206,24 @@ function Get-TypeShorthand {
if ($dq) { $dn = $dq.SelectSingleNode('v8:DateFractions', $nsm); if ($dn) { $fr = $dn.InnerText } }
$parts += $fr; break # Date | DateTime
}
'(^|:)base64Binary$' { $parts += 'ValueStorage'; break }
'(^|:)base64Binary$' {
# xs:base64Binary — всегда ДвоичныеДанные (ХранилищеЗначения — это v8:ValueStorage).
# Узел без квалификаторов встречается только в рукописном XML: замерено на 8.3.24.1691 —
# платформа читает его как безлимит (Length 0, Variable) и так же выгружает обратно.
$bq = $typeNode.SelectSingleNode('v8:BinaryDataQualifiers', $nsm)
if ($bq) {
$blen = $bq.SelectSingleNode('v8:Length', $nsm)
$bal = $bq.SelectSingleNode('v8:AllowedLength', $nsm)
$blenVal = if ($blen) { $blen.InnerText.Trim() } else { "" }
$balVal = if ($bal) { $bal.InnerText.Trim() } else { "" }
# Голым BinaryData сворачиваем ТОЛЬКО точный дефолт компилятора
# (4294967292/Fixed), иначе фиксированная длина терялась на раундтрипе.
if ($balVal -eq 'Variable' -and $blenVal) { $parts += "BinaryData($blenVal)" }
elseif ($blenVal -and $blenVal -ne '4294967292') { $parts += "BinaryData($blenVal,fixed)" }
else { $parts += 'BinaryData' }
} else { $parts += 'BinaryData(0)' }
break
}
default { $parts += (Strip-NsPrefix $raw) } # cfg:CatalogRef.X → CatalogRef.X
}
} elseif ($ln -eq 'TypeSet') {
@@ -287,12 +305,19 @@ function Attr-ToDsl {
param($attrNode)
$ap = $attrNode.SelectSingleNode('md:Properties', $nsm)
$nm = ($ap.SelectSingleNode('md:Name', $nsm)).InnerText
# Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля,
# поэтому в DSL попадает только отличающееся.
$edsNids = $ap.SelectSingleNode('md:NameInDataSource', $nsm)
$edsRo = $ap.SelectSingleNode('md:ReadOnly', $nsm)
$edsNull = $ap.SelectSingleNode('md:AllowNull', $nsm)
$ts = Get-TypeShorthand ($ap.SelectSingleNode('md:Type', $nsm))
$flags = @()
$fc = $ap.SelectSingleNode('md:FillChecking', $nsm); if ($fc -and $fc.InnerText -eq 'ShowError') { $flags += 'req' }
$ix = $ap.SelectSingleNode('md:Indexing', $nsm)
if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } }
$ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' }
if ($edsRo -and $edsRo.InnerText -eq 'true') { $flags += 'readonly' }
if ($edsNull -and $edsNull.InnerText -eq 'true') { $flags += 'nullable' }
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
$synNode = $ap.SelectSingleNode('md:Synonym', $nsm)
@@ -406,6 +431,7 @@ function Attr-ToDsl {
# Пустой <Type/> (реквизит без типа / произвольный) → $ts=''. Отличаем от «дефолтного» отсутствия:
# заставляем объектную форму с явным type:'' (компилятор без маркера подставил бы xs:string).
$typeEmpty = ($ts -eq '')
if ($edsNids -and $edsNids.InnerText -and $edsNids.InnerText -cne $nm) { $extra['nameInDataSource'] = $edsNids.InnerText }
if ($synCustom -or $synEmpty -or ($null -ne $ttVal) -or $extra.Count -gt 0 -or $typeEmpty) {
$o = [ordered]@{ name = $nm }
if ($ts) { $o['type'] = $ts } elseif ($typeEmpty) { $o['type'] = '' }
@@ -1657,6 +1683,122 @@ if ($objType -eq 'ExchangePlan') {
}
}
# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) ---
if ($objType -eq 'ExternalDataSource') {
$dlcmVal = P 'DataLockControlMode'
if ($dlcmVal -and $dlcmVal -cne 'Automatic') { $dsl['dataLockControlMode'] = $dlcmVal }
# Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П
function Short-FieldRef { param([string]$ref) if ($ref) { return ($ref -split '\.')[-1] } else { return $null } }
function Field-RefList { param($parent, [string]$tag)
$out = [System.Collections.ArrayList]@()
foreach ($f in @($parent.SelectNodes("md:$tag/xr:Field", $nsm))) { [void]$out.Add((Short-FieldRef $f.InnerText)) }
# Запятая обязательна: return разворачивает коллекцию из одного элемента в скаляр,
# и список ключевых полей из одного поля уехал бы в JSON строкой вместо массива.
return ,$out
}
$srcDir = Join-Path (Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path) $objName
$childObjsEds = $objNode.SelectSingleNode('md:ChildObjects', $nsm)
if ($childObjsEds) {
$tablesMap = [ordered]@{}
foreach ($tNode in @($childObjsEds.SelectNodes('md:Table', $nsm))) {
$tblName = $tNode.InnerText.Trim()
$tblPath = Join-Path (Join-Path $srcDir 'Tables') "$tblName.xml"
if (-not (Test-Path -LiteralPath $tblPath)) {
[Console]::Error.WriteLine("meta-decompile: файл таблицы не найден: $tblPath")
continue
}
$tdoc = New-Object System.Xml.XmlDocument
$tdoc.PreserveWhitespace = $true
$tdoc.Load($tblPath)
$tObjNode = $null
foreach ($c in $tdoc.DocumentElement.ChildNodes) { if ($c.NodeType -eq 'Element') { $tObjNode = $c; break } }
$tp = $tObjNode.SelectSingleNode('md:Properties', $nsm)
function TP { param([string]$tag) $n = $tp.SelectSingleNode("md:$tag", $nsm); if ($n) { return $n.InnerText } else { return $null } }
$tbl = [ordered]@{}
$tSynNode = $tp.SelectSingleNode('md:Synonym', $nsm)
$tSyn = Get-MLValue $tSynNode
if ($tSyn -is [string]) { if ($tSyn -cne (Split-CamelWords $tblName)) { $tbl['synonym'] = $tSyn } }
elseif ($null -ne $tSyn) { $tbl['synonym'] = $tSyn }
# Пустой <Synonym/> ≠ авто-синоним из имени: без явного '' компилятор до-генерит его из имени.
elseif ($tSynNode) { $tbl['synonym'] = '' }
$tCmt = TP 'Comment'; if ($tCmt) { $tbl['comment'] = $tCmt }
$tType = TP 'TableType'; if ($tType -and $tType -cne 'Table') { $tbl['tableType'] = $tType }
$nids = TP 'NameInDataSource'; if ($nids -and $nids -cne $tblName) { $tbl['nameInDataSource'] = $nids }
$expr = TP 'ExpressionInDataSource'; if ($expr) { $tbl['expressionInDataSource'] = $expr }
$tdt = TP 'TableDataType'; if ($tdt -and $tdt -cne 'NonobjectData') { $tbl['tableDataType'] = $tdt }
$keys = Field-RefList $tp 'KeyFields'; if ($keys.Count -gt 0) { $tbl['keyFields'] = $keys }
foreach ($pair in @(@('PresentationField','presentationField'), @('ParentField','parentField'), @('DataVersionField','dataVersionField'))) {
$v = Short-FieldRef (TP $pair[0]); if ($v) { $tbl[$pair[1]] = $v }
}
$ibs = Field-RefList $tp 'InputByString'
# Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем.
$ibsAuto = if ($tbl['presentationField']) { @($tbl['presentationField']) } else { @() }
if (($ibs -join ',') -cne ($ibsAuto -join ',')) { $tbl['inputByString'] = $ibs }
$dlf = Field-RefList $tp 'DataLockFields'; if ($dlf.Count -gt 0) { $tbl['dataLockFields'] = $dlf }
if ((TP 'ReadOnly') -eq 'true') { $tbl['readOnly'] = $true }
$til = TP 'TransactionsIsolationLevel'; if ($til -and $til -cne 'Auto') { $tbl['transactionsIsolationLevel'] = $til }
$tdlcm = TP 'DataLockControlMode'; if ($tdlcm -and $tdlcm -cne 'Automatic') { $tbl['dataLockControlMode'] = $tdlcm }
if ((TP 'UseStandardCommands') -eq 'false') { $tbl['useStandardCommands'] = $false }
if ((TP 'QuickChoice') -eq 'true') { $tbl['quickChoice'] = $true }
$tet = TP 'EditType'; if ($tet -and $tet -cne 'InDialog') { $tbl['editType'] = $tet }
# Слоты форм — такая же часть свойств таблицы, как у прочих объектов (сами формы
# вне скоупа раундтрипа: это отдельные файлы, их делает навык form-add).
foreach ($fp in @(@('DefaultObjectForm','defaultObjectForm'), @('DefaultRecordForm','defaultRecordForm'),
@('DefaultListForm','defaultListForm'), @('DefaultChoiceForm','defaultChoiceForm'))) {
$fv = TP $fp[0]; if ($fv) { $tbl[$fp[1]] = $fv }
}
$basedOn = [System.Collections.ArrayList]@()
foreach ($it in @($tp.SelectNodes('md:BasedOn/xr:Item', $nsm))) { [void]$basedOn.Add($it.InnerText) }
if ($basedOn.Count -gt 0) { $tbl['basedOn'] = $basedOn }
$fieldsArr = [System.Collections.ArrayList]@()
$tChild = $tObjNode.SelectSingleNode('md:ChildObjects', $nsm)
if ($tChild) {
foreach ($f in @($tChild.SelectNodes('md:Field', $nsm))) { [void]$fieldsArr.Add((Attr-ToDsl $f)) }
}
# Таблица без собственных свойств — короткая форма: просто массив полей.
if ($tbl.Count -eq 0) { $tablesMap[$tblName] = $fieldsArr }
else { $tbl['fields'] = $fieldsArr; $tablesMap[$tblName] = $tbl }
}
if ($tablesMap.Count -gt 0) { $dsl['tables'] = $tablesMap }
$fnMap = [ordered]@{}
foreach ($fnNode in @($childObjsEds.SelectNodes('md:Function', $nsm))) {
$fp = $fnNode.SelectSingleNode('md:Properties', $nsm)
$fnName = ($fp.SelectSingleNode('md:Name', $nsm)).InnerText
$fnExprNode = $fp.SelectSingleNode('md:ExpressionInDataSource', $nsm)
$fnExpr = if ($fnExprNode) { $fnExprNode.InnerText } else { '' }
$fnRetNode = $fp.SelectSingleNode('md:ReturnValue', $nsm)
$fnReturns = Get-TypeShorthand ($fp.SelectSingleNode('md:Type', $nsm))
$fnSyn = Get-MLValue ($fp.SelectSingleNode('md:Synonym', $nsm))
$fnCmtNode = $fp.SelectSingleNode('md:Comment', $nsm)
$fnCmt = if ($fnCmtNode) { $fnCmtNode.InnerText } else { '' }
$fnNoValue = ($fnRetNode -and $fnRetNode.InnerText -eq 'false')
$synCustomFn = ($fnSyn -isnot [string]) -and ($null -ne $fnSyn)
if ($fnSyn -is [string]) { $synCustomFn = ($fnSyn -cne (Split-CamelWords $fnName)) -and ($fnSyn -ne '') }
# Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем,
# иначе короткая форма (одна строка выражения) не срабатывала бы никогда.
if (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn -and ($fnReturns -cne 'String(10)')) {
$fo = [ordered]@{ expression = $fnExpr; returns = $fnReturns }
$fnMap[$fnName] = $fo
} elseif (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn) {
# Тип по умолчанию String — короткая форма: одна строка выражения.
$fnMap[$fnName] = $fnExpr
} else {
$fo = [ordered]@{ expression = $fnExpr }
if ($fnNoValue) { $fo['returnValue'] = $false } elseif ($fnReturns) { $fo['returns'] = $fnReturns }
if ($synCustomFn) { $fo['synonym'] = $fnSyn }
if ($fnCmt) { $fo['comment'] = $fnCmt }
$fnMap[$fnName] = $fo
}
}
if ($fnMap.Count -gt 0) { $dsl['functions'] = $fnMap }
}
}
# === Вывод ===
$json = ConvertTo-CompactJson $dsl 0
if ($OutputPath) {
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# meta-decompile v0.64 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# meta-decompile v0.69 — XML объекта метаданных 1С → JSON-черновик формата meta-compile
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
#
# Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии.
@@ -311,7 +311,25 @@ def get_type_shorthand(type_node):
fr = _text(dn)
parts.append(fr) # Date | DateTime
elif re.search(r'(^|:)base64Binary$', raw, re.I):
parts.append('ValueStorage')
# xs:base64Binary — всегда ДвоичныеДанные (ХранилищеЗначения — это v8:ValueStorage).
# Узел без квалификаторов встречается только в рукописном XML: замерено на 8.3.24.1691 —
# платформа читает его как безлимит (Length 0, Variable) и так же выгружает обратно.
bq = type_node.find('v8:BinaryDataQualifiers', NS)
if bq is not None:
blen = bq.find('v8:Length', NS)
bal = bq.find('v8:AllowedLength', NS)
blen_val = _text(blen).strip() if blen is not None else ''
bal_val = _text(bal).strip() if bal is not None else ''
# Голым BinaryData сворачиваем ТОЛЬКО точный дефолт компилятора
# (4294967292/Fixed), иначе фиксированная длина терялась на раундтрипе.
if bal_val.lower() == 'variable' and blen_val:
parts.append(f'BinaryData({blen_val})')
elif blen_val and blen_val != '4294967292':
parts.append(f'BinaryData({blen_val},fixed)')
else:
parts.append('BinaryData')
else:
parts.append('BinaryData(0)')
else:
parts.append(strip_ns_prefix(raw)) # cfg:CatalogRef.X → CatalogRef.X
elif ln == 'TypeSet':
@@ -420,6 +438,11 @@ def parse_choice_parameters(parent, tag):
def attr_to_dsl(attr_node):
ap = _single(attr_node, 'md:Properties')
nm = _text(_single(ap, 'md:Name'))
# Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля,
# поэтому в DSL попадает только отличающееся.
eds_nids = _single(ap, 'md:NameInDataSource')
eds_ro = _single(ap, 'md:ReadOnly')
eds_null = _single(ap, 'md:AllowNull')
ts = get_type_shorthand(_single(ap, 'md:Type'))
flags = []
fc = _single(ap, 'md:FillChecking')
@@ -435,6 +458,10 @@ def attr_to_dsl(attr_node):
ml = _single(ap, 'md:MultiLine')
if ml is not None and _text(ml) == 'true':
flags.append('multiline')
if eds_ro is not None and _text(eds_ro) == 'true':
flags.append('readonly')
if eds_null is not None and _text(eds_null) == 'true':
flags.append('nullable')
# Синоним/подсказка (строка ru-only ИЛИ {ru,en}).
syn_node = _single(ap, 'md:Synonym')
@@ -618,6 +645,8 @@ def attr_to_dsl(attr_node):
# Пустой <Type/> (реквизит без типа) → ts=''. Отличаем от «дефолтного» отсутствия: явный type:''.
type_empty = (ts == '')
if eds_nids is not None and _text(eds_nids) and _text(eds_nids) != nm:
extra['nameInDataSource'] = _text(eds_nids)
if syn_custom or syn_empty or (tt_val is not None) or len(extra) > 0 or type_empty:
o = {'name': nm}
if ts:
@@ -2112,6 +2141,7 @@ SUPPORTED_TYPES = (
'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob',
'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter',
'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService',
'ExternalDataSource',
)
@@ -2154,6 +2184,159 @@ def main():
build_dsl()
# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) ---
if obj_type == 'ExternalDataSource':
dlcm_val = P('DataLockControlMode')
if dlcm_val and dlcm_val != 'Automatic':
dsl['dataLockControlMode'] = dlcm_val
def short_field_ref(ref):
"""Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П"""
return ref.split('.')[-1] if ref else None
def field_ref_list(parent, tag):
return [short_field_ref(_text(f)) for f in parent.findall('md:%s/xr:Field' % tag, NS)]
src_dir = os.path.join(os.path.dirname(os.path.abspath(args.ObjectPath)), obj_name)
child_objs_eds = _single(obj_node, 'md:ChildObjects')
if child_objs_eds is not None:
tables_map = {}
for t_node in child_objs_eds.findall('md:Table', NS):
tbl_name = (_text(t_node) or '').strip()
tbl_path = os.path.join(src_dir, 'Tables', tbl_name + '.xml')
if not os.path.isfile(tbl_path):
sys.stderr.write("meta-decompile: файл таблицы не найден: %s\n" % tbl_path)
continue
t_root = etree.parse(tbl_path).getroot()
t_obj_node = next((c for c in t_root if isinstance(c.tag, str)), None)
tp = _single(t_obj_node, 'md:Properties')
def TP(tag, _tp=None):
n = _single(_tp if _tp is not None else tp, 'md:%s' % tag)
return _text(n) if n is not None else None
tbl = {}
t_syn_node = _single(tp, 'md:Synonym')
t_syn = get_ml_value(t_syn_node)
if isinstance(t_syn, str):
if t_syn != split_camel_words(tbl_name):
tbl['synonym'] = t_syn
elif t_syn is not None:
tbl['synonym'] = t_syn
elif t_syn_node is not None:
# Пустой <Synonym/> != авто-синоним из имени: без явного '' компилятор до-генерит его.
tbl['synonym'] = ''
t_cmt = TP('Comment')
if t_cmt:
tbl['comment'] = t_cmt
t_type = TP('TableType')
if t_type and t_type != 'Table':
tbl['tableType'] = t_type
nids = TP('NameInDataSource')
if nids and nids != tbl_name:
tbl['nameInDataSource'] = nids
expr = TP('ExpressionInDataSource')
if expr:
tbl['expressionInDataSource'] = expr
tdt = TP('TableDataType')
if tdt and tdt != 'NonobjectData':
tbl['tableDataType'] = tdt
keys = field_ref_list(tp, 'KeyFields')
if keys:
tbl['keyFields'] = keys
for tag, key in (('PresentationField', 'presentationField'), ('ParentField', 'parentField'),
('DataVersionField', 'dataVersionField')):
v = short_field_ref(TP(tag))
if v:
tbl[key] = v
ibs = field_ref_list(tp, 'InputByString')
# Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем.
ibs_auto = [tbl['presentationField']] if tbl.get('presentationField') else []
if ibs != ibs_auto:
tbl['inputByString'] = ibs
dlf = field_ref_list(tp, 'DataLockFields')
if dlf:
tbl['dataLockFields'] = dlf
if TP('ReadOnly') == 'true':
tbl['readOnly'] = True
til = TP('TransactionsIsolationLevel')
if til and til != 'Auto':
tbl['transactionsIsolationLevel'] = til
tdlcm = TP('DataLockControlMode')
if tdlcm and tdlcm != 'Automatic':
tbl['dataLockControlMode'] = tdlcm
if TP('UseStandardCommands') == 'false':
tbl['useStandardCommands'] = False
if TP('QuickChoice') == 'true':
tbl['quickChoice'] = True
t_et = TP('EditType')
if t_et and t_et != 'InDialog':
tbl['editType'] = t_et
# Слоты форм — такая же часть свойств таблицы, как у прочих объектов (сами формы
# вне скоупа раундтрипа: это отдельные файлы, их делает навык form-add).
for xml_tag, dsl_key in (('DefaultObjectForm', 'defaultObjectForm'),
('DefaultRecordForm', 'defaultRecordForm'),
('DefaultListForm', 'defaultListForm'),
('DefaultChoiceForm', 'defaultChoiceForm')):
fv = TP(xml_tag)
if fv:
tbl[dsl_key] = fv
based_on = [_text(it) for it in tp.findall('md:BasedOn/xr:Item', NS)]
if based_on:
tbl['basedOn'] = based_on
fields_arr = []
t_child = _single(t_obj_node, 'md:ChildObjects')
if t_child is not None:
for f in t_child.findall('md:Field', NS):
fields_arr.append(attr_to_dsl(f))
# Таблица без собственных свойств — короткая форма: просто массив полей.
if not tbl:
tables_map[tbl_name] = fields_arr
else:
tbl['fields'] = fields_arr
tables_map[tbl_name] = tbl
if tables_map:
dsl['tables'] = tables_map
fn_map = {}
for fn_node in child_objs_eds.findall('md:Function', NS):
fp = _single(fn_node, 'md:Properties')
fn_name = _text(_single(fp, 'md:Name'))
fn_expr_node = _single(fp, 'md:ExpressionInDataSource')
fn_expr = _text(fn_expr_node) if fn_expr_node is not None else ''
fn_ret_node = _single(fp, 'md:ReturnValue')
fn_returns = get_type_shorthand(_single(fp, 'md:Type'))
fn_syn = get_ml_value(_single(fp, 'md:Synonym'))
fn_cmt_node = _single(fp, 'md:Comment')
fn_cmt = _text(fn_cmt_node) if fn_cmt_node is not None else ''
fn_no_value = fn_ret_node is not None and _text(fn_ret_node) == 'false'
if isinstance(fn_syn, str):
syn_custom_fn = fn_syn != split_camel_words(fn_name) and fn_syn != ''
else:
syn_custom_fn = fn_syn is not None
# Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем,
# иначе короткая форма (одна строка выражения) не срабатывала бы никогда.
if not fn_no_value and not fn_cmt and not syn_custom_fn and fn_returns != 'String(10)':
fn_map[fn_name] = {'expression': fn_expr, 'returns': fn_returns}
elif not fn_no_value and not fn_cmt and not syn_custom_fn:
# Тип по умолчанию String — короткая форма: одна строка выражения.
fn_map[fn_name] = fn_expr
else:
fo = {'expression': fn_expr}
if fn_no_value:
fo['returnValue'] = False
elif fn_returns:
fo['returns'] = fn_returns
if syn_custom_fn:
fo['synonym'] = fn_syn
if fn_cmt:
fo['comment'] = fn_cmt
fn_map[fn_name] = fo
if fn_map:
dsl['functions'] = fn_map
# === Вывод ===
json_str = convert_to_compact_json(dsl, 0)
if args.OutputPath:
+20 -28
View File
@@ -35,48 +35,28 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/meta-edit.ps1" -Def
| DefinitionFile | JSON-файл с операциями (альтернатива Operation) |
| NoValidate | Не запускать meta-validate после правки |
## Операции — сводная таблица
## Частые операции
Batch через `;;` во всех операциях. Подробный синтаксис — в файлах по ссылкам.
### Дочерние элементы — [child-operations.md](child-operations.md)
Batch через `;;` во всех операциях.
| Операция | Формат Value | Пример |
|----------|-------------|--------|
| `add-attribute` | `Имя: Тип \| флаги` | `"Сумма: Число(15,2) \| req, index"` |
| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` |
| `add-dimension` | `Имя: Тип \| флаги` | `"Организация: CatalogRef.Организации \| master"` |
| `add-resource` | `Имя: Тип` | `"Сумма: Число(15,2)"` |
| `add-enumValue` | `Имя` | `"Значение1 ;; Значение2"` |
| `add-column` | `Имя: Тип` | `"Тип: EnumRef.ТипыДокументов"` |
| `add-form` / `add-template` / `add-command` | `Имя` | `"ФормаЭлемента"` |
| `add-ts` | `ТЧ: Рекв1: Тип1, Рекв2: Тип2` | `"Товары: Ном: CatalogRef.Ном, Кол: Число(15,3)"` |
| `add-ts-attribute` | `ТЧ.Имя: Тип` | `"Товары.Скидка: Число(15,2)"` |
| `remove-*` | `Имя` | `"СтарыйРеквизит ;; ЕщёОдин"` |
| `remove-ts-attribute` | `ТЧ.Имя` | `"Товары.УстаревшийРекв"` |
| `modify-attribute` | `Имя: ключ=значение` | `"СтароеИмя: name=НовоеИмя, type=Строка(500)"` |
| `modify-ts-attribute` | `ТЧ.Имя: ключ=значение` | `"Товары.Рекв: name=НовоеИмя"` |
| `modify-ts` | `ТЧ: ключ=значение` | `"Товары: synonym=Товарный состав"` |
| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` |
Позиционная вставка: `"Склад: CatalogRef.Склады >> after Организация"`.
`modify-attribute` умеет и структурные свойства реквизита — формат/подсказку, форму и параметры выбора,
значение заполнения, границы (`Format`, `ChoiceForm`, `ChoiceParameters`, `FillValue`, `MinValue`/`MaxValue` и др.).
### Свойства объекта — [properties-reference.md](properties-reference.md)
| Операция | Формат Value | Пример |
|----------|-------------|--------|
| `modify-property` | `Ключ=Значение` | `"CodeLength=11 ;; DescriptionLength=150"` |
| `add-owner` | `MetaType.Name` | `"Catalog.Контрагенты ;; Catalog.Организации"` |
| `add-registerRecord` | `MetaType.Name` | `"AccumulationRegister.ОстаткиТоваров"` |
| `add-basedOn` | `MetaType.Name` | `"Document.ЗаказКлиента"` |
| `add-inputByString` | `Путь поля` | `"StandardAttribute.Description"` |
| `set-owners` / `set-registerRecords` / `set-basedOn` / `set-inputByString` | Замена всего списка | `"Catalog.Орг ;; Catalog.Контр"` |
| `remove-owner` / `remove-registerRecord` / ... | Удаление из списка | `"Catalog.Контрагенты"` |
### JSON DSL — [json-dsl.md](json-dsl.md)
Для комбинированных операций (add + remove + modify в одном файле), синонимы ключей/типов, таблица поддерживаемых объектов.
Составной тип — через `+`: `"Значение: Строка + Число(15,2) + Дата"`.
## Быстрые примеры
@@ -84,9 +64,6 @@ Batch через `;;` во всех операциях. Подробный си
# Добавить реквизиты
-Operation add-attribute -Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index"
# Составной тип (несколько типов через +)
-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"
# Добавить ТЧ с реквизитами
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2)"
@@ -103,6 +80,21 @@ Batch через `;;` во всех операциях. Подробный си
-Operation set-owners -Value "Catalog.Контрагенты ;; Catalog.Организации"
```
## Индекс: что правишь → файл
| Что нужно | Файл |
|-----------|------|
| Реквизит, измерение, ресурс, графа: флаги, составные типы, структурные свойства (`Format`, `ChoiceParameters`, `FillValue`, …) | `reference/attributes.md` |
| Табличная часть и её реквизиты | `reference/tabular-sections.md` |
| Свойства самого объекта, списочные свойства (владельцы, движения, основание, ввод по строке) | `reference/properties.md` |
| Предопределённые элементы | `reference/predefined.md` |
| Значения перечисления, команды | `reference/other-children.md` |
| Внешний источник данных: поля, таблицы, функции | `reference/external-data-source.md` |
| Комбинированные операции в одном JSON, синонимы ключей и типов, какие дети допустимы у типа объекта | `reference/json-dsl.md` |
Форму добавляет и удаляет навык `form-add` / `form-remove`, макет — навык `template-add` /
`template-remove`: кроме записи в `ChildObjects` у них есть собственные файлы, и `meta-edit` их не трогает.
## Верификация
```
@@ -1,6 +1,7 @@
# Inline-операции над дочерними элементами
# Реквизиты, измерения, ресурсы, графы
Подробный справочник операций `add-*` / `remove-*` / `modify-*` для дочерних элементов объекта метаданных.
Операции над «плоскими» дочерними элементами объекта: реквизит (`attribute`), измерение (`dimension`),
ресурс (`resource`), графа журнала (`column`). Синтаксис у всех один.
## Общие правила
@@ -9,9 +10,10 @@
-Value "Комментарий: Строка(200) ;; Сумма: Число(15,2) | index"
```
**Shorthand-формат** реквизитов: `ИмяРеквизита: Тип | флаги`
**Shorthand-формат**: `ИмяРеквизита: Тип | флаги`
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только dimensions); `mainFilter` — основной отбор (только dimensions).
Флаги: `req` — обязательное заполнение; `index` — индексировать; `master` — ведущее измерение (только
измерения); `mainFilter` — основной отбор (только измерения).
**Позиционная вставка**: `>> after ИмяЭлемента` или `<< before ИмяЭлемента`:
```powershell
@@ -20,11 +22,10 @@
## Составные типы
Для реквизитов с несколькими допустимыми типами — разделитель `+`:
Разделитель `+`:
```powershell
-Operation add-attribute -Value "Значение: Строка + Число(15,2) + Дата + CatalogRef.Контрагенты"
-Operation add-attribute -Value "Значение: Строка + Число(15,2) | req"
-Operation modify-ts-attribute -Value "Данные.Значение: type=Строка + Число(15,2) + Дата"
```
В JSON DSL — массив в `type`:
@@ -43,84 +44,11 @@
-Operation add-column -Value "Тип: EnumRef.ТипыДокументов"
```
## add-ts
Формат: `ИмяТЧ: Реквизит1: Тип1, Реквизит2: Тип2, ...`
```powershell
-Operation add-ts -Value "Товары: Ном: CatalogRef.Ном | req, Кол: Число(15,3), Цена: Число(15,2), Сумма: Число(15,2)"
```
## add-ts-attribute / remove-ts-attribute / modify-ts-attribute
Операции над реквизитами **внутри существующей ТЧ**. Формат: `ИмяТЧ.ОпределениеРеквизита` (dot-нотация).
```powershell
# Добавить реквизит в ТЧ
-Operation add-ts-attribute -Value "Товары.СтавкаНДС: EnumRef.СтавкиНДС"
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) ;; Товары.Бонус: Число(15,2)"
# Позиционная вставка в ТЧ
-Operation add-ts-attribute -Value "Товары.Скидка: Число(15,2) >> after Цена"
# Удалить реквизит из ТЧ
-Operation remove-ts-attribute -Value "Товары.УстаревшийРекв"
-Operation remove-ts-attribute -Value "Товары.Рекв1 ;; Товары.Рекв2"
# Изменить реквизит в ТЧ (rename, type change и т.д.)
-Operation modify-ts-attribute -Value "Товары.СтароеИмя: name=НовоеИмя, type=Строка(500)"
```
Batch через `;;` — можно указать разные ТЧ: `"Товары.А: Строка(50) ;; Услуги.Б: Число(10)"`.
## modify-ts
Изменение свойств **самой табличной части** (Synonym, FillChecking, Use и др.):
```powershell
-Operation modify-ts -Value "Товары: synonym=Товарный состав"
-Operation modify-ts -Value "Товары: fillChecking=ShowError"
```
Формат аналогичен `modify-attribute`: `ИмяТЧ: ключ=значение, ключ=значение`.
## add-predefined
Добавить предопределённые элементы (Catalog, ChartOfCharacteristicTypes). Существующие элементы и их
идентификаторы сохраняются, новые получают свежий id.
Inline — строка `(Код) Имя [Наименование]` (batch через `;;`; `[Наименование]` необязательно — иначе авто из имени):
```powershell
-Operation add-predefined -Value "(001) Основной ;; (002) Резервный [Резервный склад]"
```
JSON — строки и/или объекты (для групп с вложенными):
```json
{ "add": { "predefined": [
"(001) Основной",
{ "name": "Группа", "isFolder": true, "childItems": ["(002) Вложенный"] }
] } }
```
Ключи объекта: `name`, `code`, `description`, `isFolder`, `childItems` (дерево). Тип кода (строковый/числовой)
берётся из объекта автоматически.
## add-enumValue / add-form / add-template / add-command
Просто имена (batch через `;;`):
```powershell
-Operation add-enumValue -Value "Значение1 ;; Значение2 ;; Значение3"
-Operation add-form -Value "ФормаЭлемента ;; ФормаСписка"
-Operation add-template -Value "ПечатнаяФорма"
-Operation add-command -Value "Команда1"
```
## remove-*
Имя элемента (или несколько через `;;`):
```powershell
-Operation remove-attribute -Value "СтарыйРеквизит ;; ЕщёОдин"
-Operation remove-ts -Value "УстаревшаяТЧ"
-Operation remove-enumValue -Value "НеиспользуемоеЗначение"
```
@@ -140,7 +68,7 @@ JSON — строки и/или объекты (для групп с вложе
-Operation modify-enumValue -Value "СтароеЗначение: name=НовоеЗначение"
```
### Структурные свойства реквизита
## Структурные свойства реквизита
Свойства со сложным значением задавайте через JSON DSL (`{ "modify": { "attributes": { "Имя": { ... } } } }`):
@@ -0,0 +1,44 @@
# Внешние источники данных
Точечные правки внутри уже созданного источника: поле в таблицу, таблица и функция в источник.
Источник целиком собирает навык `meta-compile` — его повторный запуск заменяет файл источника,
выдаёт новый uuid и оставляет сиротами таблицы, которых нет в описании.
## add-field — поле таблицы
Правится файл таблицы: `ExternalDataSources/<Источник>/Tables/<Таблица>.xml`.
Поле задаётся как обычный реквизит, плюс три своих ключа: `nameInDataSource` (умолчание — имя поля),
`readOnly`, `allowNull`. Флаги строковой формы — `readonly`, `nullable`.
```json
{ "add": { "fields": [
"barcode: String(20) | nullable",
{ "name": "cost", "type": "Number(15,2)", "nameInDataSource": "cost_net", "readOnly": true }
] } }
```
Типы поля: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу того же
источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`. Составной тип платформа запрещает.
## add-tables / add-functions — в файле источника
Правится файл источника: `ExternalDataSources/<Источник>.xml`. Таблица — единственная операция
навыка, создающая **файл**: `<Источник>/Tables/<Имя>.xml` плюс имя в `ChildObjects` источника.
Синтаксис таблицы и функции тот же, что у навыка `meta-compile` (`reference/external-data-source.md` там же).
```json
{ "add": {
"tables": { "sales": { "keyFields": ["id"], "fields": ["id: Number(10,0)", "summa: Number(15,2)"] } },
"functions": { "nextKey": "NEXT VALUE FOR public.seq_key" }
} }
```
При добавлении таблицы не поддержаны ключи `characteristics` и `default*Form` — навык отвергает их,
а не проглатывает молча. Характеристики задаются при сборке источника навыком `meta-compile`,
форму назначает навык `form-add`.
## Удаление
Таблицу удаляет навык `meta-remove`: `ExternalDataSource.<Источник>.Table.<Таблица>` — вместе с файлом
и записью в `ChildObjects`. Весь источник — `ExternalDataSource.<Источник>`.

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